diff --git a/.github/actions/setup-idaes/action.yml b/.github/actions/setup-idaes/action.yml index 88258ab3e3..734ca09fa4 100644 --- a/.github/actions/setup-idaes/action.yml +++ b/.github/actions/setup-idaes/action.yml @@ -40,5 +40,5 @@ runs: shell: bash -l {0} run: | echo '::group::Output of "idaes get-extensions" command' - idaes get-extensions --verbose + idaes get-extensions --extra petsc --verbose echo '::endgroup::' diff --git a/README.md b/README.md index 2a036e967b..d890f65ba4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ computational tools and models to support the design, analysis, optimization, scale-up, operation and troubleshooting of innovative, advanced energy systems. -## Build statuses +## Project Build and Download Statuses ![Tests](https://github.com/IDAES/idaes-pse/workflows/Tests/badge.svg?branch=main) ![Integration](https://github.com/IDAES/idaes-pse/workflows/Integration/badge.svg?branch=main) [![codecov](https://codecov.io/gh/IDAES/idaes-pse/branch/main/graph/badge.svg?token=1lNQNbSB29)](https://codecov.io/gh/IDAES/idaes-pse) @@ -13,6 +13,7 @@ scale-up, operation and troubleshooting of innovative, advanced energy systems. [![GitHub contributors](https://img.shields.io/github/contributors/IDAES/idaes-pse.svg)](https://github.com/IDAES/idaes-pse/graphs/contributors) [![Merged PRs](https://img.shields.io/github/issues-pr-closed-raw/IDAES/idaes-pse.svg?label=merged+PRs)](https://github.com/IDAES/idaes-pse/pulls?q=is:pr+is:merged) [![Issue stats](http://isitmaintained.com/badge/resolution/IDAES/idaes-pse.svg)](http://isitmaintained.com/project/IDAES/idaes-pse) +[![Downloads](https://pepy.tech/badge/idaes-pse)](https://pepy.tech/project/idaes-pse) ## Getting Started @@ -88,6 +89,14 @@ General, background and overview information is available at the [IDAES main web Framework development happens at our [GitHub repo](https://github.com/IDAES/idaes-pse) where you can ask questions by starting a [discussion](https://github.com/IDAES/idaes-pse/discussions), [report issues/bugs](https://github.com/IDAES/idaes-pse/issues) or [make contributions](https://github.com/IDAES/idaes-pse/pulls). For further enquiries, send an email to: +## Funding acknowledgements + +This work was conducted as part of the [Institute for the Design of Advanced Energy Systems (IDAES)](https://idaes.org) +with support through the [Simulation-Based Engineering, Crosscutting Research Program](https://netl.doe.gov/coal/simulation-based-engineering) +within the U.S. Department of Energy’s [Office of Fossil Energy and Carbon Management (FECM)](https://www.energy.gov/fecm/office-fossil-energy-and-carbon-management). +As of 2021, additional support was provided by FECM’s [Solid Oxide Fuel Cell Program](https://www.energy.gov/fecm/science-innovation/clean-coal-research/solid-oxide-fuel-cells), +and [Transformative Power Generation Program](https://www.energy.gov/fecm/science-innovation/office-clean-coal-and-carbon-management/advanced-energy-systems/transformative). + ## Contributing Please see our [Advanced User Guide](https://idaes-pse.readthedocs.io/en/stable/advanced_user_guide/) and [Developer Documentation](https://idaes-pse.readthedocs.io/en/stable/advanced_user_guide/developer/) on how to work with the idaes-pse source code and contirbute changes to the project. diff --git a/docs/advanced_user_guide/index.rst b/docs/advanced_user_guide/index.rst deleted file mode 100644 index b15834a3c0..0000000000 --- a/docs/advanced_user_guide/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Advanced User Guide -=================== - -.. toctree:: - :maxdepth: 1 - - advanced_install/index - developer/index - custom_models/general_model_development \ No newline at end of file diff --git a/docs/copyright.rst b/docs/copyright.rst deleted file mode 100644 index 880bc56b91..0000000000 --- a/docs/copyright.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _idaes-copyright: - -.. include:: ../COPYRIGHT.md diff --git a/docs/user_guide/components/dmf/dmf-api.rst b/docs/explanations/components/dmf/dmf-api.rst similarity index 100% rename from docs/user_guide/components/dmf/dmf-api.rst rename to docs/explanations/components/dmf/dmf-api.rst diff --git a/docs/user_guide/components/dmf/dmf-cli.rst b/docs/explanations/components/dmf/dmf-cli.rst similarity index 100% rename from docs/user_guide/components/dmf/dmf-cli.rst rename to docs/explanations/components/dmf/dmf-cli.rst diff --git a/docs/user_guide/components/dmf/dmf-overview.rst b/docs/explanations/components/dmf/dmf-overview.rst similarity index 100% rename from docs/user_guide/components/dmf/dmf-overview.rst rename to docs/explanations/components/dmf/dmf-overview.rst diff --git a/docs/user_guide/components/dmf/index.rst b/docs/explanations/components/dmf/index.rst similarity index 100% rename from docs/user_guide/components/dmf/index.rst rename to docs/explanations/components/dmf/index.rst diff --git a/docs/user_guide/components/flowsheet/index.rst b/docs/explanations/components/flowsheet/index.rst similarity index 56% rename from docs/user_guide/components/flowsheet/index.rst rename to docs/explanations/components/flowsheet/index.rst index 6a6fbeff14..359b30ca84 100644 --- a/docs/user_guide/components/flowsheet/index.rst +++ b/docs/explanations/components/flowsheet/index.rst @@ -4,28 +4,28 @@ .. toctree:: :glob: :hidden: - + * -Flowsheet models are the top level of the IDAES modeling hierarchy. The -flowsheet is implemented with a -:ref:`FlowsheetBlock `, -which provides a container for other components. Flowsheet models generally contain +Flowsheet models are the top level of the IDAES modeling hierarchy. The +flowsheet is implemented with a +:ref:`FlowsheetBlock `, +which provides a container for other components. Flowsheet models generally contain three types of components: -1. :ref:`Unit models`, representing unit operations -2. :ref:`Property packages`, representing the parameters and relationships for property calculations +1. :ref:`Unit models`, representing unit operations +2. :ref:`Property packages`, representing the parameters and relationships for property calculations 3. Arcs, representing connections between unit models The FlowsheetBlock is also where the -:ref:`time domain` +:ref:`time domain` is implemented. While the time domain is essential for dynamic modeling, the time domain exists even for steady state models (single point in time). -Flowsheet models may also contain additional constraints relating to how different unit models -behave and interact, such as control and operational constraints. Generally speaking, if a -constraint is purely internal to a single unit, and does not depend on information from other -units in the flowsheet, then the constraint should be placed inside the relevant unit model. +Flowsheet models may also contain additional constraints relating to how different unit models +behave and interact, such as control and operational constraints. Generally speaking, if a +constraint is purely internal to a single unit, and does not depend on information from other +units in the flowsheet, then the constraint should be placed inside the relevant unit model. Otherwise, the constraint should be placed at the flowsheet level. diff --git a/docs/user_guide/components/flowsheet/time_domain.rst b/docs/explanations/components/flowsheet/time_domain.rst similarity index 72% rename from docs/user_guide/components/flowsheet/time_domain.rst rename to docs/explanations/components/flowsheet/time_domain.rst index 52560c908f..1ec59ff3e2 100644 --- a/docs/user_guide/components/flowsheet/time_domain.rst +++ b/docs/explanations/components/flowsheet/time_domain.rst @@ -1,26 +1,26 @@ Time Domain =========== -Time domain is an essential component of the IDAES framework. When a user first declares a -Flowsheet model a time domain is created, the form of which depends on whether the Flowsheet -is declared to be dynamic or steady-state -(see :ref:`FlowsheetBlock `). -In situations where the user makes use of nested flowsheets, each sub-flowsheet refers to its +Time domain is an essential component of the IDAES framework. When a user first declares a +Flowsheet model a time domain is created, the form of which depends on whether the Flowsheet +is declared to be dynamic or steady-state +(see :ref:`FlowsheetBlock `). +In situations where the user makes use of nested flowsheets, each sub-flowsheet refers to its parent flowsheet for the time domain. -Different models may handle the time domain differently, but in general all IDAES models refer -to the time domain of their parent flowsheet. The only exception to this are blocks associated -with Property calculations. PropertyBlocks (i.e. StateBlocks and ReactionBlocks) represent the state of the material at a single point -in space and time, and thus do not contain the time domain. Instead, PropertyBlocks are indexed -by time (and space where applicable) - i.e. there is a separate StateBlock for each point in -time. The user should keep this in mind when working with IDAES models, as it is important for +Different models may handle the time domain differently, but in general all IDAES models refer +to the time domain of their parent flowsheet. The only exception to this are blocks associated +with Property calculations. PropertyBlocks (i.e. StateBlocks and ReactionBlocks) represent the state of the material at a single point +in space and time, and thus do not contain the time domain. Instead, PropertyBlocks are indexed +by time (and space where applicable) - i.e. there is a separate StateBlock for each point in +time. The user should keep this in mind when working with IDAES models, as it is important for understanding where the time index appears within a model. -In order to facilitate referencing of the time domain, all Flowsheet objects have a `time` -configuration argument which is a reference to the time domain for that flowsheet. All IDAES -models contain a `flowsheet` method which returns the parent flowsheet object, thus a reference +In order to facilitate referencing of the time domain, all Flowsheet objects have a `time` +configuration argument which is a reference to the time domain for that flowsheet. All IDAES +models contain a `flowsheet` method which returns the parent flowsheet object, thus a reference to the time domain can always be found using the following code: `flowsheet().config.time`. Another important thing to note is that steady-state models do contain a time domain. While the -time domain for steady-stage models is a single point at time = 0.0, they still contain a +time domain for steady-stage models is a single point at time = 0.0, they still contain a reference to the time domain and the components (e.g. StateBlocks) are indexed by time. \ No newline at end of file diff --git a/docs/user_guide/components/index.rst b/docs/explanations/components/index.rst similarity index 80% rename from docs/user_guide/components/index.rst rename to docs/explanations/components/index.rst index eac572a6b3..40d4675b97 100644 --- a/docs/user_guide/components/index.rst +++ b/docs/explanations/components/index.rst @@ -22,24 +22,24 @@ detail with a link in their description. .. rubric:: Flowsheet -:ref:`Flowsheet models` +:ref:`Flowsheet models` are the top level of the modeling heirachy. Flowsheet models represent traditional process flowsheets, containing a number of unit models connected together into a flow network and the property packages. .. rubric:: Property Package -:ref:`Property packages` are a +:ref:`Property packages` are a collection of related models that represent the physical, thermodynamic, and reactive properties of the process streams. .. rubric:: Unit Model -:ref:`Unit models` +:ref:`Unit models` represent individual pieces of equipment and their processes. .. rubric:: Data Management Framework -The :ref:`Data Management Framework ` +The :ref:`Data Management Framework ` is used to manage all the data needed by the platform, including flowsheets, models, and results. It stores metadata and data in persistent storage. diff --git a/docs/user_guide/components/property_package/comp.rst b/docs/explanations/components/property_package/comp.rst similarity index 83% rename from docs/user_guide/components/property_package/comp.rst rename to docs/explanations/components/property_package/comp.rst index 935f6081ee..5ae06dc809 100644 --- a/docs/user_guide/components/property_package/comp.rst +++ b/docs/explanations/components/property_package/comp.rst @@ -1,10 +1,10 @@ Component Object ================ -Component objects are used to identify the chemical -species of interest in a property package and to contain information describing the behavior -of that component (such as properties of that component). Additional information on the -:ref:`Component Class` is provided in the technical +Component objects are used to identify the chemical +species of interest in a property package and to contain information describing the behavior +of that component (such as properties of that component). Additional information on the +:ref:`Component Class` is provided in the technical specifications. The following types of components are currently supported. @@ -16,11 +16,11 @@ The following types of components are currently supported. * `Anion` - component object for representing ion species with a negative charge (`LiquidPhase` only). * `Cation` - component object for representing ion species with a positive charger(`LiquidPhase` only). -Component objects are intended to store all the necessary information regarding a given -chemical species for use within a process model. Examples of such information include the -methods and parameters required for calculating thermophysical properties. Additionally, -certain unit operations handle components in different ways depending on certain criteria. -An example of this is Reverse Osmosis, where the driving force across the membrane is calculated +Component objects are intended to store all the necessary information regarding a given +chemical species for use within a process model. Examples of such information include the +methods and parameters required for calculating thermophysical properties. Additionally, +certain unit operations handle components in different ways depending on certain criteria. +An example of this is Reverse Osmosis, where the driving force across the membrane is calculated differently for solvent species and solute species. Component objects implement the following methods for determining species behavior: diff --git a/docs/user_guide/components/property_package/general/bubble_dew.rst b/docs/explanations/components/property_package/general/bubble_dew.rst similarity index 100% rename from docs/user_guide/components/property_package/general/bubble_dew.rst rename to docs/explanations/components/property_package/general/bubble_dew.rst diff --git a/docs/user_guide/components/property_package/general/component_def.rst b/docs/explanations/components/property_package/general/component_def.rst similarity index 93% rename from docs/user_guide/components/property_package/general/component_def.rst rename to docs/explanations/components/property_package/general/component_def.rst index 137cfbe090..1b8343b63a 100644 --- a/docs/user_guide/components/property_package/general/component_def.rst +++ b/docs/explanations/components/property_package/general/component_def.rst @@ -1,7 +1,7 @@ Defining Components =================== -The first step in defining a generic property package is to describe each of the chemical species of interest within the system, including methods for calculating the necessary thermophysical properties of the pure component. Components are defined using :ref:`IDAES Component objects`, and are automatically constructed using the `components` configuration argument from the `GenericParameterBlock`. +The first step in defining a generic property package is to describe each of the chemical species of interest within the system, including methods for calculating the necessary thermophysical properties of the pure component. Components are defined using :ref:`IDAES Component objects`, and are automatically constructed using the `components` configuration argument from the `GenericParameterBlock`. The `components` Argument ------------------------- @@ -17,7 +17,7 @@ Each `GenericParameterBlock` has a configuration argument named `components` whi Configuration Arguments ----------------------- -The configuration arguments for each chemical species are used to define methods for calculating pure component properties and defining the parameters associated with these. A full list of the supported configuration arguments for `Component` objects can be found :ref:`here`. +The configuration arguments for each chemical species are used to define methods for calculating pure component properties and defining the parameters associated with these. A full list of the supported configuration arguments for `Component` objects can be found :ref:`here`. Type Argument ^^^^^^^^^^^^^ @@ -53,7 +53,7 @@ Within the IDAES Generic Property Package Framework, pure component property cor * functions are used for self-contained correlations with hard-coded parameters, * classes are used for more generic correlations which require associated parameters. -When providing a method via the `components` configuration argument, users can either provide a pointer to the desired class/method directly, or to a Python module containing a class or method with the same name as the property to be calculated. More details on the uses of these and how to construct your own can be found in the :ref:`developer documentation`. +When providing a method via the `components` configuration argument, users can either provide a pointer to the desired class/method directly, or to a Python module containing a class or method with the same name as the property to be calculated. More details on the uses of these and how to construct your own can be found in the :ref:`developer documentation`. Pure Component Libraries """""""""""""""""""""""" @@ -86,7 +86,7 @@ As a given system may incorporate multiple phase equilibria, the `phase_equilibr "phase_equilibrium_form": {("Vap", "Liq"): fugacity} -The IDAES Generic Property Package Framework contains a library of common forms for the equilibrium condition, which is described :ref:`here`. +The IDAES Generic Property Package Framework contains a library of common forms for the equilibrium condition, which is described :ref:`here`. Parameter Data ^^^^^^^^^^^^^^ diff --git a/docs/user_guide/components/property_package/general/developer/eos_dev.rst b/docs/explanations/components/property_package/general/developer/eos_dev.rst similarity index 100% rename from docs/user_guide/components/property_package/general/developer/eos_dev.rst rename to docs/explanations/components/property_package/general/developer/eos_dev.rst diff --git a/docs/user_guide/components/property_package/general/developer/phase_equil_dev.rst b/docs/explanations/components/property_package/general/developer/phase_equil_dev.rst similarity index 100% rename from docs/user_guide/components/property_package/general/developer/phase_equil_dev.rst rename to docs/explanations/components/property_package/general/developer/phase_equil_dev.rst diff --git a/docs/user_guide/components/property_package/general/developer/pure_component_dev.rst b/docs/explanations/components/property_package/general/developer/pure_component_dev.rst similarity index 100% rename from docs/user_guide/components/property_package/general/developer/pure_component_dev.rst rename to docs/explanations/components/property_package/general/developer/pure_component_dev.rst diff --git a/docs/user_guide/components/property_package/general/developer/state_definition_dev.rst b/docs/explanations/components/property_package/general/developer/state_definition_dev.rst similarity index 100% rename from docs/user_guide/components/property_package/general/developer/state_definition_dev.rst rename to docs/explanations/components/property_package/general/developer/state_definition_dev.rst diff --git a/docs/user_guide/components/property_package/general/developers.rst b/docs/explanations/components/property_package/general/developers.rst similarity index 100% rename from docs/user_guide/components/property_package/general/developers.rst rename to docs/explanations/components/property_package/general/developers.rst diff --git a/docs/user_guide/components/property_package/general/eos/cubic.rst b/docs/explanations/components/property_package/general/eos/cubic.rst similarity index 100% rename from docs/user_guide/components/property_package/general/eos/cubic.rst rename to docs/explanations/components/property_package/general/eos/cubic.rst diff --git a/docs/user_guide/components/property_package/general/eos/ideal.rst b/docs/explanations/components/property_package/general/eos/ideal.rst similarity index 100% rename from docs/user_guide/components/property_package/general/eos/ideal.rst rename to docs/explanations/components/property_package/general/eos/ideal.rst diff --git a/docs/user_guide/components/property_package/general/generic_definition.rst b/docs/explanations/components/property_package/general/generic_definition.rst similarity index 97% rename from docs/user_guide/components/property_package/general/generic_definition.rst rename to docs/explanations/components/property_package/general/generic_definition.rst index b5be4ff258..56ff36f63f 100644 --- a/docs/user_guide/components/property_package/general/generic_definition.rst +++ b/docs/explanations/components/property_package/general/generic_definition.rst @@ -12,7 +12,7 @@ In order to create and use a property package using the IDAES Generic Property P Units of Measurement -------------------- -When defining a property package using the generic framework, users must define the base units for the property package (see :ref:`link`). The approach for setting the base units depends on the approach used to define the property package, and is discussed in more detail in each section. +When defining a property package using the generic framework, users must define the base units for the property package (see :ref:`link`). The approach for setting the base units depends on the approach used to define the property package, and is discussed in more detail in each section. The Generic Property Package Framework includes the necessary code to convert between different units of measurement as required, allowing users to combine property methods with different sets of units into a single property package. In these cases, each property method is written in its natural units (including parameters), and the final result is automatically converted to the base units. diff --git a/docs/user_guide/components/property_package/general/global_options.rst b/docs/explanations/components/property_package/general/global_options.rst similarity index 100% rename from docs/user_guide/components/property_package/general/global_options.rst rename to docs/explanations/components/property_package/general/global_options.rst diff --git a/docs/user_guide/components/property_package/general/index.rst b/docs/explanations/components/property_package/general/index.rst similarity index 63% rename from docs/user_guide/components/property_package/general/index.rst rename to docs/explanations/components/property_package/general/index.rst index ed0118d3e3..c77b9599b5 100644 --- a/docs/user_guide/components/property_package/general/index.rst +++ b/docs/explanations/components/property_package/general/index.rst @@ -25,18 +25,18 @@ Introduction Property packages represent the core of any process model, and having a suitable property package is key to successfully modeling any process system. However, developing property packages is a significant challenge even for experienced modelers as they involve large numbers of tightly coupled constraints and parameters. The goal of the IDAES Generic Property Package Framework is to provide a flexible platform on which users can build property packages for common types of systems by calling upon libraries of modular sub-models to build up complex property calculations with the least effort possible. -The Generic Property Package Framework breaks down property packages into a number of components which can be assembled in a modular fashion. Users need only provide those components which they require for their system of interest, and components can be drawn from libraries of existing components or provided by the user as custom code. Details on how to set up the definition of a property package using the generic framework are given :ref:`here`. +The Generic Property Package Framework breaks down property packages into a number of components which can be assembled in a modular fashion. Users need only provide those components which they require for their system of interest, and components can be drawn from libraries of existing components or provided by the user as custom code. Details on how to set up the definition of a property package using the generic framework are given :ref:`here`. The components which make up a generic property package are as follows: -1. Choose a base set of :ref:`units of measurement` for the property package. -2. Define the :ref:`components` which make up the material of interest, including methods for calculating the pure component properties of interest in the system. -3. Define the :ref:`phases of interest` for the application, including equations of state and other phase specific decisions. -4. Choose the set of :ref:`state variables` you wish to use and a reference state for the system. -5. (Optional) Define any :ref:`phase equilibria` which occurs in the system and methods associated with calculating this. -6. (Optional) A number of :ref:`global options` are avaialble for further customizing behavior of certain property calculations. +1. Choose a base set of :ref:`units of measurement` for the property package. +2. Define the :ref:`components` which make up the material of interest, including methods for calculating the pure component properties of interest in the system. +3. Define the :ref:`phases of interest` for the application, including equations of state and other phase specific decisions. +4. Choose the set of :ref:`state variables` you wish to use and a reference state for the system. +5. (Optional) Define any :ref:`phase equilibria` which occurs in the system and methods associated with calculating this. +6. (Optional) A number of :ref:`global options` are avaialble for further customizing behavior of certain property calculations. -The following sections will describe how to define a property package using the Generic Property Package Framework along with the libraries of sub-models currently available. Finally, the :ref:`developers` section describes how to go about defining your own custom components to use when creating custom property packages. +The following sections will describe how to define a property package using the Generic Property Package Framework along with the libraries of sub-models currently available. Finally, the :ref:`developers` section describes how to go about defining your own custom components to use when creating custom property packages. .. note:: Within most IDAES models "parameters" are in fact defined as Pyomo 'Vars' (i.e. variables) which are fixed at their defined values. Whilst `Params` would seem to be the logical choice for these, parameter estimation problems require the parameters being estimated to be defined as `Vars` so that the solver is free to vary them. diff --git a/docs/user_guide/components/property_package/general/pe/pe_forms.rst b/docs/explanations/components/property_package/general/pe/pe_forms.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pe/pe_forms.rst rename to docs/explanations/components/property_package/general/pe/pe_forms.rst diff --git a/docs/user_guide/components/property_package/general/pe/smooth_flash.rst b/docs/explanations/components/property_package/general/pe/smooth_flash.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pe/smooth_flash.rst rename to docs/explanations/components/property_package/general/pe/smooth_flash.rst diff --git a/docs/user_guide/components/property_package/general/phase_def.rst b/docs/explanations/components/property_package/general/phase_def.rst similarity index 92% rename from docs/user_guide/components/property_package/general/phase_def.rst rename to docs/explanations/components/property_package/general/phase_def.rst index d2ed238de8..22d64e9f17 100644 --- a/docs/user_guide/components/property_package/general/phase_def.rst +++ b/docs/explanations/components/property_package/general/phase_def.rst @@ -1,7 +1,7 @@ Defining Phases =============== -The second step in defining a property package using the Generic Property Package Framework is to define the phases of interest in the system. Due to the equation-oriented nature of the IDAES modeling framework, it is necessary to define any phases the user believes may be important *a priori* as it is not possible to determine what phases should be included on-the-fly. Phases are defined using `IDAES Phase objects`, and are automatically constructed using the `phases` configuration argument from the `GenericParameterBlock`. +The second step in defining a property package using the Generic Property Package Framework is to define the phases of interest in the system. Due to the equation-oriented nature of the IDAES modeling framework, it is necessary to define any phases the user believes may be important *a priori* as it is not possible to determine what phases should be included on-the-fly. Phases are defined using `IDAES Phase objects`, and are automatically constructed using the `phases` configuration argument from the `GenericParameterBlock`. The `phases` Argument --------------------- @@ -48,7 +48,7 @@ Equation of State Libraries Phase-Specific Parameter ^^^^^^^^^^^^^^^^^^^^^^^^ -In some cases, a property package may include parameters which are specific to a given phase. In these cases, these parameters are stored as part of the associated `Phase` object and the values of these set using the `parameter_data` argument when declaring the phase. This is done in the same fashion as for :ref:`component specific parameters`. +In some cases, a property package may include parameters which are specific to a given phase. In these cases, these parameters are stored as part of the associated `Phase` object and the values of these set using the `parameter_data` argument when declaring the phase. This is done in the same fashion as for :ref:`component specific parameters`. Phases with Partial Component Lists ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/user_guide/components/property_package/general/phase_equilibrium.rst b/docs/explanations/components/property_package/general/phase_equilibrium.rst similarity index 100% rename from docs/user_guide/components/property_package/general/phase_equilibrium.rst rename to docs/explanations/components/property_package/general/phase_equilibrium.rst diff --git a/docs/user_guide/components/property_package/general/pure/ConstantProperties.rst b/docs/explanations/components/property_package/general/pure/ConstantProperties.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/ConstantProperties.rst rename to docs/explanations/components/property_package/general/pure/ConstantProperties.rst diff --git a/docs/user_guide/components/property_package/general/pure/NIST.rst b/docs/explanations/components/property_package/general/pure/NIST.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/NIST.rst rename to docs/explanations/components/property_package/general/pure/NIST.rst diff --git a/docs/user_guide/components/property_package/general/pure/Perrys.rst b/docs/explanations/components/property_package/general/pure/Perrys.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/Perrys.rst rename to docs/explanations/components/property_package/general/pure/Perrys.rst diff --git a/docs/user_guide/components/property_package/general/pure/RPP3.rst b/docs/explanations/components/property_package/general/pure/RPP3.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/RPP3.rst rename to docs/explanations/components/property_package/general/pure/RPP3.rst diff --git a/docs/user_guide/components/property_package/general/pure/RPP4.rst b/docs/explanations/components/property_package/general/pure/RPP4.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/RPP4.rst rename to docs/explanations/components/property_package/general/pure/RPP4.rst diff --git a/docs/user_guide/components/property_package/general/pure/RPP5.rst b/docs/explanations/components/property_package/general/pure/RPP5.rst similarity index 100% rename from docs/user_guide/components/property_package/general/pure/RPP5.rst rename to docs/explanations/components/property_package/general/pure/RPP5.rst diff --git a/docs/user_guide/components/property_package/general/state/FPhx.rst b/docs/explanations/components/property_package/general/state/FPhx.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state/FPhx.rst rename to docs/explanations/components/property_package/general/state/FPhx.rst diff --git a/docs/user_guide/components/property_package/general/state/FTPx.rst b/docs/explanations/components/property_package/general/state/FTPx.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state/FTPx.rst rename to docs/explanations/components/property_package/general/state/FTPx.rst diff --git a/docs/user_guide/components/property_package/general/state/FcPh.rst b/docs/explanations/components/property_package/general/state/FcPh.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state/FcPh.rst rename to docs/explanations/components/property_package/general/state/FcPh.rst diff --git a/docs/user_guide/components/property_package/general/state/FcTP.rst b/docs/explanations/components/property_package/general/state/FcTP.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state/FcTP.rst rename to docs/explanations/components/property_package/general/state/FcTP.rst diff --git a/docs/user_guide/components/property_package/general/state/FpcTP.rst b/docs/explanations/components/property_package/general/state/FpcTP.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state/FpcTP.rst rename to docs/explanations/components/property_package/general/state/FpcTP.rst diff --git a/docs/user_guide/components/property_package/general/state_definition.rst b/docs/explanations/components/property_package/general/state_definition.rst similarity index 100% rename from docs/user_guide/components/property_package/general/state_definition.rst rename to docs/explanations/components/property_package/general/state_definition.rst diff --git a/docs/user_guide/components/property_package/general_reactions/equil_constant.rst b/docs/explanations/components/property_package/general_reactions/equil_constant.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/equil_constant.rst rename to docs/explanations/components/property_package/general_reactions/equil_constant.rst diff --git a/docs/user_guide/components/property_package/general_reactions/equil_form.rst b/docs/explanations/components/property_package/general_reactions/equil_form.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/equil_form.rst rename to docs/explanations/components/property_package/general_reactions/equil_form.rst diff --git a/docs/user_guide/components/property_package/general_reactions/equil_rxns.rst b/docs/explanations/components/property_package/general_reactions/equil_rxns.rst similarity index 90% rename from docs/user_guide/components/property_package/general_reactions/equil_rxns.rst rename to docs/explanations/components/property_package/general_reactions/equil_rxns.rst index 4261f70b4a..2f5a51b191 100644 --- a/docs/user_guide/components/property_package/general_reactions/equil_rxns.rst +++ b/docs/explanations/components/property_package/general_reactions/equil_rxns.rst @@ -38,7 +38,7 @@ The `stoichiometry` configuration argument is used to define which components ta Concentration Form ^^^^^^^^^^^^^^^^^^ -See :ref:`rate reaction` documentation. +See :ref:`rate reaction` documentation. Other Reaction Properties ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -48,5 +48,5 @@ The remaining configuration arguments are used to define how different propertie * functions are used for self-contained correlations with hard-coded parameters, * classes are used for more generic correlations which require associated parameters. -A list of the libraries of methods available in the IDAES Framework can be found :ref:`here`. +A list of the libraries of methods available in the IDAES Framework can be found :ref:`here`. diff --git a/docs/user_guide/components/property_package/general_reactions/generic_reactions.rst b/docs/explanations/components/property_package/general_reactions/generic_reactions.rst similarity index 95% rename from docs/user_guide/components/property_package/general_reactions/generic_reactions.rst rename to docs/explanations/components/property_package/general_reactions/generic_reactions.rst index f511d2ac2b..d75317546b 100644 --- a/docs/user_guide/components/property_package/general_reactions/generic_reactions.rst +++ b/docs/explanations/components/property_package/general_reactions/generic_reactions.rst @@ -12,7 +12,7 @@ In order to create and use a property package using the IDAES Generic Reaction P Units of Measurement -------------------- -As with generic thermophysical property packages, when defining a reaction package using the generic framework users must define the base units for the reaction package (see :ref:`link`). The approach for setting the base units and units for all parameters is the same as for thermophysical property packages and depends on the approach used to define the reaction package. +As with generic thermophysical property packages, when defining a reaction package using the generic framework users must define the base units for the reaction package (see :ref:`link`). The approach for setting the base units and units for all parameters is the same as for thermophysical property packages and depends on the approach used to define the reaction package. Config Dictionary ----------------- diff --git a/docs/user_guide/components/property_package/general_reactions/heat_of_reaction.rst b/docs/explanations/components/property_package/general_reactions/heat_of_reaction.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/heat_of_reaction.rst rename to docs/explanations/components/property_package/general_reactions/heat_of_reaction.rst diff --git a/docs/user_guide/components/property_package/general_reactions/index.rst b/docs/explanations/components/property_package/general_reactions/index.rst similarity index 57% rename from docs/user_guide/components/property_package/general_reactions/index.rst rename to docs/explanations/components/property_package/general_reactions/index.rst index 66f2c4409e..c03127bb77 100644 --- a/docs/user_guide/components/property_package/general_reactions/index.rst +++ b/docs/explanations/components/property_package/general_reactions/index.rst @@ -20,17 +20,17 @@ Introduction The generic reaction package framework builds upon the existing framework for implementing reaction packages within IDAES, and will not prevent the use of custom written reaction packages in the future. Whilst it is hoped that the generic framework will be able to handle most common applications, users with more unusual systems or those solving computationally intensive problems may need to write custom reaction packages for their cases. -The Generic Reaction Package Framework breaks down reaction packages into a number of components which can be assembled in a modular fashion. Users need only provide those components which they require for their system of interest, and components can be drawn from libraries of existing components or provided by the user as custom code. Details on how to set up the definition of a reaction package using the generic framework are given :ref:`here`. +The Generic Reaction Package Framework breaks down reaction packages into a number of components which can be assembled in a modular fashion. Users need only provide those components which they require for their system of interest, and components can be drawn from libraries of existing components or provided by the user as custom code. Details on how to set up the definition of a reaction package using the generic framework are given :ref:`here`. The components which make up a generic reaction package are as follows: -1. Choose a base set of :ref:`units of measurement` for the property package. -2. :ref:`Associate` the reaction package with an appropriate thermodynamic property package. The thermodynamic property package must use the same set of base units of measurement, -3. Define the :ref:`basis of the reaction terms` for the reaction package. -4. Define the :ref:`rate-based reactions` of interest in the system. -5. Define the :ref:`equilibrium-based reactions` of interest in the system. Nore that phase equilibrium is generally handled in the thermodynamic property package. +1. Choose a base set of :ref:`units of measurement` for the property package. +2. :ref:`Associate` the reaction package with an appropriate thermodynamic property package. The thermodynamic property package must use the same set of base units of measurement, +3. Define the :ref:`basis of the reaction terms` for the reaction package. +4. Define the :ref:`rate-based reactions` of interest in the system. +5. Define the :ref:`equilibrium-based reactions` of interest in the system. Nore that phase equilibrium is generally handled in the thermodynamic property package. -The following sections will describe how to define a reaction package using the Generic Reaction Package Framework along with the libraries of sub-models currently available. Finally, the :ref:`developers` section describes how to go about defining your own custom components to use when creating custom property packages. +The following sections will describe how to define a reaction package using the Generic Reaction Package Framework along with the libraries of sub-models currently available. Finally, the :ref:`developers` section describes how to go about defining your own custom components to use when creating custom property packages. .. note:: Within most IDAES models "parameters" are in fact defined as Pyomo 'Vars' (i.e. variables) which are fixed at their defined values. Whilst `Params` would seem to be the logical choice for these, parameter estimation problems require the parameters being estimated to be defined as `Vars` so that the solver is free to vary them. diff --git a/docs/user_guide/components/property_package/general_reactions/method_libraries.rst b/docs/explanations/components/property_package/general_reactions/method_libraries.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/method_libraries.rst rename to docs/explanations/components/property_package/general_reactions/method_libraries.rst diff --git a/docs/user_guide/components/property_package/general_reactions/rate_constant.rst b/docs/explanations/components/property_package/general_reactions/rate_constant.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/rate_constant.rst rename to docs/explanations/components/property_package/general_reactions/rate_constant.rst diff --git a/docs/user_guide/components/property_package/general_reactions/rate_form.rst b/docs/explanations/components/property_package/general_reactions/rate_form.rst similarity index 100% rename from docs/user_guide/components/property_package/general_reactions/rate_form.rst rename to docs/explanations/components/property_package/general_reactions/rate_form.rst diff --git a/docs/user_guide/components/property_package/general_reactions/rate_rxns.rst b/docs/explanations/components/property_package/general_reactions/rate_rxns.rst similarity index 96% rename from docs/user_guide/components/property_package/general_reactions/rate_rxns.rst rename to docs/explanations/components/property_package/general_reactions/rate_rxns.rst index 4e2c6de020..f09a343fe4 100644 --- a/docs/user_guide/components/property_package/general_reactions/rate_rxns.rst +++ b/docs/explanations/components/property_package/general_reactions/rate_rxns.rst @@ -56,4 +56,4 @@ The remaining configuration arguments are used to define how different propertie * functions are used for self-contained correlations with hard-coded parameters, * classes are used for more generic correlations which require associated parameters. -A list of the libraries of methods available in the IDAES Framework can be found :ref:`here`. +A list of the libraries of methods available in the IDAES Framework can be found :ref:`here`. diff --git a/docs/user_guide/components/property_package/index.rst b/docs/explanations/components/property_package/index.rst similarity index 62% rename from docs/user_guide/components/property_package/index.rst rename to docs/explanations/components/property_package/index.rst index 87dee3e0bd..c2f44b54af 100644 --- a/docs/user_guide/components/property_package/index.rst +++ b/docs/explanations/components/property_package/index.rst @@ -9,12 +9,12 @@ Overview .. toctree:: :glob: :hidden: - + * */index -Property packages provide the relationships and parameters necessary to determine the -properties of process streams. They may be general in purpose, such as ideal gas +Property packages provide the relationships and parameters necessary to determine the +properties of process streams. They may be general in purpose, such as ideal gas equations, or specific to a certain application. Property packages are separated into two categories: * physical and transport properties @@ -22,13 +22,13 @@ equations, or specific to a certain application. Property packages are separated While several standard property packages are provided in the IDAES model libraries, many process modeling applications will require specific property packages. Information on developing custom -property packages is provided in the -:ref:`advanced user guide`. +property packages is provided in the +:ref:`advanced user guide`. Since the effort to develop a custom property package is substantial, the IDAES modeling -framework provides a -:ref:`Generic Property Package Framework` -and :ref:`Generic Reaction Package Framework` +framework provides a +:ref:`Generic Property Package Framework` +and :ref:`Generic Reaction Package Framework` to make it easier to create a package for common property and reaction models. Units of Measurement @@ -36,96 +36,96 @@ Units of Measurement One of the most important roles property packages play within the modeling framework is to define the units of measurement that will be used for those models which use the property packages. Any variable which is created in a unit model will derive its units of measurement from those defined in the associated property package in order to ensure consistency of units. -Defining units of measurement in property packages is :ref:`discussed here`. +Defining units of measurement in property packages is :ref:`discussed here`. Physical properties ------------------- -Almost all process models depend on physical properties to some extent, such as -calculation of specific enthalpy or internal energy for energy balances. These properties -only depend on the material being considered and are independent of the unit operations in -which they are used. As such, physical property calculations can be separated from the -unit model calculations and treated as a separate submodel which is called by the unit model. -Each unit model can then create instances of these submodels as required to calculate those +Almost all process models depend on physical properties to some extent, such as +calculation of specific enthalpy or internal energy for energy balances. These properties +only depend on the material being considered and are independent of the unit operations in +which they are used. As such, physical property calculations can be separated from the +unit model calculations and treated as a separate submodel which is called by the unit model. +Each unit model can then create instances of these submodels as required to calculate those properties required by each unit. -Within IDAES, this is handled by StateBlock objects – these are self-contained submodels -containing the calculations for all necessary thermophysical properties for a given material -at a given point in space and time. IDAES UnitModels create instances of these StateBlocks -wherever they need to calculate physical properties and link to variables within the +Within IDAES, this is handled by StateBlock objects – these are self-contained submodels +containing the calculations for all necessary thermophysical properties for a given material +at a given point in space and time. IDAES UnitModels create instances of these StateBlocks +wherever they need to calculate physical properties and link to variables within the StateBlock within the unit model constraints. -However, physical property calculations depend on a set of parameters which are specific -to a given material or mixture. Thus, each instance of a StateBlock for a material use the same -set of parameters. To avoid duplicating these parameters in every instance of a StateBlock for -a given material, these parameters are instead grouped in a PhysicalParameterBlock for that -material which the StateBlocks link to. In this way, there is a single common location for all +However, physical property calculations depend on a set of parameters which are specific +to a given material or mixture. Thus, each instance of a StateBlock for a material use the same +set of parameters. To avoid duplicating these parameters in every instance of a StateBlock for +a given material, these parameters are instead grouped in a PhysicalParameterBlock for that +material which the StateBlocks link to. In this way, there is a single common location for all parameters. In summary, physical property packages consist of two parts: -* :ref:`PhysicalParameterBlocks`, which contain a set of parameters associated with the specific material(s) being modeled -* :ref:`StateBlocks`, which contain the actual calculations of the state variables and functions +* :ref:`PhysicalParameterBlocks`, which contain a set of parameters associated with the specific material(s) being modeled +* :ref:`StateBlocks`, which contain the actual calculations of the state variables and functions Reaction properties ------------------- -Reaction property packages represent a collection of calculations necessary to determine the -reaction behavior of a mixture at a given state. Reaction properties depend upon the state and -physical properties of the material, and thus must be linked to a StateBlock which provides the +Reaction property packages represent a collection of calculations necessary to determine the +reaction behavior of a mixture at a given state. Reaction properties depend upon the state and +physical properties of the material, and thus must be linked to a StateBlock which provides the necessary state and physical property information. Reaction property packages consist of two parts: -* :ref:`ReactionParameterBlocks`, which contain a set of parameters associated with the specific reaction(s) being modeled, and -* :ref:`ReactionBlocks`, which contain the actual calculations of the reaction behavior. +* :ref:`ReactionParameterBlocks`, which contain a set of parameters associated with the specific reaction(s) being modeled, and +* :ref:`ReactionBlocks`, which contain the actual calculations of the reaction behavior. Component and Phase Objects --------------------------- Property packages also rely on component and phase objects. -:ref:`Component Objects` -are used to identify the chemical species of interest -in a property package and to contain information describing the behavior of that component +:ref:`Component Objects` +are used to identify the chemical species of interest +in a property package and to contain information describing the behavior of that component (such as properties of that component). -:ref:`Phase Objects` -are used to identify the thermodynamic phases of -interest in a property package and to contain information describing the behavior of that phase +:ref:`Phase Objects` +are used to identify the thermodynamic phases of +interest in a property package and to contain information describing the behavior of that phase (for example the equation of state which describes that phase). As Needed Properties -------------------- -Process flow sheets often require a large number of properties to be calculate, but not all of -these are required in every unit operation. Calculating additional properties that are not -required is undesirable, as it leads to larger problem sizes and unnecessary complexity of the +Process flow sheets often require a large number of properties to be calculate, but not all of +these are required in every unit operation. Calculating additional properties that are not +required is undesirable, as it leads to larger problem sizes and unnecessary complexity of the resulting model. -To address this, IDAES supports "as needed" construction of properties, -where the variables and constraints required to calculate a given quantity are not added to a -model unless the model calls for this quantity. To designate a property as an "as needed" -quantity, a method can be declared in the associated property BlockData class (StateBlockData or -ReactionBlockData) which contains the instructions for constructing the variables and -constraints associated with the quantity (rather than declaring these within the BlockData's -build method). The name of this method can then be associated with the property via the -add_properties metadata in the property packages ParameterBlock, which indicates that when +To address this, IDAES supports "as needed" construction of properties, +where the variables and constraints required to calculate a given quantity are not added to a +model unless the model calls for this quantity. To designate a property as an "as needed" +quantity, a method can be declared in the associated property BlockData class (StateBlockData or +ReactionBlockData) which contains the instructions for constructing the variables and +constraints associated with the quantity (rather than declaring these within the BlockData's +build method). The name of this method can then be associated with the property via the +add_properties metadata in the property packages ParameterBlock, which indicates that when this property is called for, the associated method should be run. -The add_properties metadata can also indicate that a property should always be present -(i.e. constructed in the BlockData's build method) by setting the method to `None`, or that it is +The add_properties metadata can also indicate that a property should always be present +(i.e. constructed in the BlockData's build method) by setting the method to `None`, or that it is not supported by setting the method to `False`. Generic Property Package Framework ---------------------------------- -Property packages represent the core of any process model, and having a suitable property -package is key to successfully modeling any process system. However, developing property -packages is a significant challenge even for experienced modelers as they involve large numbers +Property packages represent the core of any process model, and having a suitable property +package is key to successfully modeling any process system. However, developing property +packages is a significant challenge even for experienced modelers as they involve large numbers of tightly coupled constraints and parameters. The -:ref:`Generic Property Package Framework` -was designed to help users build property packages with the least effort possible by levarging libraries +:ref:`Generic Property Package Framework` +was designed to help users build property packages with the least effort possible by levarging libraries of modular sub-models that include common types of property calculations. @@ -133,6 +133,6 @@ Generic Reaction Package Framework ---------------------------------- Similar to the Generic Property Package Framework, the -:ref:`Generic Reaction Package Framework` +:ref:`Generic Reaction Package Framework` helps users create reaction property packages for common systems. diff --git a/docs/user_guide/components/property_package/phase.rst b/docs/explanations/components/property_package/phase.rst similarity index 78% rename from docs/user_guide/components/property_package/phase.rst rename to docs/explanations/components/property_package/phase.rst index 43f6f4e350..cbcf719c07 100644 --- a/docs/user_guide/components/property_package/phase.rst +++ b/docs/explanations/components/property_package/phase.rst @@ -1,10 +1,10 @@ Phase Object ============ -Phase objects are used to identify the thermodynamic -phases of interest in a property package and to contain information describing the behavior of -that phase (for example the equation of state which describes that phase). Additional -information on the :ref:`Phase Class` is +Phase objects are used to identify the thermodynamic +phases of interest in a property package and to contain information describing the behavior of +that phase (for example the equation of state which describes that phase). Additional +information on the :ref:`Phase Class` is provided in the technical specifications. TThe following types of phases, along with a generic Phase object, are supported: @@ -13,26 +13,26 @@ TThe following types of phases, along with a generic Phase object, are supported * `SolidPhase` * `VaporPhase` -In a number of unit operations, different phases behave in different ways. For example, in a -Flash operation, the vapor phase exits through the top outlet whilst liquid phase(s) -(and any solids) exit through the bottom outlet. In order to determine how a given phase should +In a number of unit operations, different phases behave in different ways. For example, in a +Flash operation, the vapor phase exits through the top outlet whilst liquid phase(s) +(and any solids) exit through the bottom outlet. In order to determine how a given phase should behave in these situations, each Phase object implements the following three methods: * `is_liquid_phase()` * `is_solid_phase()` * `is_vapor_phase()` -These methods return a boolean (`True` or `False`) indicating whether the unit operation should -treat the phase as being of the specified type in order to decide on how it should behave. Each -type of phase returns `True` for its type and `False` for all other types (e.g. LiquidPhase +These methods return a boolean (`True` or `False`) indicating whether the unit operation should +treat the phase as being of the specified type in order to decide on how it should behave. Each +type of phase returns `True` for its type and `False` for all other types (e.g. LiquidPhase returns `True` for `is_liquid_phase()` and `False` for `is_solid_phase()` and `is_vapor_phase()`. -The generic Phase object determines what to return for each method based on the user-provided +The generic Phase object determines what to return for each method based on the user-provided name for the instance of the Phase object as shown below: * `is_liquid_phase()` returns `True` if the Phase name contains the string `Liq`, otherwise it returns `False`. * `is_solid_phase()` returns `True` if the Phase name contains the string `Sol`, otherwise it returns `False`. * `is_vapor_phase()` returns `True` if the Phase name contains the string `Vap`, otherwise it returns `False`. -Users should avoid using the generic `Phase` object, as this is primarily intended as a base +Users should avoid using the generic `Phase` object, as this is primarily intended as a base class for the specific phase classes and for backwards compatibility. diff --git a/docs/user_guide/components/property_package/physical_param.rst b/docs/explanations/components/property_package/physical_param.rst similarity index 79% rename from docs/user_guide/components/property_package/physical_param.rst rename to docs/explanations/components/property_package/physical_param.rst index 40ce1dbc5f..2e3a807f10 100644 --- a/docs/user_guide/components/property_package/physical_param.rst +++ b/docs/explanations/components/property_package/physical_param.rst @@ -1,17 +1,17 @@ Physical Parameter Block ======================== -PhysicalParameterBlocks serve as a central location for linking to a property package, and +PhysicalParameterBlocks serve as a central location for linking to a property package, and contain all the parameters and indexing sets used by a given property package. -The role of the :ref:`PhysicalParameterBlock Class` -is to set up the references required by the rest of the IDAES Core Modeling Framework for constructing -instances of :ref:`StateBlocks` -and attaching these to the PhysicalParameterBlock for ease of use. This allows other models to -be pointed to the PhysicalParameterBlock in order to collect the necessary information and to +The role of the :ref:`PhysicalParameterBlock Class` +is to set up the references required by the rest of the IDAES Core Modeling Framework for constructing +instances of :ref:`StateBlocks` +and attaching these to the PhysicalParameterBlock for ease of use. This allows other models to +be pointed to the PhysicalParameterBlock in order to collect the necessary information and to construct the necessary StateBlocks without the need for the user to do this manually. -Several attributes in the PhysicalParameterBlock are used to +Several attributes in the PhysicalParameterBlock are used to inform the construction of other components. These attributes include: * `state_block_class` - a pointer to the associated class that should be called when constructing StateBlocks. This should only be set by the property package developer. diff --git a/docs/user_guide/components/property_package/reaction_block.rst b/docs/explanations/components/property_package/reaction_block.rst similarity index 77% rename from docs/user_guide/components/property_package/reaction_block.rst rename to docs/explanations/components/property_package/reaction_block.rst index c29f3588c5..0a7d47bdc5 100644 --- a/docs/user_guide/components/property_package/reaction_block.rst +++ b/docs/explanations/components/property_package/reaction_block.rst @@ -1,20 +1,20 @@ Reaction Block ============== -ReactionBlocks are used within IDAES UnitModels (generally within ControlVolumeBlocks) in -order to calculate reaction properties given the state of the material (provided by an -associated StateBlock). ReactionBlocks are notably different to other types of Blocks within -IDAES as they are always indexed by time (and possibly space as well), and are also not fully -self contained (in that they depend upon the associated state block for certain variables). +ReactionBlocks are used within IDAES UnitModels (generally within ControlVolumeBlocks) in +order to calculate reaction properties given the state of the material (provided by an +associated StateBlock). ReactionBlocks are notably different to other types of Blocks within +IDAES as they are always indexed by time (and possibly space as well), and are also not fully +self contained (in that they depend upon the associated state block for certain variables). ReactionBlocks are composed of two parts: * ReactionBlockDataBase forms the base class for all ReactionBlockData objects, which contain the instructions on how to construct each instance of a Reaction Block. * ReactionBlockBase is used for building classes which contain methods to be applied to sets of Indexed Reaction Blocks (or to a subset of these). See the documentation on `declare_process_block_class` and the IDAES tutorials and examples for more information. -ReactionBlocks can be constructed directly from the associated ReactionParameterBlock by -calling the `build_reaction_block()` method on the ReactionParameterBlock. The `parameters` -construction argument will be automatically set, and any other arguments (including indexing +ReactionBlocks can be constructed directly from the associated ReactionParameterBlock by +calling the `build_reaction_block()` method on the ReactionParameterBlock. The `parameters` +construction argument will be automatically set, and any other arguments (including indexing sets) may be provided to the `build_reaction_block` method as usual. -Additional details on :ref:`ReactionBlocks` +Additional details on :ref:`ReactionBlocks` are located in the technical specifications. diff --git a/docs/user_guide/components/property_package/reaction_param.rst b/docs/explanations/components/property_package/reaction_param.rst similarity index 85% rename from docs/user_guide/components/property_package/reaction_param.rst rename to docs/explanations/components/property_package/reaction_param.rst index 2f088a5510..55b3b13cb7 100644 --- a/docs/user_guide/components/property_package/reaction_param.rst +++ b/docs/explanations/components/property_package/reaction_param.rst @@ -1,19 +1,19 @@ Reaction Parameter Block ======================== -ReactionParameterBlocks serve as a central location for linking to a property package, and +ReactionParameterBlocks serve as a central location for linking to a property package, and contain all the parameters and indexing sets used by a given property package. -The role of the :ref:`ReactionParameterBlock Class` -is to set up the references required by the rest of the IDAES framework for constructing -instances of :ref:`ReactionBlocks` -and attaching these to the ReactionParameterBlock for ease of use. This allows other models to -be pointed to the ReactionParameterBlock in order to collect the necessary information and to +The role of the :ref:`ReactionParameterBlock Class` +is to set up the references required by the rest of the IDAES framework for constructing +instances of :ref:`ReactionBlocks` +and attaching these to the ReactionParameterBlock for ease of use. This allows other models to +be pointed to the ReactionParameterBlock in order to collect the necessary information and to construct the necessary ReactionBlocks without the need for the user to do this manually. -Reaction property packages are used by all of the other modeling components to inform them of -what needs to be constructed when dealing with chemical reactions. In order to do this, the -IDAES modeling framework looks for a number of attributes in the ReactionParameterBlock which +Reaction property packages are used by all of the other modeling components to inform them of +what needs to be constructed when dealing with chemical reactions. In order to do this, the +IDAES modeling framework looks for a number of attributes in the ReactionParameterBlock which are used to inform the construction of other components. These attributes include: * `reaction_block_class` - a pointer to the associated class that should be called when constructing ReactionBlocks. This should only be set by the property package developer. diff --git a/docs/user_guide/components/property_package/state_block.rst b/docs/explanations/components/property_package/state_block.rst similarity index 78% rename from docs/user_guide/components/property_package/state_block.rst rename to docs/explanations/components/property_package/state_block.rst index a230eab81f..ddccbea2ff 100644 --- a/docs/user_guide/components/property_package/state_block.rst +++ b/docs/explanations/components/property_package/state_block.rst @@ -1,20 +1,20 @@ State Block =========== -StateBlocks are used within all IDAES UnitModels (generally within ControlVolumeBlocks) in -order to calculate physical properties given the state of the material. StateBlocks are -notably different to other types of Blocks within IDAES as they are always indexed by time +StateBlocks are used within all IDAES UnitModels (generally within ControlVolumeBlocks) in +order to calculate physical properties given the state of the material. StateBlocks are +notably different to other types of Blocks within IDAES as they are always indexed by time (and possibly space as well). StateBlocks consist of two parts: * StateBlockData forms the base class for all StateBlockData objects, which contain the instructions on how to construct each instance of a State Block. * StateBlock is used for building classes which contain methods to be applied to sets of Indexed State Blocks (or to a subset of these). See the documentation on declare_process_block_class and the IDAES tutorials and examples for more information. -StateBlocks can be constructed directly from the associated PhysicalParameterBlock by calling -the `build_state_block()` method on the PhysicalParameterBlock. The `parameters` construction -argument will be automatically set, and any other arguments (including indexing sets) may be +StateBlocks can be constructed directly from the associated PhysicalParameterBlock by calling +the `build_state_block()` method on the PhysicalParameterBlock. The `parameters` construction +argument will be automatically set, and any other arguments (including indexing sets) may be provided to the `build_state_block` method as usual. -Additional details on :ref:`State Blocks` +Additional details on :ref:`State Blocks` are located in the technical specifications. diff --git a/docs/user_guide/components/property_package/uom.rst b/docs/explanations/components/property_package/uom.rst similarity index 100% rename from docs/user_guide/components/property_package/uom.rst rename to docs/explanations/components/property_package/uom.rst diff --git a/docs/user_guide/components/unit_model/control_volume.rst b/docs/explanations/components/unit_model/control_volume.rst similarity index 71% rename from docs/user_guide/components/unit_model/control_volume.rst rename to docs/explanations/components/unit_model/control_volume.rst index 97045eb517..58eead28c5 100644 --- a/docs/user_guide/components/unit_model/control_volume.rst +++ b/docs/explanations/components/unit_model/control_volume.rst @@ -6,28 +6,28 @@ Overview -------- -Control volumes serve as the -fundamental building block of all unit operations. Control Volumes represent a single, -well-defined volume of material over which material, energy and/or momentum balances will +Control volumes serve as the +fundamental building block of all unit operations. Control Volumes represent a single, +well-defined volume of material over which material, energy and/or momentum balances will be performed. -The IDAES ControlVolume classes are designed to facilitate the construction of these balance -equations by providing the model developer with a set of pre-built methods to perform the most -common tasks in developing models of unit operations. The ControlVolume classes contain methods -for creating and linking the necessary property calculations and writing common forms of the -balance equations so that the model developer can focus their time on the aspects that make each +The IDAES ControlVolume classes are designed to facilitate the construction of these balance +equations by providing the model developer with a set of pre-built methods to perform the most +common tasks in developing models of unit operations. The ControlVolume classes contain methods +for creating and linking the necessary property calculations and writing common forms of the +balance equations so that the model developer can focus their time on the aspects that make each unit model unique. The IDAES process modeling framework currently supports two types of ControlVolumes: -* :ref:`ControlVolume0DBlock` represents a single well-mixed volume of material with a single inlet and a single outlet. This type of control volume is sufficient to model most inlet-outlet type unit operations which do not require spatial discretization. -* :ref:`ControlVolume1DBlock` represents a volume with spatial variation in one dimension parallel to the material flow. This type of control volume is useful for representing flow in pipes and simple 1D flow reactors. +* :ref:`ControlVolume0DBlock` represents a single well-mixed volume of material with a single inlet and a single outlet. This type of control volume is sufficient to model most inlet-outlet type unit operations which do not require spatial discretization. +* :ref:`ControlVolume1DBlock` represents a volume with spatial variation in one dimension parallel to the material flow. This type of control volume is useful for representing flow in pipes and simple 1D flow reactors. Common Control Volume Tasks --------------------------- -All of the IDAES ControlVolume classes are built on a common core ControlVolumeBlockData which -defines a set of common tasks required for all Control Volumes. The more specific ControlVolume classes +All of the IDAES ControlVolume classes are built on a common core ControlVolumeBlockData which +defines a set of common tasks required for all Control Volumes. The more specific ControlVolume classes then build upon these common tasks to provide tools appropriate for their specific application. All ControlVolume classes begin with the following tasks: @@ -41,23 +41,23 @@ All ControlVolume classes begin with the following tasks: Setting up the time domain -------------------------- -The first common task the ControlVolumeBlock performs is to determine if it should be dynamic -or steady-state and to collect the time domain from the UnitModel. ControlVolumeBlocks have -an argument `dynamic` which can be provided during construction which specifies if the -Control Volume should be dynamic (`dynamic=True`) or steady-state (`dynamic=False`). If the -argument is not provided, the ControlVolumeBlock will inherit this argument from its parent +The first common task the ControlVolumeBlock performs is to determine if it should be dynamic +or steady-state and to collect the time domain from the UnitModel. ControlVolumeBlocks have +an argument `dynamic` which can be provided during construction which specifies if the +Control Volume should be dynamic (`dynamic=True`) or steady-state (`dynamic=False`). If the +argument is not provided, the ControlVolumeBlock will inherit this argument from its parent Unit model. -Finally, the ControlVolume checks that the `has_holdup` argument is consistent with the +Finally, the ControlVolume checks that the `has_holdup` argument is consistent with the `dynamic` argument, and raises a `ConfigurationError` if it is not. Getting Property Package Information ------------------------------------ -If a reference to a property package was not provided by the UnitModel as an argument, -the Control Volume first checks to see if the unit model has a `property_package` argument -set, and uses this if present. Otherwise, the ControlVolumeBlock begins searching up the model -tree looking for an argument named `default_property_package` and uses the first of these +If a reference to a property package was not provided by the UnitModel as an argument, +the Control Volume first checks to see if the unit model has a `property_package` argument +set, and uses this if present. Otherwise, the ControlVolumeBlock begins searching up the model +tree looking for an argument named `default_property_package` and uses the first of these that it finds. If no `default_property_package` is found, a `ConfigurationError` is returned. Collecting Indexing Sets for Property Package @@ -73,19 +73,19 @@ The indexing sets the ControlVolume looks for are: ControlVolume and ControlVolumeBlockData Classes ------------------------------------------------ -A key purpose of ControlVolumes is to automate as much of the task of writing a unit model as -possible. For this purpose, ControlVolumes support a number of methods for common tasks model -developers may want to perform. The specifics of these methods will be different between -different types of ControlVolumes, and certain methods may not be applicable to some types of -Control Volumes (in which case a `NotImplementedError` will be returned). A full list of -potential methods is provided here, however users should check the documentation for the -specific Control Volume they are using for more details on what methods are supported in that +A key purpose of ControlVolumes is to automate as much of the task of writing a unit model as +possible. For this purpose, ControlVolumes support a number of methods for common tasks model +developers may want to perform. The specifics of these methods will be different between +different types of ControlVolumes, and certain methods may not be applicable to some types of +Control Volumes (in which case a `NotImplementedError` will be returned). A full list of +potential methods is provided here, however users should check the documentation for the +specific Control Volume they are using for more details on what methods are supported in that specific Control Volume. -A key feature of the IDAES Core Modeling Framework is the use of ControlVolumeBlocks. ControlVolumes -represent a volume of material over which material, energy and/or momentum balances -can be performed. ControlVolumeBlocks contain methods to automate the task of writing common -forms of these balance equations. ControlVolumeBlocks can also automate the creation of +A key feature of the IDAES Core Modeling Framework is the use of ControlVolumeBlocks. ControlVolumes +represent a volume of material over which material, energy and/or momentum balances +can be performed. ControlVolumeBlocks contain methods to automate the task of writing common +forms of these balance equations. ControlVolumeBlocks can also automate the creation of StateBlocks and ReactionBlocks associated with the control volume. diff --git a/docs/user_guide/components/unit_model/index.rst b/docs/explanations/components/unit_model/index.rst similarity index 59% rename from docs/user_guide/components/unit_model/index.rst rename to docs/explanations/components/unit_model/index.rst index d8a551e8f7..3a9b3683a5 100644 --- a/docs/user_guide/components/unit_model/index.rst +++ b/docs/explanations/components/unit_model/index.rst @@ -16,8 +16,8 @@ These models contain the unit performance constraints and associated variables f IDAES includes libraries of UnitModel classes. These models are composed of the following components: - 1. :ref:`ControlVolumeBlocks`, which represent volume of material over which we wish to perform material, energy and/or momentum balances - 2. :ref:`StateBlocks` and :ref:`ReactionBlocks`, which represent the thermophysical, transport and reaction properties of the material at a specific point in space and time + 1. :ref:`ControlVolumeBlocks`, which represent volume of material over which we wish to perform material, energy and/or momentum balances + 2. :ref:`StateBlocks` and :ref:`ReactionBlocks`, which represent the thermophysical, transport and reaction properties of the material at a specific point in space and time 3. Inlets and Outlets, which allow UnitModels to connect to other UnitModels diff --git a/docs/user_guide/concepts.rst b/docs/explanations/concepts.rst similarity index 96% rename from docs/user_guide/concepts.rst rename to docs/explanations/concepts.rst index b47da2cff9..8f7db8e62c 100644 --- a/docs/user_guide/concepts.rst +++ b/docs/explanations/concepts.rst @@ -112,7 +112,7 @@ The IDAES Integrated Platform represents each level within the hierarchy above u “modeling components”. Each of these components represents a part of the overall model structure and form the basic building blocks of any IDAES process model. An introduction to each of the IDAES modeling components can be found -:ref:`here`. +:ref:`here`. Model Libraries ^^^^^^^^^^^^^^^ @@ -122,11 +122,11 @@ properties. Modelers can use these out-of-the-box models to represent their proc as building blocks for developing their own models. All models within IDAES are designed to be fully open and extensible, allowing users to inspect and modify them to suit their needs. Documentation of the available model libraries can be found -:ref:`here`. +:ref:`here`. Modeling Extensions ^^^^^^^^^^^^^^^^^^^ The IDAES Integrated Platform also provides users with access to a number of cutting edge tools not directly related to process modeling. These tools are collected under the heading of Modeling Extensions, and information on them can be found -:ref:`here`. +:ref:`here`. diff --git a/docs/user_guide/conventions.rst b/docs/explanations/conventions.rst similarity index 98% rename from docs/user_guide/conventions.rst rename to docs/explanations/conventions.rst index 812ad6b0e3..5903add2c6 100644 --- a/docs/user_guide/conventions.rst +++ b/docs/explanations/conventions.rst @@ -7,7 +7,7 @@ Units of Measurement and Reference States ----------------------------------------- -Due to the flexibility provided by the IDAES Integrated Platform, there is no standard set of units of measurement or standard reference state that should be used in models. Units of measurement are defined by the modeler for the 7 base quantities (time, length, mass, amount, temperature, current and luminous intensity) in each property package, and the platform makes use of this and Pyomo's Units container to automatically determine the units of all variables and expressions within a model. Thus, all components within a model using a given property package must use units based on the units chosen for the base quantities (to ensure consistency of units). However, flowsheets may contain property packages which use different sets of base units, however users should be careful to ensure units are converted correctly where property packages interact. For more detail on defining units of measurement see :ref:`Defining Units of Measurement`. +Due to the flexibility provided by the IDAES Integrated Platform, there is no standard set of units of measurement or standard reference state that should be used in models. Units of measurement are defined by the modeler for the 7 base quantities (time, length, mass, amount, temperature, current and luminous intensity) in each property package, and the platform makes use of this and Pyomo's Units container to automatically determine the units of all variables and expressions within a model. Thus, all components within a model using a given property package must use units based on the units chosen for the base quantities (to ensure consistency of units). However, flowsheets may contain property packages which use different sets of base units, however users should be careful to ensure units are converted correctly where property packages interact. For more detail on defining units of measurement see :ref:`Defining Units of Measurement`. Pyomo also provides convenient tools for converting between different units of measurement and checking for unit consistency, of which a few are highlighted below: diff --git a/docs/explanations/copyright.rst b/docs/explanations/copyright.rst new file mode 100644 index 0000000000..240a3999e2 --- /dev/null +++ b/docs/explanations/copyright.rst @@ -0,0 +1,3 @@ +.. _idaes-copyright: + +.. include:: ../../COPYRIGHT.md diff --git a/docs/faq.rst b/docs/explanations/faq.rst similarity index 100% rename from docs/faq.rst rename to docs/explanations/faq.rst diff --git a/docs/explanations/index.rst b/docs/explanations/index.rst new file mode 100644 index 0000000000..f41dd5e6b7 --- /dev/null +++ b/docs/explanations/index.rst @@ -0,0 +1,15 @@ +Explanations +============= + +.. toctree:: + :maxdepth: 2 + + why_idaes + concepts + components/index + conventions + modeling_extensions/index + related_packages/index + faq + license + copyright diff --git a/docs/explanations/license.rst b/docs/explanations/license.rst new file mode 100644 index 0000000000..5c5c207f6f --- /dev/null +++ b/docs/explanations/license.rst @@ -0,0 +1,3 @@ +.. _idaes-license: + +.. include:: ../../LICENSE.md diff --git a/docs/user_guide/modeling_extensions/caprese/index.rst b/docs/explanations/modeling_extensions/caprese/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/caprese/index.rst rename to docs/explanations/modeling_extensions/caprese/index.rst diff --git a/docs/user_guide/modeling_extensions/caprese/mhe.rst b/docs/explanations/modeling_extensions/caprese/mhe.rst similarity index 100% rename from docs/user_guide/modeling_extensions/caprese/mhe.rst rename to docs/explanations/modeling_extensions/caprese/mhe.rst diff --git a/docs/user_guide/modeling_extensions/caprese/nmpc.rst b/docs/explanations/modeling_extensions/caprese/nmpc.rst similarity index 100% rename from docs/user_guide/modeling_extensions/caprese/nmpc.rst rename to docs/explanations/modeling_extensions/caprese/nmpc.rst diff --git a/docs/user_guide/modeling_extensions/diagnostics/index.rst b/docs/explanations/modeling_extensions/diagnostics/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/diagnostics/index.rst rename to docs/explanations/modeling_extensions/diagnostics/index.rst diff --git a/docs/user_guide/modeling_extensions/index.rst b/docs/explanations/modeling_extensions/index.rst similarity index 76% rename from docs/user_guide/modeling_extensions/index.rst rename to docs/explanations/modeling_extensions/index.rst index 3da640e2f2..f0b4e15d41 100644 --- a/docs/user_guide/modeling_extensions/index.rst +++ b/docs/explanations/modeling_extensions/index.rst @@ -19,13 +19,13 @@ provided below. .. rubric:: ALAMOPY: ALAMO Python -:ref:`ALAMOPY` +:ref:`ALAMOPY` provides a wrapper for the software ALAMO which generates algebraic surrogate models of black-box systems for which a simulator or experimental setup is available. .. rubric:: RIPE: Reaction Identification and Parameter Estimation -:ref:`RIPE` +:ref:`RIPE` provides tools for reaction network identification. RIPE uses reactor data consisting of concentration, or conversion, values for multiple species that are obtained dynamically, or at multiple process conditions (temperatures, flow rates, working volumes) to identify probable @@ -34,13 +34,13 @@ design. .. rubric:: HELMET: HELMholtz Energy Thermodynamics -:ref:`HELMET` +:ref:`HELMET` provides a framework for regressing multiparameter equations of state that identify an equation for Helmholtz energy and multiple thermodynamic properties simultaneously. .. rubric:: PySMO: Python-based Surrogate Modelling Objects -:ref:`PySMO` +:ref:`PySMO` provides tools for generating different types of reduced order models. It provides IDAES users with a set of surrogate modeling tools which supports flowsheeting and direct integration into an equation-oriented modeling framework. It allows users to directly integrate reduced order @@ -52,7 +52,7 @@ models with algebraic high-fidelity process models within an single IDAES flowsh .. rubric:: MatOpt: Nanomaterials Optimization -:ref:`MatOpt` +:ref:`MatOpt` provides tools for nanomaterials design using Mathematical Optimization. MatOpt can be used to design crystalline nanostructured materials, including but not limited to particles, wires, surfaces, and periodic bulk structures. @@ -63,7 +63,7 @@ surfaces, and periodic bulk structures. .. rubric:: Caprese -:ref:`Caprese` +:ref:`Caprese` is a module for the simulation of IDAES flowsheets with nonlinear program (NLP)-based control and estimation strategies, namely Nonlinear Model Predictive Control (NMPC) and Moving Horizon Estimation (MHE). @@ -74,10 +74,10 @@ Estimation (MHE). .. rubric:: Uncertainty Propagation Toolbox -:ref:`uncertainty_propagation` +:ref:`uncertainty_propagation` is a module for quantifying and propagating parametric uncertainty through an optimization or simulation problem based on an IDAES model. .. rubric:: Degeneracy Hunter -:ref:`Degeneracy Hunter` +:ref:`Degeneracy Hunter` is coming soon! diff --git a/docs/user_guide/modeling_extensions/matopt/index.rst b/docs/explanations/modeling_extensions/matopt/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/matopt/index.rst rename to docs/explanations/modeling_extensions/matopt/index.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/alamopy/alamopy-cli.rst b/docs/explanations/modeling_extensions/surrogate/alamopy/alamopy-cli.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/alamopy/alamopy-cli.rst rename to docs/explanations/modeling_extensions/surrogate/alamopy/alamopy-cli.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/alamopy/index.rst b/docs/explanations/modeling_extensions/surrogate/alamopy/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/alamopy/index.rst rename to docs/explanations/modeling_extensions/surrogate/alamopy/index.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/helmet/index.rst b/docs/explanations/modeling_extensions/surrogate/helmet/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/helmet/index.rst rename to docs/explanations/modeling_extensions/surrogate/helmet/index.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/index.rst b/docs/explanations/modeling_extensions/surrogate/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/index.rst rename to docs/explanations/modeling_extensions/surrogate/index.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/index.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/index.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/index.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo-captioned.png b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo-captioned.png similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo-captioned.png rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo-captioned.png diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_cvt.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_cvt.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_cvt.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_cvt.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_halton.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_halton.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_halton.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_halton.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_hammersley.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_hammersley.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_hammersley.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_hammersley.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_kriging.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_kriging.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_kriging.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_kriging.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_lhs.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_lhs.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_lhs.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_lhs.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_polyregression.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_polyregression.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_polyregression.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_polyregression.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_radialbasisfunctions.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_radialbasisfunctions.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_radialbasisfunctions.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_radialbasisfunctions.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_sampling_properties.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_sampling_properties.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_sampling_properties.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_sampling_properties.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_uniform.rst b/docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_uniform.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/pysmo/pysmo_uniform.rst rename to docs/explanations/modeling_extensions/surrogate/pysmo/pysmo_uniform.rst diff --git a/docs/user_guide/modeling_extensions/surrogate/ripe/index.rst b/docs/explanations/modeling_extensions/surrogate/ripe/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/surrogate/ripe/index.rst rename to docs/explanations/modeling_extensions/surrogate/ripe/index.rst diff --git a/docs/user_guide/modeling_extensions/uncertainty_propagation/index.rst b/docs/explanations/modeling_extensions/uncertainty_propagation/index.rst similarity index 100% rename from docs/user_guide/modeling_extensions/uncertainty_propagation/index.rst rename to docs/explanations/modeling_extensions/uncertainty_propagation/index.rst diff --git a/docs/related_packages/index.rst b/docs/explanations/related_packages/index.rst similarity index 100% rename from docs/related_packages/index.rst rename to docs/explanations/related_packages/index.rst diff --git a/docs/related_packages/pecos.rst b/docs/explanations/related_packages/pecos.rst similarity index 100% rename from docs/related_packages/pecos.rst rename to docs/explanations/related_packages/pecos.rst diff --git a/docs/user_guide/why_idaes.rst b/docs/explanations/why_idaes.rst similarity index 97% rename from docs/user_guide/why_idaes.rst rename to docs/explanations/why_idaes.rst index 315cf1ea37..3e00ca0531 100644 --- a/docs/user_guide/why_idaes.rst +++ b/docs/explanations/why_idaes.rst @@ -32,7 +32,7 @@ Open Source ^^^^^^^^^^^ All IDAES Code is completely free and redistributable, the license is avaliable -:ref:`here`. Users are free to modify and redistribute code, and community +:ref:`here`. Users are free to modify and redistribute code, and community development is encouraged. Equation Oriented diff --git a/docs/advanced_user_guide/custom_models/general_model_development.rst b/docs/how_to_guides/custom_models/general_model_development.rst similarity index 98% rename from docs/advanced_user_guide/custom_models/general_model_development.rst rename to docs/how_to_guides/custom_models/general_model_development.rst index 73cb699ec9..36863a35b2 100644 --- a/docs/advanced_user_guide/custom_models/general_model_development.rst +++ b/docs/how_to_guides/custom_models/general_model_development.rst @@ -24,7 +24,7 @@ An example of declaring new model (named NewModel) is shown below: @declare_process_block_class("NewModel") class NewModelData(BaseClass): -The first part of the declaration is the `declare_process_block_class` decorator, which automates all the code required to create a new type of Pyomo `Block`. This decorator needs to be provided with a name for the new model (NewModel). Understanding the details of class decoration within Python and the function of the `declare_process_block_class` decorator are not necessary for developing new models, however users who wish to read more can see the :ref:`technical specifications`. +The first part of the declaration is the `declare_process_block_class` decorator, which automates all the code required to create a new type of Pyomo `Block`. This decorator needs to be provided with a name for the new model (NewModel). Understanding the details of class decoration within Python and the function of the `declare_process_block_class` decorator are not necessary for developing new models, however users who wish to read more can see the :ref:`technical specifications`. The second part of the declaration creates a `NewModelData` class which inherits from an existing `BaseClass`. The `NewModelData` class needs to contain the instructions necessary for building the desired model and must be populated by the user. In practice, each type of model (unit operation, thermophysical properties, etc.) has a set of common tasks which appear in most models of that type. To assist users with developing new model and reduce the need for them to rewrite these common tasks, IDAES provides a set of base classes which contain many of these common instructions. Model developers can use these base classes as a foundation for their new models using what is referred to as “inheritance”, as shown in the example above. In doing so, the new model class automatically gains access to all of the common instructions in the base class which can be used in constructing the new model. @@ -84,9 +84,9 @@ Types of Models .. toctree:: :maxdepth: 1 - + unit_model_development property_package_development reaction_package_development - + diff --git a/docs/advanced_user_guide/custom_models/property_package_development.rst b/docs/how_to_guides/custom_models/property_package_development.rst similarity index 98% rename from docs/advanced_user_guide/custom_models/property_package_development.rst rename to docs/how_to_guides/custom_models/property_package_development.rst index 12ecbfd738..603fcbe2cb 100644 --- a/docs/advanced_user_guide/custom_models/property_package_development.rst +++ b/docs/how_to_guides/custom_models/property_package_development.rst @@ -73,7 +73,7 @@ Next, the user must declare an attribute named “_state_block_class” which is super().build() self._state_block_class = NewStateBlock -The next step in the `build` method is to define the chemical species and phases necessary to describe the material of interest. This is done by adding :ref:`Component` and :ref:`Phase` objects, as shown below. +The next step in the `build` method is to define the chemical species and phases necessary to describe the material of interest. This is done by adding :ref:`Component` and :ref:`Phase` objects, as shown below. .. code-block:: python @@ -135,7 +135,7 @@ The primary purpose of the properties metadata is to set up the build-on-demand .. note:: - The name of a property in the metadata dictionary must match the name of the property component (normally a variable) that will be called for. These names should be drawn form the :ref:`standard naming conventions`. + The name of a property in the metadata dictionary must match the name of the property component (normally a variable) that will be called for. These names should be drawn form the :ref:`standard naming conventions`. The State Block --------------- @@ -178,7 +178,7 @@ State Variables and Properties The most important part of the construction of a State Block is defining the necessary set of variables, expression and constraints that make up the property model. There are many different ways in which these can be defined and formulated, and there is no single “best” way to do this; different approaches may work better for different applications. However, there are some general rules that should be followed when defining the variables which make up a State Block. 1. All state variables and properties should use the IDAES naming conventions. Standard names allow linking between different types of models to be automated, as no cross-referencing of names is required. -2. All properties within a property package should use a consistent set of base units. This is most easily accomplished by selecting a set of units for the 7 base SI quantities (time, length, mass, amount, temperature, current and luminous intensity) and deriving units for all quantities from these. Modelers should also select units based solely on convenience or ease of use – scaling of variables and equations is better handled separately using the :ref:`IDAES scaling tools`. +2. All properties within a property package should use a consistent set of base units. This is most easily accomplished by selecting a set of units for the 7 base SI quantities (time, length, mass, amount, temperature, current and luminous intensity) and deriving units for all quantities from these. Modelers should also select units based solely on convenience or ease of use – scaling of variables and equations is better handled separately using the :ref:`IDAES scaling tools`. Beyond these requirements, modelers are free to choose the form of their model to best suit theirs needs and make the most tractable problem possible. Modelers are also free to combine variable and constraints with expression for some quantities as needed. The IDAES Process Modeling Framework is concerned only that the expected quantities are present (i.e. the expected variable/expression names), not their exact form or how they are calculated. @@ -192,10 +192,10 @@ As the foundation of the entire Process Modeling Framework, the definition of a Below is a list of the required methods, along with a short description. * `get_material_flow_basis(block)` – this method is used to define the basis on which material balance terms will be expressed. This is used by the framework to automatically convert between mass and mole basis if required, and the method needs to return a `MaterialFlowBasis` `Enum`. -* `get_material_flow_terms(block, phase, component)` – this method is used to determine the form of the material flow terms that are constructed as part of the material balance equations in each unit model. This method needs to take three arguments; a reference to the current state block, a phase name and a component name, and must return an expression for the material flow term for the given phase and component. +* `get_material_flow_terms(block, phase, component)` – this method is used to determine the form of the material flow terms that are constructed as part of the material balance equations in each unit model. This method needs to take three arguments; a reference to the current state block, a phase name and a component name, and must return an expression for the material flow term for the given phase and component. * `get_material_density_terms(block, phase, component)` – similar to the `get_material_flow_terms` method, this method is used to determine the form of the density term which should be used when constructing material holdup terms in the material balances. This method also needs to take three arguments; a reference to the current state block, a phase name and a component name, and must return an expression for the material density term for the given phase and component. * `get_material_diffusion_terms(block, phase, component)` – Support for this is not currently implemented. -* `get_enthalpy_flow_terms(block, phase)` – this method is used to determine the form of the enthalpy flow terms that are constructed as part of the energy balance equations in each unit model. This method needs to take two arguments; a reference to the current state block and a phase name, and must return an expression for the enthalpy flow term for the given phase and component. +* `get_enthalpy_flow_terms(block, phase)` – this method is used to determine the form of the enthalpy flow terms that are constructed as part of the energy balance equations in each unit model. This method needs to take two arguments; a reference to the current state block and a phase name, and must return an expression for the enthalpy flow term for the given phase and component. * `get_energy_density_terms(block, phase)` – this method is used to determine the form of the energy density terms that are required for the holdup terms in the energy balance equations. This method needs to take two arguments; a reference to the current state block and a phase name, and must return an expression for the energy density term for the given phase and component. Note that the holdup/density term needs to be in terms of internal energy, not enthalpy. * `get_energy_diffusion_terms(block, phase)` – Support for this is not currently implemented. * `default_material_balance_type(block)` – this method is used to set a default for the type of material balance to be written by a Control Volume if the user does not specify which type to use. This method needs to return a `MaterialBalanceType` `Enum`. @@ -245,4 +245,4 @@ More details on writing initialization methods will be provided elsewhere in the Tutorials --------- -Tutorials demonstrating how to create custom property packages are being developed. Once they are created, they will be found :ref:`here`. +Tutorials demonstrating how to create custom property packages are being developed. Once they are created, they will be found :ref:`here`. diff --git a/docs/advanced_user_guide/custom_models/reaction_package_development.rst b/docs/how_to_guides/custom_models/reaction_package_development.rst similarity index 95% rename from docs/advanced_user_guide/custom_models/reaction_package_development.rst rename to docs/how_to_guides/custom_models/reaction_package_development.rst index 25452c1c3f..b063920f6c 100644 --- a/docs/advanced_user_guide/custom_models/reaction_package_development.rst +++ b/docs/how_to_guides/custom_models/reaction_package_development.rst @@ -7,7 +7,7 @@ Chemical reactions are a fundamental part of most processes, and models for these come in a wide range of different forms. Much like thermophysical property packages, the ability for users to define custom reaction formulations is a key aspect of the IDAES modeling paradigm. -Reaction packages within IDAES share many similarities with thermophysical property packages, both in form and content. Rather than repeat much of that documentation here, users should start by reading the :ref:`thermophysical property package documentation`, as this document will focus on the content of the reaction package. +Reaction packages within IDAES share many similarities with thermophysical property packages, both in form and content. Rather than repeat much of that documentation here, users should start by reading the :ref:`thermophysical property package documentation`, as this document will focus on the content of the reaction package. What Belongs in a Reaction Package? ----------------------------------- @@ -18,7 +18,7 @@ Chemical reactions are fundamentally governed by the same laws of thermodynamics For the context of IDAES, chemical reactions are defined as phenomena where one chemical species is converted into another. This includes both rate limited and equilibrium reactions. - On the other hand, phase equilibrium phenomena (where a chemical species changes phase) are handled via the thermophysical property package. + On the other hand, phase equilibrium phenomena (where a chemical species changes phase) are handled via the thermophysical property package. However, users should note that reaction properties are fundamentally linked to the thermophysical properties, and that a reaction package should only be used with the thermophysical property package they were developed with (in theory at least). Due to this, when a reactions package is added to a model it must be coupled to a thermophysical property package. The modeling framework performs some limited checks to ensure the two packages are compatible (e.g. same set of base units) and that each reaction packages is only used in conjunction with its coupled thermophysical property package in unit models. @@ -131,7 +131,7 @@ The last part of creating a new Reaction Parameter block is to define the metada Setting Default Units """"""""""""""""""""" -As with thermophysical property packages, the most important part of defining the metadata for a property package is to set the default units of measurement for each of the 7 base quantities (time, length, mass, amount, temperature, current (optional) and luminous intensity (optional)). These units are used by the modeling framework to determine the units of measurement for all other quantities in the process that are related to this property package. More importantly, the units metadata is used to determine if a reaction package is comparable with a given thermophysical property package when they are declared – if the units metadata does not match, an exception will be raised and the two packages cannot be used together. +As with thermophysical property packages, the most important part of defining the metadata for a property package is to set the default units of measurement for each of the 7 base quantities (time, length, mass, amount, temperature, current (optional) and luminous intensity (optional)). These units are used by the modeling framework to determine the units of measurement for all other quantities in the process that are related to this property package. More importantly, the units metadata is used to determine if a reaction package is comparable with a given thermophysical property package when they are declared – if the units metadata does not match, an exception will be raised and the two packages cannot be used together. Units must be defined using Pyomo `Units` components, as shown in the example below: @@ -150,7 +150,7 @@ Units must be defined using Pyomo `Units` components, as shown in the example be Setting Reaction Metadata """"""""""""""""""""""""" -Similar to thermophysical property packages, reaction packages allow users to specify the set of reaction properties supported by a given reaction package. This is also used to set up the build-on-demand properties system in the same way as thermophysical properties. For more information, see the documentation for :ref:`thermophysical properties metadata`. +Similar to thermophysical property packages, reaction packages allow users to specify the set of reaction properties supported by a given reaction package. This is also used to set up the build-on-demand properties system in the same way as thermophysical properties. For more information, see the documentation for :ref:`thermophysical properties metadata`. The Reaction Block ------------------ @@ -206,7 +206,7 @@ As with all IDAES components, the `build` method forms the core of a `ReactionBl Variables and Properties """""""""""""""""""""""" -The same set of guidelines for defining thermophysical properties apply to reaction properties, :ref:`which can be found here`. +The same set of guidelines for defining thermophysical properties apply to reaction properties, :ref:`which can be found here`. Required Methods """""""""""""""" @@ -218,7 +218,7 @@ In addition to the `build` method, Reaction Blocks require one additional method The Reaction Block Methods Class ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The Reaction Block Methods class is very similar to the :ref:`State Block Methods class`. The Reaction Block Methods class needs to contain an `initialize` method (however a `release_state` method is not required as Reaction Blocks do not contain state variables). +The Reaction Block Methods class is very similar to the :ref:`State Block Methods class`. The Reaction Block Methods class needs to contain an `initialize` method (however a `release_state` method is not required as Reaction Blocks do not contain state variables). The `initialize` Method """"""""""""""""""""""" @@ -230,5 +230,5 @@ However, Reaction Blocks also tend to be much simpler than State Blocks, involvi Tutorials --------- -Tutorials demonstrating how to create custom reaction packages are being developed. Once they are created, they will be found :ref:`here`. +Tutorials demonstrating how to create custom reaction packages are being developed. Once they are created, they will be found :ref:`here`. diff --git a/docs/advanced_user_guide/custom_models/unit_model_development.rst b/docs/how_to_guides/custom_models/unit_model_development.rst similarity index 94% rename from docs/advanced_user_guide/custom_models/unit_model_development.rst rename to docs/how_to_guides/custom_models/unit_model_development.rst index b54559af3c..7133d14e3c 100644 --- a/docs/advanced_user_guide/custom_models/unit_model_development.rst +++ b/docs/how_to_guides/custom_models/unit_model_development.rst @@ -13,7 +13,7 @@ The starting point for all unit models within the IDAES Process Modeling Framewo * creating simple material balances between states (equal flow of each component in each phase), and, * a method for initializing simple unit models. -More details on the `UnitModelBlockData` class can be found in the :ref:`technical specifications`. +More details on the `UnitModelBlockData` class can be found in the :ref:`technical specifications`. Unit Model Configuration ------------------------ @@ -67,7 +67,7 @@ The `build` method for a unit model must include all the instructions necessary For some applications, not all of these steps will be required (e.g. a process in which pressure drop is negligible may be able to skip adding momentum balances). -The above steps represent a significant amount of work, and in many cases require a detailed understanding of how the IDAES framework is structured. To reduce the effort and knowledge required to create new models, the framework provides a number of tools to automate these steps for common cases. Users are encouraged to familiarize themselves with the methods available in :ref:`UnitModelBlockData` and the use of control volumes. +The above steps represent a significant amount of work, and in many cases require a detailed understanding of how the IDAES framework is structured. To reduce the effort and knowledge required to create new models, the framework provides a number of tools to automate these steps for common cases. Users are encouraged to familiarize themselves with the methods available in :ref:`UnitModelBlockData` and the use of control volumes. Control Volumes ^^^^^^^^^^^^^^^ @@ -76,8 +76,8 @@ The IDAES Process Modeling Framework includes tools to assist users with creatin The IDAES Process Modeling Framework currently includes two types of Control Volumes: -1. :ref:`ControlVolume0D` for inlet-outlet type models where spatial variation are not significant. -2. :ref:`ControlVolume1D` for models where spatial variation in one-dimension are required. +1. :ref:`ControlVolume0D` for inlet-outlet type models where spatial variation are not significant. +2. :ref:`ControlVolume1D` for models where spatial variation in one-dimension are required. Unit Model Initialization ------------------------- @@ -120,6 +120,6 @@ The `_get_performance_contents` method should take two arguments, the first bein Tutorials --------- Tutorials demonstrating how to create custom unit models are found -:ref:`here`. +:ref:`here`. + - diff --git a/docs/how_to_guides/index.rst b/docs/how_to_guides/index.rst new file mode 100644 index 0000000000..9beabcd089 --- /dev/null +++ b/docs/how_to_guides/index.rst @@ -0,0 +1,9 @@ +How-To-Guides +======================== + +.. toctree:: + :maxdepth: 2 + + custom_models/general_model_development + vis/index + workflow/index \ No newline at end of file diff --git a/docs/user_guide/vis/index.rst b/docs/how_to_guides/vis/index.rst similarity index 99% rename from docs/user_guide/vis/index.rst rename to docs/how_to_guides/vis/index.rst index 97b7c6e58f..ccafaf0915 100644 --- a/docs/user_guide/vis/index.rst +++ b/docs/how_to_guides/vis/index.rst @@ -31,7 +31,7 @@ This guide describes how to invoke (i.e., start) and use the IFV. Invocation ^^^^^^^^^^ The IFV visualizes *flowsheets*. To get started with creating a flowsheet with IDAES, see the -:ref:`Flowsheet models` documentation page. +:ref:`Flowsheet models` documentation page. Once you have created your flowsheet, simply call the :ref:`visualize ` method on that object, passing some parameters to give it a name and optional file for saving changes:: diff --git a/docs/user_guide/workflow/data_rec_parmest.rst b/docs/how_to_guides/workflow/data_rec_parmest.rst similarity index 98% rename from docs/user_guide/workflow/data_rec_parmest.rst rename to docs/how_to_guides/workflow/data_rec_parmest.rst index 593d61883b..9fe104fa1e 100644 --- a/docs/user_guide/workflow/data_rec_parmest.rst +++ b/docs/how_to_guides/workflow/data_rec_parmest.rst @@ -5,12 +5,12 @@ This workflow generally describes features of the IDAES framework that are usefu for data reconciliation and parameter estimation. Many of these features can be used for any task where plant data is to be used in conjunction with a process model. It is assumed that the user is familiar with the IDAES modeling -platform and Pyomo. See the :ref:`General Workflow ` +platform and Pyomo. See the :ref:`General Workflow ` for more information on how to set up a model. This provides general information about IDAES functionality laid out in terms of typical use cases. See -:ref:`Tutorials and Examples`, for +:ref:`Tutorials and Examples`, for specific complete examples of data reconciliation and parameter estimation examples. Relevant tutorials can be found in ``tutorials/advanced/data_recon_and_parameter_estimation``. @@ -57,7 +57,7 @@ column are ignored and can be used to store any additional information. +--------+--------------------------+---------------+--------------------+ The unit strings should be interpretable by `pint `_, -with the additional unit strings given in :ref:`Unit String Information `. +with the additional unit strings given in :ref:`Unit String Information `. The model reference string is the a string to reverence a model quantity. In the reference string the top-level model is always represented by ``m``. For example, the reference string for a heater block outlet temperature could be diff --git a/docs/user_guide/workflow/general.rst b/docs/how_to_guides/workflow/general.rst similarity index 79% rename from docs/user_guide/workflow/general.rst rename to docs/how_to_guides/workflow/general.rst index 5865bcf1da..bf0b9f5201 100644 --- a/docs/user_guide/workflow/general.rst +++ b/docs/how_to_guides/workflow/general.rst @@ -1,18 +1,18 @@ General Workflow ================ -While IDAES offers significant freedom in how users write their models, they -are encouraged to follow this general workflow in order to make it easier for others to follow +While IDAES offers significant freedom in how users write their models, they +are encouraged to follow this general workflow in order to make it easier for others to follow their code. This workflow is used throughout the tutorials and examples on the |examples-site|. .. note:: - It is important to note that IDAES models are constructed upon execution of each line of - code, and that most user defined options are only processed on model construction. This - means that if the user wishes to make changes to any model construction option, it is - necessary to rebuild the model from the beginning. Users should not be put off by this + It is important to note that IDAES models are constructed upon execution of each line of + code, and that most user defined options are only processed on model construction. This + means that if the user wishes to make changes to any model construction option, it is + necessary to rebuild the model from the beginning. Users should not be put off by this however, as model construction is generally very quick. The general workflow for working with a model in IDAES is shown below: @@ -22,30 +22,30 @@ The general workflow for working with a model in IDAES is shown below: 1. Importing Modules -------------------- -IDAES is built upon a modular, object-oriented platform using Python, which requires users to -import the components from the appropriate model libraries. The necessary components and -libraries will vary from application to application, and were discussed earlier in this User +IDAES is built upon a modular, object-oriented platform using Python, which requires users to +import the components from the appropriate model libraries. The necessary components and +libraries will vary from application to application, and were discussed earlier in this User Guide, however some common components users will need include: * Pyomo environment components (e.g. ConcreteModel, SolverFactory, TransformationFactory, Var, Constraint, objective) imported from `pyomo.environ` * Pyomo network components (e.g. Arc, expand_arcs) from `pyomo.network` * IDAES FlowsheetBlock, from `idaes.core` -* :ref:`Property packages` for materials of interest +* :ref:`Property packages` for materials of interest * Unit models for process equipment, drawn from either the IDAES model libraries and/or user-defined models -* Data visualization and analysis tools. Common tools include degrees of freedom and scaling, a full list is provided :ref:`here`. +* Data visualization and analysis tools. Common tools include degrees of freedom and scaling, a full list is provided :ref:`here`. * External packages of interest to the user. Being built upon Python, users have access to the full range of Python libraries for working with and analyzing their models. 2. Building a Model ------------------- -The next step in the workflow is to create a model object which represents the problem to be -solved. The steps involved in this may vary depending on the problem being solved, but the +The next step in the workflow is to create a model object which represents the problem to be +solved. The steps involved in this may vary depending on the problem being solved, but the general procedure is as follows: 2.1 Create a Model Object ^^^^^^^^^^^^^^^^^^^^^^^^^ -The foundation of any model in IDAES is a Pyomo `ConcreteModel` object, which is created as +The foundation of any model in IDAES is a Pyomo `ConcreteModel` object, which is created as follows: .. code-block:: python @@ -59,8 +59,8 @@ follows: 2.2 Add a Flowsheet to the Model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The foundation of a process model within IDAES is the `FlowsheetBlock`, which forms the canvas -upon which the process will be constructed. A key aspect of the `FlowsheetBlock` is to define +The foundation of a process model within IDAES is the `FlowsheetBlock`, which forms the canvas +upon which the process will be constructed. A key aspect of the `FlowsheetBlock` is to define whether the model will be steady-state or dynamic, and to define the time domain as appropriate. .. code-block:: python @@ -69,14 +69,14 @@ whether the model will be steady-state or dynamic, and to define the time domain .. note:: - IDAES supports nested flowsheets to allow complex processes + IDAES supports nested flowsheets to allow complex processes to be broken down into smaller sub-processes. 2.3 Add Property Packages to Flowsheet ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -All process models depend on calculations of thermophysical and chemical reaction properties, -which are represented in IDAES using property packages. Users need to add the property packages +All process models depend on calculations of thermophysical and chemical reaction properties, +which are represented in IDAES using property packages. Users need to add the property packages they intend to use to the flowsheet. .. code-block:: python @@ -85,13 +85,13 @@ they intend to use to the flowsheet. .. note:: - Users can add as many property packages as they need to a flowsheet, and can determine which + Users can add as many property packages as they need to a flowsheet, and can determine which property package will be used for each unit operation as it is created. 2.4 Add Unit Models to Flowsheet ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Next, the user can add Unit Models to their flowsheet to represent each unit operation in the +Next, the user can add Unit Models to their flowsheet to represent each unit operation in the process. .. code-block:: python @@ -111,26 +111,26 @@ In order to describe the flow of material between unit operations, users must de 2.6 Expand Arcs ^^^^^^^^^^^^^^^ -It is important to note that `Arcs` only define the connectivity between unit operations, but -do not create the actual model constraints needed to describe this. Once all `Arcs` in a -flowsheet have been defined, it is necessary to expand these `Arcs` using the Pyomo +It is important to note that `Arcs` only define the connectivity between unit operations, but +do not create the actual model constraints needed to describe this. Once all `Arcs` in a +flowsheet have been defined, it is necessary to expand these `Arcs` using the Pyomo `TransformationFactory`. .. code-block:: python - + TransformationFactory("network.expand_arcs").apply_to(m) .. note:: - Pyomo provides a number of other Transformations and tools that may be useful to the user + Pyomo provides a number of other Transformations and tools that may be useful to the user depending on the application. Examples include the `gdp` and `dae` transformations. 2.7 Add Variables, Constraints and Objectives ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Finally, users can add any additional variables, constraints and objectives to their model. -These could include the objective function for which they wish to optimize, additional -constraints that provide limits on process performance, or simply additional quantities that +Finally, users can add any additional variables, constraints and objectives to their model. +These could include the objective function for which they wish to optimize, additional +constraints that provide limits on process performance, or simply additional quantities that the user wishes to use in analyzing or visualizing the results. 3. Scaling the Model @@ -140,25 +140,25 @@ the user wishes to use in analyzing or visualizing the results. The IDAES scaling tools are currently under development. -Ensuring that a model is well scaled is important for increasing the efficiency and reliability -of solvers, and users should consider model scaling as an integral part of the modeling process. -IDAES provides a number of tool for assisting users with scaling their models, and details on -these can be found :ref:`here`. +Ensuring that a model is well scaled is important for increasing the efficiency and reliability +of solvers, and users should consider model scaling as an integral part of the modeling process. +IDAES provides a number of tool for assisting users with scaling their models, and details on +these can be found :ref:`here`. 4. Specifying the Model ----------------------- .. note:: - IDAES is in the process of developing a set of tools to assist users with working with units + IDAES is in the process of developing a set of tools to assist users with working with units of measurement when fixing and displaying values. -The next step is to specify the model by fixing variables. which can be done using the form -`variable_name.fix(value)`. The variables that need to be fixed are application dependent, +The next step is to specify the model by fixing variables. which can be done using the form +`variable_name.fix(value)`. The variables that need to be fixed are application dependent, but commonly include the feed state variables. -In order to prepare the model for initialization, it is necessary to fully specify the model, -such that there are no degrees of freedom. IDAES provides a tools for counting and reporting +In order to prepare the model for initialization, it is necessary to fully specify the model, +such that there are no degrees of freedom. IDAES provides a tools for counting and reporting the degrees of freedom in any model (or sub-model/block): .. code-block:: python @@ -169,30 +169,30 @@ the degrees of freedom in any model (or sub-model/block): .. note:: - Whilst it is not always necessary to fully define a model before initialization, it is much - safer to do so as it ensures the model is well-defined. Most IDAES initialization tools - check that the model is well-defined before proceeding, and will raise an Exception if it is + Whilst it is not always necessary to fully define a model before initialization, it is much + safer to do so as it ensures the model is well-defined. Most IDAES initialization tools + check that the model is well-defined before proceeding, and will raise an Exception if it is not. .. note:: - Depending on the solver to be used during initialization, it can be better to avoid putting - bounds on variables and adding inequality constraints at this stage. For solving square - problems (i.e. zero degrees of freedom), some solvers (e.g. IPOPT) perform better without - bounds on the problem. These bounds and constraints can be added later when it comes time to + Depending on the solver to be used during initialization, it can be better to avoid putting + bounds on variables and adding inequality constraints at this stage. For solving square + problems (i.e. zero degrees of freedom), some solvers (e.g. IPOPT) perform better without + bounds on the problem. These bounds and constraints can be added later when it comes time to optimize the problem. 5. Initializing the Model ------------------------- -The next step is to initialize the model. All IDAES models have established initialization -methods that can be called using `model.initialize()` which can be expected to take a model -from its initial state to a feasible solution for a set of initial guesses (within the models +The next step is to initialize the model. All IDAES models have established initialization +methods that can be called using `model.initialize()` which can be expected to take a model +from its initial state to a feasible solution for a set of initial guesses (within the models expected operating range). -IDEAS workflows generally use a sequential-modular approach to -initialize flowsheets, where unit models are initialized sequentially, passing the outlet -state from one unit as the initial state for the next. An automated sequential-modular tool is +IDEAS workflows generally use a sequential-modular approach to +initialize flowsheets, where unit models are initialized sequentially, passing the outlet +state from one unit as the initial state for the next. An automated sequential-modular tool is available through Pyomo and demonstrated in the tutorials. 6. Solving the Model @@ -200,12 +200,12 @@ available through Pyomo and demonstrated in the tutorials. .. important:: - The sequential-modular approach initializes each unit model individually, thus it is - important to do a final solve of the overall flowsheet/model in order to complete the - initialization process. In most cases, this final solve should only take a few iterations, + The sequential-modular approach initializes each unit model individually, thus it is + important to do a final solve of the overall flowsheet/model in order to complete the + initialization process. In most cases, this final solve should only take a few iterations, as the state of each unit model should be at or near the final solution already. -In order to solve the model, it is necessary to create a solve object and set any desired solver +In order to solve the model, it is necessary to create a solve object and set any desired solver options (such as tolerances, iteration limits etc.). .. code-block:: python @@ -215,21 +215,21 @@ options (such as tolerances, iteration limits etc.). results = solver.solve(m) -Users should check the output from the solver to ensure a feasible solution was found using +Users should check the output from the solver to ensure a feasible solution was found using the following: .. code-block:: python print(results.solver.termination_condition) -Different problems will require different solvers, and users will need to experiment to find -those that work best for their problems. The default solver for most IDAES applications is +Different problems will require different solvers, and users will need to experiment to find +those that work best for their problems. The default solver for most IDAES applications is IPOPT, which can be downloaded using the ``idaes get-extensions`` command line. 7. Optimizing the Model ----------------------- -Once an initial solution has been found, users can proceed to solving the optimization problem +Once an initial solution has been found, users can proceed to solving the optimization problem of interest. This procedure will vary by application but generally involves the following steps: 7.1) Unfix some degrees of freedom to provide the problem with decision variables, `variable_name.unfix()`. @@ -240,15 +240,15 @@ of interest. This procedure will vary by application but generally involves the .. note:: - Users may wish/need to use different solvers for initialization and optimization. IDAES and - Pyomo support the use of multiple solvers as part of the same workflow for solving different + Users may wish/need to use different solvers for initialization and optimization. IDAES and + Pyomo support the use of multiple solvers as part of the same workflow for solving different types of problems. 8. Analyzing and Visualizing the Results ---------------------------------------- -One of the benefits of the IDAES Integrated Platform is that it operates in a fully featured -programming language, which provides users a high degree of flexibility in analyzing their -models. For example, users can automate the simulation of the model across multiple objectives -or a range of parameters, store and save results from one or multiple solutions. Users also have +One of the benefits of the IDAES Integrated Platform is that it operates in a fully featured +programming language, which provides users a high degree of flexibility in analyzing their +models. For example, users can automate the simulation of the model across multiple objectives +or a range of parameters, store and save results from one or multiple solutions. Users also have access to a wide range of tools for manipulating, plotting and visualizing the results. diff --git a/docs/user_guide/workflow/index.rst b/docs/how_to_guides/workflow/index.rst similarity index 100% rename from docs/user_guide/workflow/index.rst rename to docs/how_to_guides/workflow/index.rst diff --git a/docs/index.rst b/docs/index.rst index 37af7e567c..632e9f8884 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,48 @@ of the full range of advanced fossil energy systems, including chemical looping and other transformational |CO2| capture technologies, as well as integration with other new technologies such as supercritical |CO2|. -For a more detailed overview of the :term:`IDAES integrated platform `, see :doc:`this page `. +For a more detailed overview of the :term:`IDAES integrated platform `, see :doc:`this page `. + +Contents +-------- +.. list-table:: + :class: index-table + + * - Getting Started (Tutorials) + * :doc:`Installing IDAES for users ` + * :doc:`Installing IDAES for developers ` + * :doc:`IDAES Examples & Tutorials ` + - How-To Guides + * :doc:`Setting up IDAES Models ` + * :doc:`Developing Custom Models ` + * :doc:`Using IDAES Flowsheet Visualizer ` + * - Explanations + * :doc:`Why IDAES ` + * :doc:`Concepts ` + * :doc:`Components of IDAES ` + * :doc:`Conventions ` + * :doc:`Modeling Extensions ` + * :doc:`Related Packages ` + * :doc:`FAQ ` + * :doc:`License ` + * :doc:`Copyright ` + - Reference Guides + * :doc:`IDAES Model Libraries ` + * :doc:`IDAES Core ` + * :doc:`Command-line interface tools ` + * :doc:`Configuring IDAES ` + * :doc:`Logging Processing & Outputs ` + * :doc:`Developing for IDAES ` + +.. toctree:: + :hidden: + :maxdepth: 2 + + tutorials/index + how_to_guides/index + explanations/index + reference_guides/index + Collaborating institutions -------------------------- @@ -37,24 +78,8 @@ repo `_ where you can `report issues/bugs email to: -Contents --------- - -.. toctree:: - :maxdepth: 2 - - getting_started/index - user_guide/index - advanced_user_guide/index - tutorials_examples - technical_specs/index - related_packages/index - faq - license - copyright - Indices and tables -================== +------------------ * :ref:`genindex` * :ref:`modindex` diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index f7ec3fc34e..0000000000 --- a/docs/license.rst +++ /dev/null @@ -1,3 +0,0 @@ -.. _idaes-license: - -.. include:: ../LICENSE.md diff --git a/docs/user_guide/commands/bin_directory.rst b/docs/reference_guides/commands/bin_directory.rst similarity index 100% rename from docs/user_guide/commands/bin_directory.rst rename to docs/reference_guides/commands/bin_directory.rst diff --git a/docs/user_guide/commands/copyright.rst b/docs/reference_guides/commands/copyright.rst similarity index 100% rename from docs/user_guide/commands/copyright.rst rename to docs/reference_guides/commands/copyright.rst diff --git a/docs/user_guide/commands/data_directory.rst b/docs/reference_guides/commands/data_directory.rst similarity index 100% rename from docs/user_guide/commands/data_directory.rst rename to docs/reference_guides/commands/data_directory.rst diff --git a/docs/user_guide/commands/env_info.rst b/docs/reference_guides/commands/env_info.rst similarity index 100% rename from docs/user_guide/commands/env_info.rst rename to docs/reference_guides/commands/env_info.rst diff --git a/docs/user_guide/commands/get_examples.rst b/docs/reference_guides/commands/get_examples.rst similarity index 100% rename from docs/user_guide/commands/get_examples.rst rename to docs/reference_guides/commands/get_examples.rst diff --git a/docs/user_guide/commands/get_extensions.rst b/docs/reference_guides/commands/get_extensions.rst similarity index 100% rename from docs/user_guide/commands/get_extensions.rst rename to docs/reference_guides/commands/get_extensions.rst diff --git a/docs/user_guide/commands/index.rst b/docs/reference_guides/commands/index.rst similarity index 100% rename from docs/user_guide/commands/index.rst rename to docs/reference_guides/commands/index.rst diff --git a/docs/user_guide/commands/lib_directory.rst b/docs/reference_guides/commands/lib_directory.rst similarity index 100% rename from docs/user_guide/commands/lib_directory.rst rename to docs/reference_guides/commands/lib_directory.rst diff --git a/docs/user_guide/commands/version.rst b/docs/reference_guides/commands/version.rst similarity index 100% rename from docs/user_guide/commands/version.rst rename to docs/reference_guides/commands/version.rst diff --git a/docs/user_guide/configuration.rst b/docs/reference_guides/configuration.rst similarity index 100% rename from docs/user_guide/configuration.rst rename to docs/reference_guides/configuration.rst diff --git a/docs/technical_specs/core/comp.rst b/docs/reference_guides/core/comp.rst similarity index 100% rename from docs/technical_specs/core/comp.rst rename to docs/reference_guides/core/comp.rst diff --git a/docs/technical_specs/core/control_volume_0d.rst b/docs/reference_guides/core/control_volume_0d.rst similarity index 95% rename from docs/technical_specs/core/control_volume_0d.rst rename to docs/reference_guides/core/control_volume_0d.rst index 16006a1243..ee0771dc8b 100644 --- a/docs/technical_specs/core/control_volume_0d.rst +++ b/docs/reference_guides/core/control_volume_0d.rst @@ -4,12 +4,12 @@ .. contents:: Contents :depth: 2 -The ControlVolume0DBlock block is the most commonly used Control Volume class, and is used for -systems where there is a well-mixed volume of fluid, or where variations in spatial domains are -considered to be negligible. ControlVolume0DBlock blocks generally contain two -:ref:`StateBlocks ` - one for the incoming material and one for -the material within and leaving the volume - and one -:ref:`StateBlocks `. +The ControlVolume0DBlock block is the most commonly used Control Volume class, and is used for +systems where there is a well-mixed volume of fluid, or where variations in spatial domains are +considered to be negligible. ControlVolume0DBlock blocks generally contain two +:ref:`StateBlocks ` - one for the incoming material and one for +the material within and leaving the volume - and one +:ref:`StateBlocks `. .. module:: idaes.core.control_volume0d @@ -22,7 +22,7 @@ the material within and leaving the volume - and one ControlVolume0DBlock Equations ------------------------------- -This section documents the variables and constraints created by each of the methods provided by +This section documents the variables and constraints created by each of the methods provided by the ControlVolume0DBlock class. * :math:`t` indicates time index @@ -34,8 +34,8 @@ the ControlVolume0DBlock class. add_geometry ^^^^^^^^^^^^ -The add_geometry method creates a single variable within the control volume named `volume` indexed -by time (allowing for varying volume over time). A number of other methods depend on this variable +The add_geometry method creates a single variable within the control volume named `volume` indexed +by time (allowing for varying volume over time). A number of other methods depend on this variable being present, thus this method should generally be called first. **Variables** @@ -53,9 +53,9 @@ No additional constraints add_phase_component_balances ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Material balances are written for each component in each phase (e.g. separate balances for liquid -water and steam). Physical property packages may include information to indicate that certain species -do not appear in all phases, and material balances will not be written in these cases (if +Material balances are written for each component in each phase (e.g. separate balances for liquid +water and steam). Physical property packages may include information to indicate that certain species +do not appear in all phases, and material balances will not be written in these cases (if `has_holdup` is True holdup terms will still appear for these species, however these will be set to 0). **Variables** diff --git a/docs/technical_specs/core/control_volume_1d.rst b/docs/reference_guides/core/control_volume_1d.rst similarity index 100% rename from docs/technical_specs/core/control_volume_1d.rst rename to docs/reference_guides/core/control_volume_1d.rst diff --git a/docs/technical_specs/core/flowsheet_block.rst b/docs/reference_guides/core/flowsheet_block.rst similarity index 100% rename from docs/technical_specs/core/flowsheet_block.rst rename to docs/reference_guides/core/flowsheet_block.rst diff --git a/docs/technical_specs/core/index.rst b/docs/reference_guides/core/index.rst similarity index 100% rename from docs/technical_specs/core/index.rst rename to docs/reference_guides/core/index.rst diff --git a/docs/technical_specs/core/phase.rst b/docs/reference_guides/core/phase.rst similarity index 100% rename from docs/technical_specs/core/phase.rst rename to docs/reference_guides/core/phase.rst diff --git a/docs/technical_specs/core/physical_property_class.rst b/docs/reference_guides/core/physical_property_class.rst similarity index 100% rename from docs/technical_specs/core/physical_property_class.rst rename to docs/reference_guides/core/physical_property_class.rst diff --git a/docs/technical_specs/core/process_block.rst b/docs/reference_guides/core/process_block.rst similarity index 100% rename from docs/technical_specs/core/process_block.rst rename to docs/reference_guides/core/process_block.rst diff --git a/docs/technical_specs/core/reaction_property_class.rst b/docs/reference_guides/core/reaction_property_class.rst similarity index 100% rename from docs/technical_specs/core/reaction_property_class.rst rename to docs/reference_guides/core/reaction_property_class.rst diff --git a/docs/reference_guides/core/solvers.rst b/docs/reference_guides/core/solvers.rst new file mode 100644 index 0000000000..d1b87218c6 --- /dev/null +++ b/docs/reference_guides/core/solvers.rst @@ -0,0 +1,269 @@ +Solvers +======= + +This section provides an overview of using and configuring solvers for IDAES. +In general, standard Pyomo solver interfaces and features are used in IDAES, +but IDAES provides a few extensions to make working with solvers slightly easier. +Some IDAES solver features are documented in other sections, so references are +provided as appropriate. + +Default Solver Config +--------------------- + +The global solver settings can be set via the +:ref:`IDAES configuration system`. +This feature is handy in IDAES where multiple solver objects are used for +initialization before finally solving a problem. Since IDAES default solver +settings differ from Pyomo, users must explicitly enable the IDAES solver +configuration system with the ``use_idaes_solver_configuration_defaults()`` +function. + +.. autofunction:: idaes.core.solvers.config.use_idaes_solver_configuration_defaults + +Getting a Solver +---------------- + +Typically users can use the standard Pyomo SoverFactory to get a solver. If a +solver is needed in a general model or utility, a +:ref:`utility function ` (``idaes.core.util.misc.get_solver``) +provides a default or user configured solver at runtime. This is used by IDAES +core models and tests. + +Solver Logging +-------------- + +A logger for solver-related log messages can be obtained from the +``idaes.logger.getSolveLogger()`` function +(:ref:`documented here`). +IDAES also has features for redirecting solver output to a log (see +:ref:`Logging Solver Output`). + + +Solver Feature Checking +----------------------- + +There are some functions available to check what features are available to +solvers and to help with basic solver testing. + +.. autofunction:: idaes.core.solvers.ipopt_has_linear_solver + + +PETSc Utilities +--------------- + +IDAES provides an AMPL solver interface for the PETSc solver suite, +`(see the PETSc website) `_. PETSc provides +nonlinear equation (NLE) and differential algebraic equation (DAE) solvers. Both +NLE and DAE solvers are capable of solving simulation problems with zero degrees +of freedom. These solvers may be useful for initial model development, +initialization, and running simulation cases without optimization. + +PETSc includes optimization solvers, but they are not currently supported by the +IDAES AMPL solver wrapper. Optimization support will likely be added in the +future. + +DAE Terminology +~~~~~~~~~~~~~~~ + +For the following discussion regarding the PETSc solver interface, the following +terminology is used. + +* Derivative variable: a time derivative +* Differential variable: a variable that is differentiated with respect to time +* Algebraic variable: a variable with no explicit time derivative appearing in the problem +* State variables: the set of algebraic and differential variables +* Time variable: a variable representing time + +DAE problems do not need to include a time variable, but, if they do, there can +only be one. Differential variables do not need to explicitly appear in +constraints, but their time derivatives do. DAE problems must have zero degrees +of freedom, which means the number of constraints must equal the number of state +variables. + +Installing PETSc +~~~~~~~~~~~~~~~~ + +The PETSc solver is an extra binary package, and not installed by default. If +you are using a supported Linux distribution, you can use the command +``idaes get-extensions --extra petsc`` to install it. + +There is no precompiled PETSc solver for Windows, but here are two options for +Windows installation. The easiest option is to run the available precompiled +Linux version via the WSL +(see :ref:`binary installation ` +for details). Expert users may wish to compile their own solver. Source code is +available in the `idaes-ext repo `_. +If you can compile PETSc for Windows, compiling the interface is trivial (see +`PETSc's windows installation documentation `_). + +The IDAES PETSc package also includes Python modules for reading binary data +written by the PETSc solver. On Windows, some manual installation of the Python +modules is required. If you are using the WSL method to run PETSc, copy the +``petscpy`` directory from the Linux package you are using to the IDAES binary +directory. You can find the IDAES binary directory by running the command +``idaes bin-directory`` in the OS command shell (e.g. Bash, Windows CMD, +PowerShell). If IDAES is installed in a Python environment, the environment +must be active. The primary use for these Python modules is to read trajectory +files saved by the TS solver. + +Registered Solvers +~~~~~~~~~~~~~~~~~~ + +Importing ``idaes.core.solvers.petsc`` registers two new solvers "petsc_snes" +and "petsc_ts." The "petsc_snes" solver provides nonlinear equation solvers. +The SNES (Scalable Nonlinear Equation Solvers) solvers are strictly nonlinear +equations solvers, so they cannot directly handle optimization problems and the +problem must have zero degrees of freedom. The TS (time-stepping) solvers require +specialized suffixes to designate derivative, differential, algebraic, and time +variables and the associations between derivative and differential variables. +Both the SNES and TS solvers accept the standard scaling factor suffixes, but +for TS solvers, derivatives and differential variables cannot be scaled +independently, so the differential variable scale is used. Currently, time +cannot be scaled for TS solvers. + +Standard PETSc command line options are available to the solvers except that for +compatibility reasons, they are specified with a double dash instead of single. +Command line options can be used to set up the SNES and TS solvers. Currently +only implicit TS solvers are supported. Commonly used TS types are: + +* "beuler", implicit Euler, +* "cn", Crank-Nicolson, and +* "alpha", generalized-alpha method. + +To get started, important command line options are for SNES solvers are described +``_ +and TS options are described +``_. +Remember that options specified through the IDAES AMPL interface use a double +dash rather than the single dash shown in the PETSc documentation. Users can +also set linear solver and preconditioner options, and are encouraged to +read the PETSc documentation if needed. + +Utilities for DAEs with Pyomo.DAE +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to use the "petsc_ts" solver is to use the utility method that +converts a standard Pyomo.DAE to the form used by the solver. + +Discretization +"""""""""""""" + +The utility for solving Pyomo.DAE problems uses the PETSc TS solvers to integrate +between each time point in the Pyomo.DAE discretization. The results are stored +for each time point in the Pyomo model. This can be used to initialize and verify +the results of the full time-discretized model. For example this could be used to +determine if the time steps used in the discretization are too big by comparing +the integrator solution to the fully discretized model solution. + +To quickly run a DAE model, a time discretization with one element can be made. +In this case, the PETSc TS solver will integrate from the initial condition to +the end point. Results for intermediate times can be read from PETSc's stored +trajectory data if the proper solver options are specified. + +Time Variable +""""""""""""" + +Although it is probably not typical of Pyomo.DAE models, a time variable can be +specified. Constraints can be written as explicit functions of time. For +example, some model input could be ramped up or down as a function of time. + +Limitations +""""""""""" + +The integrator approach does not support some constraints that can be solved +using the full discretized model. For example, you can have constraints to +calculate initial conditions, but cannot have constraints that specify final +or intermediate conditions. Optimization is not directly possible, but future +implementation of optimization solvers in combination with adjoint sensitivity +calculations may enable optimization. + +Non-time-indexed variables and constraints should usually be solved with the +initial conditions in the first step. Non-time-indexed variables can optionally +be detected and added to the equations solved for the initial conditions, or +explicitly specified by the user. Users will have to take care not to include +non-time indexed constraints that contain time-indexed variables at times other +than the initial time. If such constraints exist for the fully discretized +model users should deactivate them as appropriate. + +Solving +""""""" + +The following function can be used to solve the DAE. + +.. autofunction:: idaes.core.solvers.petsc.petsc_dae_by_time_element + +Reading Trajectory Data +""""""""""""""""""""""" + +Usually if you want to read the trajectory data from the solver, you will want to +solve the whole time domain at once, so you will want to specify one time element +in the Pyomo.DAE discretization. By specifying the ``--ts_save_trajectory=1`` and +``--ts_trajectory_type=visualization`` options the trajectory information will +be saved. Supplying the ``vars_stub`` argument to the ``petsc_dae_by_time_element()`` +function will copy the `*.col` and `*.typ` files needed to interpret the trajectory +data to the current working directory. + +The ``PetscTrajectory`` class has methods to read in trajectory data and +interpolate time points as needed. + +.. autoclass:: idaes.core.solvers.petsc.PetscTrajectory + :members: + +Using TS Solvers without Pyomo.DAE +"""""""""""""""""""""""""""""""""" + +Most IDAES models use Pyomo.DAE and that is probably the easiest way to set up +a DAE problem, however you may directly construct a DAE problem. + +There are two suffixes that need to be specified to use the PETSc TS solvers from +Pyomo. The first is an integer suffix ``dae_suffix``, which specifies the variable +types. The algebraic variables do not need to be included, but 0 specifies +algebraic variables, 1 specifies differential variables, 2 specifies derivative +variables, and 3 specifies the time variable. A variable for time is optional, +and only one time variable can be specified. The other suffix is an integer +suffix ``dae_link`` which contains differential and derivative variables. The +integer in the suffix links the derivative to it's differential variable, by +specifying an integer greater than 0 that is unique to the pair. + +If there are differential variables that do not appear in the constraints, +they can be supplied to the ``export_nonlinear_variables`` argument of solve. +For the trajectory data, you will also want to use ``symbolic_solver_labels``. + +To solve the problem, start with the initial conditions in the Pyomo model. +After the solve the final conditions will be in the Pyomo model. To get +intermediate results, you will need to store the solver trajectory as described +previously. + +Planned Future PETSc Support +"""""""""""""""""""""""""""" + +This section provides PETSc features that are planned to be supported in the +future, but are not currently supported. + +* Enable parallel methods +* Enable IMEX methods for TS solvers +* Enable TAO optimization solvers +* Provide PETSc Python functions for reading trajectory data (rather than + requiring users to get them manually). + +Test Models +----------- + +The ``idaes.core.solvers.features`` module provides functions to return simple +models of various types. These models can be used to test if solvers are +available and functioning properly. They can also be used to test that various +optional solver features are available. These functions all return a tuple where +the first element is a model of the specified type, and the remaining elements are +the correct solved values for select variables. + +.. autofunction:: idaes.core.solvers.features.lp + +.. autofunction:: idaes.core.solvers.features.milp + +.. autofunction:: idaes.core.solvers.features.nlp + +.. autofunction:: idaes.core.solvers.features.minlp + +.. autofunction:: idaes.core.solvers.features.nle + +.. autofunction:: idaes.core.solvers.features.dae diff --git a/docs/technical_specs/core/unit_model_block.rst b/docs/reference_guides/core/unit_model_block.rst similarity index 100% rename from docs/technical_specs/core/unit_model_block.rst rename to docs/reference_guides/core/unit_model_block.rst diff --git a/docs/technical_specs/core/util/dyn_utils.rst b/docs/reference_guides/core/util/dyn_utils.rst similarity index 100% rename from docs/technical_specs/core/util/dyn_utils.rst rename to docs/reference_guides/core/util/dyn_utils.rst diff --git a/docs/technical_specs/core/util/homotopy.rst b/docs/reference_guides/core/util/homotopy.rst similarity index 100% rename from docs/technical_specs/core/util/homotopy.rst rename to docs/reference_guides/core/util/homotopy.rst diff --git a/docs/technical_specs/core/util/index.rst b/docs/reference_guides/core/util/index.rst similarity index 100% rename from docs/technical_specs/core/util/index.rst rename to docs/reference_guides/core/util/index.rst diff --git a/docs/technical_specs/core/util/initialization.rst b/docs/reference_guides/core/util/initialization.rst similarity index 100% rename from docs/technical_specs/core/util/initialization.rst rename to docs/reference_guides/core/util/initialization.rst diff --git a/docs/technical_specs/core/util/misc.rst b/docs/reference_guides/core/util/misc.rst similarity index 100% rename from docs/technical_specs/core/util/misc.rst rename to docs/reference_guides/core/util/misc.rst diff --git a/docs/technical_specs/core/util/model_serializer.rst b/docs/reference_guides/core/util/model_serializer.rst similarity index 97% rename from docs/technical_specs/core/util/model_serializer.rst rename to docs/reference_guides/core/util/model_serializer.rst index 88b820a578..f6ebef887d 100644 --- a/docs/technical_specs/core/util/model_serializer.rst +++ b/docs/reference_guides/core/util/model_serializer.rst @@ -100,7 +100,7 @@ to_json Despite the name of the ``to_json`` function it is capable of creating Python dictionaries, json files, gzipped json files, and json strings. The function -documentation is below. A :ref:`StoreSpec ` +documentation is below. A :ref:`StoreSpec ` object provides the function with details on what to store and how to handle special cases of Pyomo component attributes. @@ -111,7 +111,7 @@ from_json The ``from_json`` function puts data from Python dictionaries, json files, gzipped json files, and json strings back into a Pyomo model. The function -documentation is below. A :ref:`StoreSpec ` +documentation is below. A :ref:`StoreSpec ` object provides the function with details on what to read and how to handle special cases of Pyomo component attributes. @@ -199,9 +199,9 @@ value of the suffix for the corresponding component. } As a more concrete example, here is the json generated for example model 2 in -:ref:`Examples `. +:ref:`Examples `. This code can be appended to the -:ref:`example boilerplate above `. +:ref:`example boilerplate above `. To generate the example json shown. .. testcode:: diff --git a/docs/technical_specs/core/util/model_statistics.rst b/docs/reference_guides/core/util/model_statistics.rst similarity index 100% rename from docs/technical_specs/core/util/model_statistics.rst rename to docs/reference_guides/core/util/model_statistics.rst diff --git a/docs/technical_specs/core/util/phase_equilibria.rst b/docs/reference_guides/core/util/phase_equilibria.rst similarity index 100% rename from docs/technical_specs/core/util/phase_equilibria.rst rename to docs/reference_guides/core/util/phase_equilibria.rst diff --git a/docs/technical_specs/core/util/scaling.rst b/docs/reference_guides/core/util/scaling.rst similarity index 100% rename from docs/technical_specs/core/util/scaling.rst rename to docs/reference_guides/core/util/scaling.rst diff --git a/docs/technical_specs/core/util/tables.rst b/docs/reference_guides/core/util/tables.rst similarity index 100% rename from docs/technical_specs/core/util/tables.rst rename to docs/reference_guides/core/util/tables.rst diff --git a/docs/technical_specs/core/util/tags.rst b/docs/reference_guides/core/util/tags.rst similarity index 100% rename from docs/technical_specs/core/util/tags.rst rename to docs/reference_guides/core/util/tags.rst diff --git a/docs/technical_specs/core/util/unit_costing.rst b/docs/reference_guides/core/util/unit_costing.rst similarity index 100% rename from docs/technical_specs/core/util/unit_costing.rst rename to docs/reference_guides/core/util/unit_costing.rst diff --git a/docs/technical_specs/core/util/utility_minimization.rst b/docs/reference_guides/core/util/utility_minimization.rst similarity index 100% rename from docs/technical_specs/core/util/utility_minimization.rst rename to docs/reference_guides/core/util/utility_minimization.rst diff --git a/docs/advanced_user_guide/developer/codereview.rst b/docs/reference_guides/developer/codereview.rst similarity index 100% rename from docs/advanced_user_guide/developer/codereview.rst rename to docs/reference_guides/developer/codereview.rst diff --git a/docs/advanced_user_guide/developer/devsw.rst b/docs/reference_guides/developer/devsw.rst similarity index 100% rename from docs/advanced_user_guide/developer/devsw.rst rename to docs/reference_guides/developer/devsw.rst diff --git a/docs/advanced_user_guide/developer/docker.rst b/docs/reference_guides/developer/docker.rst similarity index 100% rename from docs/advanced_user_guide/developer/docker.rst rename to docs/reference_guides/developer/docker.rst diff --git a/docs/advanced_user_guide/developer/glossary.rst b/docs/reference_guides/developer/glossary.rst similarity index 100% rename from docs/advanced_user_guide/developer/glossary.rst rename to docs/reference_guides/developer/glossary.rst diff --git a/docs/advanced_user_guide/developer/howto.rst b/docs/reference_guides/developer/howto.rst similarity index 100% rename from docs/advanced_user_guide/developer/howto.rst rename to docs/reference_guides/developer/howto.rst diff --git a/docs/advanced_user_guide/developer/index.rst b/docs/reference_guides/developer/index.rst similarity index 100% rename from docs/advanced_user_guide/developer/index.rst rename to docs/reference_guides/developer/index.rst diff --git a/docs/advanced_user_guide/developer/repos.rst b/docs/reference_guides/developer/repos.rst similarity index 100% rename from docs/advanced_user_guide/developer/repos.rst rename to docs/reference_guides/developer/repos.rst diff --git a/docs/advanced_user_guide/developer/standards.rst b/docs/reference_guides/developer/standards.rst similarity index 69% rename from docs/advanced_user_guide/developer/standards.rst rename to docs/reference_guides/developer/standards.rst index 0eb4d60aee..cef2726b8f 100644 --- a/docs/advanced_user_guide/developer/standards.rst +++ b/docs/reference_guides/developer/standards.rst @@ -1,19 +1,19 @@ Developer Standards =================== -.. contents:: Contents +.. contents:: Contents :depth: 3 Model Formatting and General Standards -------------------------------------- -The section describes the recommended formatting used within the IDAES framework. Users are -strongly encouraged to follow these standards in developing their models in order to improve +The section describes the recommended formatting used within the IDAES framework. Users are +strongly encouraged to follow these standards in developing their models in order to improve readability of their code. Headers and Meta-data ^^^^^^^^^^^^^^^^^^^^^ -Model developers are encouraged to include some documentation in the header of their model -files which provides a brief description of the purpose of the model and how it was developed. +Model developers are encouraged to include some documentation in the header of their model +files which provides a brief description of the purpose of the model and how it was developed. Some suggested information to include is: * Model name, @@ -28,46 +28,46 @@ All code developed as part of IDAES should conform to the PEP-8 standard. Model Organization ^^^^^^^^^^^^^^^^^^ -Whilst the overall IDAES modeling framework enforces a hierarchical structure on models, model -developers are still encouraged to arrange their models in a logical fashion to aid other users -in understanding the model. Model constraints should be grouped with similar constraints, and -each grouping of constraints should be clearly commented. +Whilst the overall IDAES modeling framework enforces a hierarchical structure on models, model +developers are still encouraged to arrange their models in a logical fashion to aid other users +in understanding the model. Model constraints should be grouped with similar constraints, and +each grouping of constraints should be clearly commented. -For property packages, it is recommended that all the equations necessary for calculating a +For property packages, it is recommended that all the equations necessary for calculating a given property be grouped together, clearly separated and identified by using comments. -Additionally, model developers are encouraged to consider breaking their model up into a number -of smaller methods where this makes sense. This can facilitate modification of the code by -allowing future users to inherit from the base model and selectively overload sub-methods where +Additionally, model developers are encouraged to consider breaking their model up into a number +of smaller methods where this makes sense. This can facilitate modification of the code by +allowing future users to inherit from the base model and selectively overload sub-methods where desired. Commenting ^^^^^^^^^^ -To help other modelers and users understand the how a model works, model builders are strongly -encouraged to comment their code. It is suggested that every constraint should be commented -with a description of the purpose of the constraint, and if possible/necessary a reference to a -source or more detailed explanation. Any deviations from standard units or formatting should be -clearly identified here. Any initialization procedures, or other procedures required to get the -model to converge should be clearly commented and explained where they appear in the code. -Additionally, modelers are strongly encouraged to add additional comments explaining how their +To help other modelers and users understand the how a model works, model builders are strongly +encouraged to comment their code. It is suggested that every constraint should be commented +with a description of the purpose of the constraint, and if possible/necessary a reference to a +source or more detailed explanation. Any deviations from standard units or formatting should be +clearly identified here. Any initialization procedures, or other procedures required to get the +model to converge should be clearly commented and explained where they appear in the code. +Additionally, modelers are strongly encouraged to add additional comments explaining how their model works to aid others in understanding the model. Units of Measurement and Reference States ----------------------------------------- -Due to the flexibility provided by the IDAES modeling framework, there is no standard set of -units of measurement or standard reference state that should be used in models. This places the -onus on the user to understand the units of measurement being used within their models and to +Due to the flexibility provided by the IDAES modeling framework, there is no standard set of +units of measurement or standard reference state that should be used in models. This places the +onus on the user to understand the units of measurement being used within their models and to ensure that they are consistent. -The standard units and reference states are described in the -:ref:`user guide`. +The standard units and reference states are described in the +:ref:`user guide`. Standard Variable Names ----------------------- The standard variable names are described in the -:ref:`user guide`. +:ref:`user guide`. Testing ------- -The testing standards are included :ref:`here`. +The testing standards are included :ref:`here`. diff --git a/docs/advanced_user_guide/developer/testing.rst b/docs/reference_guides/developer/testing.rst similarity index 100% rename from docs/advanced_user_guide/developer/testing.rst rename to docs/reference_guides/developer/testing.rst diff --git a/docs/reference_guides/index.rst b/docs/reference_guides/index.rst new file mode 100644 index 0000000000..011e668e31 --- /dev/null +++ b/docs/reference_guides/index.rst @@ -0,0 +1,13 @@ +Reference Guides +================= + +.. toctree:: + :maxdepth: 2 + + model_libraries/index + core/index + commands/index + configuration + logging + developer/index + API Reference <../apidoc/modules> \ No newline at end of file diff --git a/docs/user_guide/logging.rst b/docs/reference_guides/logging.rst similarity index 100% rename from docs/user_guide/logging.rst rename to docs/reference_guides/logging.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/dyn_TGA_example.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/dyn_TGA_example.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/dyn_TGA_example.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/dyn_TGA_example.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_OC_oxidation.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_OC_oxidation.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_OC_oxidation.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_OC_oxidation.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/heterogeneous_reaction_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/heterogeneous_reaction_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/heterogeneous_reaction_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/heterogeneous_reaction_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/gas_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/gas_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/gas_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/gas_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/heterogeneous_reaction_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/heterogeneous_reaction_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/heterogeneous_reaction_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/heterogeneous_reaction_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/solid_properties.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/solid_properties.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/solid_properties.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/solid_properties.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/bubbling_fluidized_bed.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/bubbling_fluidized_bed.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/bubbling_fluidized_bed.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/bubbling_fluidized_bed.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/fixed_bed_0D.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/fixed_bed_0D.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/fixed_bed_0D.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/fixed_bed_0D.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/index.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/index.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/index.rst diff --git a/docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/moving_bed.rst b/docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/moving_bed.rst similarity index 100% rename from docs/technical_specs/model_libraries/gas_solid_contactors/unit_models/moving_bed.rst rename to docs/reference_guides/model_libraries/gas_solid_contactors/unit_models/moving_bed.rst diff --git a/docs/technical_specs/model_libraries/generic/control/index.rst b/docs/reference_guides/model_libraries/generic/control/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/control/index.rst rename to docs/reference_guides/model_libraries/generic/control/index.rst diff --git a/docs/technical_specs/model_libraries/generic/control/pid.rst b/docs/reference_guides/model_libraries/generic/control/pid.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/control/pid.rst rename to docs/reference_guides/model_libraries/generic/control/pid.rst diff --git a/docs/technical_specs/model_libraries/generic/index.rst b/docs/reference_guides/model_libraries/generic/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/index.rst rename to docs/reference_guides/model_libraries/generic/index.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/activity_coefficient.rst b/docs/reference_guides/model_libraries/generic/property_models/activity_coefficient.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/property_models/activity_coefficient.rst rename to docs/reference_guides/model_libraries/generic/property_models/activity_coefficient.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/ceos.rst b/docs/reference_guides/model_libraries/generic/property_models/ceos.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/property_models/ceos.rst rename to docs/reference_guides/model_libraries/generic/property_models/ceos.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/helmholtz.rst b/docs/reference_guides/model_libraries/generic/property_models/helmholtz.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/property_models/helmholtz.rst rename to docs/reference_guides/model_libraries/generic/property_models/helmholtz.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/iapws95.rst b/docs/reference_guides/model_libraries/generic/property_models/iapws95.rst similarity index 91% rename from docs/technical_specs/model_libraries/generic/property_models/iapws95.rst rename to docs/reference_guides/model_libraries/generic/property_models/iapws95.rst index c7dc6da30f..4af72589d7 100644 --- a/docs/technical_specs/model_libraries/generic/property_models/iapws95.rst +++ b/docs/reference_guides/model_libraries/generic/property_models/iapws95.rst @@ -16,14 +16,14 @@ the critical point, a feature which is undesirable in optimization problems. The IDAES implementation provides features which make the water and steam property calculations amenable to rigorous mathematical optimization. -Please see the :ref:`general Helmholtz documentation ` +Please see the :ref:`general Helmholtz documentation ` for more information. Example ------- The Heater unit model -:ref:`example `, +:ref:`example `, provides a simple example for using water properties. .. testcode:: @@ -65,14 +65,14 @@ viscosity of both phases, the lines below could be added. mu_v = pe.value(model.fs.heater.control_volume.properties_out[0].visc_d_phase["Vap"]) For more information about how StateBlocks and PropertyParameterBlocks work see -the :ref:`StateBlock documentation `. Expressions ----------- The IAPWS-95 property package contains the standard expressions described in -the :ref:`general Helmholtz documentation `, +the :ref:`general Helmholtz documentation `, but it also defines expressions for transport properties. ==================================== ===================================================== diff --git a/docs/technical_specs/model_libraries/generic/property_models/index.rst b/docs/reference_guides/model_libraries/generic/property_models/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/property_models/index.rst rename to docs/reference_guides/model_libraries/generic/property_models/index.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/interrogator.rst b/docs/reference_guides/model_libraries/generic/property_models/interrogator.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/property_models/interrogator.rst rename to docs/reference_guides/model_libraries/generic/property_models/interrogator.rst diff --git a/docs/technical_specs/model_libraries/generic/property_models/swco2.rst b/docs/reference_guides/model_libraries/generic/property_models/swco2.rst similarity index 87% rename from docs/technical_specs/model_libraries/generic/property_models/swco2.rst rename to docs/reference_guides/model_libraries/generic/property_models/swco2.rst index fbb1b7f996..da57ac36b3 100644 --- a/docs/technical_specs/model_libraries/generic/property_models/swco2.rst +++ b/docs/reference_guides/model_libraries/generic/property_models/swco2.rst @@ -7,14 +7,14 @@ Span-Wager CO2 .. module:: idaes.generic_models.properties.swco2 This implements the Span-Wagner equation of state for CO2 :ref:`"Span-Wagner equation of state for CO2" ` -Please see the :ref:`general Helmholtz documentation ` +Please see the :ref:`general Helmholtz documentation ` for more information. Example ------- The Heater unit model -:ref:`example `, +:ref:`example `, provides a simple example for using water properties. .. testcode:: @@ -46,14 +46,14 @@ provides a simple example for using water properties. For more information about how StateBlocks and PropertyParameterBlocks work see -the :ref:`StateBlock documentation `. Expressions ----------- The Span-Wager property package contains the standard expressions described in -the :ref:`general Helmholtz documentation `, +the :ref:`general Helmholtz documentation `, but it also defines expressions for transport properties. ==================================== ===================================================== diff --git a/docs/technical_specs/model_libraries/generic/unit_models/compressor.rst b/docs/reference_guides/model_libraries/generic/unit_models/compressor.rst similarity index 78% rename from docs/technical_specs/model_libraries/generic/unit_models/compressor.rst rename to docs/reference_guides/model_libraries/generic/unit_models/compressor.rst index 13140513e2..9d55ec1fb1 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/compressor.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/compressor.rst @@ -2,10 +2,10 @@ Compressor ========== The Compressor model is a -:ref:`PressureChanger `, +:ref:`PressureChanger `, where the configuration is set so that the "compressor" option can only be True, and the default "thermodynamic_assumption" is "isentropic." See the -:ref:`PressureChanger documentation ` +:ref:`PressureChanger documentation ` for details. Example diff --git a/docs/technical_specs/model_libraries/generic/unit_models/cstr.rst b/docs/reference_guides/model_libraries/generic/unit_models/cstr.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/cstr.rst rename to docs/reference_guides/model_libraries/generic/unit_models/cstr.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/equilibrium.rst b/docs/reference_guides/model_libraries/generic/unit_models/equilibrium.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/equilibrium.rst rename to docs/reference_guides/model_libraries/generic/unit_models/equilibrium.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/feed.rst b/docs/reference_guides/model_libraries/generic/unit_models/feed.rst similarity index 70% rename from docs/technical_specs/model_libraries/generic/unit_models/feed.rst rename to docs/reference_guides/model_libraries/generic/unit_models/feed.rst index 114ce34e0e..2cc86f55f5 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/feed.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/feed.rst @@ -1,7 +1,7 @@ Feed Block ========== -Feed Blocks are used to represent sources of material in Flowsheets. Feed blocks do not require calculation of the phase equilibrium of the feed stream (some property packages, like the :ref:`Generic Property Package ` with a :ref:`FTPx ` state definition, always flash regardless), and the composition of the material in the outlet stream will be exactly as specified in the input. For applications where the users wishes the outlet stream to be in phase equilibrium, see the Feed_Flash unit model. +Feed Blocks are used to represent sources of material in Flowsheets. Feed blocks do not require calculation of the phase equilibrium of the feed stream (some property packages, like the :ref:`Generic Property Package ` with a :ref:`FTPx ` state definition, always flash regardless), and the composition of the material in the outlet stream will be exactly as specified in the input. For applications where the users wishes the outlet stream to be in phase equilibrium, see the Feed_Flash unit model. Degrees of Freedom ------------------ diff --git a/docs/technical_specs/model_libraries/generic/unit_models/feed_flash.rst b/docs/reference_guides/model_libraries/generic/unit_models/feed_flash.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/feed_flash.rst rename to docs/reference_guides/model_libraries/generic/unit_models/feed_flash.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/flash.rst b/docs/reference_guides/model_libraries/generic/unit_models/flash.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/flash.rst rename to docs/reference_guides/model_libraries/generic/unit_models/flash.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/gibbs_reactor.rst b/docs/reference_guides/model_libraries/generic/unit_models/gibbs_reactor.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/gibbs_reactor.rst rename to docs/reference_guides/model_libraries/generic/unit_models/gibbs_reactor.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger.rst b/docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger.rst similarity index 94% rename from docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger.rst rename to docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger.rst index 85bea35228..2388fae9c2 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger.rst @@ -70,7 +70,7 @@ side. Aside from the sign convention there is no requirement that the hot side than the cold side. The control volumes are configured the same as the ``ControlVolume0DBlock`` in the -:ref:`Heater model `. The ``HeatExchanger`` model contains additional +:ref:`Heater model `. The ``HeatExchanger`` model contains additional constraints that calculate the amount of heat transferred from the hot side to the cold side. The ``HeatExchanger`` has two inlet ports and two outlet ports. By default these are @@ -95,7 +95,7 @@ than the specified hot side this value will be negative. Constraints ----------- -The default constants can be overridden by providing :ref:`alternative rules ` for +The default constants can be overridden by providing :ref:`alternative rules ` for the heat transfer equation, temperature difference, and heat transfer coefficient. The section describes the default constraints. @@ -117,7 +117,7 @@ Class Documentation .. Note:: The ``hot_side_config`` and ``cold_side_config`` can also be supplied using the name of - the hot and cold sides (``shell`` and ``tube`` by default) as in :ref:`the example `. + the hot and cold sides (``shell`` and ``tube`` by default) as in :ref:`the example `. .. autoclass:: HeatExchanger :members: diff --git a/docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger_1D.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger_1D.rst rename to docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger_1D.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger_ntu.rst b/docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger_ntu.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/heat_exchanger_ntu.rst rename to docs/reference_guides/model_libraries/generic/unit_models/heat_exchanger_ntu.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/heater.rst b/docs/reference_guides/model_libraries/generic/unit_models/heater.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/heater.rst rename to docs/reference_guides/model_libraries/generic/unit_models/heater.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/index.rst b/docs/reference_guides/model_libraries/generic/unit_models/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/index.rst rename to docs/reference_guides/model_libraries/generic/unit_models/index.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/mixer.rst b/docs/reference_guides/model_libraries/generic/unit_models/mixer.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/mixer.rst rename to docs/reference_guides/model_libraries/generic/unit_models/mixer.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/pfr.rst b/docs/reference_guides/model_libraries/generic/unit_models/pfr.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/pfr.rst rename to docs/reference_guides/model_libraries/generic/unit_models/pfr.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/pressure_changer.rst b/docs/reference_guides/model_libraries/generic/unit_models/pressure_changer.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/pressure_changer.rst rename to docs/reference_guides/model_libraries/generic/unit_models/pressure_changer.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/product.rst b/docs/reference_guides/model_libraries/generic/unit_models/product.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/product.rst rename to docs/reference_guides/model_libraries/generic/unit_models/product.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/pump.rst b/docs/reference_guides/model_libraries/generic/unit_models/pump.rst similarity index 77% rename from docs/technical_specs/model_libraries/generic/unit_models/pump.rst rename to docs/reference_guides/model_libraries/generic/unit_models/pump.rst index d5a596cf8d..b63e8b3f0b 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/pump.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/pump.rst @@ -2,10 +2,10 @@ Pump ==== The Pump model is a -:ref:`PressureChanger `, +:ref:`PressureChanger `, where the configuration is set so that the "compressor" option can only be True, and the default "thermodynamic_assumption" is "pump." See the -:ref:`PressureChanger documentation ` +:ref:`PressureChanger documentation ` for details. diff --git a/docs/technical_specs/model_libraries/generic/unit_models/separator.rst b/docs/reference_guides/model_libraries/generic/unit_models/separator.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/separator.rst rename to docs/reference_guides/model_libraries/generic/unit_models/separator.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/skeleton_unit.rst b/docs/reference_guides/model_libraries/generic/unit_models/skeleton_unit.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/skeleton_unit.rst rename to docs/reference_guides/model_libraries/generic/unit_models/skeleton_unit.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/statejunction.rst b/docs/reference_guides/model_libraries/generic/unit_models/statejunction.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/statejunction.rst rename to docs/reference_guides/model_libraries/generic/unit_models/statejunction.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/stoichiometric_reactor.rst b/docs/reference_guides/model_libraries/generic/unit_models/stoichiometric_reactor.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/stoichiometric_reactor.rst rename to docs/reference_guides/model_libraries/generic/unit_models/stoichiometric_reactor.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/translator.rst b/docs/reference_guides/model_libraries/generic/unit_models/translator.rst similarity index 100% rename from docs/technical_specs/model_libraries/generic/unit_models/translator.rst rename to docs/reference_guides/model_libraries/generic/unit_models/translator.rst diff --git a/docs/technical_specs/model_libraries/generic/unit_models/turbine.rst b/docs/reference_guides/model_libraries/generic/unit_models/turbine.rst similarity index 79% rename from docs/technical_specs/model_libraries/generic/unit_models/turbine.rst rename to docs/reference_guides/model_libraries/generic/unit_models/turbine.rst index 3455440268..a682db7ebd 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/turbine.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/turbine.rst @@ -2,10 +2,10 @@ Turbine ======= The Turbine model is a -:ref:`PressureChanger `, +:ref:`PressureChanger `, where the configuration is set so that the "compressor" option can only be False, and the default "thermodynamic_assumption" is "isentropic." See the -:ref:`PressureChanger documentation ` +:ref:`PressureChanger documentation ` for details. Example diff --git a/docs/technical_specs/model_libraries/generic/unit_models/valve.rst b/docs/reference_guides/model_libraries/generic/unit_models/valve.rst similarity index 97% rename from docs/technical_specs/model_libraries/generic/unit_models/valve.rst rename to docs/reference_guides/model_libraries/generic/unit_models/valve.rst index cb630ee314..eded1461d6 100644 --- a/docs/technical_specs/model_libraries/generic/unit_models/valve.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/valve.rst @@ -9,7 +9,7 @@ Valve This section describes the generic adiabatic valve model. By default the model is based on molar flow, but the pressure-flow equation and the flow basis is configurable. This model inherits the :ref:`PressureChanger model -` +` with the adiabatic options. Beyond the base pressure changer model this provides a pressure flow relation as a function of the valve opening fraction. @@ -135,7 +135,7 @@ Constraints ----------- The pressure flow relation is added to the inherited constraints from the :ref:`PressureChanger model -`. +`. The default pressure-flow relation is given below where :math:`F` is the molar flow. The default valve function assumes an incompressible fluid of constant density. In this case the diff --git a/docs/technical_specs/model_libraries/index.rst b/docs/reference_guides/model_libraries/index.rst similarity index 76% rename from docs/technical_specs/model_libraries/index.rst rename to docs/reference_guides/model_libraries/index.rst index cbe6d0b606..96c7c99395 100644 --- a/docs/technical_specs/model_libraries/index.rst +++ b/docs/reference_guides/model_libraries/index.rst @@ -1,5 +1,5 @@ -Model Libaries -============== +Model Libraries +=============== .. toctree:: :maxdepth: 2 diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/index.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/index.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/index.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/index.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/index.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/index.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/liquid_prop.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/liquid_prop.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/liquid_prop.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/liquid_prop.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/vapor_prop.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/vapor_prop.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/vapor_prop.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/properties/vapor_prop.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst similarity index 87% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst index 585c6d030e..985e11eb75 100644 --- a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst +++ b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column.rst @@ -74,13 +74,13 @@ Model Structure --------------- The ``PackedColumn`` unit model consists of two -:ref:`ControlVolume1D Blocks ` +:ref:`ControlVolume1D Blocks ` (named ``vapor_phase`` and ``liquid_phase``), each with one Inlet Port (named ``vapor_inlet`` and ``liquid_inlet``) and one Outlet Port (named ``vapor_outlet`` and ``liquid_outlet``) as shown in Figure 1(B). The ``vapor_phase`` ControlVolume1D Block uses the -:ref:`Vapor Phase Property Methods ` while the ``liquid_phase`` -ControlVolume1D Block block uses the :ref:`Liquid Phase Property Methods `. Both property packages are built -off of the :ref:`Physical Property Package Class `. +:ref:`Vapor Phase Property Methods ` while the ``liquid_phase`` +ControlVolume1D Block block uses the :ref:`Liquid Phase Property Methods `. Both property packages are built +off of the :ref:`Physical Property Package Class `. @@ -88,7 +88,7 @@ Additional Constraints (Performance Equations) ---------------------------------------------- The ``PackedColumn`` unit writes additional ``Constraints`` beyond those written -by the :ref:`ControlVolume1D Blocks ` +by the :ref:`ControlVolume1D Blocks ` to describe the reactive absorption / desorption process for post-combustion carbon capture using MEA solvent. diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_1.svg b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_1.svg similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_1.svg rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_1.svg diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_2.png b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_2.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_2.png rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/column_2.png diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/index.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/index.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst similarity index 95% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst index 97f297c90d..2065f64ee7 100644 --- a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst +++ b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe.rst @@ -49,8 +49,8 @@ Design Parameters and Variables: Model Structure --------------- -The Plate Heat Exchanger unit model builds off the core -:ref:`HeatExchangerNTU model `, +The Plate Heat Exchanger unit model builds off the core +:ref:`HeatExchangerNTU model `, and heat transfer is based on the Effectiveness-Number of Transfer Units (e-NTU method). diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_a.png b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_a.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_a.png rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_a.png diff --git a/docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_b.png b/docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_b.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_b.png rename to docs/reference_guides/model_libraries/power_generation/carbon_capture/mea_solvent_system/unit_models/phe_b.png diff --git a/docs/technical_specs/model_libraries/power_generation/costing/power_plant_costing.rst b/docs/reference_guides/model_libraries/power_generation/costing/power_plant_costing.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/costing/power_plant_costing.rst rename to docs/reference_guides/model_libraries/power_generation/costing/power_plant_costing.rst diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/Boiler_scpc_PFD.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/Boiler_scpc_PFD.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/Boiler_scpc_PFD.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/Boiler_scpc_PFD.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/SCPC_power_plant.rst b/docs/reference_guides/model_libraries/power_generation/flowsheets/SCPC_power_plant.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/SCPC_power_plant.rst rename to docs/reference_guides/model_libraries/power_generation/flowsheets/SCPC_power_plant.rst diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/coal_flowrate.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/coal_flowrate.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/coal_flowrate.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/coal_flowrate.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/coordinated_control.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/coordinated_control.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/coordinated_control.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/coordinated_control.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/drum_controller.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/drum_controller.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/drum_controller.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/drum_controller.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/index.rst b/docs/reference_guides/model_libraries/power_generation/flowsheets/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/index.rst rename to docs/reference_guides/model_libraries/power_generation/flowsheets/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/power_demand.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/power_demand.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/power_demand.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/power_demand.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/subcr_pc_PFD.png b/docs/reference_guides/model_libraries/power_generation/flowsheets/subcr_pc_PFD.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/subcr_pc_PFD.png rename to docs/reference_guides/model_libraries/power_generation/flowsheets/subcr_pc_PFD.png diff --git a/docs/technical_specs/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst b/docs/reference_guides/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst similarity index 83% rename from docs/technical_specs/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst rename to docs/reference_guides/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst index 0966b19fbc..a273eca4d6 100644 --- a/docs/technical_specs/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst +++ b/docs/reference_guides/model_libraries/power_generation/flowsheets/subcritical_power_plant.rst @@ -4,15 +4,15 @@ Subcritical Coal-Fired Power Plant Flowsheet (steady state and dynamic) .. currentmodule:: idaes.power_generation.flowsheets.subcritical_power_plant.subcritical_power_plant -.. note:: +.. note:: This is an example of a subcritical pulverized coal-fired power plant. This simulation model consists of a ~320 MW gross coal fired power plant, the dimensions and operating conditions used for this simulation do not represent any specific power plant. This model is for demonstration and tutorial purposes only. Introduction ++++++++++++ -A 320 MW gross subcritical coal-fired power plant is modeled using the unit model library has been developed for demonstration purposes only. -This plant simulation does not represent any power plant. This subcritical unit burns Illinois #6 high-volatile bituminous coal. +A 320 MW gross subcritical coal-fired power plant is modeled using the unit model library has been developed for demonstration purposes only. +This plant simulation does not represent any power plant. This subcritical unit burns Illinois #6 high-volatile bituminous coal. The fuel is identical to the NETL baseline case for a 650 MW unit and its analysis data are listed in Table 1. Table 1. coal specifications - Proximate Analysis (weight %) @@ -52,8 +52,8 @@ Higher Heating Value (HHV), kJ/kg (Btu/lb) 27,113 (11,666) 30,506 (13,126) Lower Heating Value (LHV), kJ/kg (Btu/lb) 26,151 (11,252) 29,544 (12,712) ========================================== ================ =============== -The power plant is a generic subcritical unit, where the boiler has 4 levels of wall burners and one level of overfire airports. -There are 18 platen superheaters hanging over the furnace roof serving as the finishing superheater. +The power plant is a generic subcritical unit, where the boiler has 4 levels of wall burners and one level of overfire airports. +There are 18 platen superheaters hanging over the furnace roof serving as the finishing superheater. The platen superheater panels are parallel to the furnace side walls. The boiler has one drum, eight downcomers, backpass superheater, platen superheater, a reheater section (represented with 2 heat exchanger model), a primary superheater, an economizer, and the air preheater (this model is a simplified tri-sector Ljungström type) The steam cycle equipment includes a multistage steam turbine with single reheat. It has a throttle valve, multiple stages for HP, IP, and LP sections with steam extraction to 3 low-pressure feed water heaters and 2 high-pressure feed water heaters as well as a deaerator and a boiler feed pump turbine. The steam cycle also includes the main and auxiliary condensers, a hotwell tank, a condensate pump, a booster pump and a main pump. Multiple control valves are used to control the water levels of hotwell tank, deaerator tank, and feed water heaters and the spray flow to main steam attemperator. @@ -68,8 +68,8 @@ Figure 1: Process Flow Diagram Property packages used: -* Helmholtz Equation of State (IAPWS95): Water and steam :ref:`IAWPS95 ` -* IDEAL GAS: Air and Flue Gas :ref:`FlueGas ` +* Helmholtz Equation of State (IAPWS95): Water and steam :ref:`IAWPS95 ` +* IDEAL GAS: Air and Flue Gas :ref:`FlueGas ` It can be seen from the Figure 1 that primary air is first split to two inlet streams, one goes through the primary air sector of the regenerative air preheater where it is heated, and the other, also known as tempering air, bypasses the air preheater and is used for primary air temperature control. The two streams are then mixed and connected with the fire side of the boiler. The coal stream is also fed to the fire side of the boiler. While this boiler sub-flowsheet does not contain a specific coal mill model, the partial vaporization of the moisture in the raw coal is modeled in the fire-side boiler model. The secondary air enters the secondary air sector of the air preheater. After being heated in the air preheater, the hot secondary air stream enters boiler’s windbox, from which it enters the furnace either as the secondary air of the burners or as overfire air. The pulverized coal from primary air stream is eventually burned in the boiler by both primary and secondary air to form flue gas that leaves the boiler with small amount of unburned fuel in fly ash. The hot flue gas then goes through the boiler backpass consisting of multiple convective heat exchangers including first the hot reheater, then the cold reheater, the primary superheater, and the economizer. Finally, the flue gas enters the air preheater to heat the cold primary air and secondary air before entering the downstream equipment which is not modeled. The feed water from steam cycle system enters the economizer to absorb the heat transferred from the flue gas and leaves the economizer at a temperature considerably below its saturation temperature. The subcooled water then goes to the boiler drum through water pipes where it mixes with the saturated water separated from the water/steam mixture from the boiler waterwall. The mixed water stream splits to two streams. A small amount of water leaves the system as a blowdown water to prevent buildup of slag and the main portion of the water stream goes through eight downcomers to enter the bottom of waterwall tubes. The vertical waterwall is modeled by multiple waterwall section models in series. The subcooled water from the downcomers is heated by the combustion products inside the boiler and part of the liquid are vaporized, forming a liquid-vapor 2-phase mixture and eventually enters the drum to complete a circuit for natural circulation of the feed water, in which the density difference between the liquid in the downcomers and the 2-phase mixture in the waterwall tubes drives the circulating flow. @@ -88,21 +88,21 @@ Table 4. List of unit models on the boiler system sub-flowsheet =============== ========================== ================================================================================================================================================================================= =========== Unit Name Description Unit Library Name Dynamic =============== ========================== ================================================================================================================================================================================= =========== -aBoiler Boiler fire-side surrogate :ref:`BoilerFireside ` False -aDrum 1D boiler drum :ref:`Drum1D ` True +aBoiler Boiler fire-side surrogate :ref:`BoilerFireside ` False +aDrum 1D boiler drum :ref:`Drum1D ` True blowdown_split Splitter for blowdown HelmSplitter False -aDowncomer Downcomer :ref:`Downcomer ` True -Waterwalls 12 waterwall zones :ref:`WaterwallSection ` True -aRoof Roof superheater :ref:`SteamHeater ` False -aPlaten Platen superheater :ref:`SteamHeater ` False -aRH1 2D Cold reheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * -aRH2 2D Hot reheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * -aPSH 2D Primary superheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * -aECON 2D Economizer HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * -aPipe Pipes from eco. to drum :ref:`WaterPipe ` False +aDowncomer Downcomer :ref:`Downcomer ` True +Waterwalls 12 waterwall zones :ref:`WaterwallSection ` True +aRoof Roof superheater :ref:`SteamHeater ` False +aPlaten Platen superheater :ref:`SteamHeater ` False +aRH1 2D Cold reheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * +aRH2 2D Hot reheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * +aPSH 2D Primary superheater HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * +aECON 2D Economizer HeatExchangerCrossFlow2D_Header :ref:`HX2D ` True * +aPipe Pipes from eco. to drum :ref:`WaterPipe ` False Mixer_PA Mixer of hot PA and TA Mixer False Attemp Attemperator HelmMixer False -aAPH Air preheater :ref:`HeatExchangerWith3Streams ` False +aAPH Air preheater :ref:`HeatExchangerWith3Streams ` False =============== ========================== ================================================================================================================================================================================= =========== * The heat held by tube metal is modeled as dynamic while fluids are modeled as steady-state @@ -112,34 +112,34 @@ Table 5. List of unit models on the steam cycle system sub-flowsheet ================= ============================= ======================================================================================================================================= =========== Unit Name Description Unit Library Name Dynamic ================= ============================= ======================================================================================================================================= =========== -turb Multistage turbine :ref:`HelmTurbineMultistage ` False -bfp_turb_valve BFPT regulating valve :ref:`HelmValve ` False -bfp_turb Front stage of BFPT :ref:`HelmTurbineStage ` False -bfp_turb_os Outlet stage of BFPT :ref:`HelmTurbineOutletStage ` False +turb Multistage turbine :ref:`HelmTurbineMultistage ` False +bfp_turb_valve BFPT regulating valve :ref:`HelmValve ` False +bfp_turb Front stage of BFPT :ref:`HelmTurbineStage ` False +bfp_turb_os Outlet stage of BFPT :ref:`HelmTurbineOutletStage ` False condenser Main condenser HelmNtuCondenser False aux_condenser Auxiliary condenser HelmNtuCondenser False condenser_hotwell Mixer of 3 water streams HelmMixer False -makeup_valve Makeup water valve :ref:`HelmValve ` False -hotwell_tank Hotwell tank :ref:`WaterTank ` Dynamic +makeup_valve Makeup water valve :ref:`HelmValve ` False +hotwell_tank Hotwell tank :ref:`WaterTank ` Dynamic cond_pump Condensate pump HelmIsentropicCompressor False cond_valve Condensate Valve HelmValve False -fwh1 Feed water heater 1 :ref:`FWH0D ` Dynamic +fwh1 Feed water heater 1 :ref:`FWH0D ` Dynamic fwh1_drain_pump Drain pump after FWH 1 HelmIsentropicCompressor False fwh1_drain_return Mixer of drain and condensate HelmMixer False -fwh2 Feed water heater 2 :ref:`FWH0D ` Dynamic -fwh2_valve Drain valve for FWH 2 :ref:`HelmValve ` False -fwh3 Feed water heater 3 :ref:`FWH0D ` Dynamic -Fwh3_valve Drain valve for FWH 3 :ref:`HelmValve ` False +fwh2 Feed water heater 2 :ref:`FWH0D ` Dynamic +fwh2_valve Drain valve for FWH 2 :ref:`HelmValve ` False +fwh3 Feed water heater 3 :ref:`FWH0D ` Dynamic +Fwh3_valve Drain valve for FWH 3 :ref:`HelmValve ` False fwh4_deair Mixer for deaerator HelmMixer False -da_tank Deserator water tank :ref:`WaterTank ` Dynamic +da_tank Deserator water tank :ref:`WaterTank ` Dynamic booster Booster pump HelmIsentropicCompressor False bfp Main boiler feed pump HelmIsentropicCompressor False split_attemp Splitter for spray water HelmSplitter False -spray_valve Control valve for water spray :ref:`HelmValve ` False -Fwh5 Feed water heater 5 :ref:`FWH0D ` Dynamic -Fwh5_valve Drain valve for FWH 5 :ref:`HelmValve ` False -Fwh6 Feed water heater 6 :ref:`FWH0D ` Dynamic -Fwh6_valve Drain valve for FWH 6 :ref:`HelmValve ` False +spray_valve Control valve for water spray :ref:`HelmValve ` False +Fwh5 Feed water heater 5 :ref:`FWH0D ` Dynamic +Fwh5_valve Drain valve for FWH 5 :ref:`HelmValve ` False +Fwh6 Feed water heater 6 :ref:`FWH0D ` Dynamic +Fwh6_valve Drain valve for FWH 6 :ref:`HelmValve ` False ================= ============================= ======================================================================================================================================= =========== * Dynamic flag is true for condensing section only @@ -190,7 +190,7 @@ boiler_master_ctrl Boiler master controller PI No Main Steady-state power plant example: +++++++++++++++++++++++++++++++++ -A steady state version of the power plant flowsheet is constructed by calling the `m_ss = main_steady_state()` method line 1824 in the subcritical_power_plant.py file. This function will build a steady state version of the power plant in Figure 1. This power plant model consists of two subflowsheets, the boiler subsystem (m_ss.fs_main.fs_blr) and the steam cycle subsystem (m_ss.fs_main.fs_stc. These two subsystems are connected trough arcs at the flowsheet level. +A steady state version of the power plant flowsheet is constructed by calling the `m_ss = main_steady_state()` method line 1824 in the subcritical_power_plant.py file. This function will build a steady state version of the power plant in Figure 1. This power plant model consists of two subflowsheets, the boiler subsystem (m_ss.fs_main.fs_blr) and the steam cycle subsystem (m_ss.fs_main.fs_stc. These two subsystems are connected trough arcs at the flowsheet level. A custom initialization procedure has been developed, in which we initialize each subflowsheet at the time at a given load. After initializing the subflowsheet the example solves the entire power plant model for a given load (degrees of freedom = 0). Main Fixed Variables: @@ -212,13 +212,13 @@ Main unfixed variables calculated by the model: Dynamic power plant example: ++++++++++++++++++++++++++++ -The dynamic simulation version of the power plant examples is built when the user calls the `m_dyn = main_dynamic()` method in line 1820 in the subcritical_power_plant.py file. The user should note that this method takes a long time to solve (~60 min). -This method builds and runs a subcritical coal-fired power plant dynamic simulation. The demonstration example prepared for this simulation consists of 5%/min ramping down from full load to 50% load, holding for 30 minutes and then ramping up to 100% load and holding for 20 minutes. +The dynamic simulation version of the power plant examples is built when the user calls the `m_dyn = main_dynamic()` method in line 1820 in the subcritical_power_plant.py file. The user should note that this method takes a long time to solve (~60 min). +This method builds and runs a subcritical coal-fired power plant dynamic simulation. The demonstration example prepared for this simulation consists of 5%/min ramping down from full load to 50% load, holding for 30 minutes and then ramping up to 100% load and holding for 20 minutes. -This method first creates a steady state version of the power plant, initializes the steady state model, and then uses this steady state model for initializing the dynamic model. -Two dynamic flowsheets are constructed here, the main difference is that they have different time steps in the discretization domain. Dynamic flowsheet 1 uses a step size of 30 seconds and dynamic flowsheet 2 uses a time step of 60 seconds. -This is useful to speed up the overall simulation time and to reduce the final number of variables and constraints. -Note that the dynamic flowsheet 1 is used when the load is changing to capture the dynamic transient conditions of the plant change (i.e., while plant is ramping). While the dynamic flowsheet 2 is used when process is near steady state transient conditions. +This method first creates a steady state version of the power plant, initializes the steady state model, and then uses this steady state model for initializing the dynamic model. +Two dynamic flowsheets are constructed here, the main difference is that they have different time steps in the discretization domain. Dynamic flowsheet 1 uses a step size of 30 seconds and dynamic flowsheet 2 uses a time step of 60 seconds. +This is useful to speed up the overall simulation time and to reduce the final number of variables and constraints. +Note that the dynamic flowsheet 1 is used when the load is changing to capture the dynamic transient conditions of the plant change (i.e., while plant is ramping). While the dynamic flowsheet 2 is used when process is near steady state transient conditions. To simulate the dynamic case, this example implements a ramp function for the power demand and fixes the set point to match the power demand (see code below). .. code:: python @@ -229,8 +229,8 @@ To simulate the dynamic case, this example implements a ramp function for the po Solving the dynamic model: -At this point we have a very large mathematical model, therefore, to exploit the temporal distribution of the model. The team implemented a rolling horizon approach (also known as receding horizon or moving time window), in which the full space model is divided into subproblems with 2 time periods each, then we solve the subproblem and use the solution of the previous subproblem to connect with the next time window (each time window consists of a dynamic model with 2 time periods). -Thus, the dynamic model with 2 time steps is solved based on the disturbance of load demand specified by the user (power demand function described above). If the time duration for the simulation is longer than the period of the 2 time steps, the results of the solved dynamic model at the end of the second time step will be copied as the initial condition for the simulation of the next 2 time steps. The results to be copied include the errors for the integral and derivative parts of individual controllers. In case the error term for the integral part of a controller is too large (windup error), the user has an option to reset the windup error. If the time step size is changed, the user needs to choose a different dynamic model to copy to (dynamic flowsheet 1 or dynamic flowsheet 2). After that the time window is rolled to the second one and the simulation for the second time period can then be solved. This process is repeated for multiple time periods until the entire duration for the dynamic simulation is solved. +At this point we have a very large mathematical model, therefore, to exploit the temporal distribution of the model. The team implemented a rolling horizon approach (also known as receding horizon or moving time window), in which the full space model is divided into subproblems with 2 time periods each, then we solve the subproblem and use the solution of the previous subproblem to connect with the next time window (each time window consists of a dynamic model with 2 time periods). +Thus, the dynamic model with 2 time steps is solved based on the disturbance of load demand specified by the user (power demand function described above). If the time duration for the simulation is longer than the period of the 2 time steps, the results of the solved dynamic model at the end of the second time step will be copied as the initial condition for the simulation of the next 2 time steps. The results to be copied include the errors for the integral and derivative parts of individual controllers. In case the error term for the integral part of a controller is too large (windup error), the user has an option to reset the windup error. If the time step size is changed, the user needs to choose a different dynamic model to copy to (dynamic flowsheet 1 or dynamic flowsheet 2). After that the time window is rolled to the second one and the simulation for the second time period can then be solved. This process is repeated for multiple time periods until the entire duration for the dynamic simulation is solved. Note that during each rolling time window simulation, the results at individual time step for the main performance variables and equipment health are saved. Those results are plotted after the last simulation and written to a text file for review and further processing. Figure 4 shows an example of dynamic model simulation result, the load demand (ramp function) and coal feed rate for as functions of time. diff --git a/docs/technical_specs/model_libraries/power_generation/index.rst b/docs/reference_guides/model_libraries/power_generation/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/index.rst rename to docs/reference_guides/model_libraries/power_generation/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/properties/flue_gas.rst b/docs/reference_guides/model_libraries/power_generation/properties/flue_gas.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/properties/flue_gas.rst rename to docs/reference_guides/model_libraries/power_generation/properties/flue_gas.rst diff --git a/docs/technical_specs/model_libraries/power_generation/properties/index.rst b/docs/reference_guides/model_libraries/power_generation/properties/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/properties/index.rst rename to docs/reference_guides/model_libraries/power_generation/properties/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler2D_1.png b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler2D_1.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler2D_1.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler2D_1.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler2D_2.png b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler2D_2.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler2D_2.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler2D_2.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_fireside.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_fireside.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler_fireside.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler_fireside.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst similarity index 93% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst index 78f9afe914..aa7edbaa90 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger.rst @@ -6,19 +6,19 @@ BoilerHeatExchanger .. currentmodule:: idaes.power_generation.unit_models.boiler_heat_exchanger -The BoilerHeatExchanger model can be used to represent boiler heat exchangers in -sub-critical and super critical power plant flowsheets (i.e. econmizer, primary superheater, secondary superheater, finishing superheater, reheater, etc.). -The model consists of a shell and tube crossflow heat exchanger, in which the shell is used as the gas side and the tube is used as the water or steam side. +The BoilerHeatExchanger model can be used to represent boiler heat exchangers in +sub-critical and super critical power plant flowsheets (i.e. econmizer, primary superheater, secondary superheater, finishing superheater, reheater, etc.). +The model consists of a shell and tube crossflow heat exchanger, in which the shell is used as the gas side and the tube is used as the water or steam side. Rigorous heat transfer calculations (convective heat transfer for shell side, and convective heat transfer for tube side) and shell and tube pressure drop calculations have been included. The BoilerHeatExchanger model can be imported from :code:`idaes.power_generation.unit_models`, while additional rules and utility functions can be imported from -``idaes.power_generation.unit_models.boiler_heat_exchanger``. +``idaes.power_generation.unit_models.boiler_heat_exchanger``. Example ------- -The example below demonstrates how to initialize the BoilerHeatExchanger model, +The example below demonstrates how to initialize the BoilerHeatExchanger model, and override the default temperature difference calculation. .. code:: python @@ -59,7 +59,7 @@ and override the default temperature difference calculation. "tube_arrangement": TubeArrangement.inLine, "side_1_water_phase": "Liq", "has_radiation": False}) - + # Set Inputs # BFW Boiler Feed Water inlet temeperature = 555 F = 563.706 K # inputs based on NETL Baseline Report v3 (SCPC 650 MW net, no carbon capture case) @@ -67,7 +67,7 @@ and override the default temperature difference calculation. m.fs.ECON.side_1_inlet.flow_mol[0].fix(24678.26) # mol/s m.fs.ECON.side_1_inlet.enth_mol[0].fix(h) m.fs.ECON.side_1_inlet.pressure[0].fix(2.5449e7) # Pa - + # FLUE GAS Inlet from Primary Superheater FGrate = 28.3876e3 # mol/s equivalent of ~1930.08 klb/hr # Use FG molar composition to set component flow rates (baseline report) @@ -83,13 +83,13 @@ and override the default temperature difference calculation. # economizer design variables and parameters ITM = 0.0254 # inch to meter conversion # Based on NETL Baseline Report Rev3 - m.fs.ECON.tube_di.fix((2-2*0.188)*ITM) # calc inner diameter + m.fs.ECON.tube_di.fix((2-2*0.188)*ITM) # calc inner diameter # (2 = outer diameter, thickness = 0.188) m.fs.ECON.tube_thickness.fix(0.188*ITM) # tube thickness m.fs.ECON.pitch_x.fix(3.5*ITM) # pitch_y = (54.5) gas path transverse width /columns - m.fs.ECON.pitch_y.fix(5.03*ITM) - m.fs.ECON.tube_length.fix(53.41*12*ITM) # use tube length (53.41 ft) + m.fs.ECON.pitch_y.fix(5.03*ITM) + m.fs.ECON.tube_length.fix(53.41*12*ITM) # use tube length (53.41 ft) m.fs.ECON.tube_nrow.fix(36*2.5) # use to match baseline performance m.fs.ECON.tube_ncol.fix(130) # 130 from NETL report m.fs.ECON.nrow_inlet.fix(2) @@ -107,7 +107,7 @@ and override the default temperature difference calculation. m.fs.ECON.fcorrection_dp_tube.fix(1.0) # correction factor for pressure drop calc shell side m.fs.ECON.fcorrection_dp_shell.fix(1.0) - + # Initialize the model m.fs.ECON.initialize() @@ -123,14 +123,14 @@ frequently fixed are two of: * temperature approach. In order to capture off design conditions and heat transfer coefficients at ramp up/down or load following conditions, the BoilerHeatExanger -model includes rigorous heat transfer calculations. Therefore, additional degrees of freedom are required to calculate Nusselt, Prandtl, Reynolds numbers, such as: +model includes rigorous heat transfer calculations. Therefore, additional degrees of freedom are required to calculate Nusselt, Prandtl, Reynolds numbers, such as: * tube_di (inner diameter) -* tube length +* tube length * tube number of rows (tube_nrow), columns (tube_ncol), and inlet flow (nrow_inlet) * pitch in x and y axis (pitch_x and pitch_y, respectively) -If pressure drop calculation is enabled, additional degrees of freedom are required: +If pressure drop calculation is enabled, additional degrees of freedom are required: * elevation with respect to ground level (delta_elevation) * tube fouling resistance (tube_r_fouling) @@ -145,8 +145,8 @@ The sign convention is that duty is positive for heat flowing from the hot side side. The control volumes are configured the same as the ``ControlVolume0DBlock`` in the -:ref:`Heater model `. -The ``BoilerHeatExchanger`` model contains additional constraints that calculate the amount +:ref:`Heater model `. +The ``BoilerHeatExchanger`` model contains additional constraints that calculate the amount of heat transferred from the hot side to the cold side. The ``BoilerHeatExchanger`` has two inlet ports and two outlet ports. By default these are @@ -171,9 +171,9 @@ than the specified hot side this value will be negative. Constraints ----------- -The default constraints can be overridden by providing :ref:`alternative rules -` for -the heat transfer equation, temperature difference, heat transfer coefficient, shell +The default constraints can be overridden by providing :ref:`alternative rules +` for +the heat transfer equation, temperature difference, heat transfer coefficient, shell and tube pressure drop. This section describes the default constraints. Heat transfer from shell to tube: @@ -191,7 +191,7 @@ The overall heat transfer coefficient is calculated as a function of convective Convective heat transfer equations: -.. math:: +.. math:: \frac{1}{U}*fcorrection_{htc} = [\frac{1}{hconv_{tube}} + \frac{1}{hconv_{shell}} + r + tube_{r fouling} + shell_{r fouling}] .. math:: @@ -232,30 +232,30 @@ where: * fcorrection_htc: correction factor for overall heat trasnfer * f_arrangement: tube arrangement factor -Note: +Note: by default fcorrection_htc is set to 1, however, this variable can be used to match unit performance (i.e. as a parameter estimation problem using real plant data). -Tube arrangement factor is a config argument with two different type of arrangements supported at the moment: +Tube arrangement factor is a config argument with two different type of arrangements supported at the moment: 1.- In-line tube arrangement factor (f_arrangement = 0.788), and 2.- Staggered tube arrangement factor (f_arrangement = 1). f_arrangement is a parameter that can be adjusted by the user. The ``BoilerHeatExchanger`` includes an argument to compute heat tranfer due to radiation of the flue gases. If has_radiation = True the model builds additional heat transfer calculations that will be added to the hconv_shell resistances. Radiation effects are calculated based on the gas gray fraction and gas-surface radiation (between gas and shell). -.. math:: +.. math:: Gas_{gray frac} = f (gas_{emissivity}) .. math:: frad_{gas gray frac} = f (wall_{emissivity}, gas_{emissivity}) -.. math:: +.. math:: hconv_{shell_rad} = f (k_{boltzmann}, frad_{gas gray frac}, T_{gas in}, T_{gas out}, T_{fluid in}, T_{fluid out}) -Note: +Note: Gas emissivity is calculated with surrogate models (see more details in boiler_heat_exchanger.py). -Radiation = True when flue gas temperatures are higher than 700 K (for example, when the model is used for units like Primary superheater, Reheater, or Finishing Superheater; +Radiation = True when flue gas temperatures are higher than 700 K (for example, when the model is used for units like Primary superheater, Reheater, or Finishing Superheater; while Radiation = False when the model is used to represent the economizer in a power plant flowsheet). -If pressure change is set to True, :math:`deltaP_{uturn} and friction_{factor}` are calculated +If pressure change is set to True, :math:`deltaP_{uturn} and friction_{factor}` are calculated Tube side: @@ -274,7 +274,7 @@ Shell side: .. math:: \Delta P_{shell} = 1.4 \Delta P_{shell friction} \rho V_{shell}^2 -:math:`\Delta P_{shell friction}` is calculated based on the tube arrangement type: +:math:`\Delta P_{shell friction}` is calculated based on the tube arrangement type: In-line: :math:`\Delta P_{shell friction} = \frac{ 0.044 + \frac{0.08 ( \frac{P_x}{tube_{do}} ) } {(\frac{P_y}{tube_{do}}-1)^{0.43+\frac{1.13}{(\frac{P_x}{tube_{do}})}}}}{Re^{0.15}}` @@ -295,8 +295,8 @@ Class Documentation .. Note:: The ``hot_side_config`` and ``cold_side_config`` can also be supplied using the name of - the hot and cold sides (``shell`` and ``tube`` by default) as in - :ref:`the example `. + the hot and cold sides (``shell`` and ``tube`` by default) as in + :ref:`the example `. .. autoclass:: BoilerHeatExchanger :members: diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst similarity index 94% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst index 46e3c2e00a..a0bad01ea9 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger2D.rst @@ -6,9 +6,9 @@ BoilerHeatExchanger2D .. currentmodule:: idaes.power_generation.unit_models.boiler_heat_exchanger_2D -The BoilerHeatExchanger2D model can be used to represent boiler heat exchangers in -sub-critical and super critical power plant flowsheets (i.e. econmizer, primary superheater, secondary superheater, finishing superheater, reheater, etc.). -The model consists of a shell and tube crossflow heat exchanger, in which the shell is used as the gas side and the tube is used as the water or steam side. +The BoilerHeatExchanger2D model can be used to represent boiler heat exchangers in +sub-critical and super critical power plant flowsheets (i.e. econmizer, primary superheater, secondary superheater, finishing superheater, reheater, etc.). +The model consists of a shell and tube crossflow heat exchanger, in which the shell is used as the gas side and the tube is used as the water or steam side. Due to the fluid temperature changes along the flow paths inside and outside of the tubes, the velocities of the fluids also change from the inlet to the outlet, causing the changes of heat transfer coefficients and friction factors on both sides along the flow paths. If the flows on both sides can be discretized along the flow paths, local temperature difference between the hot and cold streams (the driving force for heat transfer), local heat transfer coefficients and local friction factors can be used and a more accurate model can be constructed. Figure 1 shows a schematic of the shell and tube cross-flow heat exchanger. In this figure, the hot fluid on the shell side flows from left to right while the cold fluid flows through the tubes up and down. Notice that the cold fluid may enter the tube bundle in multiple rows (2 rows shown in the figure) and flow in parallel. The dash lines show the discretization along the flow path of the hot shell-side flow. The dash lines also cut the tube side flow to multiple segments with the direction of the flow inside the tube switching in two neighboring segments. The flow properties such as heat transfer coefficients and friction factors are calculated in individual discretized elements. Meanwhile the overall flow configuration is either a co-current or counter-current. Counter-current configuration are shown in Figure 1. Since the tube-side flow switches direction from one discretized section to another, pressure drop due the U-turn is also modeled based on the loss coefficient of the U-turn. If the elevation changes between the tube inlet and outlet, the pressure change due to gravity for the tube side fluid is also modeled in each element. Rigorous heat transfer calculations (convective heat transfer for shell side, and convective heat transfer for tube side) and shell and tube pressure drop calculations have been included. @@ -19,7 +19,7 @@ Rigorous heat transfer calculations (convective heat transfer for shell side, an Cross-flow heat exchanger In a transient heat transfer process such as in a load ramping operating condition, tube metal wall contains internal energy and its change with time represents the accumulation term (source or sink) in the energy conservation equation. Due to the high density and high heat capacity of the tube metal, its energy holdup should not be ignored. In other words, the transient tube wall temperatures and its distribution along the wall thickness and along the flow path need to be solved. In addition to the discretization along the flow path direction, the discretization along the tube wall thickness at each discretized flow path section is required, which make the heat exchanger model a 2-D model. Besides, the temperature gradient along the tube thickness is also required to calculate the thermal stress and other equipment health related properties. -Figure 2 shows the discretization of tube wall temperature along the tube radius direction. +Figure 2 shows the discretization of tube wall temperature along the tube radius direction. The transient tube wall temperature T_(w,r) at each discretized radius r is calculated based on transient heat conduction equation(Eqn. 1), and in the cylindrical coordinate system the heat conduction equation is shown in equation 2. .. figure:: boiler2D_2.png @@ -32,28 +32,28 @@ where, T_(w,r) is the tube metal temperature, t is time, alfa is termal diffusiv The HeatExchangerCrossFlow2D_Header model can be imported from :code:`idaes.power_generation.unit_models`, while additional rules and utility functions can be imported from -``idaes.power_generation.unit_models.boiler_heat_exchanger2D``. +``idaes.power_generation.unit_models.boiler_heat_exchanger2D``. Degrees of Freedom ------------------ -The configuration variables for the 2-D heat exchanger model include the inside diameter of the tube and thickness of the tube. -They are used as parameters of the model and have to be declared for discretization in the radius direction. +The configuration variables for the 2-D heat exchanger model include the inside diameter of the tube and thickness of the tube. +They are used as parameters of the model and have to be declared for discretization in the radius direction. Once declared as configuration arguments, they are not allowed to change (immutable). Other configuration variables include “finite_elements” (the number of elements) in the flow path direction, “radial_elements” (the number of elements in radius direction, “tube_arrangement” for either staggered or in-line arrangement, “has_radiation” if shell-side radiation heat transfer is considered, and “flow_type” for either co-current or counter-current configuration. Additionally, has_header has been added as a configuration argument, when it is True, the health of the water/steam headers is calculated (see header section). The main input variables for the 2-D cross-flow heat exchanger model include design variables such as number of tube segments, number of tube columns, number of tube inlet rows, length of the tube in each segment (each pass), pitches in directions parallel and perpendicular to the shell fluid flow, and elevation change from tube inlet to tube outlet. The thermal and transport properties are also required as well as the mechanical properties if the equipment health model is used. Other required operating variables include fouling resistances on both tube and shell sides, tube wall emissivity if radiation model is turned on, and correction factors for heat transfer and pressure drops on both sides. Given the inlet conditions such as pressures, temperatures and flow rates on both sides, the outlet conditions will be predicted by the model. Meanwhile the temperature and pressure distributions along the flow path direction will be solved on both sides. The 2-D tube wall temperature distribution will also be solved. In order to capture off design conditions and heat transfer coefficients at ramp up/down or load following conditions, the BoilerHeatExchanger2D -model includes rigorous heat transfer calculations. Therefore, additional degrees of freedom are required to calculate Nusselt, Prandtl, Reynolds numbers, such as: +model includes rigorous heat transfer calculations. Therefore, additional degrees of freedom are required to calculate Nusselt, Prandtl, Reynolds numbers, such as: * tube_di (inner diameter) -* tube length +* tube length * tube number of rows (tube_nrow), columns (tube_ncol), and inlet flow (nrow_inlet) * pitch in x and y axis (pitch_x and pitch_y, respectively) -If pressure drop calculation is enabled, additional degrees of freedom are required: +If pressure drop calculation is enabled, additional degrees of freedom are required: * elevation with respect to ground level (delta_elevation) * tube fouling resistance (tube_r_fouling) @@ -68,8 +68,8 @@ The sign convention is that duty is positive for heat flowing from the hot side side. The control volumes are configured the same as the ``ControlVolume1DBlock`` in the -:ref:`Heater model `. -The ``HeatExchangerCrossFlow2D_Header`` model contains additional constraints that calculate the amount +:ref:`Heater model `. +The ``HeatExchangerCrossFlow2D_Header`` model contains additional constraints that calculate the amount of heat transferred from the hot side to the cold side. The ``HeatExchangerCrossFlow2D_Header`` has two inlet ports and two outlet ports. By default these are @@ -94,9 +94,9 @@ than the specified hot side this value will be negative. Constraints ----------- -The default constraints can be overridden by providing :ref:`alternative rules -` for -the heat transfer equation, temperature difference, heat transfer coefficient, shell +The default constraints can be overridden by providing :ref:`alternative rules +` for +the heat transfer equation, temperature difference, heat transfer coefficient, shell and tube pressure drop. This section describes the default constraints. Heat transfer from shell to tube: @@ -114,7 +114,7 @@ The overall heat transfer coefficient is calculated as a function of convective Convective heat transfer equations: -.. math:: +.. math:: \frac{1}{U}*fcorrection_{htc} = [\frac{1}{hconv_{tube}} + \frac{1}{hconv_{shell}} + r + tube_{r fouling} + shell_{r fouling}] Tube convective heat transfer (for all elements in tube discretization approach): @@ -161,30 +161,30 @@ where: * fcorrection_htc: correction factor for overall heat trasnfer * f_arrangement: tube arrangement factor -Note: +Note: by default fcorrection_htc is set to 1, however, this variable can be used to match unit performance (i.e. as a parameter estimation problem using real plant data). -Tube arrangement factor is a config argument with two different type of arrangements supported at the moment: +Tube arrangement factor is a config argument with two different type of arrangements supported at the moment: 1.- In-line tube arrangement factor (f_arrangement = 0.788), and 2.- Staggered tube arrangement factor (f_arrangement = 1). f_arrangement is a parameter that can be adjusted by the user. The ``HeatExchangerCrossFlow2D_Header`` model includes an argument to compute heat tranfer due to radiation of the flue gases. If has_radiation = True the model builds additional heat transfer calculations that will be added to the hconv_shell resistances. Radiation effects are calculated based on the gas gray fraction and gas-surface radiation (between gas and shell). -.. math:: +.. math:: Gas_{gray frac} = f (gas_{emissivity}) .. math:: frad_{gas gray frac} = f (wall_{emissivity}, gas_{emissivity}) -.. math:: +.. math:: hconv_{shell_rad} = f (k_{boltzmann}, frad_{gas gray frac}, T_{gas in}, T_{gas out}, T_{fluid in}, T_{fluid out}) -Note: +Note: Gas emissivity is calculated with surrogate models (see more details in boiler_heat_exchanger.py). -Radiation = True when flue gas temperatures are higher than 700 K (for example, when the model is used for units like Primary superheater, Reheater, or Finishing Superheater; +Radiation = True when flue gas temperatures are higher than 700 K (for example, when the model is used for units like Primary superheater, Reheater, or Finishing Superheater; while Radiation = False when the model is used to represent the economizer in a power plant flowsheet). -If pressure change is set to True, :math:`deltaP_{uturn} and friction_{factor}` are calculated +If pressure change is set to True, :math:`deltaP_{uturn} and friction_{factor}` are calculated Tube side: @@ -203,7 +203,7 @@ Shell side: .. math:: \Delta P_{shell} = 1.4 \Delta P_{shell friction} \rho V_{shell}^2 -:math:`\Delta P_{shell friction}` is calculated based on the tube arrangement type: +:math:`\Delta P_{shell friction}` is calculated based on the tube arrangement type: In-line: :math:`\Delta P_{shell friction} = \frac{ 0.044 + \frac{0.08 ( \frac{P_x}{tube_{do}} ) } {(\frac{P_y}{tube_{do}}-1)^{0.43+\frac{1.13}{(\frac{P_x}{tube_{do}})}}}}{Re^{0.15}}` @@ -220,9 +220,9 @@ Figure. Tube Arrangement Header Health Model ------------------- -The heat exchanger 2D model allows the user to calculate the thermal and mechanical stresses of the water/steam headers connected to the outlet of the tube side. Additionally, the rupture time and fatigue calculation of allowable cycles are computed by the model. -A simplified 1D PDE problem is developed to represent the heat conduction transient through the radius of the superheater/reheater headers. -Regarding to the flow path configuration (counter-current or co-current) of the 2D heat exchanger, the first or the last discretization point will be used to define the boundary of the headers. +The heat exchanger 2D model allows the user to calculate the thermal and mechanical stresses of the water/steam headers connected to the outlet of the tube side. Additionally, the rupture time and fatigue calculation of allowable cycles are computed by the model. +A simplified 1D PDE problem is developed to represent the heat conduction transient through the radius of the superheater/reheater headers. +Regarding to the flow path configuration (counter-current or co-current) of the 2D heat exchanger, the first or the last discretization point will be used to define the boundary of the headers. For this example, the last discretization point will be used for the outlet superheater header due to the counter-current flow configuration. Under the assumptions of constant conductivity and no heat generation, the Fourier’s equation is converted to the Eq. (h1) for the cylindrical header. The Pyomo.DAE framework is applied to solve the PDE problem. The thermal and mechanical stresses are calculated based on the pressure and temperature difference between both sides of the header which can be used to evaluate the allowable number of cycles of the main body and the critical point of the edge of the hole. .. math:: \frac{1}{a} \frac{\partial T}{\partial t} = \frac{{\partial}^2 T}{\partial r^2} + \frac{1}{r} \frac{\partial T}{\partial r} @@ -231,12 +231,12 @@ where T is wall temperature (K), r is radius (m), and a is the material thermal Detailed description of the mechanical stress calculations and thermal stress calculations can be found in S. Bracco, 2012 and Taler & Duda, 2006, respectively. -Rupture time calculation: The creep phenomenon is an important design consideration in the analysis of structures. -At the high temperature operation, the creep is coupled with fatigue due to cycling, the damage will be much higher than that occurring if the same fatigue or creep is working alone. +Rupture time calculation: The creep phenomenon is an important design consideration in the analysis of structures. +At the high temperature operation, the creep is coupled with fatigue due to cycling, the damage will be much higher than that occurring if the same fatigue or creep is working alone. For example, a long-time creep rupture strength values can be derived by using the Manson-Haferd model. However, depending on the investigated material, users can find another correlation to calculate the rupture strength in the open literature. Fatigue calculation of allowable cycles: -For general ferritic and austenitic materials, the calculation of the allowable number of cycles are expressed in the following equation. However, the users can be recommended to find a specific fatigue equation for their own material to obtain a better result. +For general ferritic and austenitic materials, the calculation of the allowable number of cycles are expressed in the following equation. However, the users can be recommended to find a specific fatigue equation for their own material to obtain a better result. Using the calculated stresses above, the number of allowable cycles of the component can be evaluated based on fatigue assessment standard, such as EN 13445. The detail of the developed approach can be found in Bracco’s report (S. Bracco, 2012). This model can be applied for both drum and thick-walled components such as header. According to the EN 13445 standard, for a single cycle, the allowable number of fatigue cycles N can be computed as: .. math:: N = \frac{46000}{\Delta\Sigma R_i - 0.63 R_m + 11.5} diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst similarity index 93% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst index 6dee5f1d74..13a9ac5189 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/boiler_heat_exchanger_3streams.rst @@ -9,7 +9,7 @@ Heat Exchanger With Three Streams The HeatExchangerWith3Streams model consists of a heat exchanger with three inlets, `side_1` represents the hot stream, while `side_2` and `side_3` are cold streams. This model is a simplified generic heat exchanger model with lumped UA (the product of the overall heat transfer coefficient and the heat transfer area). -In a power plant flowsheet this model is used to represent an air preheater unit. This is because modeling the Ljungström type preheater is quite challenging since it involves not only the hot and cold gas streams but also the energy stored in and relased from the metal parts. +In a power plant flowsheet this model is used to represent an air preheater unit. This is because modeling the Ljungström type preheater is quite challenging since it involves not only the hot and cold gas streams but also the energy stored in and relased from the metal parts. Degrees of Freedom @@ -36,8 +36,8 @@ The sign convention is that duty is positive for heat flowing from the hot side side. The control volumes are configured the same as the ``ControlVolume0DBlock`` in the -:ref:`Heater model `. -The ``HeatExchangerWith3Streams`` model contains additional constraints that calculate the amount +:ref:`Heater model `. +The ``HeatExchangerWith3Streams`` model contains additional constraints that calculate the amount of heat transferred from the hot side to the cold side. The ``HeatExchangerWith3Streams`` has three inlet ports and three outlet ports. By default these are @@ -58,9 +58,9 @@ LMTD :math:`LMTD` time Log Mean Temperature Constraints ----------- -The default constraints can be overridden by providing :ref:`alternative rules -` for -the heat transfer equation, temperature difference, heat transfer coefficient, shell +The default constraints can be overridden by providing :ref:`alternative rules +` for +the heat transfer equation, temperature difference, heat transfer coefficient, shell and tube pressure drop. This section describes the default constraints. Heat transfer from hot to cold sides: diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/boilerfireside.png b/docs/reference_guides/model_libraries/power_generation/unit_models/boilerfireside.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/boilerfireside.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/boilerfireside.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/downcomer.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/downcomer.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/downcomer.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/downcomer.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/drum.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/drum.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/drum.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/drum.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/drum1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/drum1D.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/drum1D.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/drum1D.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/drum1D_1.png b/docs/reference_guides/model_libraries/power_generation/unit_models/drum1D_1.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/drum1D_1.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/drum1D_1.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/drum1D_2.png b/docs/reference_guides/model_libraries/power_generation/unit_models/drum1D_2.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/drum1D_2.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/drum1D_2.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/drum_1.png b/docs/reference_guides/model_libraries/power_generation/unit_models/drum_1.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/drum_1.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/drum_1.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst similarity index 83% rename from docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst index 74dfd857d7..9c7f3d58b3 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D.rst @@ -9,7 +9,7 @@ Feedwater Heater (0D) The FWH0D model is a 0D feedwater heater model suitable for steady state modeling. It is intended to be used primarily with the -:ref:`IAWPS95 ` property package. +:ref:`IAWPS95 ` property package. The feedwater heater is split into three sections the condensing section is required while the desuperheating and drain cooling sections are optional. There is also an optional mixer for adding a drain stream from another feedwater heater to the condensing section. The figure @@ -78,21 +78,21 @@ Model Structure --------------- The condensing section uses the -:ref:`FWHCondensing0D ` +:ref:`FWHCondensing0D ` model to calculate a steam flow rate such that all steam is condensed in the condensing section. This allows turbine steam extraction rates to be calculated. The other sections are regular -:ref:`HeatExchanger ` models. +:ref:`HeatExchanger ` models. The table below shows the unit models which make up the feedwater heater, and the option to include or exclude them. =========================== ====================== ==================================================================================================================================================================== Unit Option Doc =========================== ====================== ==================================================================================================================================================================== -``condense`` -- Condensing section (:ref:`FWHCondensing0D `) -``desuperheat`` ``has_desuperheat`` Desuperheating section (:ref:`HeatExchanger `) -``cooling`` ``has_drain_cooling`` Drain cooling section (:ref:`HeatExchanger `) -``drain_mix`` ``has_drain_mixer`` Mixer for steam and other FWH drain (:ref:`Mixer `) +``condense`` -- Condensing section (:ref:`FWHCondensing0D `) +``desuperheat`` ``has_desuperheat`` Desuperheating section (:ref:`HeatExchanger `) +``cooling`` ``has_drain_cooling`` Drain cooling section (:ref:`HeatExchanger `) +``drain_mix`` ``has_drain_mixer`` Mixer for steam and other FWH drain (:ref:`Mixer `) =========================== ====================== ==================================================================================================================================================================== diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D.svg b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D.svg similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D.svg rename to docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D.svg diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst similarity index 76% rename from docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst index c90e8efb36..94acb96128 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_0D_dynamic.rst @@ -7,12 +7,12 @@ Feedwater Heater Dynamic (0D) .. module:: idaes.power_generation.unit_models.feedwater_heater_0D_dynamic :noindex: -The FWH0DDynamic model is a 0D feedwater heater model suitable for dynamic modeling. -It is intended to be used primarily with the -:ref:`IAWPS95 ` property package. -The feedwater heater is split into three sections. The condensing section is required while -the desuperheating and drain cooling sections are optional. There is also an optional mixer -for adding a drain stream from another feedwater heater to the condensing section. The figure +The FWH0DDynamic model is a 0D feedwater heater model suitable for dynamic modeling. +It is intended to be used primarily with the +:ref:`IAWPS95 ` property package. +The feedwater heater is split into three sections. The condensing section is required while +the desuperheating and drain cooling sections are optional. There is also an optional mixer +for adding a drain stream from another feedwater heater to the condensing section. The figure below shows the layout of the feedwater heater. All but the condensing section are optional. .. figure:: feedwater_heater_0D.svg @@ -24,48 +24,48 @@ below shows the layout of the feedwater heater. All but the condensing section Features of Dynamic Model ------------------------- -The dynamic version of the 0-D feed water heater model is based on the steady-state feed water heater 0D model -:ref:`FWH0D `. -It contains additional variables related to the mass and energy inventories inside the condensing section of the feed water heater -on both the tube and the shell sides. The desuperheater and drain cooler sections, if any, are usually treated as steady-state. -For the condensing section, the tubes are always filled with condensate or feed water and the total volume of the liquid inside the tube is usually specified as a user input. -The shell side, however, is partially filled with saturated liquid water and the water level changes with time and so is the volume of the saturated water. -Only the horizontal design of the feed water heater is modeled here and it usually consists of a cylindrical tank with a fraction of its internal volume -occupied by the heat transfer tubes with the remaining gaps between the tubes occupied either by the steam or the saturated water. -The shell side of the condenser section is modeled as a horizontal cylinder similar to that in a drum model :ref:`Drum1D ` -with the total volume of the shell side liquid and the submerged tubes calculated from the water level (see the description of the drum model). -The volume of the saturated liquid is simply a fraction of the total volume. -Therefore, the additional input variables in the dynamic version of the feed water heater model include the inner diameter of the feed water heater cylinder, -the length of the condensing section, the fraction of the volume occupied by the shell side liquid (gaps between the tubes), -and the volume of the feed water inside the tubes of the condensing section. -Those input variables are constant (do not change with time) for a given design. In addition, -the water level defined as the distance from the bottom of the cylinder to the top of the water is also included in the model and indexed by time. -The dynamic version of the feed water heater model provides the constraint (equation) to calculate the volume of the shell-side liquid as -a function of the water level. The mass and energy accumulation terms are handled by the IDAES control volume class based on -the volumes of the tube-side and the shell-side liquids. Since the density of the steam on the shell side is much lower than the density of the liquid, +The dynamic version of the 0-D feed water heater model is based on the steady-state feed water heater 0D model +:ref:`FWH0D `. +It contains additional variables related to the mass and energy inventories inside the condensing section of the feed water heater +on both the tube and the shell sides. The desuperheater and drain cooler sections, if any, are usually treated as steady-state. +For the condensing section, the tubes are always filled with condensate or feed water and the total volume of the liquid inside the tube is usually specified as a user input. +The shell side, however, is partially filled with saturated liquid water and the water level changes with time and so is the volume of the saturated water. +Only the horizontal design of the feed water heater is modeled here and it usually consists of a cylindrical tank with a fraction of its internal volume +occupied by the heat transfer tubes with the remaining gaps between the tubes occupied either by the steam or the saturated water. +The shell side of the condenser section is modeled as a horizontal cylinder similar to that in a drum model :ref:`Drum1D ` +with the total volume of the shell side liquid and the submerged tubes calculated from the water level (see the description of the drum model). +The volume of the saturated liquid is simply a fraction of the total volume. +Therefore, the additional input variables in the dynamic version of the feed water heater model include the inner diameter of the feed water heater cylinder, +the length of the condensing section, the fraction of the volume occupied by the shell side liquid (gaps between the tubes), +and the volume of the feed water inside the tubes of the condensing section. +Those input variables are constant (do not change with time) for a given design. In addition, +the water level defined as the distance from the bottom of the cylinder to the top of the water is also included in the model and indexed by time. +The dynamic version of the feed water heater model provides the constraint (equation) to calculate the volume of the shell-side liquid as +a function of the water level. The mass and energy accumulation terms are handled by the IDAES control volume class based on +the volumes of the tube-side and the shell-side liquids. Since the density of the steam on the shell side is much lower than the density of the liquid, the mass and energy accumulations of the steam above the water level are ignored. -Note that the total heat transfer area and overall heat transfer coefficient are required inputs as in the steady-state model for the condensing section. -The overall heat transfer coefficient is dominated by the tube-side convective heat transfer coefficient since the shell-side heat transfer coefficient -is usually very high due to the phase change. Based on an empirical correlation (Bird et al, 1960), -the Nusselt number on the tube side is proportional to the Reynolds number to the power of 0.8. Therefore, the overall heat transfer coefficient is -approximately proportional to the feed water flow rate to the power of 0.8. +Note that the total heat transfer area and overall heat transfer coefficient are required inputs as in the steady-state model for the condensing section. +The overall heat transfer coefficient is dominated by the tube-side convective heat transfer coefficient since the shell-side heat transfer coefficient +is usually very high due to the phase change. Based on an empirical correlation (Bird et al, 1960), +the Nusselt number on the tube side is proportional to the Reynolds number to the power of 0.8. Therefore, the overall heat transfer coefficient is +approximately proportional to the feed water flow rate to the power of 0.8. A flowsheet level constraint can be imposed to account for the effect of feed water flow rate on the overall heat transfer coefficient. Initialial Condition of Dynamic Model ------------------------------------- -Typical initial condition for the dynamic model is a steady state condition. The user can call `set_initial_condition` function of the model to -initialize the variables related to the material and energy accumulation terms for the dynamic model. Note that the water level at the initial time +Typical initial condition for the dynamic model is a steady state condition. The user can call `set_initial_condition` function of the model to +initialize the variables related to the material and energy accumulation terms for the dynamic model. Note that the water level at the initial time usually should be fixed to ensure the inventories of mass and energy are well defined. Degrees of Freedom ------------------ -The ``area`` and ``overall_heat_transfer_coefficient`` should be fixed or constraints should be provided to calculate ``overall_heat_transfer_coefficient``. -In addition, the geometry variables related to the condensing section including ``heater_diameter``, ``cond_sect_length``, ``vol_frac_shell``, and ``tube.volume`` should be fixed. +The ``area`` and ``overall_heat_transfer_coefficient`` should be fixed or constraints should be provided to calculate ``overall_heat_transfer_coefficient``. +In addition, the geometry variables related to the condensing section including ``heater_diameter``, ``cond_sect_length``, ``vol_frac_shell``, and ``tube.volume`` should be fixed. The initial value of ``level`` should also be fixed. If the inlets are also fixed except for the inlet steam flow rate (``inlet_1.flow_mol``), the model will have 0 degrees of freedom. diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst similarity index 63% rename from docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst index 0ac7df082c..4db338dbb3 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/feedwater_heater_condensing_0D.rst @@ -7,10 +7,10 @@ Feedwater Heater (Condensing Section 0D) .. module:: idaes.power_generation.unit_models.feedwater_heater_0D The condensing feedwater heater is the same as the -:ref:`HeatExchanger ` +:ref:`HeatExchanger ` model with one additional constraint to calculate the inlet flow rate such that all the entering steam is condensed. This model is suitable for steady state modeling, and is -intended to be used with the :ref:`IAWPS95 ` +intended to be used with the :ref:`IAWPS95 ` property package. For dynamic modeling, the 1D feedwater heater models should be used (not yet publicly available). @@ -22,12 +22,12 @@ Usually ``area`` and ``overall_heat_transfer_coefficient`` are fixed or constrai Variables --------- -The variables are the same as :ref:`HeatExchanger `. +The variables are the same as :ref:`HeatExchanger `. Constraints ----------- -In addition to the :ref:`HeatExchanger ` constraints, there is one additional constraint to calculate the inlet steam flow such that all steam condenses. The constraint is called ``extraction_rate_constraint``, and is defined below. +In addition to the :ref:`HeatExchanger ` constraints, there is one additional constraint to calculate the inlet steam flow such that all steam condenses. The constraint is called ``extraction_rate_constraint``, and is defined below. .. math:: diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/index.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/index.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/index.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/index.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/phase_separator.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/phase_separator.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/phase_separator.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/phase_separator.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/steam_valve.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/steam_valve.rst similarity index 97% rename from docs/technical_specs/model_libraries/power_generation/unit_models/steam_valve.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/steam_valve.rst index c4a880aa1b..24801c0972 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/steam_valve.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/steam_valve.rst @@ -67,7 +67,7 @@ Constraints ----------- The pressure flow relation is added to the inherited constraints from the :ref:`PressureChanger model -`. +`. If the ``phase`` option is set to ``"Liq"`` the following equation describes the pressure-flow relation. diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/steamheater.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/steamheater.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/steamheater.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/steamheater.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/tube_arrangement.png b/docs/reference_guides/model_libraries/power_generation/unit_models/tube_arrangement.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/tube_arrangement.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/tube_arrangement.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine.rst similarity index 90% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine.rst index 3d730adfe4..d9721d12e4 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine.rst @@ -8,12 +8,12 @@ Turbine (Isentropic) This is a steam power generation turbine model for the basic isentropic turbine calculations. It is the basis of the :ref:`TurbineInletStage -`, +`, `TurbineOutletStage -`, +`, and, `TurbineOutletStage -` +` models. Variables diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_inlet.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_inlet.rst similarity index 94% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine_inlet.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine_inlet.rst index 01811445ee..164c4888ad 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_inlet.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_inlet.rst @@ -8,7 +8,7 @@ Turbine (Inlet Stage) This is a steam power generation turbine model for the inlet stage. It inherits `HelmIsentropicTurbine -`. +`. The turbine inlet model is based on: @@ -54,9 +54,9 @@ Model Structure --------------- The turbine inlet stage model contains one :ref:`ControlVolume0DBlock block -` called control\_volume and +` called control\_volume and inherits `HelmIsentropicTurbine -`. +`. Variables --------- @@ -105,7 +105,7 @@ Constraints ----------- In addition to the constraints inherited from the `HelmTurbineStage -`, +`, this model contains two more constraints, one to estimate efficiency and one pressure-flow relation. From the isentropic pressure changer model, these constraints eliminate the need to specify efficiency and either inlet flow or diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_multistage.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_multistage.rst similarity index 87% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine_multistage.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine_multistage.rst index 150ea51957..9758027777 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_multistage.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_multistage.rst @@ -140,17 +140,17 @@ The multistage turbine model contains the models in the table below. The splitt =========================== ==================== ====================================================================================================================================================== Unit Index Sets Doc =========================== ==================== ====================================================================================================================================================== -``inlet_split`` None Splitter to split the main steam feed into steams for each arc (:ref:`Separator `) -``throttle_valve`` Admission Arcs Throttle valves for each admission arc (:ref:`HelmValve `) -``inlet_stage`` Admission Arcs Parallel inlet turbine stages that represent admission arcs (:ref:`TurbineInlet `) -``inlet_mix`` None Mixer to combine the streams from each arc back to one stream (:ref:`Mixer `) -``hp_stages`` HP stages Turbine stages in the high-pressure section (:ref:`TurbineStage `) -``ip_stages`` IP stages Turbine stages in the intermediate-pressure section (:ref:`TurbineStage `) -``lp_stages`` LP stages Turbine stages in the low-pressure section (:ref:`TurbineStage `) -``hp_splits`` subset of HP stages Extraction splitters in the high-pressure section (:ref:`Separator `) -``ip_splits`` subset of IP stages Extraction splitters in the high-pressure section (:ref:`Separator `) -``lp_splits`` subset of LP stages Extraction splitters in the high-pressure section (:ref:`Separator `) -``outlet_stage`` None The final stage in the turbine, which calculates exhaust losses (:ref:`TurbineOutlet `) +``inlet_split`` None Splitter to split the main steam feed into steams for each arc (:ref:`Separator `) +``throttle_valve`` Admission Arcs Throttle valves for each admission arc (:ref:`HelmValve `) +``inlet_stage`` Admission Arcs Parallel inlet turbine stages that represent admission arcs (:ref:`TurbineInlet `) +``inlet_mix`` None Mixer to combine the streams from each arc back to one stream (:ref:`Mixer `) +``hp_stages`` HP stages Turbine stages in the high-pressure section (:ref:`TurbineStage `) +``ip_stages`` IP stages Turbine stages in the intermediate-pressure section (:ref:`TurbineStage `) +``lp_stages`` LP stages Turbine stages in the low-pressure section (:ref:`TurbineStage `) +``hp_splits`` subset of HP stages Extraction splitters in the high-pressure section (:ref:`Separator `) +``ip_splits`` subset of IP stages Extraction splitters in the high-pressure section (:ref:`Separator `) +``lp_splits`` subset of LP stages Extraction splitters in the high-pressure section (:ref:`Separator `) +``outlet_stage`` None The final stage in the turbine, which calculates exhaust losses (:ref:`TurbineOutlet `) =========================== ==================== ====================================================================================================================================================== Initialization @@ -167,7 +167,7 @@ in inlet stage, outlet stage, or valves that do not pair well with the specified The flow coefficients for the inlet and outlet stage can be difficult to determine, therefore the initialization arguments ``calculate_outlet_cf`` and ``calculate_outlet_cf`` are provided. If these are True, the first stage flow coefficient is calculated from the flow and pressure ratio guesses, and the -outlet flow coefficient is calculated from the exhaust pressure and flow. +outlet flow coefficient is calculated from the exhaust pressure and flow. TurbineMultistage Class diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_multistage.svg b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_multistage.svg similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine_multistage.svg rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine_multistage.svg diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_outlet.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_outlet.rst similarity index 93% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine_outlet.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine_outlet.rst index b502fee3e3..1c2ace736c 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_outlet.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_outlet.rst @@ -43,15 +43,15 @@ Model Structure --------------- The turbine outlet stage model contains one :ref:`ControlVolume0DBlock block -` called control\_volume and +` called control\_volume and inherits the `HelmIsentropicTurbine -`. +`. Variables --------- The variables below are defined int the TurbineInletStage model. Additional variables are in inherited from the :`HelmIsentropicTurbine -` model. +` model. =========================== ======================== =========== ====================================================================== Variable Symbol Index Sets Doc @@ -95,7 +95,7 @@ Constraints ----------- In addition to the constraints inherited from the :ref:`PressureChanger model -` with the isentropic options, this +` with the isentropic options, this model contains two more constraints, one to estimate efficiency and one pressure-flow relation. From the isentropic pressure changer model, these constraints eliminate the need to specify efficiency and either inlet flow or outlet pressure. diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_stage.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_stage.rst similarity index 97% rename from docs/technical_specs/model_libraries/power_generation/unit_models/turbine_stage.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/turbine_stage.rst index 23f023d033..98ab245dd8 100644 --- a/docs/technical_specs/model_libraries/power_generation/unit_models/turbine_stage.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/turbine_stage.rst @@ -8,7 +8,7 @@ Turbine (Stage) This is a steam power generation turbine model for intermediate stages between the inlet and outlet. It inherits `HelmIsentropicTurbine -`. +`. Example ------- diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/waterpipe.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/waterpipe.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/waterpipe.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/waterpipe.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/watertank.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/watertank.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/watertank.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/watertank.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/waterwall.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/waterwall.rst similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/waterwall.rst rename to docs/reference_guides/model_libraries/power_generation/unit_models/waterwall.rst diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/waterwall_1.png b/docs/reference_guides/model_libraries/power_generation/unit_models/waterwall_1.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/waterwall_1.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/waterwall_1.png diff --git a/docs/technical_specs/model_libraries/power_generation/unit_models/waterwall_2.png b/docs/reference_guides/model_libraries/power_generation/unit_models/waterwall_2.png similarity index 100% rename from docs/technical_specs/model_libraries/power_generation/unit_models/waterwall_2.png rename to docs/reference_guides/model_libraries/power_generation/unit_models/waterwall_2.png diff --git a/docs/static/css/custom.css b/docs/static/css/custom.css index 2fa0916949..a94295209c 100644 --- a/docs/static/css/custom.css +++ b/docs/static/css/custom.css @@ -37,4 +37,19 @@ h2 { font-weight: bold; color: rgb(64, 64, 64); font-size: 110%; +} + +/* Set table values to top align & add padding in index-table*/ + +.index-table.docutils tbody td { + vertical-align: top; + padding-top: 16px; +} + +/* Set defined list to more generic styling in index-table*/ +.index-table dl:not(.docutils) dt { + font-size: 100%; + background: none; + color: #404040; + border-top: none; } \ No newline at end of file diff --git a/docs/technical_specs/core/solvers.rst b/docs/technical_specs/core/solvers.rst deleted file mode 100644 index bda1789bef..0000000000 --- a/docs/technical_specs/core/solvers.rst +++ /dev/null @@ -1,65 +0,0 @@ -Solvers -======= - -This section provides information on using and configuring solvers for IDAES. -Some IDAES solver features are documented in other sections, so references are -provided as appropriate. In general, standard Pyomo solver interfaces and -features can be used in IDAES, but IDAES provides a few extensions to make -working with solvers slightly easier. - -Default Solver Config ---------------------- - -The default solver settings can be set via the IDAES :ref:`IDAES configuration system`. By -default the solver settings are not taken from the IDAES configuration system -to avoid confusion where IDAES defaults differ from Pyomo defaults. To use the -IDAES solver defaults a user must explicitly enable them with the -``use_idaes_solver_configuration_defaults()`` function. - -.. autofunction:: idaes.core.solvers.config.use_idaes_solver_configuration_defaults - -Getting a Solver ----------------- - -A :ref:`utility function ` (``idaes.core.util.misc.get_solver``) provides -functionality to get a default or user specified solver at run time. This is -usually used by IDAES core models and tests. If no solver is specified the -IDAES default (usually Ipopt) is used. This is handy for method such as problem -initialization, where a default solver and options is usually used, but a user -can also choose a different solver or different options. - -Solver Logging --------------- - -A logger for solver-related log messages can be obtained from the -:ref:`IDAES logging system`. - -IDAES has features for redirecting solver output to a log. This is documented -in the :ref:`logging section`. - - -Solver Feature Checking ------------------------ - -There are some functions available to check what features are available to -solvers and to help with basic solver testing. - -.. autofunction:: idaes.core.solvers.ipopt_has_linear_solver - -Test Models ------------ - -The ``idaes.core.solvers.features`` provides functions to return simple models -of various types. These models can be used to test if solvers are available and -functioning properly. They can also be used to test that various option solver -features are available. These functions all return a tuple where the first -element is a model of the specified type, and the second element is the correct -value for the x variable after the problem is solved. - -.. autofunction:: idaes.core.solvers.features.lp - -.. autofunction:: idaes.core.solvers.features.milp - -.. autofunction:: idaes.core.solvers.features.nlp - -.. autofunction:: idaes.core.solvers.features.minlp diff --git a/docs/technical_specs/index.rst b/docs/technical_specs/index.rst deleted file mode 100644 index 1312646579..0000000000 --- a/docs/technical_specs/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Technical Specifications -======================== - -.. toctree:: - :maxdepth: 1 - - core/index - model_libraries/index - API Reference <../apidoc/modules> \ No newline at end of file diff --git a/docs/advanced_user_guide/advanced_install/index.rst b/docs/tutorials/advanced_install/index.rst similarity index 90% rename from docs/advanced_user_guide/advanced_install/index.rst rename to docs/tutorials/advanced_install/index.rst index c9f3a3b54e..216cbbbfbb 100644 --- a/docs/advanced_user_guide/advanced_install/index.rst +++ b/docs/tutorials/advanced_install/index.rst @@ -1,7 +1,7 @@ Advanced User Installation ========================== -Advanced users who plan to develop their own models or tools are encouraged to install IDAES using Git and GitHub as described in this section, rather than using the instructions in the :ref:`getting started` section. These advanced users will greatly benefit from improved version control and code integration capabilities. +Advanced users who plan to develop their own models or tools are encouraged to install IDAES using Git and GitHub as described in this section, rather than using the instructions in the :ref:`getting started` section. These advanced users will greatly benefit from improved version control and code integration capabilities. .. contents:: :local: @@ -12,7 +12,7 @@ Git is a distributed version control system that keeps track of changes in a set Both Git and GitHub are widely used and there are excellent tutorials and resources for each. See `Atlassian Github tutorials `_ , `GitHub help `_, and `Git documentation `_. -A limited reference for Git and GitHub terminology and commands is provided :ref:`here`, users that are new to Git and GitHub are strongly encouraged to use the more detailed resources above. +A limited reference for Git and GitHub terminology and commands is provided :ref:`here`, users that are new to Git and GitHub are strongly encouraged to use the more detailed resources above. .. toctree:: :hidden: @@ -39,10 +39,10 @@ You use a "fork" of a repository (or "repo" for short) to create a space where y .. figure:: /images/github-fork-repo_pse.png :align: right :width: 500px - + Figure 1. Screenshot showing where to click to fork the Github repo -You should first visit the idaes-pse repo on Github at https://github.com/IDAES/idaes-pse/. Then you should click on the fork icon in the top right and click on your username. These steps will have created your own fork of the repo with the same name under your username. +You should first visit the idaes-pse repo on Github at https://github.com/IDAES/idaes-pse/. Then you should click on the fork icon in the top right and click on your username. These steps will have created your own fork of the repo with the same name under your username. Clone Your Fork ^^^^^^^^^^^^^^^ @@ -62,21 +62,21 @@ In order to guarantee that your fork can be synchronized with the "main" idaes-p git remote add upstream https://github.com/IDAES/idaes-pse.git To check to see if you added the remote correctly use the following command:: - + git remote -v -You should see that there are two remotes, origin and upstream. Both have two lines showing the remote name, the url, and the access (fetch or push). Origin is the pointer to your fork and was automatically added with the clone command, while upstream is the pointer to the main idaes-pse repo that you just added. +You should see that there are two remotes, origin and upstream. Both have two lines showing the remote name, the url, and the access (fetch or push). Origin is the pointer to your fork and was automatically added with the clone command, while upstream is the pointer to the main idaes-pse repo that you just added. Create the Python Environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Once you have the repo cloned, you can change into that directory (by default, it will be called "idaes-pse" like the repo) and install the Python packages. -But before you do that, you need to get the Python package manager fully up and running. We use a Python packaging system called Conda_ and we specifically use its minimal version Miniconda_. If you do not already have Conda, please follow the installation instructions for your operating system in :ref:`getting started`. +But before you do that, you need to get the Python package manager fully up and running. We use a Python packaging system called Conda_ and we specifically use its minimal version Miniconda_. If you do not already have Conda, please follow the installation instructions for your operating system in :ref:`getting started`. .. _Conda: https://conda.io/ .. _Miniconda: https://conda.io/en/latest/miniconda.html -After Miniconda is installed, we recommend creating a separate conda environment for IDAES. If you are unfamiliar with environments, a good starting guide is `here `__. Create and activate a conda environment for the new IDAES installation with the following commands (we officially support python 3.7, but you may choose a version you prefer): +After Miniconda is installed, we recommend creating a separate conda environment for IDAES. If you are unfamiliar with environments, a good starting guide is `here `__. Create and activate a conda environment for the new IDAES installation with the following commands (we officially support python 3.7, but you may choose a version you prefer): .. code-block:: sh @@ -100,8 +100,8 @@ Now that conda and pip are installed, and you are in the "idaes" conda environme The IDAES binary extensions are not yet supported on Mac/OSX .. note:: - This ``pip install`` command would override any package within the conda environment, - so if you would like to use a specific version of a package (e.g. a local clone of the Pyomo git repository), you should look at the + This ``pip install`` command would override any package within the conda environment, + so if you would like to use a specific version of a package (e.g. a local clone of the Pyomo git repository), you should look at the ``requirements-dev.txt`` file and use it as a reference to either install the individual packages manually, or create a separate requirements file customized to your development use case. You can test that everything is installed properly by running the tests with @@ -117,9 +117,9 @@ The not integration tag skips some tests that are slow. If you like, you can run Update IDAES ^^^^^^^^^^^^^^^^^^^^^^^ -The main branch of idaes-pse is frequently updated and a new IDAES release occurs quarterly. It is recommended that you update your fork and local repositories and conda environment periodically. +The main branch of idaes-pse is frequently updated and a new IDAES release occurs quarterly. It is recommended that you update your fork and local repositories and conda environment periodically. .. code-block:: sh - + pip install -U idaes-pse pip install -U . diff --git a/docs/advanced_user_guide/advanced_install/terms_commands.rst b/docs/tutorials/advanced_install/terms_commands.rst similarity index 100% rename from docs/advanced_user_guide/advanced_install/terms_commands.rst rename to docs/tutorials/advanced_install/terms_commands.rst diff --git a/docs/getting_started/binaries.rst b/docs/tutorials/getting_started/binaries.rst similarity index 80% rename from docs/getting_started/binaries.rst rename to docs/tutorials/getting_started/binaries.rst index ae4bba035f..d29bd4dfab 100644 --- a/docs/getting_started/binaries.rst +++ b/docs/tutorials/getting_started/binaries.rst @@ -7,7 +7,7 @@ distribution contains mainly third-party solvers compiled for user convenience a function libraries used for some IDAES physical property packages. Installation is generally done through the -:doc:`idaes get-extensions command<../user_guide/commands/get_extensions>`. +:doc:`idaes get-extensions command<../../reference_guides/commands/get_extensions>`. What is Included ---------------- @@ -33,17 +33,17 @@ Main Package - `pynumero `_ - `clp `_ - `cbc `_ -- `bonmin, `_ (uses Ipopt with HSL) +- `bonmin, `_ (uses Ipopt with HSL) - `couenne `_ (uses Ipopt with HSL) -- iapws95_external, function for the IAPWS95 EoS water -- swco2_external, functions for the Span-Wagner EoS for CO2 -- cubic_roots, function for finding root of a cubic equation -- functions, miscellaneous simple math functions, examples, and test code +- `iapws95_external `_, function for the IAPWS95 EoS water +- `swco2_external `_, functions for the Span-Wagner EoS for CO2 +- `cubic_roots `_, function for finding root of a cubic equation +- `functions `_, miscellaneous simple math functions, examples, and test code Extras ~~~~~~ -- petsc, `AMPL solver library `_ interface wrapper for `PETSc's `_ SNES and TS solvers +- petsc, `AMPL solver library `_ `interface wrapper `_ for `PETSc's `_ SNES and TS solvers. Install Location ---------------- @@ -94,7 +94,7 @@ installation above. Then install required libraries for your release (see installation instructions `here `_ for supported distributions). -In the WSL environment you can check that PETSc is installed correctly. In your +In the WSL environment you can check that PETSc is installed correctly. In your desired Linux release navigate to the folder containing the compiled PETSc binary and run the command ``./petsc --version``. If setup correctly, details of the PETSc release will be printed on the screen. @@ -126,4 +126,15 @@ While for powershell, open a powershell window and run the command In all cases, the conda environment where the solver is installed has to be active (i.e. activate it with ``conda activate ``). Details of the solver release version will be printed on the screen if the setup has been completed correctly. +If you want to use user-defined AMPL functions with a WSL solver while running IDAES +in the normal Windows environment, you will need to set the ``AMPLFUNC`` environment +variable in the WSL environment manually. The ``AMPLFUNC`` variable is a newline-separated +list of paths to shared libraries containing user-defined AMPL functions. The easiest way +to handle user-defined functions in this case is to just set ``AMPLFUNC`` in your +``.bashrc`` file then run the WSL command in interactive mode, with the batch file +setup below. +.. code-block :: + + @echo off + idaes solver-wsl --distribution Ubuntu-20.04 --user john --executable bash -ic '~/local/bin/petsc %*' diff --git a/docs/getting_started/index.rst b/docs/tutorials/getting_started/index.rst similarity index 93% rename from docs/getting_started/index.rst rename to docs/tutorials/getting_started/index.rst index de1c9253f4..c3745983d1 100644 --- a/docs/getting_started/index.rst +++ b/docs/tutorials/getting_started/index.rst @@ -13,7 +13,7 @@ After installing and testing IDAES, it is strongly recommended to do the IDAES t located on the |examples-site|. If you expect to develop custom models, we recommend following the -:ref:`advanced user installation`. +:ref:`advanced user installation`. The OS specific instructions provide information about optionally installing Miniconda. If you already have a Python installation you prefer, you can skip @@ -137,11 +137,11 @@ Powershell Prompt. Regardless of OS and shell, the following steps are the same pip install 'idaes-pse[prerelease] @ https://github.com/myfork/idaes-pse/archive/mybranch.zip' - e. For developers: follow the :ref:`advanced user installation`. + e. For developers: follow the :ref:`advanced user installation`. -2. Run the :doc:`idaes get-extensions command<../user_guide/commands/get_extensions>` +2. Run the :doc:`idaes get-extensions command<../../../reference_guides/commands/get_extensions>` to install the compiled binaries. These binaries include solvers and function libraries. - See :ref:`Binary Packages ` + See :ref:`Binary Packages ` for more details. idaes get-extensions @@ -165,7 +165,7 @@ Powershell Prompt. Regardless of OS and shell, the following steps are the same conda-forge ipopt`` though this will not have all the features of our extensions package. -3. Run the :doc:`idaes get-examples command <../user_guide/commands/get_examples>` to download +3. Run the :doc:`idaes get-examples command <../../../reference_guides/commands/get_examples>` to download and install the example files:: idaes get-examples @@ -184,7 +184,7 @@ Powershell Prompt. Regardless of OS and shell, the following steps are the same idaes get-examples --dir C:\Users\MyName\IDAES\Examples - Refer to the full :doc:`idaes get-examples command documentation <../user_guide/commands/get_examples>` + Refer to the full :doc:`idaes get-examples command documentation <../../../reference_guides/commands/get_examples>` for more information. 4. Run tests:: @@ -231,7 +231,7 @@ As an alternative to the ``pip install`` method described above, IDAES can also Optional Dependencies --------------------- Some tools in IDAES may require additional dependencies. Instructions for installing these dependencies -are located :ref:`here`. +are located :ref:`here`. .. toctree:: :glob: @@ -264,7 +264,7 @@ assuming that the installation was done using one of the methods described earli idaes --version -3. Run the ``idaes get-extension`` command to install compiled binaries compatible with the newly upgraded IDAES version. These binaries include solvers and function libraries. See :ref:`Binary Packages ` for more details.:: +3. Run the ``idaes get-extension`` command to install compiled binaries compatible with the newly upgraded IDAES version. These binaries include solvers and function libraries. See :ref:`Binary Packages ` for more details.:: idaes get-extensions diff --git a/docs/getting_started/opt_dependencies.rst b/docs/tutorials/getting_started/opt_dependencies.rst similarity index 100% rename from docs/getting_started/opt_dependencies.rst rename to docs/tutorials/getting_started/opt_dependencies.rst diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 0000000000..0484db27aa --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,9 @@ +Getting Started (Tutorials) +=========================== + +.. toctree:: + :maxdepth: 1 + + getting_started/index + tutorials_examples + advanced_install/index \ No newline at end of file diff --git a/docs/tutorials_examples.rst b/docs/tutorials/tutorials_examples.rst similarity index 83% rename from docs/tutorials_examples.rst rename to docs/tutorials/tutorials_examples.rst index 0f0eb5415a..04eac4b3a7 100644 --- a/docs/tutorials_examples.rst +++ b/docs/tutorials/tutorials_examples.rst @@ -11,7 +11,7 @@ Running locally --------------- To run and use the examples on your own computer, once you have installed IDAES, you should run ``idaes get-examples`` in a command-line shell. -Please see :ref:`this page` for details on how to use this program. +Please see :ref:`this page` for details on how to use this program. Once you have installed the examples, change the directory where you downloaded them and run the following command\ :sup:`1` :: @@ -30,6 +30,6 @@ The sources for the tutorials and examples are maintained on the `IDAES examples repository `_. If you want to develop custom unit and property models, please refer to the -:ref:`advanced user guide `. +:doc:`custom model development guide <../../how_to_guides/custom_models/general_model_development>`. diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst deleted file mode 100644 index cc9802a6d2..0000000000 --- a/docs/user_guide/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -User Guide -========== - -.. toctree:: - :maxdepth: 1 - - why_idaes - concepts - components/index - conventions - workflow/index - commands/index - vis/index - model_libraries/index - configuration - logging - modeling_extensions/index diff --git a/docs/user_guide/model_libraries/index.rst b/docs/user_guide/model_libraries/index.rst deleted file mode 100644 index b1b8f7713a..0000000000 --- a/docs/user_guide/model_libraries/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -IDAES Model Libraries -===================== - -The documentation for the models are found in the technical specifications linked below. - -* :ref:`Generic IDAES Model Library` -* :ref:`Power Generation Model Library` -* :ref:`Gas Solid Contactors Model Library` diff --git a/idaes/apps/caprese/controller.py b/idaes/apps/caprese/controller.py index 94cff38800..bc6add7c6c 100644 --- a/idaes/apps/caprese/controller.py +++ b/idaes/apps/caprese/controller.py @@ -42,7 +42,7 @@ from pyomo.environ import ( Objective, - TerminationCondition, + check_optimal_termination, Constraint, Block, ) @@ -137,7 +137,7 @@ def solve_setpoint(self, solver, **kwargs): # assert dof == (len(self.INPUT_SET) + # len(self.DIFFERENTIAL_SET)) results = solver.solve(self, tee=config.tee) - if results.solver.termination_condition == TerminationCondition.optimal: + if check_optimal_termination(results): pass else: msg = 'Failed to solve for full state setpoint values' diff --git a/idaes/apps/caprese/examples/cstr_model.py b/idaes/apps/caprese/examples/cstr_model.py index 0799fb6892..b85e56636e 100644 --- a/idaes/apps/caprese/examples/cstr_model.py +++ b/idaes/apps/caprese/examples/cstr_model.py @@ -14,26 +14,18 @@ Test for Cappresse's module for NMPC. """ -import pytest -from pyomo.environ import (Block, ConcreteModel, Constraint, Expression, - Set, SolverFactory, Var, value, units as pyunits, - TransformationFactory, TerminationCondition) +from pyomo.environ import ( + ConcreteModel, SolverFactory, units as pyunits, TransformationFactory) from pyomo.network import Arc -from pyomo.common.collections import ComponentSet from idaes.core import (FlowsheetBlock, MaterialBalanceType, EnergyBalanceType, - MomentumBalanceType) + MomentumBalanceType) from idaes.core.util.tests.test_initialization import ( - AqueousEnzymeParameterBlock, EnzymeReactionParameterBlock, - EnzymeReactionBlock) -from idaes.core.util.model_statistics import (degrees_of_freedom, - activated_equalities_generator) -from idaes.core.util.initialization import initialize_by_time_element -from idaes.core.util.exceptions import ConfigurationError + AqueousEnzymeParameterBlock, EnzymeReactionParameterBlock) +from idaes.core.util.model_statistics import degrees_of_freedom from idaes.generic_models.unit_models import CSTR, Mixer, MomentumMixingType from idaes.apps.caprese import nmpc from idaes.apps.caprese.nmpc import * -import idaes.logger as idaeslog __author__ = "Robert Parker" diff --git a/idaes/apps/caprese/tests/test_categorize_cstr.py b/idaes/apps/caprese/tests/test_categorize_cstr.py index b536fdd950..ec6c1364b4 100644 --- a/idaes/apps/caprese/tests/test_categorize_cstr.py +++ b/idaes/apps/caprese/tests/test_categorize_cstr.py @@ -15,23 +15,14 @@ """ import pytest -from pyomo.environ import (Block, ConcreteModel, Constraint, Expression, - Set, SolverFactory, Var, value, - TransformationFactory, TerminationCondition) -from pyomo.network import Arc +from pyomo.environ import Var from pyomo.common.collections import ComponentSet from pyomo.dae.flatten import flatten_dae_components -from idaes.core import (FlowsheetBlock, MaterialBalanceType, EnergyBalanceType, - MomentumBalanceType) -from idaes.core.util.model_statistics import (degrees_of_freedom, - activated_equalities_generator) from idaes.apps.caprese.categorize import ( categorize_dae_variables, - categorize_dae_variables_and_constraints, ) from idaes.apps.caprese.common.config import VariableCategory as VC -import idaes.logger as idaeslog from idaes.apps.caprese.examples.cstr_model import make_model __author__ = "Robert Parker" diff --git a/idaes/apps/caprese/util.py b/idaes/apps/caprese/util.py index 1c300b0716..f0995ea153 100644 --- a/idaes/apps/caprese/util.py +++ b/idaes/apps/caprese/util.py @@ -16,9 +16,9 @@ """ from pyomo.environ import ( + check_optimal_termination, Constraint, Var, - TerminationCondition, SolverFactory, ) from pyomo.common.collections import ComponentSet, ComponentMap @@ -156,7 +156,7 @@ def initialize_by_element_in_range(model, time, t_start, t_end, assert degrees_of_freedom(model) == 0 with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(model, tee=slc.tee) - if results.solver.termination_condition == TerminationCondition.optimal: + if check_optimal_termination(results): pass else: raise ValueError( @@ -245,7 +245,7 @@ def initialize_by_element_in_range(model, time, t_start, t_end, with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(model, tee=slc.tee) - if results.solver.termination_condition == TerminationCondition.optimal: + if check_optimal_termination(results): pass else: raise ValueError( diff --git a/idaes/commands/base.py b/idaes/commands/base.py index 943ab5ab9b..9d649997f2 100644 --- a/idaes/commands/base.py +++ b/idaes/commands/base.py @@ -60,7 +60,7 @@ def how_to_report_an_error(embed=False): @click.group() -@click.version_option(version=None) +@click.version_option(version=None, package_name="idaes-pse") @click.option( "--verbose", "-v", diff --git a/idaes/commands/tests/test_commands.py b/idaes/commands/tests/test_commands.py index a7f2c37372..dd34bd5d20 100644 --- a/idaes/commands/tests/test_commands.py +++ b/idaes/commands/tests/test_commands.py @@ -14,6 +14,7 @@ Tests for idaes.commands """ # stdlib +from functools import partial import json import logging import os @@ -29,7 +30,7 @@ import pytest # package -from idaes.commands import examples, extensions, convergence, config, env_info +from idaes.commands import examples, extensions, convergence, config, env_info, base from idaes.util.system import TemporaryDirectory from . import create_module_scratch, rmtree_scratch import idaes @@ -73,6 +74,26 @@ def tempdir(request): return sub_path +class TestBaseCommand: + + @pytest.fixture + def run_idaes(self, runner): + return partial(runner.invoke, base.command_base) + + @pytest.mark.unit + @pytest.mark.parametrize( + "flags", + [ + ["--version"], + ["--help"], + ], + ids=" ".join, + ) + def test_flags(self, run_idaes, flags): + result = run_idaes(flags) + assert result.exit_code == 0 + + ################ # get-examples # ################ diff --git a/idaes/config.py b/idaes/config.py index 456649571f..06f15c4846 100644 --- a/idaes/config.py +++ b/idaes/config.py @@ -19,7 +19,7 @@ _log = logging.getLogger(__name__) # Default release version if no options provided for get-extensions -default_binary_release = "2.5.2" +default_binary_release = "2.5.5" # Where to download releases from get-extensions release_base_url = "https://github.com/IDAES/idaes-ext/releases/download" # Where to get release checksums @@ -170,7 +170,7 @@ def _new_idaes_config_block(): pyomo.common.config.ConfigBlock( implicit=False, description="Default config for 'ipopt' solver", - doc="Default config for 'ipopt-iades' solver" + doc="Default config for 'ipopt' solver" ), ) @@ -203,6 +203,74 @@ def _new_idaes_config_block(): ), ) + cfg.declare( + "petsc_ts", + pyomo.common.config.ConfigBlock( + implicit=False, + description="Default config for 'petsc_ts' solver", + doc="Default config for 'petsc_ts' solver" + ), + ) + + cfg["petsc_ts"].declare( + "options", + pyomo.common.config.ConfigBlock( + implicit=True, + description="Default solver options for 'petsc_ts'", + doc="Default solver options for 'petsc_ts' solver" + ), + ) + + cfg["petsc_ts"]["options"].declare( + "--ts_max_snes_failures", + pyomo.common.config.ConfigValue( + domain=int, + default=200, + description="Number of nonliner solver failures before giving up", + doc="Number of nonliner solver failures before giving up" + ), + ) + + cfg["petsc_ts"]["options"].declare( + "--ts_max_reject", + pyomo.common.config.ConfigValue( + domain=int, + default=20, + description="Maximum number of time steps to reject", + doc="Maximum number of time steps to reject" + ), + ) + + cfg["petsc_ts"]["options"].declare( + "--ts_type", + pyomo.common.config.ConfigValue( + domain=str, + default="beuler", + description="TS solver to use", + doc="TS solver to use" + ), + ) + + cfg["petsc_ts"]["options"].declare( + "--ts_adapt_type", + pyomo.common.config.ConfigValue( + domain=str, + default="basic", + description="TS adaptive step size method to use", + doc="TS adaptive step size method to use" + ), + ) + + cfg["petsc_ts"]["options"].declare( + "--ts_exact_final_time", + pyomo.common.config.ConfigValue( + domain=str, + default="matchstep", + description="How to handle the final time step", + doc="How to handle the final time step" + ), + ) + cfg.declare( "default_solver", pyomo.common.config.ConfigValue( diff --git a/idaes/core/components.py b/idaes/core/components.py index fa5f9f5a8d..659db299b5 100755 --- a/idaes/core/components.py +++ b/idaes/core/components.py @@ -135,29 +135,24 @@ def build(self): self._add_to_electrolyte_component_list() base_units = self.parent_block().get_metadata().default_units + der_units = self.parent_block().get_metadata().derived_units p_units = (base_units["mass"] / base_units["length"] / base_units["time"]**2) # Create Param for molecular weight if provided - if "mw" in self.config.parameter_data: - if isinstance(self.config.parameter_data["mw"], tuple): - mw_init = pyunits.convert_value( - self.config.parameter_data["mw"][0], - from_units=self.config.parameter_data["mw"][1], - to_units=base_units["mass"]/base_units["amount"]) - else: - _log.debug("{} no units provided for parameter mw - assuming " - "default units".format(self.name)) - mw_init = self.config.parameter_data["mw"] - self.mw = Param(initialize=mw_init, - units=base_units["mass"]/base_units["amount"]) + param_dict = {"mw": base_units["mass"]/base_units["amount"]} + for p, u in param_dict.items(): + if p in self.config.parameter_data: + self.add_component(p, Param(mutable=True, units=u)) + set_param_from_config(self, p) # Create Vars for common parameters - param_dict = {"pressure_crit": p_units, - "temperature_crit": base_units["temperature"], - "omega": None} - for p, u in param_dict.items(): + var_dict = {"dens_mol_crit": der_units["density_mole"], + "omega": pyunits.dimensionless, + "pressure_crit": p_units, + "temperature_crit": base_units["temperature"]} + for p, u in var_dict.items(): if p in self.config.parameter_data: self.add_component(p, Var(units=u)) set_param_from_config(self, p) diff --git a/idaes/core/control_volume0d.py b/idaes/core/control_volume0d.py index 8914903ef0..687dc3e509 100644 --- a/idaes/core/control_volume0d.py +++ b/idaes/core/control_volume0d.py @@ -269,6 +269,22 @@ def _add_material_balance_common(self, else: acc_units = None + # Check if reaction package exists, and get units + if hasattr(self.config, "reaction_package"): + if self.config.reaction_package is not None: + if (self.reactions[self.flowsheet().time.first()] + .get_reaction_rate_basis() == MaterialFlowBasis.molar): + rxn_flow_units = units('flow_mole') + elif (self.reactions[self.flowsheet().time.first()] + .get_reaction_rate_basis() == MaterialFlowBasis.mass): + rxn_flow_units = units('flow_mass') + else: # reaction basis not defined + rxn_flow_units = None + else: # reaction package is NoneType object + rxn_flow_units = None + else: # reaction package not defined + rxn_flow_units = None + # Test for components that must exist prior to calling this method if has_holdup: if not hasattr(self, "volume"): @@ -308,7 +324,7 @@ def _add_material_balance_common(self, initialize=0.0, doc="Amount of component generated in " "unit by kinetic reactions", - units=flow_units) + units=rxn_flow_units) # use reaction package flow basis # Equilibrium reaction generation if has_equilibrium_reactions: @@ -326,7 +342,7 @@ def _add_material_balance_common(self, initialize=0.0, doc="Amount of component generated in control volume " "by equilibrium reactions", - units=flow_units) + units=rxn_flow_units) # use reaction package flow basis # Inherent reaction generation if self.properties_out.include_inherent_reactions: @@ -344,7 +360,7 @@ def _add_material_balance_common(self, initialize=0.0, doc="Amount of component generated in control volume " "by inherent reactions", - units=flow_units) + units=flow_units) # use property package flow basis # Phase equilibrium generation if has_phase_equilibrium and \ @@ -361,7 +377,7 @@ def _add_material_balance_common(self, domain=Reals, initialize=0.0, doc="Amount of generation in control volume by phase equilibria", - units=flow_units) + units=flow_units) # use property package flow basis # Material transfer term if has_mass_transfer: @@ -443,7 +459,7 @@ def material_holdup_calculation(b, t, p, j): domain=Reals, initialize=0.0, doc="Extent of kinetic reactions", - units=flow_units) + units=rxn_flow_units) # use reaction package flow basis @self.Constraint(self.flowsheet().time, pc_set, @@ -467,7 +483,7 @@ def rate_reaction_stoichiometry_constraint(b, t, p, j): domain=Reals, initialize=0.0, doc="Extent of equilibrium reactions", - units=flow_units) + units=rxn_flow_units) # use reaction package flow basis @self.Constraint(self.flowsheet().time, pc_set, @@ -491,7 +507,7 @@ def equilibrium_reaction_stoichiometry_constraint(b, t, p, j): domain=Reals, initialize=0.0, doc="Extent of inherent reactions", - units=flow_units) + units=flow_units) # use property package flow basis @self.Constraint(self.flowsheet().time, pc_set, diff --git a/idaes/core/control_volume1d.py b/idaes/core/control_volume1d.py index df980d5987..3b2c6b6366 100644 --- a/idaes/core/control_volume1d.py +++ b/idaes/core/control_volume1d.py @@ -423,6 +423,24 @@ def _add_material_balance_common(self, else: acc_units = holdup_l_units/f_time_units + # Check if reaction package exists, and get units + if hasattr(self.config, "reaction_package"): + if self.config.reaction_package is not None: + if (self.reactions[self.flowsheet().time.first(), + self.length_domain.first()] + .get_reaction_rate_basis() == MaterialFlowBasis.molar): + rxn_flow_l_units = units('flow_mole')/units('length') + elif (self.reactions[self.flowsheet().time.first(), + self.length_domain.first()] + .get_reaction_rate_basis() == MaterialFlowBasis.mass): + rxn_flow_l_units = units('flow_mass')/units('length') + else: # reaction basis not defined + rxn_flow_l_units = None + else: # reaction package is NoneType object + rxn_flow_l_units = None + else: # reaction package not defined + rxn_flow_l_units = None + # Material holdup and accumulation if has_holdup: self.material_holdup = Var( @@ -479,7 +497,7 @@ def material_flow_linking_constraints(b, t, x, p, j): initialize=0.0, doc="Amount of component generated in " "by kinetic reactions per unit length", - units=flow_l_units) + units=rxn_flow_l_units) # use reaction package flow basis # Equilibrium reaction generation if has_equilibrium_reactions: @@ -498,7 +516,7 @@ def material_flow_linking_constraints(b, t, x, p, j): initialize=0.0, doc="Amount of component generated by equilibrium " "reactions per unit length", - units=flow_l_units) + units=rxn_flow_l_units) # use reaction package flow basis # Inherent reaction generation if self.properties.include_inherent_reactions: @@ -517,7 +535,7 @@ def material_flow_linking_constraints(b, t, x, p, j): initialize=0.0, doc="Amount of component generated in control volume " "by inherent reactions", - units=flow_l_units) + units=flow_l_units) # use property package flow basis # Phase equilibrium generation if has_phase_equilibrium and \ @@ -536,7 +554,7 @@ def material_flow_linking_constraints(b, t, x, p, j): initialize=0.0, doc="Amount of generation in unit by phase " "equilibria per unit length", - units=flow_l_units) + units=flow_l_units) # use property package flow basis # Material transfer term if has_mass_transfer: @@ -623,7 +641,7 @@ def material_holdup_calculation(b, t, x, p, j): domain=Reals, initialize=0.0, doc="Extent of kinetic reactions at point x", - units=flow_l_units) + units=rxn_flow_l_units) # use reaction package flow basis @self.Constraint(self.flowsheet().time, self.length_domain, @@ -653,7 +671,7 @@ def rate_reaction_stoichiometry_constraint(b, t, x, p, j): domain=Reals, initialize=0.0, doc="Extent of equilibrium reactions at point x", - units=flow_l_units) + units=rxn_flow_l_units) # use reaction package flow basis @self.Constraint(self.flowsheet().time, self.length_domain, @@ -684,7 +702,7 @@ def equilibrium_reaction_stoichiometry_constraint(b, t, x, p, j): domain=Reals, initialize=0.0, doc="Extent of inherent reactions at point x", - units=flow_l_units) + units=flow_l_units) # use property package flow basis @self.Constraint(self.flowsheet().time, self.length_domain, diff --git a/idaes/core/solvers/features.py b/idaes/core/solvers/features.py index 98039ecef1..368a33b01e 100644 --- a/idaes/core/solvers/features.py +++ b/idaes/core/solvers/features.py @@ -13,6 +13,7 @@ from functools import lru_cache import pyomo.environ as pyo +import pyomo.dae as pyodae from pyomo.common.errors import ApplicationError def lp(): @@ -65,7 +66,7 @@ def nle(): """ m = pyo.ConcreteModel() m.x = pyo.Var(initialize=-0.1) - m.eq1 = pyo.Constraint(expr=m.x**2 == 1) + m.eq1 = pyo.Constraint(expr=m.x**3 == 1) return m, 1 def nlp(): @@ -102,6 +103,100 @@ def minlp(): expr=m.i * (m.x**2 + m.y**2) + (1 - m.i) * 4 *(m.x**2 + m.y**2)) return m, 1, 1 +def dae(): + """This provides a DAE model for solver testing. + + The problem and expected result are from the problem given here: + https://archimede.dm.uniba.it/~testset/report/chemakzo.pdf. + + Args: + None + + Returns: + (tuple): Pyomo ConcreteModel, correct solved value for y[1] to y[5] and y6 + """ + model = pyo.ConcreteModel(name="chemakzo") + + # Set problem parameter values + model.k = pyo.Param([1,2,3,4], initialize={ + 1:18.7, + 2:0.58, + 3:0.09, + 4:0.42}) + model.Ke = pyo.Param(initialize=34.4) + model.klA = pyo.Param(initialize=3.3) + model.Ks = pyo.Param(initialize=115.83) + model.pCO2 = pyo.Param(initialize=0.9) + model.H = pyo.Param(initialize=737) + + # Problem variables ydot = dy/dt, + # (dy6/dt is not explicitly in the equations, so only 5 ydots i.e. + # y6 is an algebraic variable and y1 to y5 are differential variables) + model.t = pyodae.ContinuousSet(bounds=(0,180)) + model.y = pyo.Var(model.t, [1,2,3,4,5], initialize=1.0) # + model.y6 = pyo.Var(model.t, initialize=1.0) # + model.ydot = pyodae.DerivativeVar(model.y, wrt=model.t) # dy/dt + model.r = pyo.Var(model.t, [1,2,3,4,5], initialize=1.0) + model.Fin = pyo.Var(model.t, initialize=1.0) + + # Equations + @model.Constraint(model.t) + def eq_ydot1(b, t): + return b.ydot[t, 1] == -2.0*b.r[t, 1] + b.r[t, 2] - b.r[t, 3] - b.r[t, 4] + @model.Constraint(model.t) + def eq_ydot2(b, t): + return b.ydot[t, 2] == -0.5*b.r[t, 1] - b.r[t, 4] - 0.5*b.r[t, 5] + b.Fin[t] + @model.Constraint(model.t) + def eq_ydot3(b, t): + return b.ydot[t, 3] == b.r[t, 1] - b.r[t, 2] + b.r[t, 3] + @model.Constraint(model.t) + def eq_ydot4(b, t): + return b.ydot[t, 4] == -b.r[t, 2] + b.r[t, 3] - 2.0*b.r[t, 4] + @model.Constraint(model.t) + def eq_ydot5(b, t): + return b.ydot[t, 5] == b.r[t, 2] - b.r[t, 3] + b.r[t, 5] + @model.Constraint(model.t) + def eq_y6(b, t): + return 0 == b.Ks*b.y[t, 1]*b.y[t, 4] - b.y6[t] + + @model.Constraint(model.t) + def eq_r1(b, t): + return b.r[t, 1] == b.k[1]*b.y[t, 1]**4*b.y[t, 2]**0.5 + @model.Constraint(model.t) + def eq_r2(b, t): + return b.r[t, 2] == b.k[2]*b.y[t, 3]*b.y[t, 4] + @model.Constraint(model.t) + def eq_r3(b, t): + return b.r[t, 3] == b.k[2]/b.Ke*b.y[t, 1]*b.y[t, 5] + @model.Constraint(model.t) + def eq_r4(b, t): + return b.r[t, 4] == b.k[3]*b.y[t, 1]*b.y[t, 4]**2 + @model.Constraint(model.t) + def eq_r5(b, t): + return b.r[t, 5] == b.k[4]*b.y6[t]**2*b.y[t, 2]**0.5 + @model.Constraint(model.t) + def eq_Fin(b, t): + return b.Fin[t] == b.klA*(b.pCO2/b.H - b.y[t, 2]) + + # Set initial conditions and solve initial from the values of differential + # variables. + y0 = {1:0.444, 2:0.00123, 3:0.0, 4:0.007, 5:0.0} #initial differential vars + for i in y0: + model.y[0, i].fix(y0[i]) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(model, nfe=1, scheme='BACKWARD') + + return ( + model, + 0.1150794920661702, + 0.1203831471567715e-2, + 0.1611562887407974, + 0.3656156421249283e-3, + 0.1708010885264404e-1, + 0.4873531310307455e-2, + ) + @lru_cache(maxsize=10) def ipopt_has_linear_solver(linear_solver): """Check if IPOPT can use the specified linear solver. @@ -113,7 +208,7 @@ def ipopt_has_linear_solver(linear_solver): Returns: (bool): True if Ipopt is available with the specified linear solver or False - if either Ipopt or the linear solver is not available. + if either Ipopt or the linear solver is not available. """ m, x = nlp() solver = pyo.SolverFactory('ipopt', options={"linear_solver": linear_solver}) diff --git a/idaes/core/solvers/petsc.py b/idaes/core/solvers/petsc.py new file mode 100644 index 0000000000..2666963c98 --- /dev/null +++ b/idaes/core/solvers/petsc.py @@ -0,0 +1,716 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# + +import os +import sys +import shutil +import enum +import copy +import json +import gzip +import numpy as np + +import idaes +import pyomo.environ as pyo +from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.core.expr.visitor import identify_variables +import pyomo.dae as pyodae +from pyomo.common import Executable +from pyomo.dae.flatten import flatten_dae_components +from pyomo.util.subsystems import ( + TemporarySubsystemManager, + create_subsystem_block, +) +from pyomo.solvers.plugins.solvers.ASL import ASL +from pyomo.opt.solver import SystemCallSolver +from pyomo.common.tempfiles import TempfileManager +from pyomo.common.errors import ApplicationError +from idaes.core.util.model_statistics import degrees_of_freedom +import idaes.logger as idaeslog +from idaes.core.util import get_solver +import idaes.config as icfg + +PetscBinaryIOTrajectory = None +PetscBinaryIO = None + +def _import_petsc_binary_io(): + global PetscBinaryIOTrajectory + global PetscBinaryIO + try: + import PetscBinaryIOTrajectory + import PetscBinaryIO + except ImportError: + petsc_dir = os.path.join(icfg.bin_directory, "petscpy") + if not os.path.isdir(petsc_dir): + return + sys.path.append(petsc_dir) + try: + import PetscBinaryIOTrajectory + import PetscBinaryIO + except ImportError: + pass + +_import_petsc_binary_io() + +class DaeVarTypes(enum.IntEnum): + """Enum DAE variable type for suffix""" + + ALGEBRAIC = 0 + DIFFERENTIAL = 1 + DERIVATIVE = 2 + TIME = 3 + + +@pyo.SolverFactory.register("petsc", doc="ASL PETSc interface") +class Petsc(ASL): + """ASL solver plugin for the PETSc solver. This adds the option to use an + alternative executable batch file to run the solver through the WSL.""" + + def __init__(self, **kwds): + self._wsl = kwds.pop("wsl", None) + super().__init__(**kwds) + self.options.solver = "petsc" + + def _default_executable(self): + """In addition to looking for the petsc executable, optionally check for + a WSL batch file on Windows. Users could potentially also compile a + cygwin exectable on Windows, so WSL isn't the only option, but it is the + easiest for Windows.""" + executable = False + if not self._wsl or self._wsl is None: + executable = Executable("petsc") + if sys.platform.startswith("win32") and (not executable): + # On Windows, if wsl was requested or a normal petsc solver + # executable was not found, look for a batch file to run it with WSL + executable = Executable("petsc_wsl.bat") + if not executable: + raise RuntimeError("No PETSc executable found.") + return executable.path() + + +@pyo.SolverFactory.register("petsc_snes", doc="ASL PETSc SNES interface") +class PetscSNES(Petsc): + """PETSc solver plugin that sets options for SNES solver. This turns on + SNES monitoring, and checks the config for default options""" + + def __init__(self, **kwds): + if "options" in kwds and kwds["options"] is not None: + kwds["options"] = copy.deepcopy(kwds["options"]) + else: + kwds["options"] = {} + kwds["options"]["--snes_monitor"] = "" + if "petsc_snes" in idaes.cfg: + if "options" in idaes.cfg["petsc_snes"]: + default_options = dict(idaes.cfg["petsc_snes"]["options"]) + default_options.update(kwds["options"]) + kwds["options"] = default_options + super().__init__(**kwds) + self.options.solver = "petsc_snes" + + +@pyo.SolverFactory.register("petsc_ts", doc="ASL PETSc TS interface") +class PetscTS(Petsc): + """PETSc solver plugin that sets options for TS solver. This turns on + TS monitoring, sets the DAE flag, and checks the config for default options. + """ + + def __init__(self, **kwds): + self._ts_vars_stub = kwds.pop("vars_stub", None) + if "options" in kwds and kwds["options"] is not None: + kwds["options"] = copy.deepcopy(kwds["options"]) + else: + kwds["options"] = {} + kwds["options"]["--dae_solve"] = "" + kwds["options"]["--ts_monitor"] = "" + if "petsc_ts" in idaes.cfg: + if "options" in idaes.cfg["petsc_ts"]: + default_options = dict(idaes.cfg["petsc_ts"]["options"]) + default_options.update(kwds["options"]) + kwds["options"] = default_options + super().__init__(**kwds) + self.options.solver = "petsc_ts" + + def _postsolve(self): + stub = os.path.splitext(self._soln_file)[0] + # There is a type file created by the solver to give the variable types + # this is needed to read the trajectory data, and we want to include it + # with other tmp files + typ_file = stub + ".typ" + TempfileManager.add_tempfile(typ_file) + # If the vars_stub option was specified, copy the col and typ files to + # the working directory. These files are needed to get the names and + # types of variables and to make sense of trajectory data. + if self._ts_vars_stub is not None: + try: + shutil.copyfile(f"{stub}.col", f"{self._ts_vars_stub}.col") + except: + pass + try: + shutil.copyfile(f"{stub}.typ", f"{self._ts_vars_stub}.typ") + except: + pass + return ASL._postsolve(self) + + +@pyo.SolverFactory.register("petsc_tao", doc="ASL PETSc TAO interface") +class PetscTAO(Petsc): + """This is a place holder for optimization solvers""" + + def __init__(self, **kwds): + raise NotImplementedError( + "The PETSc TAO interface has not yet been implemented" + ) + + +def petsc_available(wsl=None): + """Check if the IDAES AMPL solver wrapper for PETSc is available. + + Args: + wsl (bool): If True force WSL version, if False force not WSL version, + if None, try non-WSL version then try WSL version + + Returns (bool): + True if PETSc is available + """ + solver = pyo.SolverFactory("petsc", wsl=wsl) + if solver is not None: + try: + return solver.available() + except RuntimeError: + return False + return False + + +def _copy_time(time_vars, t_from, t_to): + """PRIVATE FUNCTION: + + This is used on the flattened (only indexed by time) variable + representations to copy variable values that are unfixed at the "to" time + from the value at the "from" time. The PETSc DAE solver uses the initial + variable values as the initial condition, so this is used to copy the + previous time in as the initial condition for the next step. + + Args: + time_vars (list): list of variables or references to variables indexed + only by time + t_from (float): time point to copy from + t_to (float): time point to copy to, only unfixed vars will be + overwritten + + Returns: + None + """ + for v in time_vars: + if not v[t_to].fixed: + v[t_to].value = v[t_from].value + + +def find_discretization_equations(m, time): + """This is a generator for time discretization equations. Since we aren't + solving the whole time period simultaneously, we'll want to deactivate + these constraints. + + Args: + m (Block): model or block to search for constraints + time (ContinuousSet): + + Yields: + time discretization constraints + """ + disc_eqns = [] + for var in m.component_objects(pyo.Var): + if isinstance(var, pyodae.DerivativeVar): + if time in ComponentSet(var.get_continuousset_list()): + parent = var.parent_block() + name = var.local_name + "_disc_eq" + disc_eq = getattr(parent, name) + disc_eqns.append(disc_eq) + return disc_eqns + + +def _set_dae_suffixes_from_variables(m, variables, deriv_diff_map): + """Write suffixes used by the solver to identify different variable types + and associated derivative and differential variables. + + Args: + m: model to search for variables and write suffixes to + variables (list): List of time indexed variables at a specific time + point + deriv_diff_map (ComponentMap): Maps DerivativeVar data objects to + differential variable data objects + + Returns: + None + """ + # The dae_suffix provides the solver information about variables types + # algebraic, differential, derivative, or time, see DaeVarTypes + m.dae_suffix = pyo.Suffix( + direction=pyo.Suffix.EXPORT, + datatype=pyo.Suffix.INT, + ) + # The dae_link suffix provides the solver a link between the differential + # and derivative variable, by assigning the pair a unique integer index. + m.dae_link = pyo.Suffix( + direction=pyo.Suffix.EXPORT, + datatype=pyo.Suffix.INT, + ) + dae_var_link_index = 1 + differential_vars = [] + i = 0 + for var in variables: + if var in deriv_diff_map: + deriv = var + diffvar = deriv_diff_map[deriv] + m.dae_suffix[diffvar] = int(DaeVarTypes.DIFFERENTIAL) + m.dae_suffix[deriv] = int(DaeVarTypes.DERIVATIVE) + m.dae_link[diffvar] = dae_var_link_index + m.dae_link[deriv] = dae_var_link_index + i += 1 + dae_var_link_index += 1 + if not diffvar.fixed: + differential_vars.append(diffvar) + else: + raise RuntimeError( + f"Problem cannot contain a fixed differential variable and " + f"unfixed derivative. Consider either fixing the " + f"corresponding derivative or adding a constraint for the " + f"differential variable {diffvar} possibly using an " + f"explicit time variable." + ) + return differential_vars + + +def _get_derivative_differential_data_map(m, time): + """Get a map from data objects of derivative variables to the + corresponding data objects of differential variables. + + Args: + m: Model to search for DerivativeVars + time: Set with respect to which DerivativeVars must be differentiated + + Returns: + (ComponentMap): Map from derivative data objects to differential + data objects + """ + # Get corresponding derivative and differential data objects, + # with no attention paid to fixed or active status. + deriv_diff_list = [] + for var in m.component_objects(pyo.Var): + if (isinstance(var, pyodae.DerivativeVar) and + time in ComponentSet(var.get_continuousset_list())): + deriv = var + diffvar = deriv.get_state_var() + for idx in var: + if deriv[idx].fixed and pyo.value(abs(deriv[idx])) > 1e-10: + raise RuntimeError( + f"{deriv[idx]} is fixed to a nonzero value " + f"{pyo.value(deriv[idx])}. This is " + f"most likely a modeling error. Instead of fixing the " + f"derivative consider adding a constraint like " + f"dxdt = constant" + ) + deriv_diff_list.append((deriv[idx], diffvar[idx])) + + # Get unfixed variables in active constraints + active_con_vars = ComponentSet() + for con in m.component_data_objects(pyo.Constraint, active=True): + for var in identify_variables(con.expr, include_fixed=False): + active_con_vars.add(var) + + # Filter out derivatives that are fixed or not in an active constraint + filtered_deriv_diff_list = [] + for deriv, diff in deriv_diff_list: + if deriv in active_con_vars: + filtered_deriv_diff_list.append((deriv, diff)) + return ComponentMap(filtered_deriv_diff_list) + + +def _sub_problem_scaling_suffix(m, t_block): + """Copy scaling factors from the full model to the submodel. This assumes + the scaling suffixes could be in two places. First check the parent block + of the component (typical place for idaes models) then check the top-level + model. The top level model will take precedence. + """ + if not hasattr(t_block, "scaling_factor"): + # if the subsystem block doesn't already have a scaling suffix, make one + t_block.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + # first check the parent block for scaling factors + for c in t_block.component_data_objects((pyo.Var, pyo.Constraint)): + if hasattr(c.parent_block(), "scaling_factor"): + if c in c.parent_block().scaling_factor: + t_block.scaling_factor[c] = c.parent_block().scaling_factor[c] + # now check the top level model + if hasattr(m, "scaling_factor"): + for c in t_block.component_data_objects((pyo.Var, pyo.Constraint)): + if c in m.scaling_factor: + t_block.scaling_factor[c] = m.scaling_factor[c] + + +def petsc_dae_by_time_element( + m, + time, + timevar=None, + initial_constraints=None, + initial_variables=None, + detect_initial=True, + skip_initial=False, + snes_options=None, + ts_options=None, + wsl=None, + keepfiles=False, + symbolic_solver_labels=True, + vars_stub=None, + trajectory_save_prefix=None, +): + """Solve a DAE problem step by step using the PETSc DAE solver. This + integrates from one time point to the next. + + Args: + m (Block): Pyomo model to solve + time (ContinuousSet): Time set + timevar (Var): Optional specification of a time variable, which can be + used to write constraints that are an explicit function of time. + initial_constraints (list): Constraints to solve in the initial + condition solve step. Since the time-indexed constraints are picked + up automatically, this generally includes non-time-indexed + constraints. + initial_variables (list): This is a list of variables to fix after the + initial condition solve step. If these variables were originally + unfixed, they will be unfixed at the end of the solve. This usually + includes non-time-indexed variables that are calculated along with + the initial conditions. + detect_initial (bool): If True, add non-time-indexed variables and + constraints to initial_variables and initial_constraints. + skip_initial (bool): Don't do the initial condition calculation step, + and assume that the initial condition values have already been + calculated. This can be useful, for example, if you read initial + conditions from a separately solved steady state problem, or + otherwise know the initial conditions. + snes_options (dict): PETSc nonlinear equation solver options + ts_options (dict): PETSc time-stepping solver options + wsl (bool): if True use WSL to run PETSc, if False don't use WSL to run + PETSc, if None automatic. The WSL is only for Windows. + keepfiles (bool): pass to keepfiles arg for solvers + symbolic_solver_labels (bool): pass to symbolic_solver_labels argument + for solvers. If you want to read trajectory data from the + time-stepping solver, this should be True. + vars_stub (str or None): Copy the `*.col` and `*.typ` files to the + working directory using this stub if not None. These are needed to + interpret the trajectory data. + trajectory_save_prefix (str or None): If a string is provided the + trajectory data will be saved as gzipped json + + Returns: + List of solver results objects from each solve. If there are initial + condition constraints and they are not skipped, the first object will + be from the initial condition solve. Then there should be one for each + time element for each TS solve. + """ + solve_log = idaeslog.getSolveLogger("petsc-dae") + regular_vars, time_vars = flatten_dae_components(m, time, pyo.Var) + regular_cons, time_cons = flatten_dae_components(m, time, pyo.Constraint) + tdisc = find_discretization_equations(m, time) + + solver_snes = pyo.SolverFactory("petsc_snes", options=snes_options, wsl=wsl) + solver_dae = pyo.SolverFactory( + "petsc_ts", options=ts_options, wsl=wsl, vars_stub=vars_stub + ) + + if initial_variables is None: + initial_variables = [] + if initial_constraints is None: + initial_constraints = [] + + if detect_initial: + rvset = ComponentSet(regular_vars) + rcset = ComponentSet(regular_cons) + icset = ComponentSet(initial_constraints) + ivset = ComponentSet(initial_variables) + initial_variables = list(ivset | rvset) + initial_constraints = list(icset | rcset) + + # First calculate the inital conditions and non-time-indexed constraints + res_list = [] + t0 = time.first() + if not skip_initial: + with TemporarySubsystemManager(to_deactivate=tdisc): + constraints = [ + con[t0] for con in time_cons if t0 in con + ] + initial_constraints + variables = [var[t0] for var in time_vars] + initial_variables + if len(constraints) > 0: + # if the initial condition is specified and there are no + # initial constraints, don't try to solve. + t_block = create_subsystem_block( + constraints, + variables, + ) + # set up the scaling factor suffix + _sub_problem_scaling_suffix(m, t_block) + with idaeslog.solver_log(solve_log, idaeslog.INFO) as slc: + res = solver_snes.solve(t_block, tee=slc.tee) + res_list.append(res) + + tprev = t0 + count = 1 + fix_derivs = [] + with TemporarySubsystemManager( + to_deactivate=tdisc, + to_fix=initial_variables + fix_derivs, + ): + # Solver time steps + deriv_diff_map = _get_derivative_differential_data_map(m, time) + for t in time: + if t == time.first(): + # t == time.first() was handled above + continue + constraints = [con[t] for con in time_cons if t in con] + variables = [var[t] for var in time_vars] + # Create a temporary block with references to original constraints + # and variables so we can integrate this "subsystem" without + # altering the rest of the model. + t_block = create_subsystem_block(constraints, variables) + differential_vars = _set_dae_suffixes_from_variables( + t_block, + variables, + deriv_diff_map, + ) + # We need to check if there are derivatives in the problem before + # sending this to the solver. We'll assume that if you are using + # this and don't have any differential equations, you are making a + # mistake. + if len(differential_vars) < 1: + raise RuntimeError( + "No differential equations found at t = %s, " + "you do not need a DAE solver." % t + ) + if timevar is not None: + t_block.dae_suffix[timevar[t]] = int(DaeVarTypes.TIME) + # Set up the scaling factor suffix + _sub_problem_scaling_suffix(m, t_block) + # Take initial conditions for this step from the result of previous + _copy_time(time_vars, tprev, t) + with idaeslog.solver_log(solve_log, idaeslog.INFO) as slc: + res = solver_dae.solve( + t_block, + tee=slc.tee, + keepfiles=keepfiles, + symbolic_solver_labels=symbolic_solver_labels, + export_nonlinear_variables=differential_vars, + options={"--ts_init_time": tprev, "--ts_max_time": t}, + ) + if trajectory_save_prefix is not None: + tj = PetscTrajectory( + stub=vars_stub, delete_on_read=True, unscale=t_block) + tj.to_json(f"{trajectory_save_prefix}_{count}.json.gz") + tprev = t + count += 1 + res_list.append(res) + return res_list + + +class PetscTrajectory(object): + def __init__( + self, + stub=None, + vecs=None, + json=None, + pth=None, + vis_dir="Visualization-data", + delete_on_read=False, + unscale=None, + ): + """Class to read PETSc TS solver trajectory data. This can either read + PETSc output by providing the ``stub`` argument, a trajectory dict by + providing ``vecs`` or a json file by providing ``json``. + + Args: + stub (str): file name stub for variable info + pth (str): path where variable info and trajectory data are stored + if None, use current working directory + vis_dir (str): subdirectory where visualization data is stored + delete_on_read (bool): if true delete trajectory data after reading + unscale (Block): if not None this is a block to read scale factors + to unscale the trajectory + """ + if PetscBinaryIOTrajectory is None: + raise RuntimeError("PetscBinaryIOTrajectory could not be imported") + if pth is not None: + stub = os.path.join(pth, stub) + vis_dir = os.path.join(pth, vis_dir) + if stub is not None: + self.stub = stub + self.vis_dir = vis_dir + self.path = pth + self.unscale = unscale + self._read() + if delete_on_read: + self.delete_files() + if unscale is not None: + self._unscale(unscale) + elif vecs is not None: + self.vecs = vecs + self.time = vecs["_time"] + self.vars = list(vecs.keys()) + elif json is not None: + self.from_json(json) + else: + raise RuntimeError("To read trajectory, either provide stub, vecs, or json") + + def _read(self): + with open(f'{self.stub}.col') as f: + names = list(map(str.strip, f.readlines())) + with open(f'{self.stub}.typ') as f: + typ = list(map(int,f.readlines())) + self.vars = [name for i, name in enumerate(names) if typ[i] in [0,1]] + (t, v, names) = PetscBinaryIOTrajectory.ReadTrajectory( + "Visualization-data" + ) + self.time = t + self.vecs_by_time = v + self.vecs = dict.fromkeys(self.vars, None) + for k in self.vecs.keys(): + self.vecs[k] = [0]*len(self.time) + self.vecs["_time"] = list(self.time) + for i, v in enumerate(self.vars): + for j, vt in enumerate(self.vecs_by_time): + self.vecs[v][j] = vt[i] + + def _var_name_list(self, vars): + if isinstance(vars, str): + vars = [vars] + if hasattr(vars, "ctype"): + if issubclass(vars.ctype, pyo.Var): + vars = [vars] + vars = list(map(str, vars)) + for name in vars: + if name not in self.vars: + raise RuntimeError(f"Variable {name} not found.") + return vars + + def get_vec(self, var): + """Return the vector of variable values at each time point for var. + + Args: + var (str or Var): Variable to get vector for. + + Retruns (list): + vector of variable values at each time point + + """ + var = str(var) + return self.vecs[var] + + def get_dt(self): + """Get a list of time steps + + Args: + None + + Returns: + (list) + """ + dt = [None]*(len(self.time) - 1) + for i in range(len(self.time)-1): + dt[i] = self.time[i + 1] - self.time[i] + return dt + + def interpolate_vecs(self, times): + """Create a new vector dictionary interpolated at times. This method + will also extraplote values outside the original time range, so care + should be taken not to specify times too far outside the range. + + Args: + times (list): list of times to interpolate. These must be in + increasing order. + + Returns (dict): + Dictionary of vectors for values at interpolated points + """ + vecs = dict.fromkeys(self.vars, None) + vecs["_time"] = copy.copy(times) + for var in vecs: + vecs[var] = np.interp(vecs["_time"], self.vecs["_time"], self.vecs[var]) + return vecs + + def _unscale(self, m): + """If variable scale factors are used, the solver will see scaled + variables, and the scaled trajectory will be written. This function + uses variable scaling facors from the given model to unscale the + trajectory. + + Args: + m (Block): model or block to read scale factors from. + + Returns: + None + """ + for var in m.component_data_objects(): + vname = str(var) + if vname in self.vecs: + s = None + if hasattr(var.parent_block(), "scaling_factor"): + s = var.parent_block().scaling_factor.get(var, s) + if hasattr(m, "scaling_factor"): + s = m.scaling_factor.get(var, s) + if s is not None: + for i, x in enumerate(self.vecs[vname]): + self.vecs[vname][i] = x/s + + def delete_files(self): + """Delete the trajectory data and variable information files. + + Args: + None + + Returns: + None + """ + shutil.rmtree(self.vis_dir) + os.remove(f'{self.stub}.col') + os.remove(f'{self.stub}.typ') + + def to_json(self, pth): + """Dump the trajectory data to a json file in the form of a dictionary + with variable name keys and '_time' with vectors of values at each time. + + Args: + pth (str): path for json file to write + + Returns: + None + """ + if pth.endswith(".gz"): + with gzip.open(pth, "w") as fp: + fp.write(json.dumps(self.vecs).encode("utf-8")) + else: + with open(pth, "w") as fp: + json.dump(self.vecs, fp) + + def from_json(self, pth): + """Read the trajectory data from a json file in the form of a dictionary. + + Args: + pth (str): path for json file to write + + Returns: + None + """ + if pth.endswith(".gz"): + with gzip.open(pth, "r") as fp: + self.vecs = json.loads(fp.read()) + else: + with open(pth, "r") as fp: + self.vecs = json.load(fp) + self.time = self.vecs["_time"] + self.vars = list(self.vecs.keys()) diff --git a/idaes/core/solvers/tests/test_petsc.py b/idaes/core/solvers/tests/test_petsc.py new file mode 100644 index 0000000000..4ad5a3fdec --- /dev/null +++ b/idaes/core/solvers/tests/test_petsc.py @@ -0,0 +1,546 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# + +"""Basic unit tests for PETSc solver utilities""" +import pytest +import numpy as np +import json +import os +import pyomo.environ as pyo +import pyomo.dae as pyodae +from pyomo.util.subsystems import ( + TemporarySubsystemManager, + create_subsystem_block, +) +from idaes.core.solvers import petsc +from idaes.core.solvers.features import dae + +def rp_example(): + """This example is done multiple ways to test a few common errors where the + integrator and fully time-discretized problem differ and alternative working + formulations. + + The PETSc utilities raise an exception when a differential variable is fixed. + While this is okay for the fully time-discretized problem, the integrator + will not correctly link a fixed differential variable (at non-initial time + points) with a time derivative. + """ + m = pyo.ConcreteModel() + + m.time = pyodae.ContinuousSet(initialize=(0.0, 10.0)) + m.x = pyo.Var(m.time) + m.u = pyo.Var(m.time) + m.dxdt = pyodae.DerivativeVar(m.x, wrt=m.time) + + def diff_eq_rule(m, t): + return m.dxdt[t] == m.x[t]**2 - m.u[t] + m.diff_eq = pyo.Constraint(m.time, rule=diff_eq_rule) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(m,nfe=1,scheme='BACKWARD') + + for t in m.time: + m.x[t].fix(2.0*t) + + m.u[0].fix(1.0) + + return m + +def rp_example2(): + """ This example is done multiple ways to test a few common errors where the + integrator and fully time-discretized problem differ, and alternative working + formulations. + + Rather than fixing a differential variable, we can add an explicit time + variable for the integrator and fix the time variable to the time index + for the fully discretized problem. This works as an alternative to the + fixed differential variable. + """ + m = pyo.ConcreteModel() + + m.time = pyodae.ContinuousSet(initialize=(0.0, 10.0)) + m.x = pyo.Var(m.time) + m.u = pyo.Var(m.time) + m.t = pyo.Var(m.time) + m.dxdt = pyodae.DerivativeVar(m.x, wrt=m.time) + + def diff_eq_rule(m, t): + return m.dxdt[t] == m.x[t]**2 - m.u[t] + m.diff_eq = pyo.Constraint(m.time, rule=diff_eq_rule) + + def x_eq_rule(m, t): + return m.x[t] == 2.0*m.t[t] + m.x_eq = pyo.Constraint(m.time, rule=x_eq_rule) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(m,nfe=1,scheme='BACKWARD') + + m.u[0].fix(1.0) + # For fully discretized fix all times at time index + m.t[0].fix(m.time.first()) + + return m + +def rp_example3(): + """ This example is done multiple ways to test a few common errors where the + integrator and fully time-discretized problem differ, and alternative working + formulations. + + Another way to formulate this problem is to fix the derivative. This doesn't + work for the integrator because it loses the association between the + differential variable and its derivative. Since for users, the result of + this formulation may be unexpected, the PETSc utilities will raise an + exception if derivatives are fixed to anything other than 0. + """ + m = pyo.ConcreteModel() + + m.time = pyodae.ContinuousSet(initialize=(0.0, 10.0)) + m.x = pyo.Var(m.time) + m.u = pyo.Var(m.time) + m.dxdt = pyodae.DerivativeVar(m.x, wrt=m.time) + + def diff_eq_rule(m, t): + return m.dxdt[t] == m.x[t]**2 - m.u[t] + m.diff_eq = pyo.Constraint(m.time, rule=diff_eq_rule) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(m,nfe=1,scheme='BACKWARD') + + m.u[0].fix(1.0) + m.dxdt[:].fix(2.0) + + return m + +def rp_example4(): + """ This example is done multiple ways to test a few common errors where the + integrator and fully time-discretized problem differ, and alternative working + formulations. + + Rather than fixing the derivative, we can add a constraint to set the + derivative. This should work as intended for both the fully + time-discretized problem and integrator. + """ + m = pyo.ConcreteModel() + + m.time = pyodae.ContinuousSet(initialize=(0.0, 10.0)) + m.x = pyo.Var(m.time, initialize=1) + m.u = pyo.Var(m.time, initialize=1) + m.dxdt = pyodae.DerivativeVar(m.x, wrt=m.time) + + def diff_eq1_rule(m, t): + return m.dxdt[t] == m.x[t]**2 - m.u[t] + m.diff_eq1 = pyo.Constraint(m.time, rule=diff_eq1_rule) + + def diff_eq2_rule(m, t): + return m.dxdt[t] == 2.0 + m.diff_eq2 = pyo.Constraint(m.time, rule=diff_eq2_rule) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(m,nfe=1,scheme='BACKWARD') + + m.u[0].fix(1.0) + m.x[0].fix(0.0) + m.diff_eq2[0].deactivate() + + return m + +def car_example(): + """This is to test problems where a differential variable doesn't appear in + a constraint this is based on a Pyomo example here: + https://github.com/Pyomo/pyomo/blob/main/examples/dae/car_example.py""" + m = pyo.ConcreteModel() + + m.R = pyo.Param(initialize=0.001) # Friction factor + m.L = pyo.Param(initialize=100.0) # Final position + + m.tau = pyodae.ContinuousSet(bounds=(0,1)) # Unscaled time + m.time = pyo.Var(m.tau) # Scaled time + m.tf = pyo.Var() + m.x = pyo.Var(m.tau,bounds=(0,m.L+50)) + m.v = pyo.Var(m.tau,bounds=(0,None)) + m.a = pyo.Var(m.tau, bounds=(-3.0,1.0),initialize=0) + + m.dtime = pyodae.DerivativeVar(m.time) + m.dx = pyodae.DerivativeVar(m.x) + m.dv = pyodae.DerivativeVar(m.v) + + m.obj = pyo.Objective(expr=m.tf) + + def _ode1(m,i): + if i == 0 : + return pyo.Constraint.Skip + return m.dx[i] == m.tf * m.v[i] + m.ode1 = pyo.Constraint(m.tau, rule=_ode1) + + def _ode2(m,i): + if i == 0 : + return pyo.Constraint.Skip + return m.dv[i] == m.tf*(m.a[i] - m.R*m.v[i]**2) + m.ode2 = pyo.Constraint(m.tau, rule=_ode2) + + def _ode3(m,i): + if i == 0: + return pyo.Constraint.Skip + return m.dtime[i] == m.tf + m.ode3 = pyo.Constraint(m.tau, rule=_ode3) + + def _init(m): + yield m.x[0] == 0 + #yield m.x[1] == m.L + yield m.v[0] == 0 + yield m.v[1] == 0 + yield m.time[0] == 0 + m.initcon = pyo.ConstraintList(rule=_init) + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(m,nfe=1,scheme='BACKWARD') + return m + + +def dae_with_non_time_indexed_constraint(): + """This provides a DAE model for solver testing. This model contains a non- + time-indexed variable and constraint and a fixed derivative to test some + edge cases. + + The problem and expected result are from A test problem from + https://archimede.dm.uniba.it/~testset/report/chemakzo.pdf. + + Args: + None + + Returns: + (tuple): Pyomo ConcreteModel, correct solved value for y[1] to y[6] + """ + model = pyo.ConcreteModel(name="chemakzo") + + # Set problem parameter values + model.k = pyo.Param([1,2,3,4], initialize={ + 1:18.7, + 2:0.58, + 3:0.09, + 4:0.42}) + model.Ke = pyo.Param(initialize=34.4) + model.klA = pyo.Param(initialize=3.3) + model.Ks = pyo.Param(initialize=115.83) + model.pCO2 = pyo.Param(initialize=0.9) + # The following parameter H, is best made a parameter, but will use a + # variable and constraint instead to test non-time-indexed constraints + #model.H = pyo.Param(initialize=737) + + # Problem variables ydot = dy/dt, + # (dy6/dt is not explicitly in the equations, so only 5 ydots) + model.H = pyo.Var(initialize=737) + model.t = pyodae.ContinuousSet(bounds=(0,180)) + model.y = pyo.Var(model.t, [1,2,3,4,5,6], initialize=1.0) # + model.ydot = pyodae.DerivativeVar(model.y, wrt=model.t) # dy/dt + model.r = pyo.Var(model.t, [1,2,3,4,5], initialize=1.0) + model.Fin = pyo.Var(model.t, initialize=1.0) + + # Non-time indexed constraint (just for testing) + model.H_eqn = pyo.Constraint(expr=model.H==737) + + # Equations + @model.Constraint(model.t) + def eq_ydot1(b, t): + return b.ydot[t, 1] == -2.0*b.r[t, 1] + b.r[t, 2] - b.r[t, 3] - b.r[t, 4] + @model.Constraint(model.t) + def eq_ydot2(b, t): + return b.ydot[t, 2] == -0.5*b.r[t, 1] - b.r[t, 4] - 0.5*b.r[t, 5] + b.Fin[t] + @model.Constraint(model.t) + def eq_ydot3(b, t): + return b.ydot[t, 3] == b.r[t, 1] - b.r[t, 2] + b.r[t, 3] + @model.Constraint(model.t) + def eq_ydot4(b, t): + return b.ydot[t, 4] == -b.r[t, 2] + b.r[t, 3] - 2.0*b.r[t, 4] + @model.Constraint(model.t) + def eq_ydot5(b, t): + return b.ydot[t, 5] == b.r[t, 2] - b.r[t, 3] + b.r[t, 5] + @model.Constraint(model.t) + def eq_y6(b, t): + return b.ydot[t, 6] == b.Ks*b.y[t, 1]*b.y[t, 4] - b.y[t, 6] + + model.ydot[:, 6].fix(0) + + @model.Constraint(model.t) + def eq_r1(b, t): + return b.r[t, 1] == b.k[1]*b.y[t, 1]**4*b.y[t, 2]**0.5 + @model.Constraint(model.t) + def eq_r2(b, t): + return b.r[t, 2] == b.k[2]*b.y[t, 3]*b.y[t, 4] + @model.Constraint(model.t) + def eq_r3(b, t): + return b.r[t, 3] == b.k[2]/b.Ke*b.y[t, 1]*b.y[t, 5] + @model.Constraint(model.t) + def eq_r4(b, t): + return b.r[t, 4] == b.k[3]*b.y[t, 1]*b.y[t, 4]**2 + @model.Constraint(model.t) + def eq_r5(b, t): + return b.r[t, 5] == b.k[4]*b.y[t, 6]**2*b.y[t, 2]**0.5 + @model.Constraint(model.t) + def eq_Fin(b, t): + return b.Fin[t] == b.klA*(b.pCO2/b.H - b.y[t, 2]) + + # Set initial conditions and solve initial from the values of differential + # variables (r and y6 well and the derivative vars too). + y0 = {1:0.444, 2:0.00123, 3:0.0, 4:0.007, 5:0.0} #initial differential vars + for i in y0: + model.y[0, i].fix(y0[i]) + + model.eq_ydot1[0].deactivate() + model.eq_ydot2[0].deactivate() + model.eq_ydot3[0].deactivate() + model.eq_ydot4[0].deactivate() + model.eq_ydot5[0].deactivate() + + discretizer = pyo.TransformationFactory('dae.finite_difference') + discretizer.apply_to(model, nfe=1, scheme='BACKWARD') + + return ( + model, + 0.1150794920661702, + 0.1203831471567715e-2, + 0.1611562887407974, + 0.3656156421249283e-3, + 0.1708010885264404e-1, + 0.4873531310307455e-2, + ) + + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_car(): + m = car_example() + m.a.fix(1.0) + m.tf.fix(16.56) + + # solve + petsc.petsc_dae_by_time_element( + m, + time=m.tau, + ts_options={ + "--ts_type":"cn", # Crank–Nicolson + "--ts_adapt_type":"basic", + "--ts_dt":0.01, + }, + ) + + assert pyo.value(m.x[1]) == pytest.approx(131.273, rel=1e-2) + + +@pytest.mark.unit +def test_copy_time(): + """test the time copy function. When this is used, the model is flattened + and only indexed by time, so testing is pretty simple""" + + m = pyo.ConcreteModel() + t = [1, 2] + m.x = pyo.Var(t) + m.y = pyo.Var(t) + + m.x[1] = 1 + m.x[2] = 2 + m.y[1] = 3 + m.y[2] = 4 + + petsc._copy_time([m.x, m.y], 1, 2) + + assert pyo.value(m.x[2]) == 1 + assert pyo.value(m.y[2]) == 3 + +@pytest.mark.unit +def test_gen_time_disc_eqns(): + m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() + + # The model has one time element and derivatives for y[1] to y[5] + # the final time is 180, so create a list of what the time discretization + # constraints should be. + disc_eq = [ + id(m.ydot_disc_eq[180, 1]), + id(m.ydot_disc_eq[180, 2]), + id(m.ydot_disc_eq[180, 3]), + id(m.ydot_disc_eq[180, 4]), + id(m.ydot_disc_eq[180, 5]), + ] + + n = 0 + for cs in petsc.find_discretization_equations(m, m.t): + for c in cs.values(): + if c.index()[1] == 6: + continue + assert id(c) in disc_eq + n += 1 + + assert len(disc_eq) == n + +@pytest.mark.unit +def test_set_dae_suffix(): + m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() + regular_vars, time_vars = pyodae.flatten.flatten_dae_components(m, m.t, pyo.Var) + regular_cons, time_cons = pyodae.flatten.flatten_dae_components(m, m.t, pyo.Constraint) + t = 180 + m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.scaling_factor[m.ydot[180, 2]] = 10 + constraints = [con[t] for con in time_cons if t in con] + variables = [var[t] for var in time_vars] + t_block = create_subsystem_block(constraints, variables) + deriv_diff_map = petsc._get_derivative_differential_data_map(m, m.t) + petsc._set_dae_suffixes_from_variables(t_block, variables, deriv_diff_map) + petsc._sub_problem_scaling_suffix(m, t_block) + + assert t_block.dae_suffix[m.ydot[180, 1]] == 2 + assert t_block.dae_suffix[m.ydot[180, 2]] == 2 + assert t_block.dae_suffix[m.ydot[180, 3]] == 2 + assert t_block.dae_suffix[m.ydot[180, 4]] == 2 + assert t_block.dae_suffix[m.ydot[180, 5]] == 2 + assert t_block.scaling_factor[m.ydot[180, 2]] == 10 + assert t_block.dae_suffix[m.y[180, 1]] == 1 + assert t_block.dae_suffix[m.y[180, 2]] == 1 + assert t_block.dae_suffix[m.y[180, 3]] == 1 + assert t_block.dae_suffix[m.y[180, 4]] == 1 + assert t_block.dae_suffix[m.y[180, 5]] == 1 + + # Make sure deactivating a differential equation makes the variable that + # would have been differential go algebraic + m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() + # discretization equations would be deactivated in normal PETSc solve + for con in petsc.find_discretization_equations(m, m.t): + con.deactivate() + # deactivate a differential equation making y4 be algebraic + m.eq_ydot4[180].deactivate() + regular_vars, time_vars = pyodae.flatten.flatten_dae_components(m, m.t, pyo.Var) + regular_cons, time_cons = pyodae.flatten.flatten_dae_components(m, m.t, pyo.Constraint) + t = 180 + constraints = [con[t] for con in time_cons if t in con] + variables = [var[t] for var in time_vars] + t_block = create_subsystem_block(constraints, variables) + deriv_diff_map = petsc._get_derivative_differential_data_map(m, m.t) + petsc._set_dae_suffixes_from_variables(t_block, variables, deriv_diff_map) + petsc._sub_problem_scaling_suffix(m, t_block) + + assert m.y[t, 4] not in t_block.dae_suffix + assert m.ydot[t, 4] not in t_block.dae_suffix + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_petsc_read_trajectory(): + """ + Check that the PETSc DAE solver works. + """ + m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() + m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.scaling_factor[m.y[180, 1]] = 10 # make sure unscale works + + petsc.petsc_dae_by_time_element( + m, + time=m.t, + ts_options={ + "--ts_type":"cn", # Crank–Nicolson + "--ts_adapt_type":"basic", + "--ts_dt":0.01, + "--ts_save_trajectory":1, + "--ts_trajectory_type":"visualization", + }, + vars_stub="tj_random_junk_123", + trajectory_save_prefix="tj_random_junk_123", + ) + assert pytest.approx(y1, rel=1e-3) == pyo.value(m.y[m.t.last(), 1]) + assert pytest.approx(y2, rel=1e-3) == pyo.value(m.y[m.t.last(), 2]) + assert pytest.approx(y3, rel=1e-3) == pyo.value(m.y[m.t.last(), 3]) + assert pytest.approx(y4, rel=1e-3) == pyo.value(m.y[m.t.last(), 4]) + assert pytest.approx(y5, rel=1e-3) == pyo.value(m.y[m.t.last(), 5]) + assert pytest.approx(y6, rel=1e-3) == pyo.value(m.y[m.t.last(), 6]) + + tj = petsc.PetscTrajectory(json="tj_random_junk_123_1.json.gz") + assert tj.get_dt()[0] == pytest.approx(0.01) # if small enough shouldn't be cut + assert tj.get_vec(m.y[180, 1])[-1] == pytest.approx(y1, rel=1e-3) + assert tj.get_vec("_time")[-1] == pytest.approx(180) + + times = np.linspace(0, 180, 181) + vecs = tj.interpolate_vecs(times) + assert vecs[str(m.y[180, 1])][180] == pytest.approx(y1, rel=1e-3) + assert vecs["_time"][180] == pytest.approx(180) + + tj.to_json("some_testy_json.json") + with open("some_testy_json.json", "r") as fp: + vecs = json.load(fp) + assert vecs[str(m.y[180, 1])][-1] == pytest.approx(y1, rel=1e-3) + assert vecs["_time"][-1] == pytest.approx(180) + os.remove("some_testy_json.json") + + tj.to_json("some_testy_json.json.gz") + tj2 = petsc.PetscTrajectory(json="some_testy_json.json.gz") + assert tj2.vecs[str(m.y[180, 1])][-1] == pytest.approx(y1, rel=1e-3) + assert tj2.vecs["_time"][-1] == pytest.approx(180) + os.remove("some_testy_json.json.gz") + + tj2 = petsc.PetscTrajectory(vecs=vecs) + assert tj2.vecs[str(m.y[180, 1])][-1] == pytest.approx(y1, rel=1e-3) + assert tj2.vecs["_time"][-1] == pytest.approx(180) + + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_rp_example(): + + m = rp_example() + with pytest.raises(RuntimeError): + petsc.petsc_dae_by_time_element( + m, + time=m.time, + ) + + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_rp_example2(): + + m = rp_example2() + petsc.petsc_dae_by_time_element( + m, + time=m.time, + timevar=m.t, + ts_options={ + "--ts_dt":1, + "--ts_adapt_type":"none", + } + ) + assert pyo.value(m.u[10]) == pytest.approx(398) + assert pyo.value(m.x[10]) == pytest.approx(20) + + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_rp_example3(): + + m = rp_example3() + with pytest.raises(RuntimeError): + petsc.petsc_dae_by_time_element( + m, + time=m.time, + ) + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_rp_example4(): + + m = rp_example4() + petsc.petsc_dae_by_time_element( + m, + time=m.time, + ts_options={ + "--ts_dt":1, + "--ts_adapt_type":"none", + } + ) + assert pyo.value(m.u[10]) == pytest.approx(398) + assert pyo.value(m.x[10]) == pytest.approx(20) diff --git a/idaes/core/solvers/tests/test_petsc_pid.py b/idaes/core/solvers/tests/test_petsc_pid.py new file mode 100644 index 0000000000..00cae14444 --- /dev/null +++ b/idaes/core/solvers/tests/test_petsc_pid.py @@ -0,0 +1,267 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +"""This uses the PID controller model to do a little more testing of the +PETSc TS solver. Specifically it tests constraints with an explicit time var.""" + +__author__ = "John Eslick" + +import pytest +import pyomo.environ as pyo +from pyomo.network import Arc +from idaes.core import FlowsheetBlock, MaterialBalanceType +from idaes.generic_models.unit_models import Heater, Valve +from idaes.generic_models.properties import iapws95 +from idaes.core.util.initialization import propagate_state +from idaes.generic_models.control.controller import ( + PIDController, + ControllerType, + ControllerMVBoundType, +) +import idaes.core.util.scaling as iscale +from idaes.core.util import get_solver +from idaes.core.solvers import petsc +from idaes.core.util.math import smooth_max, smooth_min +import numpy as np + +def _valve_pressure_flow_cb(b): + """ + Callback for valve pressure-flow relation F = Cv*sqrt(Pi**2 - Po**2)*f(x) + """ + umeta = b.config.property_package.get_metadata().get_derived_units + + b.Cv = pyo.Var( + initialize=0.1, + doc="Valve flow coefficent", + units=umeta("amount") / umeta("time") / umeta("pressure"), + ) + b.Cv.fix() + + b.flow_var = pyo.Reference(b.control_volume.properties_in[:].flow_mol) + b.pressure_flow_equation_scale = lambda x: x ** 2 + + @b.Constraint(b.flowsheet().time) + def pressure_flow_equation(b2, t): + Po = b2.control_volume.properties_out[t].pressure + Pi = b2.control_volume.properties_in[t].pressure + F = b2.control_volume.properties_in[t].flow_mol + Cv = b2.Cv + fun = b2.valve_function[t] + return F ** 2 == Cv ** 2 * (Pi ** 2 - Po ** 2) * fun ** 2 + + +def create_model( + time_set=None, + time_units=pyo.units.s, + nfe=5, + tee=False, + calc_integ=True, +): + """Create a test model and solver + + Args: + time_set (list): The beginning and end point of the time domain + time_units (Pyomo Unit object): Units of time domain + nfe (int): Number of finite elements argument for the DAE + transformation. + calc_integ (bool): If True, calculate in the initial condition for + the integral term, else use a fixed variable (fs.ctrl.err_i0), + False is the better option if you have a value from a previous + time period + + Returns + (tuple): (ConcreteModel, Solver) + """ + fs_cfg = {"dynamic": True, "time_set": time_set, "time_units": time_units} + model_name = "Steam Tank, Dynamic" + + if time_set is None: + time_set = [0, 3] + + m = pyo.ConcreteModel(name=model_name) + m.fs = FlowsheetBlock(default=fs_cfg) + # Create a property parameter block + m.fs.prop_water = iapws95.Iapws95ParameterBlock( + default={"phase_presentation": iapws95.PhaseType.LG} + ) + # Create the valve and tank models + m.fs.valve_1 = Valve( + default={ + "dynamic": False, + "has_holdup": False, + "pressure_flow_callback": _valve_pressure_flow_cb, + "material_balance_type": MaterialBalanceType.componentTotal, + "property_package": m.fs.prop_water, + } + ) + m.fs.tank = Heater( + default={ + "has_holdup": True, + "material_balance_type": MaterialBalanceType.componentTotal, + "property_package": m.fs.prop_water, + } + ) + m.fs.valve_2 = Valve( + default={ + "dynamic": False, + "has_holdup": False, + "pressure_flow_callback": _valve_pressure_flow_cb, + "material_balance_type": MaterialBalanceType.componentTotal, + "property_package": m.fs.prop_water, + } + ) + # Add a controller + m.fs.ctrl = PIDController( + default={ + "process_var": m.fs.tank.control_volume.properties_out[:].pressure, + "manipulated_var": m.fs.valve_1.valve_opening, + "calculate_initial_integral": calc_integ, + "mv_bound_type": ControllerMVBoundType.SMOOTH_BOUND, + "type": ControllerType.PI, # rather use PI, but testing all terms + } + ) + # The control volume block doesn't assume the two phases are in equilibrium + # by default, so I'll make that assumption here, I don't actually expect + # liquid to form but who knows. The phase_fraction in the control volume is + # volumetric phase fraction hence the densities. + @m.fs.tank.Constraint(m.fs.time) + def vol_frac_vap(b, t): + return ( + b.control_volume.properties_out[t].phase_frac["Vap"] + * b.control_volume.properties_out[t].dens_mol + / b.control_volume.properties_out[t].dens_mol_phase["Vap"] + ) == (b.control_volume.phase_fraction[t, "Vap"]) + + # Connect the models + m.fs.v1_to_tank = Arc(source=m.fs.valve_1.outlet, destination=m.fs.tank.inlet) + m.fs.tank_to_v2 = Arc(source=m.fs.tank.outlet, destination=m.fs.valve_2.inlet) + + # Add the stream constraints and do the DAE transformation + pyo.TransformationFactory("network.expand_arcs").apply_to(m.fs) + pyo.TransformationFactory("dae.finite_difference").apply_to( + m.fs, nfe=nfe, wrt=m.fs.time, scheme="BACKWARD" + ) + + # Fix the derivative variables to zero at time 0 (steady state assumption) + m.fs.fix_initial_conditions() + + # Fix the input variables + m.fs.valve_1.inlet.enth_mol.fix(50000) + m.fs.valve_1.inlet.pressure.fix(5e5) + m.fs.valve_2.outlet.pressure.fix(101325) + m.fs.valve_1.Cv.fix(0.001) + m.fs.valve_2.Cv.fix(0.001) + m.fs.valve_1.valve_opening.fix(1) + m.fs.valve_2.valve_opening.fix(1) + m.fs.tank.heat_duty.fix(0) + m.fs.tank.control_volume.volume.fix(2.0) + + #Fix controller settings + m.fs.ctrl.gain_p.fix(1e-6) + m.fs.ctrl.gain_i.fix(1e-5) + #m.fs.ctrl.gain_d.fix(1e-6) + #m.fs.ctrl.derivative_of_error[m.fs.time.first()].fix(0) + m.fs.ctrl.setpoint.fix(3e5) + m.fs.ctrl.mv_ref.fix(0) + m.fs.ctrl.mv_lb = 0.0 + m.fs.ctrl.mv_ub = 1.0 + + for t in m.fs.time: + m.fs.valve_1.inlet.flow_mol[t] = 100 # initial guess on flow + # simple initialize + m.fs.valve_1.initialize() + propagate_state(m.fs.v1_to_tank) + m.fs.tank.initialize() + propagate_state(m.fs.tank_to_v2) + # Can't specify both flow and outlet pressure so free the outlet pressure + # for initialization and refix it after. Inlet flow gets fixed in init, but + # is unfixed for the final problem + m.fs.valve_2.outlet.pressure.unfix() + m.fs.valve_2.initialize() + m.fs.valve_2.outlet.pressure.fix(101325) + m.fs.valve_1.valve_opening.unfix() + m.fs.valve_1.valve_opening[m.fs.time.first()].fix() + # Return the model and solver + return m + +@pytest.mark.integration +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_petsc_with_pid_model(): + m = create_model( + time_set=[0, 24], + nfe=1, + calc_integ=True, + ) + # time_var will be an explicit time variable we can use in constraints. + m.fs.time_var = pyo.Var(m.fs.time) + + # We'll add a constraint to calculate the inlet pressure based on time, + # so we need to unfix pressure. + m.fs.valve_1.control_volume.properties_in[0].pressure.unfix() + m.fs.valve_1.control_volume.properties_in[24].pressure.unfix() + + # The solver will directly set the time variable for the DAE solve, but + # solving the initial conditions is just a system of nonlinear equations, + # so we need to fix the initial time. + m.fs.time_var[0].fix(m.fs.time.first()) + + # We could break up the time domain and solve this in pieces, but creative use + # of min and max will let us create the ramping function we want. + # From 10s to 12s ramp inlet pressure from 500,000 Pa to 600,000 Pa + @m.fs.Constraint(m.fs.time) + def inlet_pressure_eqn(b, t): + return b.valve_1.control_volume.properties_in[t].pressure == \ + smooth_min(600000, smooth_max(500000, 50000*(b.time_var[t] - 10) + 500000)) + + res = petsc.petsc_dae_by_time_element( + m, + time=m.fs.time, + timevar=m.fs.time_var, + ts_options={ + "--ts_type":"beuler", + "--ts_dt":0.1, + "--ts_monitor":"", # set initial step to 0.1 + "--ts_save_trajectory":1, + "--ts_trajectory_type":"visualization", + }, + vars_stub="tj_vars" + ) + + # read the trajectory data, and make it easy by interpolating a time point + # every second + tj = petsc.PetscTrajectory(stub="tj_vars", delete_on_read=True) + vecs = tj.interpolate_vecs(np.linspace(0, 24, 25)) + + # For more details about the problem behavior see the PETSc examples in the + # examples repo. + + # make sure the inlet pressure is initially 5e5 Pa + assert ( + pyo.value(vecs[str(m.fs.valve_1.control_volume.properties_in[24].pressure)][5]) + == pytest.approx(5e5) + ) + # make sure the inlet pressure ramped up to 6e5 Pa + assert ( + pyo.value(vecs[str(m.fs.valve_1.control_volume.properties_in[24].pressure)][20]) + == pytest.approx(6e5) + ) + # make sure after the controller comes on the presure goes to the set point + assert ( + pyo.value(vecs[str(m.fs.tank.control_volume.properties_out[24].pressure)][9]) + == pytest.approx(3e5) + ) + # make sure after ramping inlet pressure the tank pressure gets back to the + # setpoint + assert ( + pyo.value(vecs[str(m.fs.tank.control_volume.properties_out[24].pressure)][22]) + == pytest.approx(3e5) + ) diff --git a/idaes/core/solvers/tests/test_solvers.py b/idaes/core/solvers/tests/test_solvers.py index ae35feb671..9d74b90954 100644 --- a/idaes/core/solvers/tests/test_solvers.py +++ b/idaes/core/solvers/tests/test_solvers.py @@ -13,8 +13,9 @@ import pyomo.environ as pyo import pytest import idaes.core.plugins -from idaes.core.solvers.features import lp, milp, nlp, minlp, nle +from idaes.core.solvers.features import lp, milp, nlp, minlp, nle, dae from idaes.core.solvers import ipopt_has_linear_solver +from idaes.core.solvers import petsc @pytest.mark.unit def test_couenne_available(): @@ -84,6 +85,41 @@ def test_ipopt_has_ma27(): " or use solvers distributed by the IDAES project. See IDAES install" " guide.") +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_petsc_idaes_solve(): + """ + Make sure there is no issue with the solver class or default settings that + break the solver object. Passing a bad solver option will result in failure + """ + m, x = nle() + solver = pyo.SolverFactory("petsc_snes") + solver.solve(m, tee=True) + assert pytest.approx(x) == pyo.value(m.x) + +@pytest.mark.unit +@pytest.mark.skipif(not petsc.petsc_available(), reason="PETSc solver not available") +def test_petsc_dae_idaes_solve(): + """ + Check that the PETSc DAE solver works. + """ + m, y1, y2, y3, y4, y5, y6 = dae() + petsc.petsc_dae_by_time_element( + m, + time=m.t, + ts_options={ + "--ts_type":"cn", # Crank–Nicolson + "--ts_adapt_type":"basic", + "--ts_dt":0.1, + }, + ) + assert pytest.approx(y1, rel=1e-3) == pyo.value(m.y[m.t.last(), 1]) + assert pytest.approx(y2, rel=1e-3) == pyo.value(m.y[m.t.last(), 2]) + assert pytest.approx(y3, rel=1e-3) == pyo.value(m.y[m.t.last(), 3]) + assert pytest.approx(y4, rel=1e-3) == pyo.value(m.y[m.t.last(), 4]) + assert pytest.approx(y5, rel=1e-3) == pyo.value(m.y[m.t.last(), 5]) + assert pytest.approx(y6, rel=1e-3) == pyo.value(m.y6[m.t.last()]) + @pytest.mark.unit def test_bonmin_idaes_solve(): """ @@ -94,7 +130,7 @@ def test_bonmin_idaes_solve(): solver = pyo.SolverFactory('bonmin') solver.solve(m) assert pytest.approx(x) == pyo.value(m.x) - assert i == pyo.value(m.i) + assert i == pyo.value(m.i) @pytest.mark.unit def test_couenne_idaes_solve(): diff --git a/idaes/core/tests/test_control_volume_0d.py b/idaes/core/tests/test_control_volume_0d.py index ddc4fe766c..9c3fa05af8 100644 --- a/idaes/core/tests/test_control_volume_0d.py +++ b/idaes/core/tests/test_control_volume_0d.py @@ -17,8 +17,9 @@ """ import pytest from pyomo.environ import (ConcreteModel, Constraint, Expression, - Set, units, Var) -from pyomo.util.check_units import assert_units_consistent + Set, units, Var, value) +from pyomo.util.check_units import (assert_units_consistent, + assert_units_equivalent) from pyomo.common.config import ConfigBlock from idaes.core import (ControlVolume0DBlock, ControlVolumeBlockData, @@ -540,6 +541,102 @@ def test_add_material_balances_default(): assert_units_consistent(m) +@pytest.mark.unit +def test_add_material_balances_rxn_molar(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(default={"dynamic":False}) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) + + m.fs.rp.basis_switch = 1 + + m.fs.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp, + "reaction_package": m.fs.rp}) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units('flow_mass') # basis 2 is mass + rp_units = units('flow_mole') # basis 1 is molar + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units('mass')/units('amount')) + + # add material balances to control volume + m.fs.cv.add_material_balances(balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + +@pytest.mark.unit +def test_add_material_balances_rxn_mass(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(default={"dynamic":False}) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 1 + m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) + + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp, + "reaction_package": m.fs.rp}) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units('flow_mole') # basis 2 is molar + rp_units = units('flow_mass') # basis 1 is mass + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units('mass')/units('amount')) + + # add material balances to control volume + m.fs.cv.add_material_balances(balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + # ----------------------------------------------------------------------------- # Test add_phase_component_balances @pytest.mark.unit diff --git a/idaes/core/tests/test_control_volume_1d.py b/idaes/core/tests/test_control_volume_1d.py index 318f4fa557..ca8968fbc3 100644 --- a/idaes/core/tests/test_control_volume_1d.py +++ b/idaes/core/tests/test_control_volume_1d.py @@ -18,7 +18,8 @@ import pytest from pyomo.environ import (ConcreteModel, Constraint, Expression, Param, Set, units, value, Var) -from pyomo.util.check_units import assert_units_consistent +from pyomo.util.check_units import (assert_units_consistent, + assert_units_equivalent) from pyomo.dae import ContinuousSet, DerivativeVar from pyomo.common.config import ConfigBlock from pyomo.core.base.constraint import _GeneralConstraintData @@ -1095,6 +1096,110 @@ def test_add_material_balances_default(): assert_units_consistent(m) +@pytest.mark.unit +def test_add_material_balances_rxn_molar(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(default={"dynamic":False}) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) + + m.fs.rp.basis_switch = 1 + + m.fs.cv = ControlVolume1DBlock(default={"property_package": m.fs.pp, + "reaction_package": m.fs.rp, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "BACKWARD", + "finite_elements": 10}) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units('flow_mass')/units('length') # basis 2 is mass + rp_units = units('flow_mole')/units('length') # basis 1 is molar + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + for x in m.fs.cv.length_domain: + m.fs.cv.properties[t, x].mw_comp = Var( + m.fs.cv.properties[t, x].config.parameters.component_list, + units=units('mass')/units('amount')) + + # add material balances to control volume + m.fs.cv.add_material_balances(balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + +@pytest.mark.unit +def test_add_material_balances_rxn_mass(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(default={"dynamic":False}) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 1 + m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) + + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume1DBlock(default={"property_package": m.fs.pp, + "reaction_package": m.fs.rp, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "BACKWARD", + "finite_elements": 10}) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units('flow_mole')/units('length') # basis 2 is molar + rp_units = units('flow_mass')/units('length') # basis 1 is mass + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + for x in m.fs.cv.length_domain: + m.fs.cv.properties[t, x].mw_comp = Var( + m.fs.cv.properties[t, x].config.parameters.component_list, + units=units('mass')/units('amount')) + + # add material balances to control volume + m.fs.cv.add_material_balances(balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + # ----------------------------------------------------------------------------- # Test add_phase_component_balances @pytest.mark.unit diff --git a/idaes/core/unit_model.py b/idaes/core/unit_model.py index cd40df80a7..ff2ee9df1b 100644 --- a/idaes/core/unit_model.py +++ b/idaes/core/unit_model.py @@ -14,7 +14,7 @@ Base class for unit models """ -from pyomo.environ import Reference +from pyomo.environ import check_optimal_termination, Reference from pyomo.network import Port from pyomo.common.config import ConfigValue @@ -28,7 +28,8 @@ from idaes.core.util.exceptions import (BurntToast, ConfigurationError, PropertyPackageError, - BalanceTypeNotSupportedError) + BalanceTypeNotSupportedError, + InitializationError) from idaes.core.util.tables import create_stream_table_dataframe import idaes.core.util.unit_costing import idaes.logger as idaeslog @@ -656,5 +657,10 @@ def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, # Release Inlet state blk.control_volume.release_state(flags, outlvl) + if not check_optimal_termination(results): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info('Initialization Complete: {}' .format(idaeslog.condition(results))) diff --git a/idaes/core/util/initialization.py b/idaes/core/util/initialization.py index fb1cd82c3e..c2bb9d3491 100644 --- a/idaes/core/util/initialization.py +++ b/idaes/core/util/initialization.py @@ -16,7 +16,7 @@ """ from pyomo.environ import ( - Block, Constraint, Param, TerminationCondition, Var, value) + Block, check_optimal_termination, Constraint, Param, Var, value) from pyomo.network import Arc from pyomo.dae import ContinuousSet from pyomo.core.expr.visitor import identify_variables @@ -358,7 +358,7 @@ def initialize_by_time_element(fs, time, **kwargs): 'Model is inactive except at t=0. Solving for consistent initial conditions.') with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(fs, tee=slc.tee) - if results.solver.termination_condition == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info('Successfully solved for consistent initial conditions') else: init_log.error('Failed to solve for consistent initial conditions') @@ -456,7 +456,7 @@ def initialize_by_time_element(fs, time, **kwargs): with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(fs, tee=slc.tee) - if results.solver.termination_condition == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info(f'Successfully solved finite element {i}') else: init_log.error(f'Failed to solve finite element {i}') diff --git a/idaes/core/util/misc.py b/idaes/core/util/misc.py index 06904ddf20..ce616fc4f5 100644 --- a/idaes/core/util/misc.py +++ b/idaes/core/util/misc.py @@ -152,6 +152,15 @@ def set_param_from_config(b, param, config=None, index=None): converting units if required. This method directly sets the value of the parameter. + This method supports three forms for defining the parameter value: + + 1. a 2-tuple of the form (value, units) where units are the units that + the value are defined in + 2. a float where the float is assumed to be the value of the parameter + value in the base units of the property package + 3. a Python Class which has a get_parameter_value method which will + return a 2-tuple of (value, units) based on a lookup of the parameter name + Args: b - block on which parameter and config block are defined param - name of parameter as str. Used to find param and config arg @@ -217,6 +226,11 @@ def set_param_from_config(b, param, config=None, index=None): units = param_obj.get_units() + # Check to see if p_data is callable, and if so, try to call the + # get_parameter_value method to get 2-tuple + if hasattr(p_data, 'get_parameter_value'): + p_data = p_data.get_parameter_value(b.local_name, param) + if isinstance(p_data, tuple): # 11 Dec 2020 - There is currently a bug in Pyomo where trying to # convert the units of a unitless quantity results in a TypeError. diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index d5431eb982..570b4efdf3 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -26,8 +26,6 @@ from idaes.core.util.model_statistics import large_residuals_set, variables_near_bounds_set -from pyomo.opt import SolverStatus, TerminationCondition - import matplotlib.pyplot as plt from operator import itemgetter diff --git a/idaes/core/util/phase_equilibria.py b/idaes/core/util/phase_equilibria.py index 156275df79..0892f37a91 100644 --- a/idaes/core/util/phase_equilibria.py +++ b/idaes/core/util/phase_equilibria.py @@ -19,7 +19,8 @@ __author__ = "Alejandro Garciadiego" # Import objects from pyomo package -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, SolverFactory, value, Var, @@ -28,7 +29,6 @@ units as pyunits) import idaes.logger as idaeslog from idaes.core.util import get_solver -from pyomo.opt import TerminationCondition, SolverStatus import idaes.logger as idaeslog @@ -152,7 +152,7 @@ def Txy_data(model, component_1, component_2, pressure, num_points = 20, tempera # solve the model status = solver.solve(model, tee = False) # If solution is optimal store the concentration, and calculated temperatures in the created arrays - if (status.solver.status == SolverStatus.ok) and (status.solver.termination_condition == TerminationCondition.optimal): + if check_optimal_termination(status): print('Case: ',count,' Optimal. ', component_1, 'x = {:.2f}'.format(x_d[i])) diff --git a/idaes/core/util/tags.py b/idaes/core/util/tags.py index 147356f479..aa431b5d82 100644 --- a/idaes/core/util/tags.py +++ b/idaes/core/util/tags.py @@ -63,7 +63,10 @@ def __init__(self, expr, format_string="{}", doc="", display_units=None): """ super().__init__() self._format = format_string # format string for printing expression - self._expression = expr # tag expression (can be unnamed) + if isinstance(expr, IndexedComponent_slice): + self._expression = pyo.Reference(expr) # tag expression (can be unnamed) + else: + self._expression = expr self._doc = doc # documentation for a tag self._display_units = display_units # unit to display value in self._cache_validation_value = {} # value when converted value stored @@ -97,9 +100,7 @@ def __getitem__(self, k): raise KeyError( f"{k} is not a valid index for tag {self._name}" ) from key_err - if ( - self._root is None or self.is_slice - ): # cache the unit conversion in root object + if (self._root is None): # cache the unit conversion in root object tag._root = self else: tag._root = self._root @@ -311,14 +312,6 @@ def is_var(self): except AttributeError: return False - @property - def is_slice(self): - """Whether the tagged expression is a Pyomo slice.""" - try: - return isinstance(self._expression, IndexedComponent_slice) - except AttributeError: - return False - @property def is_indexed(self): """Returns whether the tagged expression is an indexed.""" diff --git a/idaes/core/util/tests/test_initialization.py b/idaes/core/util/tests/test_initialization.py index cb2b1165f8..3e4139548f 100644 --- a/idaes/core/util/tests/test_initialization.py +++ b/idaes/core/util/tests/test_initialization.py @@ -17,7 +17,7 @@ import pytest from pyomo.environ import (Block, ConcreteModel, Constraint, Expression, exp, Set, Var, value, Param, Reals, units as pyunits, - TransformationFactory, TerminationCondition) + TransformationFactory, check_optimal_termination) from pyomo.network import Arc, Port from idaes.core import (FlowsheetBlock, @@ -1021,4 +1021,4 @@ def test_initialize_by_time_element(): assert value(con.lower) - value(con.body) < 1e-5 results = solver.solve(m.fs) - assert results.solver.termination_condition == TerminationCondition.optimal + assert check_optimal_termination(results) diff --git a/idaes/core/util/tests/test_pid_initialization.py b/idaes/core/util/tests/test_pid_initialization.py index 276e1f9f35..128b0eb75d 100644 --- a/idaes/core/util/tests/test_pid_initialization.py +++ b/idaes/core/util/tests/test_pid_initialization.py @@ -17,27 +17,15 @@ """ import pytest -from pyomo.environ import (Block, ConcreteModel, Constraint, Expression, - Set, SolverFactory, Var, value, Param, Reals, - TransformationFactory, TerminationCondition, - exp, units as pyunits) -from pyomo.network import Arc, Port -from pyomo.dae import DerivativeVar +from pyomo.environ import (Block, ConcreteModel, Constraint, Var, + TransformationFactory, units as pyunits) +from pyomo.network import Arc from pyomo.common.collections import ComponentMap -from idaes.core import (FlowsheetBlock, - MaterialBalanceType, +from idaes.core import (FlowsheetBlock, + MaterialBalanceType, EnergyBalanceType, - MomentumBalanceType, - declare_process_block_class, - PhysicalParameterBlock, - StateBlock, - StateBlockData, - ReactionParameterBlock, - ReactionBlockBase, - ReactionBlockDataBase, - MaterialFlowBasis) -from idaes.core.util.testing import PhysicalParameterTestBlock + MomentumBalanceType) from idaes.core.util.model_statistics import degrees_of_freedom from idaes.generic_models.unit_models import CSTR, Mixer, MomentumMixingType from idaes.generic_models.control import PIDBlock, PIDForm diff --git a/idaes/core/util/tests/test_tags.py b/idaes/core/util/tests/test_tags.py index ec6d579e91..53693e4a31 100644 --- a/idaes/core/util/tests/test_tags.py +++ b/idaes/core/util/tests/test_tags.py @@ -439,3 +439,19 @@ def test_doc_example_and_bound(model): assert str(g["x"][1]) == "2.000" assert abs(g["x"][1].expression.lb - 0.001) < 1e-5 # x is in kg assert abs(g["x"][1].expression.ub - 0.003) < 1e-5 # x is in kg + +@pytest.mark.unit +def test_tag_slice(model): + m = model + + tw = ModelTag( + expr=m.w[1,:], + format_string=lambda x: "{:,.0f}" if x >= 100 else "{:.2f}", + doc="Tag for x", + display_units=pyo.units.g + ) + + tw.set(1*pyo.units.g) + assert str(tw["a"]) == "1.00 g" + tw.set(1*pyo.units.kg) + assert str(tw["b"]) == "1,000 g" diff --git a/idaes/core/util/tests/test_unit_costing.py b/idaes/core/util/tests/test_unit_costing.py index b7cb95009b..f504885f6e 100644 --- a/idaes/core/util/tests/test_unit_costing.py +++ b/idaes/core/util/tests/test_unit_costing.py @@ -98,9 +98,7 @@ def test_costing_FH_solve(): abs=1e-2) == 962795.521) results = solver.solve(m, tee=False) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert (pytest.approx(pyo.value(m.fs.unit.costing.purchase_cost), abs=1e-2) == 962795.521) # Example 22.1 Ref Book @@ -158,9 +156,7 @@ def test_costing_distillation_solve(): results = solver.solve(m, tee=False) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost), abs=1e-2) == 636959.6929) # Example 22.13 Ref Book assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost_platf_ladders), @@ -216,9 +212,7 @@ def test_blower_build_and_solve(): assert isinstance(m.fs.unit.costing.purchase_cost, pyo.Var) assert isinstance(m.fs.unit.costing.base_cost_per_unit, pyo.Var) results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost), abs=1e-2) == 56026.447) assert (pytest.approx(pyo.value(m.fs.unit.costing.purchase_cost), @@ -272,9 +266,7 @@ def test_compressor_fan(): assert isinstance(m.fs.unit.costing.purchase_cost, pyo.Var) assert isinstance(m.fs.unit.costing.base_cost_per_unit, pyo.Var) results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost), abs=1e-2) == 4543.6428) assert (pytest.approx(pyo.value(m.fs.unit.costing.purchase_cost), diff --git a/idaes/core/util/tests/test_utility_minimization.py b/idaes/core/util/tests/test_utility_minimization.py index 1fd3194c1b..d04b39fc46 100644 --- a/idaes/core/util/tests/test_utility_minimization.py +++ b/idaes/core/util/tests/test_utility_minimization.py @@ -18,10 +18,9 @@ __author__ = "Alejandro Garciadiego" import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Objective, - SolverStatus, - TerminationCondition, units as pyunits) from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util import get_solver @@ -188,9 +187,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.initialize @pytest.mark.solver diff --git a/idaes/dmf/tests/test_util.py b/idaes/dmf/tests/test_util.py index ee6c54380e..e8391a45ba 100644 --- a/idaes/dmf/tests/test_util.py +++ b/idaes/dmf/tests/test_util.py @@ -26,6 +26,7 @@ import pytest # +from pyomo.environ import value from idaes.dmf import util, resource from .util import init_logging @@ -101,7 +102,7 @@ def test_datetime_timestamp(): ts = time.time() dt = datetime.datetime.fromtimestamp(ts) ts1 = util.datetime_timestamp(dt) - assert pytest.approx(ts, ts1, 0.000001) + assert pytest.approx(ts, 0.000001) == ts1 @pytest.mark.unit diff --git a/idaes/gas_solid_contactors/flowsheets/dyn_TGA_example.py b/idaes/gas_solid_contactors/flowsheets/dyn_TGA_example.py index 98e902d501..775a31462f 100644 --- a/idaes/gas_solid_contactors/flowsheets/dyn_TGA_example.py +++ b/idaes/gas_solid_contactors/flowsheets/dyn_TGA_example.py @@ -79,7 +79,7 @@ def main(m): # is excess gas flowrate which means all state variables remain unchanged) for t in m.fs.time: m.fs.TGA.gas[t].temperature.fix(1273.15) - m.fs.TGA.gas[t].pressure.fix(1.01325) # 1atm + m.fs.TGA.gas[t].pressure.fix(1.01325E5) # 1atm m.fs.TGA.gas[t].mole_frac_comp['CO2'].fix(0.4) m.fs.TGA.gas[t].mole_frac_comp['H2O'].fix(0.5) m.fs.TGA.gas[t].mole_frac_comp['CH4'].fix(0.1) diff --git a/idaes/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.py b/idaes/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.py index 6f2a9d8a69..59936ad814 100644 --- a/idaes/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.py +++ b/idaes/gas_solid_contactors/flowsheets/ss_BFB_methane_combustion.py @@ -88,7 +88,7 @@ def main(): # Fix inlet port variables for gas and solid m.fs.BFB.gas_inlet.flow_mol[0].fix(272.81) # mol/s m.fs.BFB.gas_inlet.temperature[0].fix(373) # K - m.fs.BFB.gas_inlet.pressure[0].fix(1.86) # bar + m.fs.BFB.gas_inlet.pressure[0].fix(1.86E5) # Pa = 1E5 bar m.fs.BFB.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.4772) m.fs.BFB.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.0646) m.fs.BFB.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.4582) diff --git a/idaes/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.py b/idaes/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.py index 9858f65020..42d8aebd2b 100644 --- a/idaes/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.py +++ b/idaes/gas_solid_contactors/flowsheets/ss_MB_methane_combustion.py @@ -69,7 +69,7 @@ def main(): # Fix inlet port variables for gas and solid m.fs.MB.gas_inlet.flow_mol[0].fix(128.20513) # mol/s m.fs.MB.gas_inlet.temperature[0].fix(298.15) # K - m.fs.MB.gas_inlet.pressure[0].fix(2.00) # bar + m.fs.MB.gas_inlet.pressure[0].fix(2.00E5) # Pa = 1E5 bar m.fs.MB.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.02499) m.fs.MB.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.00001) m.fs.MB.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.975) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_phase_thermo.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_phase_thermo.py index 22ef7a73c1..df1a848c19 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_phase_thermo.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/gas_phase_thermo.py @@ -26,7 +26,6 @@ # Import Pyomo libraries from pyomo.environ import (Constraint, Param, - PositiveReals, Reals, value, Var, @@ -50,8 +49,10 @@ from idaes.core.util.model_statistics import ( degrees_of_freedom, number_unfixed_variables_in_activated_equalities) +from idaes.core.util.constants import Constants +from idaes.core.util.math import smooth_max import idaes.logger as idaeslog -from idaes.core.util import get_solver +from idaes.core.util import get_solver, scaling as iscale # Some more information about this module __author__ = "Chinedu Okoli" @@ -86,17 +87,14 @@ def build(self): self.CO2 = Component() self.H2O = Component() - # Gas Constant - self.gas_const = Param(within=PositiveReals, - default=8.314459848e-3, - doc='Gas Constant [kJ/mol.K]', - units=pyunits.kJ/pyunits.mol/pyunits.K) - # ------------------------------------------------------------------------- """ Pure gas component properties""" # Mol. weights of gas - units = kg/mol. ref: NIST webbook mw_comp_dict = {'CH4': 0.016, 'CO2': 0.044, 'H2O': 0.018} + # Molecular weight should be defined in default units + # (default mass units)/(default amount units) + # per the define_meta.add_default_units method below self.mw_comp = Param( self.component_list, mutable=False, @@ -104,22 +102,22 @@ def build(self): doc="Molecular weights of gas components [kg/mol]", units=pyunits.kg/pyunits.mol) - # Std. heat of formation of comp. - units = kJ/(mol comp) - ref: NIST - enth_mol_form_comp_dict = {'CH4': -74.8731, 'CO2': -393.5224, - 'H2O': -241.8264} + # Std. heat of formation of comp. - units = J/(mol comp) - ref: NIST + enth_mol_form_comp_dict = {'CH4': -74.8731E3, 'CO2': -393.5224E3, + 'H2O': -241.8264E3} self.enth_mol_form_comp = Param( self.component_list, mutable=False, initialize=enth_mol_form_comp_dict, - doc="Component molar heats of formation [kJ/mol]", - units=pyunits.kJ/pyunits.mol) + doc="Component molar heats of formation [J/mol]", + units=pyunits.J/pyunits.mol) # Ideal gas spec. heat capacity parameters(Shomate) of # components - ref: NIST webbook. Shomate equations from NIST. # Parameters A-E are used for cp calcs while A-H are used for enthalpy # calc. - # 1e3*cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) - # where T = Temperature (K)/1000, and cp_comp = (kJ/mol.K) + # cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) + # where T = Temperature (K)/1000, and cp_comp = (J/mol.K) # H_comp = H - H(298.15) = A*T + B*T^2/2 + C*T^3/3 + # D*T^4/4 - E/T + F - H where T = Temp (K)/1000 and H_comp = (kJ/mol) cp_param_dict = { @@ -148,11 +146,62 @@ def build(self): ('H2O', 7): 223.3967000, ('H2O', 8): -241.8264000 } - self.cp_param = Param(self.component_list, - range(1, 10), - mutable=False, - initialize=cp_param_dict, - doc="Shomate equation heat capacity parameters") + self.cp_param_1 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 1}, + doc="Shomate equation heat capacity coeff 1", + units=pyunits.J/pyunits.mol/pyunits.K) + self.cp_param_2 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 2}, + doc="Shomate equation heat capacity coeff 2", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK) + self.cp_param_3 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 3}, + doc="Shomate equation heat capacity coeff 3", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**2) + self.cp_param_4 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 4}, + doc="Shomate equation heat capacity coeff 4", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**3) + self.cp_param_5 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 5}, + doc="Shomate equation heat capacity coeff 5", + units=pyunits.J/pyunits.mol/pyunits.K*pyunits.kK**2) + self.cp_param_6 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 6}, + doc="Shomate equation heat capacity coeff 6", + units=pyunits.kJ/pyunits.mol) + self.cp_param_7 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 7}, + doc="Shomate equation heat capacity coeff 7", + units=pyunits.J/pyunits.mol/pyunits.K) + self.cp_param_8 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 8}, + doc="Shomate equation heat capacity coeff 8", + units=pyunits.kJ/pyunits.mol) # Viscosity constants: # Reference: Perry and Green Handbook; McGraw Hill, 2008 @@ -162,11 +211,37 @@ def build(self): ('CO2', 3): 290, ('CO2', 4): 0, ('H2O', 1): 1.7096e-8, ('H2O', 2): 1.1146, ('H2O', 3): 0, ('H2O', 4): 0} - self.visc_d_param = Param(self.component_list, - range(1, 10), - mutable=True, - initialize=visc_d_param_dict, - doc="Dynamic viscosity constants") + self.visc_d_param_1 = Param(self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + visc_d_param_dict.items() + if j == 1}, + doc="Dynamic viscosity constants", + units=pyunits.kg/pyunits.m/pyunits.s) + # The units of parameter 1 are dependent upon the value of parameter 2: + # [visc_d_param_1] = kg/m-s * K^(-(value([visc_d_param_2))) + # this is accounted for in the equation on line 655 + self.visc_d_param_2 = Param(self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + visc_d_param_dict.items() + if j == 2}, + doc="Dynamic viscosity constants", + units=pyunits.dimensionless) + self.visc_d_param_3 = Param(self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + visc_d_param_dict.items() + if j == 3}, + doc="Dynamic viscosity constants", + units=pyunits.K) + self.visc_d_param_4 = Param(self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + visc_d_param_dict.items() + if j == 4}, + doc="Dynamic viscosity constants", + units=pyunits.K**2) # Thermal conductivity constants: # Reference: Perry and Green Handbook; McGraw Hill, 2008 @@ -176,11 +251,41 @@ def build(self): ('CO2', 3): 964, ('CO2', 4): 1.86e6, ('H2O', 1): 6.204e-6, ('H2O', 2): 1.3973, ('H2O', 3): 0, ('H2O', 4): 0} - self.therm_cond_param = Param(self.component_list, - range(1, 10), - mutable=True, - initialize=therm_cond_param_dict, - doc="Thermal conductivity constants") + self.therm_cond_param_1 = Param( + self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + therm_cond_param_dict.items() + if j == 1}, + doc="Dynamic viscosity constants", + units=pyunits.J/pyunits.m/pyunits.s) + # The units of parameter 1 are dependent upon the value of parameter 2: + # [therm_cond_param_1] = J/m-s * K^(-(1 + value([therm_cond_param_2))) + # this is accounted for in the equation on line 734 + self.therm_cond_param_2 = Param( + self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + therm_cond_param_dict.items() + if j == 2}, + doc="Dynamic viscosity constants", + units=pyunits.dimensionless) + self.therm_cond_param_3 = Param( + self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + therm_cond_param_dict.items() + if j == 3}, + doc="Dynamic viscosity constants", + units=pyunits.K) + self.therm_cond_param_4 = Param( + self.component_list, + mutable=True, + initialize={k: v for (k, j), v in + therm_cond_param_dict.items() + if j == 4}, + doc="Dynamic viscosity constants", + units=pyunits.K**2) # Component diffusion volumes: # Ref: (1) Prop gas & liquids (2) Fuller et al. IECR, 58(5), 19, 1966 @@ -188,7 +293,15 @@ def build(self): self.diff_vol_param = Param(self.component_list, mutable=True, initialize=diff_vol_param_dict, - doc="Component diffusion volumes") + doc="Component diffusion volumes", + units=pyunits.dimensionless) + + # Set default scaling + for comp in self.component_list: + self.set_default_scaling("mole_frac_comp", 1e2, index=comp) + + self.set_default_scaling("visc_d", 1e4) + self.set_default_scaling("therm_cond", 1e2) @classmethod def define_metadata(cls, obj): @@ -198,21 +311,21 @@ def define_metadata(cls, obj): 'temperature': {'method': None, 'units': 'K'}, 'mole_frac_comp': {'method': None, 'units': None}, 'mw': {'method': '_mw', 'units': 'kg/mol'}, - 'cp_mol': {'method': '_cp_mol', 'units': 'kJ/mol.K'}, + 'cp_mol': {'method': '_cp_mol', 'units': 'J/mol.K'}, 'cp_mol_comp': {'method': '_cp_mol_comp', - 'units': 'kJ/mol.K'}, - 'cp_mass': {'method': '_cp_mass', 'units': 'kJ/kg.K'}, + 'units': 'J/mol.K'}, + 'cp_mass': {'method': '_cp_mass', 'units': 'J/kg.K'}, 'dens_mol': {'method': '_dens_mol', 'units': 'mol/m^3'}, 'dens_mol_comp': {'method': '_dens_mol_comp', 'units': 'mol/m^3'}, 'dens_mass': {'method': '_dens_mass', 'units': 'kg/m^3'}, - 'enth_mol': {'method': '_enth_mol', 'units': 'kJ/mol'}, + 'enth_mol': {'method': '_enth_mol', 'units': 'J/mol'}, 'enth_mol_comp': {'method': '_enth_mol_comp', - 'units': 'kJ/mol'}, + 'units': 'J/mol'}, 'visc_d': {'method': '_visc_d', 'units': 'kg/m.s'}, - 'therm_cond': {'method': '_therm_cond', 'units': 'kJ/m.K.s'}, + 'therm_cond': {'method': '_therm_cond', 'units': 'J/m.K.s'}, 'diffusion_comp': {'method': '_diffusion_comp', 'units': 'cm2/s'}}) @@ -284,7 +397,7 @@ def initialize(blk, state_args=None, hold_state=False, "initialization.") # --------------------------------------------------------------------- - # Initialise values + # Initialize values for k in blk.keys(): if hasattr(blk[k], "mw_eqn"): @@ -411,6 +524,8 @@ def build(self): """ super(GasPhaseStateBlockData, self).build() + units_meta = self._params.get_metadata().derived_units + # Object reference for molecular weight if needed by CV1D # Molecular weights add_object_reference(self, "mw_comp", @@ -419,37 +534,38 @@ def build(self): """List the necessary state variable objects.""" self.flow_mol = Var(initialize=1.0, domain=Reals, - doc='Component molar flowrate [mol/s]', - units=pyunits.mol/pyunits.s) + doc='Component molar flowrate', + units=units_meta['amount']/units_meta['time']) self.mole_frac_comp = Var( self._params.component_list, domain=Reals, initialize=1 / len(self._params.component_list), - doc='State component mole fractions [-]', - units=pyunits.mol/pyunits.mol) + doc='State component mole fractions', + units=units_meta['amount']/units_meta['amount']) self.pressure = Var(initialize=1.01325, domain=Reals, - doc='State pressure [bar]', - units=pyunits.bar) + doc='State pressure', + units=units_meta['pressure']) self.temperature = Var(initialize=298.15, domain=Reals, - doc='State temperature [K]', - units=pyunits.K) + doc='State temperature', + units=units_meta['temperature']) # Create standard constraints # Sum mole fractions if not inlet block if self.config.defined_state is False: def sum_component_eqn(b): - return 1e2 == 1e2 * sum(b.mole_frac_comp[j] - for j in b._params.component_list) + return 1 == sum(b.mole_frac_comp[j] + for j in b._params.component_list) self.sum_component_eqn = Constraint(rule=sum_component_eqn) def _mw(self): # Molecular weight of gas mixture + units_meta = self._params.get_metadata().derived_units self.mw = Var(domain=Reals, initialize=1.0, - doc="Molecular weight of gas mixture [kg/mol]", - units=pyunits.kg/pyunits.mol) + doc="Molecular weight of gas mixture", + units=units_meta['mass']/units_meta['amount']) def mw_eqn(b): return (b.mw == @@ -466,14 +582,19 @@ def mw_eqn(b): def _dens_mol(self): # Molar density + units_meta = self._params.get_metadata().derived_units self.dens_mol = Var(domain=Reals, initialize=1.0, - doc="Molar density/concentration [mol/m3]", - units=pyunits.mol/pyunits.m**3) + doc="Molar density/concentration", + units=units_meta['amount'] * + units_meta['length']**-3) def ideal_gas(b): - return (b.dens_mol*b._params.gas_const*b.temperature*1e-2 == - b.pressure) + return ( + b.dens_mol + * Constants.gas_constant # [=] J/mol/K + * b.temperature == + pyunits.convert(b.pressure, to_units=pyunits.Pa)) try: # Try to build constraint self.ideal_gas = Constraint(rule=ideal_gas) @@ -485,12 +606,13 @@ def ideal_gas(b): def _dens_mol_comp(self): # Component molar densities + units_meta = self._params.get_metadata().derived_units self.dens_mol_comp = Var(self._params.component_list, domain=Reals, initialize=1.0, - doc='Component molar concentration' - '[mol/m3]', - units=pyunits.mol/pyunits.m**3) + doc='Component molar concentration', + units=units_meta['amount'] * + units_meta['length']**-3) def comp_conc_eqn(b, j): return (b.dens_mol_comp[j] == @@ -507,10 +629,12 @@ def comp_conc_eqn(b, j): def _dens_mass(self): # Mass density + units_meta = self._params.get_metadata().derived_units self.dens_mass = Var(domain=Reals, initialize=1.0, - doc="Mass density [kg/m3]", - units=pyunits.kg/pyunits.m**3) + doc="Mass density", + units=units_meta['mass'] * + units_meta['length']**-3) def dens_mass_basis(b): return b.dens_mass == b.mw*b.dens_mol @@ -525,27 +649,30 @@ def dens_mass_basis(b): def _visc_d(self): # Mixture dynamic viscosity + units_meta = self._params.get_metadata().derived_units self.visc_d = Var(domain=Reals, initialize=1e-5, - doc="Mixture dynamic viscosity [kg/m.s]", - units=pyunits.kg/pyunits.m/pyunits.s) + doc="Mixture dynamic viscosity", + units=units_meta['mass'] * units_meta['length']**-1 * + units_meta['time']**-1) def visc_d_comp(i): - return self._params.visc_d_param[i, 1] * \ - (self.temperature**self._params.visc_d_param[i, 2]) \ - / ((1 + (self._params.visc_d_param[i, 3]/self.temperature)) - + (self._params.visc_d_param[i, 4] / - (self.temperature**2))) + visc_d_param_1 = self._params.visc_d_param_1[i] * \ + pyunits.K**(-self._params.visc_d_param_2[i]) + return (visc_d_param_1 * + (self.temperature**self._params.visc_d_param_2[i]) + / ((1 + (self._params.visc_d_param_3[i] / + self.temperature)) + + (self._params.visc_d_param_4[i] / + (self.temperature**2)))) def visc_d_constraint(b): - return 1e6*b.visc_d == 1e6*sum(b.mole_frac_comp[i]*visc_d_comp(i) - / (sum(b.mole_frac_comp[j] - * (b._params.mw_comp[j] / - b._params.mw_comp[i])**0.5 - for j in - b._params.component_list)) - for i in - b._params.component_list) + return b.visc_d == sum(b.mole_frac_comp[i]*visc_d_comp(i) + / (sum(b.mole_frac_comp[j] + * (b._params.mw_comp[j] / + b._params.mw_comp[i])**0.5 + for j in b._params.component_list)) + for i in b._params.component_list) try: # Try to build constraint self.visc_d_constraint = Constraint(rule=visc_d_constraint) @@ -564,14 +691,24 @@ def _diffusion_comp(self): '[cm2/s]', units=pyunits.cm**2/pyunits.s) + # Units of the parameter in the D_bin expression + param_units = (pyunits.atm * (pyunits.kg/pyunits.kmol)**0.5 * + pyunits.K**-1.75 * pyunits.cm**2 * pyunits.s**-1) + def D_bin(i, j): - # 1e3 used to multiply MW to convert from kg/mol to kg/kmol - return ((1.43e-3*(self.temperature**1.75) * - ((1e3 * self._params.mw_comp[i] + - 1e3 * self._params.mw_comp[j]) - / (2 * (1e3 * self._params.mw_comp[i]) * - (1e3*self._params.mw_comp[j])))**0.5) - / ((self.pressure) + return (((1.43e-3 * param_units) * + (self.temperature**1.75) * + ((pyunits.convert(self._params.mw_comp[i], + to_units=pyunits.kg/pyunits.kmol) + + pyunits.convert(self._params.mw_comp[j], + to_units=pyunits.kg/pyunits.kmol)) + / (2 * + (pyunits.convert(self._params.mw_comp[i], + to_units=pyunits.kg/pyunits.kmol)) * + (pyunits.convert(self._params.mw_comp[j], + to_units=pyunits.kg/pyunits.kmol)) + ))**0.5) + / ((pyunits.convert(self.pressure, to_units=pyunits.atm)) * ((self._params.diff_vol_param[i]**(1/3)) + (self._params.diff_vol_param[j]**(1/3)))**2)) @@ -593,18 +730,25 @@ def diffusion_comp_constraint(b, i): def _therm_cond(self): # Thermal conductivity of gas + units_meta = self._params.get_metadata().derived_units + units_therm_cond = (units_meta['energy'] * + units_meta['length']**-1 * + units_meta['temperature']**-1 * + units_meta['time']**-1) self.therm_cond = Var(domain=Reals, initialize=1e-5, - doc="Thermal conductivity of gas [kJ/m.K.s]", - units=pyunits.kJ/pyunits.m/pyunits.K/pyunits.s) + doc="Thermal conductivity of gas", + units=units_therm_cond) def therm_cond_comp(i): - return self._params.therm_cond_param[i, 1] \ - * (self.temperature**self._params.therm_cond_param[i, 2]) \ - / ((1 + (self._params.therm_cond_param[i, 3] / + therm_cond_param_1 = self._params.therm_cond_param_1[i] * \ + pyunits.K**(-(1 + self._params.therm_cond_param_2[i])) + return (therm_cond_param_1 * + (self.temperature**self._params.therm_cond_param_2[i]) + / ((1 + (self._params.therm_cond_param_3[i] / self.temperature)) - + (self._params.therm_cond_param[i, 4] / - (self.temperature**2))) + + (self._params.therm_cond_param_4[i] / + (self.temperature**2)))) def A_bin(i, j): return (1 + ((therm_cond_comp(j)/therm_cond_comp(i))**0.5) @@ -614,8 +758,7 @@ def A_bin(i, j): self._params.mw_comp[i])))**0.5 def therm_cond_constraint(b): - # The 1e-3 term is used as a conversion factor to a kJ basis - return 1e6*b.therm_cond == 1e6*(1e-3) * \ + return b.therm_cond == pyunits.convert( sum(b.mole_frac_comp[i] * therm_cond_comp(i) / (sum(b.mole_frac_comp[j] * @@ -623,7 +766,8 @@ def therm_cond_constraint(b): for j in b._params.component_list)) for i in - b._params.component_list) + b._params.component_list), + to_units=units_therm_cond) try: # Try to build constraint self.therm_cond_constraint = Constraint(rule=therm_cond_constraint) @@ -635,20 +779,25 @@ def therm_cond_constraint(b): def _cp_mol_comp(self): # Pure component vapour heat capacities + units_meta = self._params.get_metadata().derived_units + units_cp_mol = (units_meta['energy'] * + units_meta['amount']**-1 * + units_meta['temperature']**-1) self.cp_mol_comp = Var(self._params.component_list, domain=Reals, initialize=1.0, - doc="Pure component vapour heat capacities " - "[kJ/mol.K]", - units=pyunits.kJ/pyunits.mol/pyunits.K) + doc="Pure component vapour heat capacities", + units=units_cp_mol) def pure_component_cp_mol(b, j): - return b.cp_mol_comp[j] == 1e-3*( - b._params.cp_param[j, 1] + - b._params.cp_param[j, 2]*(b.temperature*1e-3) + - b._params.cp_param[j, 3]*(b.temperature*1e-3)**2 + - b._params.cp_param[j, 4]*(b.temperature*1e-3)**3 + - b._params.cp_param[j, 5]/((b.temperature*1e-3)**2)) + t = pyunits.convert(b.temperature, to_units=pyunits.kK) + return b.cp_mol_comp[j] == pyunits.convert(( + b._params.cp_param_1[j] + + b._params.cp_param_2[j]*t + + b._params.cp_param_3[j]*t**2 + + b._params.cp_param_4[j]*t**3 + + b._params.cp_param_5[j]/(t**2)), + to_units=units_cp_mol) try: # Try to build constraint self.cp_shomate_eqn = Constraint(self._params.component_list, @@ -661,10 +810,14 @@ def pure_component_cp_mol(b, j): def _cp_mol(self): # Mixture heat capacities + units_meta = self._params.get_metadata().derived_units + units_cp_mol = (units_meta['energy'] * + units_meta['amount']**-1 * + units_meta['temperature']**-1) self.cp_mol = Var(domain=Reals, initialize=1.0, - doc="Mixture heat capacity [kJ/mol.K]", - units=pyunits.kJ/pyunits.mol/pyunits.K) + doc="Mixture heat capacity", + units=units_cp_mol) def cp_mol(b): return b.cp_mol == sum(b.cp_mol_comp[j]*b.mole_frac_comp[j] @@ -681,10 +834,14 @@ def cp_mol(b): def _cp_mass(self): # Mixture heat capacities + units_meta = self._params.get_metadata().derived_units + units_cp_mass = (units_meta['energy'] * + units_meta['mass']**-1 * + units_meta['temperature']**-1) self.cp_mass = Var(domain=Reals, initialize=1.0, - doc="Mixture heat capacity, mass-basis [kJ/kg.K]", - units=pyunits.kJ/pyunits.kg/pyunits.K) + doc="Mixture heat capacity, mass-basis", + units=units_cp_mass) def cp_mass(b): return b.cp_mass*b.mw == b.cp_mol @@ -699,22 +856,29 @@ def cp_mass(b): def _enth_mol_comp(self): # Pure component vapour enthalpies + units_meta = self._params.get_metadata().derived_units + units_enth_mol = units_meta['energy'] * units_meta['amount']**-1 self.enth_mol_comp = Var( self._params.component_list, domain=Reals, initialize=1.0, - doc="Pure component enthalpies [kJ/mol]", - units=pyunits.kJ/pyunits.mol) + doc="Pure component enthalpies", + units=units_enth_mol) def pure_comp_enthalpy(b, j): - return b.enth_mol_comp[j] == ( - b._params.cp_param[j, 1]*(b.temperature*1e-3) + - b._params.cp_param[j, 2]*((b.temperature*1e-3)**2)/2 + - b._params.cp_param[j, 3]*((b.temperature*1e-3)**3)/3 + - b._params.cp_param[j, 4]*((b.temperature*1e-3)**4)/4 - - b._params.cp_param[j, 5]/(b.temperature*1e-3) + - b._params.cp_param[j, 6] - - b._params.cp_param[j, 8]) + t = pyunits.convert(b.temperature, to_units=pyunits.kK) + return b.enth_mol_comp[j] == pyunits.convert( + # parameters 1-5 are defined in J + b._params.cp_param_1[j]*t + + b._params.cp_param_2[j]*(t**2)/2 + + b._params.cp_param_3[j]*(t**3)/3 + + b._params.cp_param_4[j]*(t**4)/4 - + b._params.cp_param_5[j]/(t), to_units=units_enth_mol) + \ + pyunits.convert( + # parameters 6 and 8 are defined in kJ, and must be added + # after converting to the enthalpy units set + b._params.cp_param_6[j] - b._params.cp_param_8[j], + to_units=units_enth_mol) try: # Try to build constraint self.enthalpy_shomate_eqn = Constraint(self._params.component_list, @@ -727,11 +891,13 @@ def pure_comp_enthalpy(b, j): def _enth_mol(self): # Mixture molar enthalpy + units_meta = self._params.get_metadata().derived_units + units_enth_mol = units_meta['energy'] * units_meta['amount']**-1 self.enth_mol = Var( domain=Reals, initialize=1.0, - doc='Mixture specific enthalpy [kJ/mol]', - units=pyunits.kJ/pyunits.mol) + doc='Mixture specific enthalpy', + units=units_enth_mol) try: # Try to build constraint self.mixture_enthalpy_eqn = Constraint(expr=( @@ -789,3 +955,24 @@ def default_material_balance_type(blk): def default_energy_balance_type(blk): return EnergyBalanceType.enthalpyTotal + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + if self.is_property_constructed("sum_component_eqn"): + iscale.constraint_scaling_transform( + self.sum_component_eqn, + iscale.get_scaling_factor(self.mole_frac_comp['CH4']), + overwrite=False) + + if self.is_property_constructed("visc_d_constraint"): + iscale.constraint_scaling_transform( + self.visc_d_constraint, + iscale.get_scaling_factor(self.visc_d), + overwrite=False) + + if self.is_property_constructed("therm_cond_constraint"): + iscale.constraint_scaling_transform( + self.therm_cond_constraint, + iscale.get_scaling_factor(self.therm_cond), + overwrite=False) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/hetero_reactions.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/hetero_reactions.py index cf5ba220e5..0ea8bf64e5 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/hetero_reactions.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/hetero_reactions.py @@ -51,8 +51,9 @@ from idaes.core.util.config import (is_state_block, is_physical_parameter_block, is_reaction_parameter_block) +from idaes.core.util.constants import Constants import idaes.logger as idaeslog -from idaes.core.util import get_solver +from idaes.core.util import get_solver, scaling as iscale # Some more information about this module __author__ = "Chinedu Okoli" @@ -99,7 +100,8 @@ def build(self): # Smoothing factor self.eps = Param(mutable=True, default=1e-8, - doc='Smoothing Factor') + doc='Smoothing Factor', + units=pyunits.mol/pyunits.m**3) # Reaction rate scale factor self._scale_factor_rxn = Param(mutable=True, default=1, @@ -114,12 +116,16 @@ def build(self): ("R1", "Sol", "Fe3O4"): 8, ("R1", "Sol", "Al2O3"): 0} - # Standard Heat of Reaction - kJ/mol_rxn - dh_rxn_dict = {"R1": 136.5843} + # Standard Heat of Reaction - J/mol_rxn + dh_rxn_dict = {"R1": 136.5843E3} + # Heat of Reaction should be defined in default units + # (default energy units)/(default amount units) + # per the define_meta.add_default_units method in the + # relevant phase - in this case, the solid phase properties self.dh_rxn = Param(self.rate_reaction_idx, initialize=dh_rxn_dict, - doc="Heat of reaction [kJ/mol]", - units=pyunits.kJ/pyunits.mol) + doc="Heat of reaction [J/mol]", + units=pyunits.J/pyunits.mol) # ------------------------------------------------------------------------- """ Reaction properties that can be estimated""" @@ -149,9 +155,9 @@ def build(self): # Activation Energy self.energy_activation = Var(self.rate_reaction_idx, domain=Reals, - initialize=4.9e1, - doc='Activation energy [kJ/mol]', - units=pyunits.kJ/pyunits.mol) + initialize=4.9e4, + doc='Activation energy [J/mol]', + units=pyunits.J/pyunits.mol) self.energy_activation.fix() # Reaction order @@ -166,14 +172,16 @@ def build(self): domain=Reals, initialize=8e-4, doc='Pre-exponential factor' - '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') + '[mol^(1-rxn_order) * m^(3*rxn_order -2)/s]', + units=pyunits.mol**(1-1.3) * + pyunits.m**(3 * 1.3 - 2)/pyunits.s) self.k0_rxn.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'k_rxn': {'method': '_k_rxn', - 'units': 'mol^(1-N_reaction)m^(3*N_reaction -2)/s]'}, + 'units': 'mol^(1-rxn_order)m^(3*rxn_order -2)/s]'}, 'OC_conv': {'method': "_OC_conv", 'units': None}, 'OC_conv_temp': {'method': "_OC_conv_temp", 'units': None}, 'reaction_rate': {'method': "_reaction_rate", @@ -195,7 +203,7 @@ class _ReactionBlock(ReactionBlockBase): def initialize(blk, outlvl=idaeslog.NOTSET, optarg=None, solver=None): ''' - Initialisation routine for reaction package. + Initialization routine for reaction package. Keyword Arguments: outlvl : sets output level of initialization routine @@ -241,7 +249,7 @@ def initialize(blk, outlvl=idaeslog.NOTSET, blk[k].solid_state_ref.dens_mass_skeletal.fix( blk[k].solid_state_ref.dens_mass_skeletal.value) - # Initialise values + # Initialize values for k in blk.keys(): if hasattr(blk[k], "OC_conv_eqn"): calculate_variable_from_constraint( @@ -371,15 +379,19 @@ def _k_rxn(self): domain=Reals, initialize=1, doc='Rate constant ' - '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') + '[mol^(1-rxn_order) * m^(3*rxn_order -2)/s]', + units=pyunits.mol**(1-1.3) * + pyunits.m**(3 * 1.3 - 2)/pyunits.s) def rate_constant_eqn(b, j): if j == 'R1': - return 1e6 * self.k_rxn[j] == \ - 1e6 * (self._params.k0_rxn[j] * - exp(-self._params.energy_activation[j] / - (self.gas_state_ref._params.gas_const * - self.solid_state_ref.temperature))) + return self.k_rxn[j] == \ + (self._params.k0_rxn[j] * + exp(-self._params.energy_activation[j] / + (pyunits.convert( + Constants.gas_constant, # J/mol/K + to_units=pyunits.J/pyunits.mol/pyunits.K) * + self.solid_state_ref.temperature))) else: return Constraint.Skip try: @@ -395,10 +407,11 @@ def rate_constant_eqn(b, j): # Conversion of oxygen carrier def _OC_conv(self): self.OC_conv = Var(domain=Reals, initialize=0.0, - doc='Fraction of metal oxide converted') + doc='Fraction of metal oxide converted', + units=pyunits.dimensionless) def OC_conv_eqn(b): - return 1e6 * b.OC_conv * \ + return b.OC_conv * \ (b.solid_state_ref.mass_frac_comp['Fe3O4'] + (b.solid_state_ref._params.mw_comp['Fe3O4'] / b.solid_state_ref._params.mw_comp['Fe2O3']) * @@ -407,7 +420,7 @@ def OC_conv_eqn(b): / -b._params.rate_reaction_stoichiometry ['R1', 'Sol', 'Fe2O3']) * b.solid_state_ref.mass_frac_comp['Fe2O3']) == \ - 1e6 * b.solid_state_ref.mass_frac_comp['Fe3O4'] + b.solid_state_ref.mass_frac_comp['Fe3O4'] try: # Try to build constraint self.OC_conv_eqn = Constraint(rule=OC_conv_eqn) @@ -420,10 +433,11 @@ def OC_conv_eqn(b): def _OC_conv_temp(self): self.OC_conv_temp = Var(domain=Reals, initialize=1.0, doc='Reformulation term for' - 'X to help eqn scaling') + 'X to help eqn scaling', + units=pyunits.dimensionless) def OC_conv_temp_eqn(b): - return 1e3*b.OC_conv_temp**3 == 1e3*(1-b.OC_conv)**2 + return b.OC_conv_temp**3 == (1-b.OC_conv)**2 try: # Try to build constraint self.OC_conv_temp_eqn = Constraint(rule=OC_conv_temp_eqn) @@ -441,7 +455,7 @@ def _reaction_rate(self): units=pyunits.mol/pyunits.m**3/pyunits.s) def rate_rule(b, r): - return b.reaction_rate[r]*1e4 == b._params._scale_factor_rxn*1e4*( + return b.reaction_rate[r] == b._params._scale_factor_rxn*( b.solid_state_ref.mass_frac_comp['Fe2O3'] * (1 - b.solid_state_ref.particle_porosity) * b.solid_state_ref.dens_mass_skeletal * @@ -481,3 +495,42 @@ def model_check(blk): if value(blk.temperature) > blk.temperature.ub: _log.error('{} Temperature set above upper bound.'.format(blk.name) ) + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + # Set default scaling + def _set_default_factor(v, s): + for i in v: + if iscale.get_scaling_factor(v[i]) is None: + iscale.set_scaling_factor(v[i], s) + _set_default_factor(self.k_rxn, 1e3) + _set_default_factor(self.OC_conv, 1e3) + _set_default_factor(self.OC_conv_temp, 1e3) + _set_default_factor(self.reaction_rate, 1e2) + + if self.is_property_constructed("rate_constant_eqn"): + for i, c in self.rate_constant_eqn.items(): + iscale.constraint_scaling_transform( + c, + iscale.get_scaling_factor(self.k_rxn[i]), + overwrite=False) + + if self.is_property_constructed("OC_conv_eqn"): + iscale.constraint_scaling_transform( + self.OC_conv_eqn, + iscale.get_scaling_factor(self.OC_conv), + overwrite=False) + + if self.is_property_constructed("OC_conv_temp_eqn"): + iscale.constraint_scaling_transform( + self.OC_conv_temp_eqn, + iscale.get_scaling_factor(self.OC_conv_temp), + overwrite=False) + + if self.is_property_constructed("gen_rate_expression"): + for i, c in self.gen_rate_expression.items(): + iscale.constraint_scaling_transform( + c, + iscale.get_scaling_factor(self.reaction_rate[i]), + overwrite=False) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_phase_thermo.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_phase_thermo.py index 6ff213b2e4..6af08a7207 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_phase_thermo.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/solid_phase_thermo.py @@ -48,7 +48,7 @@ degrees_of_freedom, number_unfixed_variables_in_activated_equalities) import idaes.logger as idaeslog -from idaes.core.util import get_solver +from idaes.core.util import get_solver, scaling as iscale # Some more information about this module __author__ = "Chinedu Okoli" @@ -88,6 +88,9 @@ def build(self): # Mol. weights of solid components - units = kg/mol. ref: NIST webbook mw_comp_dict = {'Fe2O3': 0.15969, 'Fe3O4': 0.231533, 'Al2O3': 0.10196} + # Molecular weight should be defined in default units + # (default mass units)/(default amount units) + # per the define_meta.add_default_units method below self.mw_comp = Param( self.component_list, mutable=False, @@ -109,8 +112,8 @@ def build(self): # components - ref: NIST webbook. Shomate equations from NIST. # Parameters A-E are used for cp calcs while A-H are used for enthalpy # calc. - # 1e3*cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) - # where T = Temperature (K)/1000, and cp_comp = (kJ/mol.K) + # cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) + # where T = Temperature (K)/1000, and cp_comp = (J/mol.K) # H_comp = H - H(298.15) = A*T + B*T^2/2 + C*T^3/3 + # D*T^4/4 - E/T + F - H where T = Temp (K)/1000 and H_comp = (kJ/mol) cp_param_dict = { @@ -137,22 +140,78 @@ def build(self): ('Fe2O3', 5): 5.4336770, ('Fe2O3', 6): -843.1471000, ('Fe2O3', 7): 228.3548000, - ('Fe2O3', 8): -825.5032000} - self.cp_param = Param(self.component_list, - range(1, 10), - mutable=False, - initialize=cp_param_dict, - doc="Shomate equation heat capacity parameters") - - # Std. heat of formation of comp. - units = kJ/(mol comp) - ref: NIST - enth_mol_form_comp_dict = {'Fe2O3': -825.5032, 'Fe3O4': -1120.894, - 'Al2O3': -1675.690} + ('Fe2O3', 8): -825.5032000 + } + self.cp_param_1 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 1}, + doc="Shomate equation heat capacity coeff 1", + units=pyunits.J/pyunits.mol/pyunits.K) + self.cp_param_2 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 2}, + doc="Shomate equation heat capacity coeff 2", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK) + self.cp_param_3 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 3}, + doc="Shomate equation heat capacity coeff 3", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**2) + self.cp_param_4 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 4}, + doc="Shomate equation heat capacity coeff 4", + units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**3) + self.cp_param_5 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 5}, + doc="Shomate equation heat capacity coeff 5", + units=pyunits.J/pyunits.mol/pyunits.K*pyunits.kK**2) + self.cp_param_6 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 6}, + doc="Shomate equation heat capacity coeff 6", + units=pyunits.kJ/pyunits.mol) + self.cp_param_7 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 7}, + doc="Shomate equation heat capacity coeff 7", + units=pyunits.J/pyunits.mol/pyunits.K) + self.cp_param_8 = Param( + self.component_list, + mutable=False, + initialize={k: v for (k, j), v in + cp_param_dict.items() if j == 8}, + doc="Shomate equation heat capacity coeff 8", + units=pyunits.kJ/pyunits.mol) + + # Std. heat of formation of comp. - units = J/(mol comp) - ref: NIST + enth_mol_form_comp_dict = {'Fe2O3': -825.5032E3, 'Fe3O4': -1120.894E3, + 'Al2O3': -1675.690E3} self.enth_mol_form_comp = Param( self.component_list, mutable=False, initialize=enth_mol_form_comp_dict, - doc="Component molar heats of formation [kJ/mol]", - units=pyunits.kJ/pyunits.mol) + doc="Component molar heats of formation [J/mol]", + units=pyunits.J/pyunits.mol) + + # Set default scaling for mass fractions + for comp in self.component_list: + self.set_default_scaling("mass_frac_comp", 1e2, index=comp) # ------------------------------------------------------------------------- """ Mixed solid properties""" @@ -191,9 +250,9 @@ def build(self): # Particle thermal conductivity self.therm_cond_sol = Var( domain=Reals, - initialize=12.3e-3, - doc='Thermal conductivity of solid particles [kJ/m.K.s]', - units=pyunits.kJ/pyunits.m/pyunits.K/pyunits.s) + initialize=12.3e-0, + doc='Thermal conductivity of solid particles [J/m.K.s]', + units=pyunits.J/pyunits.m/pyunits.K/pyunits.s) self.therm_cond_sol.fix() @classmethod @@ -208,11 +267,11 @@ def define_metadata(cls, obj): 'dens_mass_particle': {'method': '_dens_mass_particle', 'units': 'kg/m3'}, 'cp_mol_comp': {'method': '_cp_mol_comp', - 'units': 'kJ/mol.K'}, - 'cp_mass': {'method': '_cp_mass', 'units': 'kJ/kg.K'}, - 'enth_mass': {'method': '_enth_mass', 'units': 'kJ/kg'}, + 'units': 'J/mol.K'}, + 'cp_mass': {'method': '_cp_mass', 'units': 'J/kg.K'}, + 'enth_mass': {'method': '_enth_mass', 'units': 'J/kg'}, 'enth_mol_comp': {'method': '_enth_mol_comp', - 'units': 'kJ/mol'}}) + 'units': 'J/mol'}}) obj.add_default_units({'time': pyunits.s, 'length': pyunits.m, @@ -282,7 +341,7 @@ def initialize(blk, state_args=None, hold_state=False, "initialization.") # --------------------------------------------------------------------- - # Initialise values + # Initialize values for k in blk.keys(): if hasattr(blk[k], "density_skeletal_constraint"): calculate_variable_from_constraint( @@ -382,38 +441,45 @@ def build(self): def _make_state_vars(self): """List the necessary state variable objects.""" + + # create units object to get default units from the param block + units_meta = self._params.get_metadata().derived_units + self.flow_mass = Var(initialize=1.0, domain=Reals, - doc='Component mass flowrate [kg/s]', - units=pyunits.kg/pyunits.s) + doc='Component mass flowrate', + units=units_meta['mass']/units_meta['time']) self.particle_porosity = Var(domain=Reals, initialize=0.27, doc='Porosity of oxygen carrier [-]', - units=pyunits.m**3/pyunits.m**3) + units=units_meta['length']**3 / + units_meta['length']**3) self.mass_frac_comp = Var( self._params.component_list, initialize=1 / len(self._params.component_list), doc='State component mass fractions [-]', - units=pyunits.kg/pyunits.kg) + units=units_meta['mass']/units_meta['mass']) self.temperature = Var(initialize=298.15, domain=Reals, - doc='State temperature [K]', - units=pyunits.K) + doc='State temperature', + units=units_meta['temperature']) # Create standard constraints # Sum mass fractions if not inlet block if self.config.defined_state is False: def sum_component_eqn(b): - return 1e2 == 1e2 * sum(b.mass_frac_comp[j] - for j in b._params.component_list) + return 1 == sum(b.mass_frac_comp[j] + for j in b._params.component_list) self.sum_component_eqn = Constraint(rule=sum_component_eqn) def _dens_mass_skeletal(self): # Skeletal density of OC solid particles + units_meta = self._params.get_metadata().derived_units self.dens_mass_skeletal = Var(domain=Reals, initialize=3251.75, - doc='Skeletal density of OC [kg/m3]', - units=pyunits.kg/pyunits.m**3) + doc='Skeletal density of OC', + units=units_meta['mass'] * + units_meta['length']**-3) def density_skeletal_constraint(b): return (b.dens_mass_skeletal * sum( @@ -433,11 +499,13 @@ def density_skeletal_constraint(b): def _dens_mass_particle(self): # Particle density of OC (includes the OC pores) + units_meta = self._params.get_metadata().derived_units self.dens_mass_particle = Var( domain=Reals, initialize=3251.75, - doc='Particle density of oxygen carrier [kg/m3]', - units=pyunits.kg/pyunits.m**3) + doc='Particle density of oxygen carrier', + units=units_meta['mass'] * + units_meta['length']**-3) def density_particle_constraint(b): return (b.dens_mass_particle == (1 - b.particle_porosity) * @@ -454,20 +522,26 @@ def density_particle_constraint(b): def _cp_mol_comp(self): # Pure component solid heat capacities + units_meta = self._params.get_metadata().derived_units + units_cp_mol = (units_meta['energy'] * + units_meta['amount']**-1 * + units_meta['temperature']**-1) self.cp_mol_comp = Var( self._params.component_list, domain=Reals, initialize=1.0, - doc="Pure component solid heat capacities [kJ/mol.K]", - units=pyunits.kJ/pyunits.mol/pyunits.K) + doc="Pure component solid heat capacities", + units=units_cp_mol) def pure_component_cp_mol(b, j): - return b.cp_mol_comp[j] == 1e-3*( - b._params.cp_param[j, 1] + - b._params.cp_param[j, 2]*(b.temperature*1e-3) + - b._params.cp_param[j, 3]*(b.temperature*1e-3)**2 + - b._params.cp_param[j, 4]*(b.temperature*1e-3)**3 + - b._params.cp_param[j, 5]/((b.temperature*1e-3)**2)) + t = pyunits.convert(b.temperature, to_units=pyunits.kK) + return b.cp_mol_comp[j] == pyunits.convert(( + b._params.cp_param_1[j] + + b._params.cp_param_2[j]*t + + b._params.cp_param_3[j]*t**2 + + b._params.cp_param_4[j]*t**3 + + b._params.cp_param_5[j]/(t**2)), + to_units=units_cp_mol) try: # Try to build constraint self.cp_shomate_eqn = Constraint(self._params.component_list, @@ -480,10 +554,14 @@ def pure_component_cp_mol(b, j): def _cp_mass(self): # Mixture heat capacities + units_meta = self._params.get_metadata().derived_units + units_cp_mass = (units_meta['energy'] * + units_meta['mass']**-1 * + units_meta['temperature']**-1) self.cp_mass = Var(domain=Reals, initialize=1.0, - doc="Mixture heat capacity, mass-basis [kJ/kg.K]", - units=pyunits.kJ/pyunits.kg/pyunits.K) + doc="Mixture heat capacity, mass-basis", + units=units_cp_mass) def cp_mass(b): return b.cp_mass == sum(b.cp_mol_comp[j]*b.mass_frac_comp[j] @@ -500,22 +578,29 @@ def cp_mass(b): def _enth_mol_comp(self): # Pure component vapour enthalpies + units_meta = self._params.get_metadata().derived_units + units_enth_mol = units_meta['energy'] * units_meta['amount']**-1 self.enth_mol_comp = Var( self._params.component_list, domain=Reals, initialize=1.0, - doc="Pure component enthalpies [kJ/mol]", - units=pyunits.kJ/pyunits.mol) + doc="Pure component enthalpies", + units=units_enth_mol) def pure_comp_enthalpy(b, j): - return b.enth_mol_comp[j] == ( - b._params.cp_param[j, 1]*(b.temperature*1e-3) + - b._params.cp_param[j, 2]*((b.temperature*1e-3)**2)/2 + - b._params.cp_param[j, 3]*((b.temperature*1e-3)**3)/3 + - b._params.cp_param[j, 4]*((b.temperature*1e-3)**4)/4 - - b._params.cp_param[j, 5]/(b.temperature*1e-3) + - b._params.cp_param[j, 6] - - b._params.cp_param[j, 8]) + t = pyunits.convert(b.temperature, to_units=pyunits.kK) + return b.enth_mol_comp[j] == pyunits.convert( + # parameters 1-5 are defined in J + b._params.cp_param_1[j]*t + + b._params.cp_param_2[j]*(t**2)/2 + + b._params.cp_param_3[j]*(t**3)/3 + + b._params.cp_param_4[j]*(t**4)/4 - + b._params.cp_param_5[j]/(t), to_units=units_enth_mol) + \ + pyunits.convert( + # parameters 6 and 8 are defined in kJ, and must be added + # after converting to the enthalpy units set + b._params.cp_param_6[j] - b._params.cp_param_8[j], + to_units=units_enth_mol) try: # Try to build constraint self.enthalpy_shomate_eqn = Constraint(self._params.component_list, @@ -528,10 +613,12 @@ def pure_comp_enthalpy(b, j): def _enth_mass(self): # Mixture mass enthalpy + units_meta = self._params.get_metadata().derived_units + units_enth_mass = units_meta['energy'] * units_meta['mass']**-1 self.enth_mass = Var(domain=Reals, initialize=0.0, - doc='Mixture specific enthalpy [kJ/kg]', - units=pyunits.kJ/pyunits.kg) + doc='Mixture specific enthalpy', + units=units_enth_mass) try: # Try to build constraint self.mixture_enthalpy_eqn = Constraint(expr=( @@ -585,3 +672,12 @@ def default_material_balance_type(blk): def default_energy_balance_type(blk): return EnergyBalanceType.enthalpyTotal + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + if self.is_property_constructed("sum_component_eqn"): + iscale.constraint_scaling_transform( + self.sum_component_eqn, + iscale.get_scaling_factor(self.mole_frac_comp['Fe2O3']), + overwrite=False) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_gas_prop.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_gas_prop.py index e3241178c3..d33fab5774 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_gas_prop.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_gas_prop.py @@ -17,10 +17,8 @@ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - Var) +from pyomo.environ import check_optimal_termination, ConcreteModel, Var +from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -50,7 +48,7 @@ def gas_prop(): m.fs.unit.flow_mol.fix(1) m.fs.unit.temperature.fix(450) - m.fs.unit.pressure.fix(1.60) + m.fs.unit.pressure.fix(1.60E5) m.fs.unit.mole_frac_comp["CO2"].fix(0.4772) m.fs.unit.mole_frac_comp["H2O"].fix(0.0646) m.fs.unit.mole_frac_comp["CH4"].fix(0.4582) @@ -95,9 +93,7 @@ def test_solve(gas_prop): results = solver.solve(gas_prop) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @@ -109,4 +105,9 @@ def test_solution(gas_prop): assert (pytest.approx(1, abs=1e-2) == gas_prop.fs.unit.cp_mol.value) assert (pytest.approx(1, abs=1e-2) == - gas_prop.fs.unit.enth_mol.value) \ No newline at end of file + gas_prop.fs.unit.enth_mol.value) + + +@pytest.mark.component +def test_units_consistent(gas_prop): + assert_units_consistent(gas_prop) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_rxn_prop.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_rxn_prop.py index b3c1b04e9d..7e60d797f3 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_rxn_prop.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_rxn_prop.py @@ -17,10 +17,8 @@ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - Var) +from pyomo.environ import check_optimal_termination, ConcreteModel, Var +from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -115,9 +113,7 @@ def test_solve(rxn_prop): results = solver.solve(rxn_prop.fs.unit) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @@ -130,3 +126,8 @@ def test_solution(rxn_prop): rxn_prop.fs.unit.OC_conv.value) assert (pytest.approx(0, abs=1e-2) == rxn_prop.fs.unit.reaction_rate['R1'].value) + + +@pytest.mark.component +def test_units_consistent(rxn_prop): + assert_units_consistent(rxn_prop) diff --git a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_solid_prop.py b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_solid_prop.py index 0d01f5b06c..d1eaeea213 100644 --- a/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_solid_prop.py +++ b/idaes/gas_solid_contactors/properties/methane_iron_OC_reduction/tests/test_CLC_solid_prop.py @@ -17,10 +17,8 @@ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - Var) +from pyomo.environ import check_optimal_termination, ConcreteModel, Var +from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -80,6 +78,7 @@ def test_initialize(solid_prop): initialization_tester( solid_prop) + @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -92,9 +91,7 @@ def test_solve(solid_prop): results = solver.solve(solid_prop) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @@ -107,3 +104,8 @@ def test_solution(solid_prop): solid_prop.fs.unit.cp_mass.value) assert (pytest.approx(0.0039, abs=1e-2) == solid_prop.fs.unit.enth_mass.value) + + +@pytest.mark.component +def test_units_consistent(solid_prop): + assert_units_consistent(solid_prop) diff --git a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_gas_prop.py b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_gas_prop.py index 0d1c984784..ea0e39e3bc 100644 --- a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_gas_prop.py +++ b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_gas_prop.py @@ -18,10 +18,9 @@ import pytest from pyomo.environ import ( + check_optimal_termination, ConcreteModel, Constraint, - TerminationCondition, - SolverStatus, Var, Reference, value, @@ -115,9 +114,7 @@ def test_solve(gas_prop): results = solver.solve(gas_prop) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver diff --git a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_rxn_prop.py b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_rxn_prop.py index a363856da4..5fb9a20dde 100644 --- a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_rxn_prop.py +++ b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_rxn_prop.py @@ -18,9 +18,8 @@ import pytest from pyomo.environ import ( + check_optimal_termination, ConcreteModel, - TerminationCondition, - SolverStatus, Var, Constraint, value, @@ -129,9 +128,7 @@ def test_solve(rxn_prop): results = solver.solve(rxn_prop.fs.unit) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver diff --git a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_solid_prop.py b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_solid_prop.py index 7d72b154ca..155d3e85d0 100644 --- a/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_solid_prop.py +++ b/idaes/gas_solid_contactors/properties/oxygen_iron_OC_oxidation/tests/test_CLC_solid_prop.py @@ -18,9 +18,8 @@ import pytest from pyomo.environ import ( + check_optimal_termination, ConcreteModel, - TerminationCondition, - SolverStatus, Var, Constraint, value, @@ -107,9 +106,7 @@ def test_solve(solid_prop): results = solver.solve(solid_prop) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver diff --git a/idaes/gas_solid_contactors/unit_models/bubbling_fluidized_bed.py b/idaes/gas_solid_contactors/unit_models/bubbling_fluidized_bed.py index d0704c6f81..61898aecb6 100644 --- a/idaes/gas_solid_contactors/unit_models/bubbling_fluidized_bed.py +++ b/idaes/gas_solid_contactors/unit_models/bubbling_fluidized_bed.py @@ -33,7 +33,7 @@ # Import Pyomo libraries from pyomo.environ import (Var, Param, Reals, - TerminationCondition, Constraint, + check_optimal_termination, Constraint, TransformationFactory, sqrt, value) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.util.calc_var_value import calculate_variable_from_constraint @@ -665,18 +665,18 @@ def _make_vars_params(self): self.length_domain, domain=Reals, doc='Bubble to Emulsion Gas Heat Transfer Coefficient' - '[kJ/m^3.K.s]') + '[J/m^3.K.s]') self.Hgbulk = Var( self.flowsheet().time, self.length_domain, domain=Reals, - doc='Gas Phase Bulk Enthalpy Transfer Rate [kJ/m.s]') + doc='Gas Phase Bulk Enthalpy Transfer Rate [J/m.s]') self.htc_conv = Var( self.flowsheet().time, self.length_domain, domain=Reals, doc='Gas to Solid Energy Convective Heat Transfer' - 'Coefficient [kJ/m^2.K.s]') + 'Coefficient [J/m^2.K.s]') # Heat transfer terms self.ht_conv = Var( @@ -684,7 +684,7 @@ def _make_vars_params(self): self.length_domain, domain=Reals, doc='Gas to Solid Convective Enthalpy Transfer in' - 'Emulsion Region [kJ/m^2.K.s]') + 'Emulsion Region [J/m^2.K.s]') # Reformulation variables self._reform_var_1 = Var( @@ -735,8 +735,8 @@ def _make_vars_params(self): doc='Bulk Gas Permeation Coefficient [m/s]') self.Kd.fix() self.deltaP_orifice = Var(domain=Reals, - initialize=3.400, - doc='Pressure Drop Across Orifice [bar]') + initialize=3.4E5, + doc='Pressure Drop Across Orifice [Pa]') self.deltaP_orifice.fix() # ========================================================================= @@ -961,9 +961,7 @@ def solid_super_vel(b, t, x): self.length_domain, doc="Gas Emulsion Pressure Drop Calculation") def gas_emulsion_pressure_drop(b, t, x): - # 1e5 = pressure unit conversion factor from Pa to bar - return (1e-2*(b.gas_emulsion.deltaP[t, x] * - 1e5) == + return (1e-2*(b.gas_emulsion.deltaP[t, x]) == 1e-2*(- constants.acceleration_gravity * (1 - b.voidage_average[t, x]) * b.solid_emulsion.properties[t, x].dens_mass_particle) @@ -1625,8 +1623,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Geometric Constraints') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 2 {}.".format( idaeslog.condition(results)) @@ -1708,8 +1705,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Hydrodynamics') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 3 {}.".format( idaeslog.condition(results)) @@ -1837,8 +1833,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info_high('initialize mass balances with no reactions') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 4a {}.".format( idaeslog.condition(results)) @@ -1961,8 +1956,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info_high('initialize mass balances with reactions') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 4b {}.".format( idaeslog.condition(results)) @@ -2024,8 +2018,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Energy Balances') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 5 {}.".format( idaeslog.condition(results)) @@ -2049,8 +2042,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Energy Balances') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 5 {}.".format( idaeslog.condition(results)) @@ -2089,8 +2081,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Outlet Conditions') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 6 {}.".format( idaeslog.condition(results)) diff --git a/idaes/gas_solid_contactors/unit_models/fixed_bed_0D.py b/idaes/gas_solid_contactors/unit_models/fixed_bed_0D.py index e59c7fd672..9aa99bdd7e 100644 --- a/idaes/gas_solid_contactors/unit_models/fixed_bed_0D.py +++ b/idaes/gas_solid_contactors/unit_models/fixed_bed_0D.py @@ -174,7 +174,7 @@ def build(self): units=units_meta_solid('length')) self.volume_bed = Var(initialize=1.0, doc="Volume of the reactor bed", - units=units_meta_solid('length')**3) + units=units_meta_solid('volume')) @self.Constraint(doc="Calculating volume of the reactor bed") def volume_bed_constraint(b): @@ -188,7 +188,7 @@ def volume_bed_constraint(b): self.flowsheet().config.time, initialize=1.0, doc="Solids phase volume including particle pores", - units=units_meta_solid('length')**3) + units=units_meta_solid('volume')) @self.Constraint(self.flowsheet().config.time, doc="Calculating solid phase volume") @@ -217,13 +217,16 @@ def solids_material_holdup_constraints(b, t, j): self.solids_material_holdup, wrt=self.flowsheet().config.time, doc="Solids material accumulation", - units=units_meta_solid('mass')/units_meta_solid('time')) + units=units_meta_solid('mass')) + # should be mass/time, to get around DAE limitations var is + # divided by time units when used @self.Constraint(self.flowsheet().config.time, self.config.solid_property_package.component_list, doc="Solid phase material accumulation constraints") def solids_material_accumulation_constraints(b, t, j): - return (b.solids_material_accumulation[t, j] == + return (b.solids_material_accumulation[t, j] / + units_meta_solid('time') == b.volume_solid[t] * b.solids[t]._params.mw_comp[j] * sum(b.reactions[t].reaction_rate[r] * @@ -268,24 +271,31 @@ def sum_component_constraint(b, t): def solids_energy_holdup_constraints(b, t): return b.solids_energy_holdup[t] == ( self.volume_solid[t] * - b.solids[t].get_energy_density_terms("Sol")) + pyunits.convert( + b.solids[t].get_energy_density_terms("Sol"), + to_units=units_meta_solid('energy') / + units_meta_solid('volume'))) self.solids_energy_accumulation = DerivativeVar( self.solids_energy_holdup, wrt=self.flowsheet().config.time, doc="Solids energy accumulation", - units=units_meta_solid('energy') / - units_meta_solid('time')) + units=units_meta_solid('energy')) + # should be energy/time, to get around DAE limitations var is + # divided by time units when used @self.Constraint(self.flowsheet().config.time, doc="Solid phase energy accumulation constraints") def solids_energy_accumulation_constraints(b, t): - return b.solids_energy_accumulation[t] == \ - - sum(b.reactions[t].reaction_rate[r] * - b.volume_solid[t] * - b.reactions[t].dh_rxn[r] - for r in b.config.reaction_package. - rate_reaction_idx) + return (b.solids_energy_accumulation[t] / + units_meta_solid('time') == + - sum(b.reactions[t].reaction_rate[r] * + b.volume_solid[t] * + pyunits.convert( + b.reactions[t].dh_rxn[r], + to_units=units_meta_solid('energy_mole')) + for r in b.config.reaction_package. + rate_reaction_idx)) if self.config.energy_balance_type == EnergyBalanceType.none: # Fix solids temperature to initial value for isothermal conditions @self.Constraint( @@ -363,9 +373,9 @@ def calculate_scaling_factors(self): super().calculate_scaling_factors() if hasattr(self, "mass_solids_constraint"): - for t, v in self.mass_solids_constraint.items(): - iscale.set_scaling_factor(v, 1e2) + for t, c in self.mass_solids_constraint.items(): + iscale.set_scaling_factor(c, 1e2) if hasattr(self, "sum_component_constraint"): - for t, v in self.sum_component_constraint.items(): - iscale.set_scaling_factor(v, 1e2) + for t, c in self.sum_component_constraint.items(): + iscale.set_scaling_factor(c, 1e2) diff --git a/idaes/gas_solid_contactors/unit_models/moving_bed.py b/idaes/gas_solid_contactors/unit_models/moving_bed.py index d93179989f..6b682fddfd 100644 --- a/idaes/gas_solid_contactors/unit_models/moving_bed.py +++ b/idaes/gas_solid_contactors/unit_models/moving_bed.py @@ -38,7 +38,7 @@ # Import Pyomo libraries from pyomo.environ import (Var, Param, Reals, value, TransformationFactory, Constraint, - TerminationCondition) + check_optimal_termination) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.dae import ContinuousSet @@ -549,9 +549,9 @@ def _make_performance(self): doc='Particle Nusselt number [-]') self.gas_solid_htc = Var(self.flowsheet().time, self.length_domain, - domain=Reals, initialize=1.0, + domain=Reals, initialize=1.0E3, doc='Gas-solid heat transfer coefficient' - '[kJ/(m2Ks)]') + '[J/(m2Ks)]') # Fixed variables (these are parameters that can be estimated) self.bed_voidage = Var(domain=Reals, @@ -620,7 +620,7 @@ def solid_super_vel(b, t, x): doc="Gas side pressure drop calculation -" "simplified pressure drop") def gas_phase_config_pressure_drop(b, t, x): - return b.gas_phase.deltaP[t, x]*1e5 == -0.2*( + return b.gas_phase.deltaP[t, x] == -0.2*( b.velocity_superficial_gas[t, x] * (b.solid_phase.properties[t, x].dens_mass_particle - b.gas_phase.properties[t, x].dens_mass)) @@ -632,7 +632,7 @@ def gas_phase_config_pressure_drop(b, t, x): doc="Gas side pressure drop calculation -" "ergun equation") def gas_phase_config_pressure_drop(b, t, x): - return (1e2*-b.gas_phase.deltaP[t, x]*1e5 == + return (1e2*-b.gas_phase.deltaP[t, x] == 1e2*( 150*(1 - b.bed_voidage) ** 2 * b.gas_phase.properties[t, x].visc_d * @@ -884,8 +884,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Hydrodynamics') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 2 {}.".format( idaeslog.condition(results)) @@ -942,8 +941,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, 'and no pressure drop') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 3a {}.".format( idaeslog.condition(results)) @@ -1052,8 +1050,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, 'and no pressure drop') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 3b {}.".format( idaeslog.condition(results)) @@ -1088,8 +1085,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, 'and pressure drop') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 3c {}.".format( idaeslog.condition(results)) @@ -1152,8 +1148,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Energy Balances') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 4 {}.".format( idaeslog.condition(results)) @@ -1182,8 +1177,7 @@ def initialize(blk, gas_phase_state_args=None, solid_phase_state_args=None, init_log.info('Initialize Energy Balances') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = opt.solve(blk, tee=slc.tee) - if results.solver.termination_condition \ - == TerminationCondition.optimal: + if check_optimal_termination(results): init_log.info_high( "Initialization Step 4 {}.".format( idaeslog.condition(results)) @@ -1243,7 +1237,7 @@ def results_plot(blk): plt.legend(loc=0, ncol=2) plt.grid() plt.xlabel("Bed height [-]") - plt.ylabel("Total Pressure [bar]") + plt.ylabel("Total Pressure [Pa]") fig_P.savefig('Pressure.png') # Temperature profile diff --git a/idaes/gas_solid_contactors/unit_models/tests/test_BFB.py b/idaes/gas_solid_contactors/unit_models/tests/test_BFB.py index 9f3c981cca..cc4c0e5e56 100644 --- a/idaes/gas_solid_contactors/unit_models/tests/test_BFB.py +++ b/idaes/gas_solid_contactors/unit_models/tests/test_BFB.py @@ -19,9 +19,8 @@ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, Var, Constraint) @@ -142,7 +141,7 @@ def iron_oc(self): # Fix inlet port variables for gas and solid m.fs.unit.gas_inlet.flow_mol[0].fix(272.81) # mol/s m.fs.unit.gas_inlet.temperature[0].fix(373) # K - m.fs.unit.gas_inlet.pressure[0].fix(1.86) # bar + m.fs.unit.gas_inlet.pressure[0].fix(1.86E5) # Pa = 1E5 bar m.fs.unit.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.4772) m.fs.unit.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.0646) m.fs.unit.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.4582) @@ -227,7 +226,7 @@ def test_initialize(self, iron_oc): optarg={'tol': 1e-6}, gas_phase_state_args={"flow_mol": 272.81, "temperature": 1186, - "pressure": 1.86}, + "pressure": 1.86E5}, solid_phase_state_args={"flow_mass": 1230, "temperature": 1186}) @@ -238,9 +237,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -262,7 +259,7 @@ def test_solution(self, iron_oc): iron_oc.fs.unit.delta[0, 0].value) assert (pytest.approx(0.307, abs=1e-2) == iron_oc.fs.unit.delta[0, 1].value) - assert (pytest.approx(1.24, abs=1e-2) == + assert (pytest.approx(124169.5118, abs=1e-2) == iron_oc.fs.unit.gas_outlet.pressure[0].value) # Check that pressure drop occurs across the bed assert value( @@ -365,7 +362,7 @@ def iron_oc(self): # # Fix inlet port variables for gas and solid m.fs.unit.gas_inlet.flow_mol[0].fix(272.81) # mol/s m.fs.unit.gas_inlet.temperature[0].fix(1186) # K - m.fs.unit.gas_inlet.pressure[0].fix(1.86) # bar + m.fs.unit.gas_inlet.pressure[0].fix(1.86E5) # Pa = 1E5 bar m.fs.unit.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.4772) m.fs.unit.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.0646) m.fs.unit.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.4582) @@ -435,7 +432,7 @@ def test_initialize(self, iron_oc): optarg={'tol': 1e-6}, gas_phase_state_args={"flow_mol": 272.81, "temperature": 1186, - "pressure": 1.86}, + "pressure": 1.86E5}, solid_phase_state_args={"flow_mass": 1422, "temperature": 1186}) @@ -446,9 +443,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/gas_solid_contactors/unit_models/tests/test_FB0D.py b/idaes/gas_solid_contactors/unit_models/tests/test_FB0D.py index 76004450d7..1d6fb7eee8 100644 --- a/idaes/gas_solid_contactors/unit_models/tests/test_FB0D.py +++ b/idaes/gas_solid_contactors/unit_models/tests/test_FB0D.py @@ -1,15 +1,15 @@ -############################################################################### +################################################################################# # The Institute for the Design of Advanced Energy Systems Integrated Platform # Framework (IDAES IP) was produced under the DOE Institute for the # Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 # by the software owners: The Regents of the University of California, through # Lawrence Berkeley National Laboratory, National Technology & Engineering -# Solutions of Sandia, LLC, Carnegie Mellon University, -# West Virginia UniversityResearch Corporation, et al. All rights reserved. +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. # # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and # license information. -############################################################################### +################################################################################# """ Tests for ControlVolumeBlockData, and for initializing the 0D fixed bed module @@ -18,10 +18,9 @@ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, TransformationFactory, - TerminationCondition, - SolverStatus, value, units as pyunits, Constraint) @@ -130,7 +129,7 @@ def iron_oc(self): # remain unchanged) for t in m.fs.time: m.fs.unit.gas[t].temperature.fix(1273.15) - m.fs.unit.gas[t].pressure.fix(1.01325) # 1atm + m.fs.unit.gas[t].pressure.fix(1.01325E5) # 1atm m.fs.unit.gas[t].mole_frac_comp['CO2'].fix(0.4) m.fs.unit.gas[t].mole_frac_comp['H2O'].fix(0.5) m.fs.unit.gas[t].mole_frac_comp['CH4'].fix(0.1) @@ -185,9 +184,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -208,7 +205,7 @@ def test_solution(self, iron_oc): @pytest.mark.component def test_units_consistent(self, iron_oc): - pass # assert_units_consistent(iron_oc) + assert_units_consistent(iron_oc) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -349,7 +346,7 @@ def iron_oc(self): # remain unchanged) for t in m.fs.time: m.fs.unit.gas[t].temperature.fix(1273.15) - m.fs.unit.gas[t].pressure.fix(1.01325) # 1atm + m.fs.unit.gas[t].pressure.fix(1.01325E5) # 1atm m.fs.unit.gas[t].mole_frac_comp['CO2'].fix(0.4) m.fs.unit.gas[t].mole_frac_comp['H2O'].fix(0.5) m.fs.unit.gas[t].mole_frac_comp['CH4'].fix(0.1) @@ -401,9 +398,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -424,7 +419,7 @@ def test_solution(self, iron_oc): # need to update these values @pytest.mark.component def test_units_consistent(self, iron_oc): - pass # assert_units_consistent(iron_oc) + assert_units_consistent(iron_oc) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/gas_solid_contactors/unit_models/tests/test_MB.py b/idaes/gas_solid_contactors/unit_models/tests/test_MB.py index 6985712bc2..59fbfdadf2 100644 --- a/idaes/gas_solid_contactors/unit_models/tests/test_MB.py +++ b/idaes/gas_solid_contactors/unit_models/tests/test_MB.py @@ -18,9 +18,8 @@ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, Var, Constraint) @@ -142,7 +141,7 @@ def iron_oc(self): # Fix inlet port variables for gas and solid m.fs.unit.gas_inlet.flow_mol[0].fix(128.20513) # mol/s m.fs.unit.gas_inlet.temperature[0].fix(298.15) # K - m.fs.unit.gas_inlet.pressure[0].fix(2.00) # bar + m.fs.unit.gas_inlet.pressure[0].fix(2.00E5) # Pa = 1E5 bar m.fs.unit.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.02499) m.fs.unit.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.00001) m.fs.unit.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.975) @@ -218,7 +217,7 @@ def test_initialize(self, iron_oc): optarg={'tol': 1e-6}, gas_phase_state_args={"flow_mol": 128.20513, "temperature": 1183.15, - "pressure": 2.00}, + "pressure": 2.00E5}, solid_phase_state_args={"flow_mass": 591.4, "temperature": 1183.15}) @@ -229,9 +228,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -243,7 +240,7 @@ def test_solution(self, iron_oc): iron_oc.fs.unit.velocity_superficial_gas[0, 1].value) assert (pytest.approx(0.0039, abs=1e-2) == iron_oc.fs.unit.velocity_superficial_solid[0].value) - assert (pytest.approx(1.975, abs=1e-2) == + assert (pytest.approx(198217.7068, abs=1e-2) == iron_oc.fs.unit.gas_outlet.pressure[0].value) # Check that pressure drop occurs across the bed assert value( @@ -347,7 +344,7 @@ def iron_oc(self): # Fix inlet port variables for gas and solid m.fs.unit.gas_inlet.flow_mol[0].fix(128.20513) # mol/s m.fs.unit.gas_inlet.temperature[0].fix(1183.15) # K - m.fs.unit.gas_inlet.pressure[0].fix(2.00) # bar + m.fs.unit.gas_inlet.pressure[0].fix(2.00E5) # Pa = 1E5 bar m.fs.unit.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.02499) m.fs.unit.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.00001) m.fs.unit.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.975) @@ -413,7 +410,7 @@ def test_initialize(self, iron_oc): optarg={'tol': 1e-6}, gas_phase_state_args={"flow_mol": 128.20513, "temperature": 1183.15, - "pressure": 2.00}, + "pressure": 2.00E5}, solid_phase_state_args={"flow_mass": 591.4, "temperature": 1183.15}) @@ -424,9 +421,7 @@ def test_solve(self, iron_oc): results = solver.solve(iron_oc) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -438,7 +433,7 @@ def test_solution(self, iron_oc): iron_oc.fs.unit.velocity_superficial_gas[0, 1].value) assert (pytest.approx(0.0039, abs=1e-2) == iron_oc.fs.unit.velocity_superficial_solid[0].value) - assert (pytest.approx(1.975, abs=1e-2) == + assert (pytest.approx(198214.8255, abs=1e-2) == iron_oc.fs.unit.gas_outlet.pressure[0].value) # Check that pressure drop occurs across the bed assert value( diff --git a/idaes/generic_models/properties/activity_coeff_models/activity_coeff_prop_pack.py b/idaes/generic_models/properties/activity_coeff_models/activity_coeff_prop_pack.py index a49f445e13..92c4b16669 100644 --- a/idaes/generic_models/properties/activity_coeff_models/activity_coeff_prop_pack.py +++ b/idaes/generic_models/properties/activity_coeff_models/activity_coeff_prop_pack.py @@ -42,7 +42,8 @@ """ # Import Pyomo libraries -from pyomo.environ import (Constraint, +from pyomo.environ import (check_optimal_termination, + Constraint, log, NonNegativeReals, value, @@ -51,9 +52,7 @@ Expression, Param, sqrt, - units as pyunits, - SolverStatus, - TerminationCondition) + units as pyunits) from pyomo.common.config import ConfigValue, In # Import IDAES cores @@ -397,8 +396,7 @@ def initialize(blk, state_args=None, hold_state=False, res = solve_indexed_blocks(opt, [blk], tee=slc.tee) init_log.info("Initialization Step 5 {}.".format(idaeslog.condition(res))) - if (res.solver.termination_condition != TerminationCondition.optimal or - res.solver.status != SolverStatus.ok): + if not check_optimal_termination(res): raise InitializationError( f"{blk.name} failed to initialize successfully. Please check " f"the output logs for more information.") diff --git a/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FTPz.py b/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FTPz.py index c7aafe19e8..dcf5bd8cdb 100644 --- a/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FTPz.py +++ b/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FTPz.py @@ -18,8 +18,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import ConcreteModel, TerminationCondition, \ - SolverStatus, value +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -137,8 +136,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_vl_FTPz, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for VLE results assert value(m.fs.state_block_ideal_vl_FTPz.mole_frac_phase_comp['Liq', @@ -153,8 +151,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_l, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for results assert value(m.fs.state_block_ideal_l.mole_frac_phase_comp['Liq', @@ -169,8 +166,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_v, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for results assert value(m.fs.state_block_ideal_v.mole_frac_phase_comp['Vap', diff --git a/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FcTP.py b/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FcTP.py index 851340fb0f..39bbade19b 100644 --- a/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FcTP.py +++ b/idaes/generic_models/properties/activity_coeff_models/tests/test_ideal_ideal_FcTP.py @@ -17,8 +17,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import ConcreteModel, TerminationCondition, \ - SolverStatus, value +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -136,8 +135,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_vl, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for VLE results assert value(m.fs.state_block_ideal_vl. @@ -152,8 +150,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_l, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for results assert value(m.fs.state_block_ideal_l.flow_mol_phase_comp['Liq', @@ -168,8 +165,7 @@ def test_solve(): results = solver.solve(m.fs.state_block_ideal_v, tee=False) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check for results assert value(m.fs.state_block_ideal_v.flow_mol_phase_comp['Vap', diff --git a/idaes/generic_models/properties/core/coolprop/__init__.py b/idaes/generic_models/properties/core/coolprop/__init__.py new file mode 100644 index 0000000000..dbf4c75b0f --- /dev/null +++ b/idaes/generic_models/properties/core/coolprop/__init__.py @@ -0,0 +1,13 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +from .coolprop_wrapper import CoolPropWrapper diff --git a/idaes/generic_models/properties/core/coolprop/coolprop_forms.py b/idaes/generic_models/properties/core/coolprop/coolprop_forms.py new file mode 100644 index 0000000000..368a56216a --- /dev/null +++ b/idaes/generic_models/properties/core/coolprop/coolprop_forms.py @@ -0,0 +1,231 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +""" +This module contains functions for constructing CoolProp expressions. +""" +from pyomo.environ import exp, units as pyunits, Var +from pyomo.core.expr.calculus.derivatives import Modes, differentiate + +from idaes.core.util.exceptions import ConfigurationError + + +# TODO : Only have temperature derivative expression for exponential_tau form +# TODO : Add other derivative forms as/if required + +def parameters_nt_sum(cobj, prop, nlist, tlist): + """ + Create parameters for expression forms using n-t parameters + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + nlist: list of values for n-parameter + tlist: list of values for t-parameter + + Returns: + None + """ + if len(nlist) != len(tlist): + raise ConfigurationError( + f"{cobj.name} mismatched length between n and t parameters " + f"for CoolProp exponential form for property {prop}. Please " + f"ensure the number of n and t parameters are equal.") + + # Use multiple Vars, instead of single indexed Var, to have same + # structure as cases where each parameter value has different units + + for i, nval in enumerate(nlist): + coeff = Var(doc="Multiplying parameter for CoolProp exponential form", + units=pyunits.dimensionless) + cobj.add_component(prop+"_coeff_n"+str(i+1), coeff) + coeff.fix(nval) + + for i, tval in enumerate(tlist): + coeff = Var(doc="Exponent parameter for CoolProp exponential form", + units=pyunits.dimensionless) + cobj.add_component(prop+"_coeff_t"+str(i+1), coeff) + coeff.fix(tval) + + +def _nt_sum(cobj, prop, theta): + """ + Create sum expressions in n-t forms (sum(n[i]*theta**t[i])) + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + theta: expression or variable to use for theta in expression + + Returns: + Pyomo expression of sum term + """ + # Build sum term + i = 1 + s = 0 + while True: + try: + ni = getattr(cobj, f"{prop}_coeff_n{i}") + ti = getattr(cobj, f"{prop}_coeff_t{i}") + s += ni*theta**ti + i += 1 + except AttributeError: + break + return s + + +def expression_exponential(cobj, prop, T, yc, tau=False): + """ + Create expressions for CoolProp exponential sum forms. This function + supports both exponential forms used by CoolProp: + + Without tau: y = yc * exp(sum(ni*theta^ti)) + With tau: y = yc * exp((Tc/T) * sum(ni*theta^ti)) + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + T: temperature to use in expression + yc: value of property at critical point + tau: whether tau=Tc/T should be included in expression (default=False) + + Returns: + Pyomo expression matching CoolProp exponential sum form + """ + Tc = cobj.temperature_crit + theta = 1 - T/Tc + + s = _nt_sum(cobj, prop, theta) + + if tau: + return yc*exp(Tc/T*s) + else: + return yc*exp(s) + + +def dT_expression_exponential(cobj, prop, T, yc, tau=False): + """ + Create expressions for temperature derivative of CoolProp exponential sum + forms with tau + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + T: temperature to use in expression + yc: value of property at critical point + tau: whether tau=Tc/T should be included in expression (default=False) + + Returns: + Pyomo expression for temperature derivative of CoolProp exponential sum + form with tau + """ + # y = yc * exp(Tc/T * sum(ni*theta^ti)) + # Need d(y)/dT + + y = expression_exponential(cobj, prop, T, yc, tau) + + return differentiate(expr=y, wrt=T, mode=Modes.reverse_symbolic) + + +def expression_nonexponential(cobj, prop, T, yc): + """ + Create expressions for CoolProp non-exponential sum forms + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + T: temperature to use in expression + yc: value of property at critical point + + Returns: + Pyomo expression mathcing CoolProp non-exponential sum form + """ + # y = yc * (1 + sum(ni*theta^ti)) + Tc = cobj.temperature_crit + theta = 1 - T/Tc + + s = _nt_sum(cobj, prop, theta) + + return yc*(1 + s) + + +def parameters_polynomial(cobj, prop, prop_units, alist, blist): + """ + Create parameters for expression forms using A-B parameters (rational + polynomial forms) + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + prop_units: units of measurement for property + Alist: list of values for A-parameter + Blist: list of values for B-parameter + + Returns: + None + """ + for i, aval in enumerate(alist): + if i == 0: + param_units = prop_units + else: + param_units = prop_units/pyunits.K**i + + coeff = Var(doc="A parameter for CoolProp polynomial form", + units=param_units) + cobj.add_component(prop+"_coeff_A"+str(i), coeff) + coeff.fix(aval) + + for i, bval in enumerate(blist): + if i == 0: + param_units = pyunits.dimensionless + else: + param_units = pyunits.K**-i + + coeff = Var(doc="B parameter for CoolProp exponential form", + units=param_units) + cobj.add_component(prop+"_coeff_B"+str(i), coeff) + coeff.fix(bval) + + +def expression_polynomial(cobj, prop, T): + """ + Create expressions for CoolProp rational polynomial forms + + Args: + cobj: Component object that will contain the parameters + prop: name of property parameters are associated with + T: temperature to use in expression + + Returns: + Pyomo expression mathcing CoolProp rational polynomial form + """ + i = 0 + asum = 0 + try: + while True: + Ai = getattr(cobj, f"{prop}_coeff_A{i}") + asum += Ai*T**i + i += 1 + except AttributeError: + pass + + i = 0 + bsum = 0 + try: + while True: + Bi = getattr(cobj, f"{prop}_coeff_B{i}") + bsum += Bi*T**i + i += 1 + except AttributeError: + pass + + return asum/bsum diff --git a/idaes/generic_models/properties/core/coolprop/coolprop_wrapper.py b/idaes/generic_models/properties/core/coolprop/coolprop_wrapper.py new file mode 100644 index 0000000000..dff071ed72 --- /dev/null +++ b/idaes/generic_models/properties/core/coolprop/coolprop_wrapper.py @@ -0,0 +1,480 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +""" +This module contains methods for looking up and using CoolProp parameters +within the IDAES generic properties framework. + +The CoolPropWrapper class contains a set of sub-classes for the supported +thermophysical properties required by the generic property framework, along +with some helper functions for common calls to the CoolProp database and +chaching data to avoid repeated database lookups. +""" +import json + +from pyomo.common.dependencies import attempt_import +from pyomo.environ import units as pyunits, Var + +import idaes.generic_models.properties.core.coolprop.coolprop_forms as cforms +from idaes.core.util.exceptions import BurntToast + +CoolProp, coolprop_available = attempt_import('CoolProp.CoolProp') + + +# Map IDAES names to Coolprop names and where to look for them +name_map = {"dens_mol_crit": ("rhomolar", "CRITICAL"), + "enth_mol_crit": ("hmolar", "CRITICAL"), + "entr_mol_crit": ("smolar", "CRITICAL"), + "mw": ("molar_mass", "EOS"), + "omega": ("acentric", "EOS"), + "pressure_crit": ("p", "CRITICAL"), + "temperature_crit": ("T", "CRITICAL")} + + +class CoolPropExpressionError(ValueError): + """ + Error message for when an unexpected expression form is called for. + + This is mostly to future proof the code against changes in CoolProp where + the expression form is changed to something we don't recognize. + """ + + def __init__(self, prop, comp): + msg = (f"Found unsupported expression form for {prop} " + f"of component {comp}. This likely occured due to " + f"changes in CoolProp and the interface should be " + f"updated.") + + super().__init__(self, msg) + + +class CoolPropPropertyError(KeyError): + """ + Error message for when a parameter is called for that is not in CoolProp's + database. + """ + + def __init__(self, prop, comp): + msg = (f"Could not retrieve parameters for {prop} of " + f"component {comp} from CoolProp. This likely indicates " + f"that CoolProp does not have values for the necessary " + f"parameters.") + + super().__init__(self, msg) + + +class CoolPropWrapper: + """ + Interface wrapper for calling CoolProp parameter database from within the + IDAES Generic Property Package Framework. + + This class is intended to be used in theromphysical property definition + dicts and directs the properties framework to use forms and parameter data + from the CoolProp libraries (if available). This requires that the user + have CoolProp installed locally. + """ + _cached_components = {} + + @staticmethod + def get_parameter_value(comp_name, param): + """ + Retrieve tuples of (value, units) for the specified property and + component for the critical state section of the CoolProp json format. + + Args: + comp_name: name of componet for which to retirve parameters + param: name of parameter to return value and units + + Returns: + tuple of parameter (value, units) + """ + map_tuple = name_map.get(param, None) + if map_tuple is None: + raise BurntToast( + "Unrecognized property name in CoolProp wrapper. Please " + "contact the IDAES developers with this bug.") + + if map_tuple[1] == "CRITICAL": + ptuple = CoolPropWrapper._get_critical_property( + comp_name, map_tuple[0]) + elif map_tuple[1] == "EOS": + ptuple = CoolPropWrapper._get_eos_property( + comp_name, map_tuple[0]) + else: + raise BurntToast( + "Unrecognized argument in CoolProp wrapper. Please contact " + "the IDAES developers with this bug.") + + return ptuple + + @staticmethod + def flush_cached_components(): + """ + Clear cached component parameter data. This sets _cached_components to + an empty dict. + + Args: + None + + Returns: + None + """ + CoolPropWrapper._cached_components = {} + + # ------------------------------------------------------------------------- + # Pure component property sub-classes + class dens_mol_liq_comp: + """ + Calculate liquid molar density using CoolProp forms and parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 8-Dec-21: CoolProp only uses rhoLnoexp for liquid density + nlist, tlist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "dens_mol_liq_comp", + "rhoL", + ["rhoLnoexp"]) + + cforms.parameters_nt_sum(cobj, "dens_mol_liq_comp", nlist, tlist) + + @staticmethod + def return_expression(b, cobj, T): + return cforms.expression_nonexponential( + cobj, "dens_mol_liq_comp", T, cobj.dens_mol_crit) + + class enth_mol_liq_comp: + """ + Calculate liquid molar enthalpy using CoolProp forms and parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 29-Dec-21: CoolProp only uses rational_polynomial. + Alist, Blist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "enth_mol_liq_comp", + "hL", + ["rational_polynomial"]) + href = cdict["EOS"][0]["STATES"]["hs_anchor"]["hmolar"] + + cforms.parameters_polynomial( + cobj, "enth_mol_liq_comp", pyunits.J/pyunits.mol, Alist, Blist) + + href_var = Var(doc="Reference heat of formation", + units=pyunits.J/pyunits.mol) + cobj.add_component("enth_mol_liq_comp_anchor", href_var) + href_var.fix(href) + + @staticmethod + def return_expression(b, cobj, T): + h = (cforms.expression_polynomial(cobj, "enth_mol_liq_comp", T) + + cobj.enth_mol_liq_comp_anchor) + + units = b.params.get_metadata().derived_units + return pyunits.convert(h, units["energy_mole"]) + + class enth_mol_ig_comp: + """ + Calculate ideal gas molar enthalpy using CoolProp forms and parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 29-Dec-21: CoolProp only uses rational_polynomial. + Alist, Blist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "enth_mol_ig_comp", + "hLV", + ["rational_polynomial"]) + + cforms.parameters_polynomial( + cobj, "enth_mol_ig_comp", pyunits.J/pyunits.mol, Alist, Blist) + + # Next, build parameters for enth_mol_liq_comp if necessary + if not hasattr(cobj, "enth_mol_liq_comp_coeff_A0"): + CoolPropWrapper.enth_mol_liq_comp.build_parameters(cobj) + + @staticmethod + def return_expression(b, cobj, T): + h = (cforms.expression_polynomial(cobj, "enth_mol_ig_comp", T) + + CoolPropWrapper.enth_mol_liq_comp.return_expression( + b, cobj, T)) + + units = b.params.get_metadata().derived_units + return pyunits.convert(h, units["energy_mole"]) + + class entr_mol_liq_comp: + """ + Calculate liquid molar entropy using CoolProp forms and parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 29-Dec-21: CoolProp only uses rational_polynomial. + Alist, Blist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "entr_mol_liq_comp", + "sL", + ["rational_polynomial"]) + sref = cdict["EOS"][0]["STATES"]["hs_anchor"]["smolar"] + + cforms.parameters_polynomial( + cobj, + "entr_mol_liq_comp", + pyunits.J/pyunits.mol/pyunits.K, + Alist, + Blist) + + sref_var = Var(doc="Reference heat of formation", + units=pyunits.J/pyunits.mol/pyunits.K) + cobj.add_component("entr_mol_liq_comp_anchor", sref_var) + sref_var.fix(sref) + + @staticmethod + def return_expression(b, cobj, T): + s = (cforms.expression_polynomial(cobj, "entr_mol_liq_comp", T) + + cobj.entr_mol_liq_comp_anchor) + + units = b.params.get_metadata().derived_units + return pyunits.convert(s, units["entropy_mole"]) + + class entr_mol_ig_comp: + """ + Calculate ideal gas molar entropy using CoolProp forms and parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 29-Dec-21: CoolProp only uses rational_polynomial. + Alist, Blist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "entr_mol_ig_comp", + "sLV", + ["rational_polynomial"]) + + cforms.parameters_polynomial( + cobj, + "entr_mol_ig_comp", + pyunits.J/pyunits.mol/pyunits.K, + Alist, + Blist) + + # Next, build parameters for entr_mol_liq_comp if necessary + if not hasattr(cobj, "entr_mol_liq_comp_coeff_A0"): + CoolPropWrapper.entr_mol_liq_comp.build_parameters(cobj) + + @staticmethod + def return_expression(b, cobj, T): + s = (cforms.expression_polynomial(cobj, "entr_mol_ig_comp", T) + + CoolPropWrapper.entr_mol_liq_comp.return_expression( + b, cobj, T)) + + units = b.params.get_metadata().derived_units + return pyunits.convert(s, units["entropy_mole"]) + + class pressure_sat_comp: + """ + Calculate pure component saturation pressure using CoolProp forms and + parameters. + """ + + @staticmethod + def build_parameters(cobj): + cname = cobj.local_name + cdict = CoolPropWrapper._get_component_data(cname) + + # 8-Dec-21: CoolProp only has two "types" for pressure_sat + # which appear to be equivalent. + nlist, tlist = CoolPropWrapper._get_param_dicts( + cname, + cdict, + "pressure_sat", + "pS", + ["pL", "pV"], + using_tau_r=True) + + cforms.parameters_nt_sum(cobj, "pressure_sat", nlist, tlist) + + @staticmethod + def return_expression(b, cobj, T, dT=False): + if dT: + return CoolPropWrapper.pressure_sat_comp.dT_expression( + b, cobj, T) + + return cforms.expression_exponential( + cobj, "pressure_sat", T, cobj.pressure_crit, tau=True) + + @staticmethod + def dT_expression(b, cobj, T): + return cforms.dT_expression_exponential( + cobj, "pressure_sat", T, cobj.pressure_crit, tau=True) + + # ------------------------------------------------------------------------- + # Internal methods + + @staticmethod + def _get_component_data(comp_name): + """ + Method to get parameter data from cached components, and to call + _load_component_data if it is not present. This method also includes + checks to handle aliases of components. + + Args: + comp_name: name of component to get data for + + Returns: + dict constructed from json string retrieved from CoolProp database. + """ + if comp_name in CoolPropWrapper._cached_components: + # First check to see if component present by comp_name + return CoolPropWrapper._cached_components[comp_name] + else: + # Check to see if comp_name is an alias for a cached component + for v in CoolPropWrapper._cached_components.values(): + if (comp_name in v["INFO"]["ALIASES"] or + comp_name in v["INFO"]["NAME"]): + CoolPropWrapper._cached_components[comp_name] = v + return v + + # If we haven't returned yet, then we need to load the component + return CoolPropWrapper._load_component_data(comp_name) + + @staticmethod + def _load_component_data(comp_name): + """ + Method to load parameter data for specified component from the CoolProp + database in json format. Loaded data is in dict form and is stored in + _cached_components to avoid need for repeated calls to CoolProp. + + Args: + comp_name: name of component ot retrieve parameters for. + + Returns: + dict constructed from json string retrieved from CoolProp database. + + Raises: + RuntimeError is component is not found in database + """ + try: + prop_str = CoolProp.get_fluid_param_string(comp_name, "JSON") + except RuntimeError: + raise RuntimeError( + f"Failed to find component {comp_name} in CoolProp JSON " + f"database.") + comp_prop = json.loads(prop_str)[0] + + CoolPropWrapper._cached_components[comp_name] = comp_prop + + return comp_prop + + @staticmethod + def _get_critical_property(comp_name, prop_name): + """ + Method to retrieve tuples of (value, units) for the specified property + and component for the critical state section of the CoolProp json + format. + """ + cdict = CoolPropWrapper._get_component_data(comp_name) + + pc = cdict["STATES"]["critical"][prop_name] + punits = getattr(pyunits, cdict["STATES"]["critical"][ + prop_name+"_units"]) + + return (pc, punits) + + @staticmethod + def _get_eos_property(comp_name, prop_name): + """ + Method to retrieve tuples of (value, units) for the specified property + and component for the EoS section of the CoolProp json format. + """ + cdict = CoolPropWrapper._get_component_data(comp_name) + + pc = cdict["EOS"][0][prop_name] + punits = cdict["EOS"][0][prop_name+"_units"] + if punits == "-": + punits = pyunits.dimensionless + else: + punits = getattr(pyunits, punits) + + return (pc, punits) + + @staticmethod + def _get_param_dicts(comp_name, + comp_data, + prop_name, + coolprop_name, + expected_forms, + using_tau_r=False): + """ + Method to get parameter sets for expression forms. Also includes check + to verify the expression form listed by CoolProp mathces the expected + for in IDAES. + + Args: + comp_name: name of the component for which parameters are required + comp_data: dict of parameter values for component (from CoolProp) + prop_name: IDAES name of property for which parameters are required + coolprop_name: name used by CoolProp for property of interest + expected_forms: list of expression forms supported by wrapper for + for property + using_tau_r: (optional) flag indicating whether to check for use + of tau_r in the epxression or not (default = False, + do not check). + + Returns: + tuple of lists of parameters required by expression + """ + try: + # First, check to make sure the listed expression form is + # supported. + if (comp_data["ANCILLARIES"][coolprop_name]["type"] not in + expected_forms): + # If not one of the forms we recognise, raise an exception + raise CoolPropExpressionError(prop_name, comp_name) + elif (using_tau_r and not + comp_data["ANCILLARIES"][coolprop_name]["using_tau_r"]): + # If not one of the forms we recognise, raise an exception + raise CoolPropExpressionError(prop_name, comp_name) + + if expected_forms == ["rational_polynomial"]: + list1 = comp_data["ANCILLARIES"][coolprop_name]["A"] + list2 = comp_data["ANCILLARIES"][coolprop_name]["B"] + else: + list1 = comp_data["ANCILLARIES"][coolprop_name]["n"] + list2 = comp_data["ANCILLARIES"][coolprop_name]["t"] + except KeyError: + raise CoolPropPropertyError(prop_name, comp_name) + + return (list1, list2) diff --git a/idaes/generic_models/properties/core/coolprop/tests/__init__.py b/idaes/generic_models/properties/core/coolprop/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_forms.py b/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_forms.py new file mode 100644 index 0000000000..14ffe344cb --- /dev/null +++ b/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_forms.py @@ -0,0 +1,328 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +""" +Tests for methods from CoolProp forms + +Authors: Andrew Lee +""" + +import pytest + +from pyomo.environ import ConcreteModel, Block, value, Var, units as pyunits +from pyomo.util.check_units import assert_units_equivalent + +import idaes.generic_models.properties.core.coolprop.coolprop_forms as cforms + +from idaes.core.util.exceptions import ConfigurationError + + +def between(y, x1, x2): + return 0 > (y-x1)*(y-x2) + + +class TestBaseForms: + # Use parameters for O2 for testing purposes + @pytest.fixture(scope="class") + def model(self): + m = ConcreteModel() + m.params = Block() + m.state = Block([0]) + + return m + + @pytest.mark.unit + def test_exponential_parameters_length_mismatch(self, model): + # Using parameters for O2 saturation pressure as test case + with pytest.raises(ConfigurationError, + match="params mismatched length between n and t " + "parameters for CoolProp exponential form for " + "property pressure_sat. Please ensure the number " + "of n and t parameters are equal."): + cforms.parameters_nt_sum( + model.params, + "pressure_sat", + [-7.645535357, + 2.42142887, + 9.642620061, + -10.09482287, + -1.685668959, + 1.277640761], + [1.019, + 1.177, + 2.44, + 2.493, + 5.646]) # Missing one term for t + + @pytest.mark.unit + def test_exponential_parameters(self, model): + # Using parameters for O2 saturation pressure as test case + cforms.parameters_nt_sum( + model.params, + "pressure_sat", + [-7.645535357, + 2.42142887, + 9.642620061, + -10.09482287, + -1.685668959, + 1.277640761], + [1.019, + 1.177, + 2.44, + 2.493, + 5.646, + 10.887]) + + assert isinstance(model.params.pressure_sat_coeff_n1, Var) + assert isinstance(model.params.pressure_sat_coeff_n2, Var) + assert isinstance(model.params.pressure_sat_coeff_n3, Var) + assert isinstance(model.params.pressure_sat_coeff_n4, Var) + assert isinstance(model.params.pressure_sat_coeff_n5, Var) + assert isinstance(model.params.pressure_sat_coeff_n6, Var) + + assert isinstance(model.params.pressure_sat_coeff_t1, Var) + assert isinstance(model.params.pressure_sat_coeff_t2, Var) + assert isinstance(model.params.pressure_sat_coeff_t3, Var) + assert isinstance(model.params.pressure_sat_coeff_t4, Var) + assert isinstance(model.params.pressure_sat_coeff_t5, Var) + assert isinstance(model.params.pressure_sat_coeff_t6, Var) + + assert model.params.pressure_sat_coeff_n1.value == -7.645535357 + assert model.params.pressure_sat_coeff_n1.fixed + assert model.params.pressure_sat_coeff_n2.value == 2.42142887 + assert model.params.pressure_sat_coeff_n2.fixed + assert model.params.pressure_sat_coeff_n3.value == 9.642620061 + assert model.params.pressure_sat_coeff_n3.fixed + assert model.params.pressure_sat_coeff_n4.value == -10.09482287 + assert model.params.pressure_sat_coeff_n4.fixed + assert model.params.pressure_sat_coeff_n5.value == -1.685668959 + assert model.params.pressure_sat_coeff_n5.fixed + assert model.params.pressure_sat_coeff_n6.value == 1.277640761 + assert model.params.pressure_sat_coeff_n6.fixed + + assert model.params.pressure_sat_coeff_t1.value == 1.019 + assert model.params.pressure_sat_coeff_t1.fixed + assert model.params.pressure_sat_coeff_t2.value == 1.177 + assert model.params.pressure_sat_coeff_t2.fixed + assert model.params.pressure_sat_coeff_t3.value == 2.44 + assert model.params.pressure_sat_coeff_t3.fixed + assert model.params.pressure_sat_coeff_t4.value == 2.493 + assert model.params.pressure_sat_coeff_t4.fixed + assert model.params.pressure_sat_coeff_t5.value == 5.646 + assert model.params.pressure_sat_coeff_t5.fixed + assert model.params.pressure_sat_coeff_t6.value == 10.887 + assert model.params.pressure_sat_coeff_t6.fixed + + @pytest.mark.unit + def test_exponential_sum(self, model): + expr = cforms._nt_sum( + model.params, "pressure_sat", 42) + + assert str(expr) == str( + model.params.pressure_sat_coeff_n1 * + 42**model.params.pressure_sat_coeff_t1 + + model.params.pressure_sat_coeff_n2 * + 42**model.params.pressure_sat_coeff_t2 + + model.params.pressure_sat_coeff_n3 * + 42**model.params.pressure_sat_coeff_t3 + + model.params.pressure_sat_coeff_n4 * + 42**model.params.pressure_sat_coeff_t4 + + model.params.pressure_sat_coeff_n5 * + 42**model.params.pressure_sat_coeff_t5 + + model.params.pressure_sat_coeff_n6 * + 42**model.params.pressure_sat_coeff_t6) + + assert_units_equivalent(expr, pyunits.dimensionless) + + @pytest.mark.unit + def test_expression_exponential_tau(self, model): + model.params.pressure_crit = Var(initialize=5043000, + units=pyunits.Pa) + model.params.temperature_crit = Var(initialize=154.581, + units=pyunits.K) + + data = {100: 254007.641, + 110: 543399.5533, + 120: 1022269.512, + 130: 1749056.109, + 140: 2787800.883, + 150: 4218667.236} + + for T, Psat in data.items(): + expr = cforms.expression_exponential( + model.params, + "pressure_sat", + T*pyunits.K, + model.params.pressure_crit, + tau=True) + + assert value(expr) == pytest.approx(Psat, rel=1e-8) + assert_units_equivalent(expr, pyunits.Pa) + + @pytest.mark.unit + def test_expression_exponential_tau_dT(self, model): + + fdiff = 1e-5 + for T in range(10, 101, 10): + dT = value(cforms.dT_expression_exponential( + model.params, + "pressure_sat", + T*pyunits.K, + model.params.pressure_crit, + tau=True)) + + em = cforms.expression_exponential( + model.params, + "pressure_sat", + (T-fdiff)*pyunits.K, + model.params.pressure_crit, + tau=True) + e = cforms.expression_exponential( + model.params, + "pressure_sat", + T*pyunits.K, + model.params.pressure_crit, + tau=True) + ep = cforms.expression_exponential( + model.params, + "pressure_sat", + (T+fdiff)*pyunits.K, + model.params.pressure_crit, + tau=True) + + dT_fe_m = value((e-em)/fdiff) + dT_fe_p = value((ep-e)/fdiff) + + assert between(dT, dT_fe_m, dT_fe_p) + assert pytest.approx(dT_fe_m, rel=1e-4) == dT + assert pytest.approx(dT_fe_p, rel=1e-4) == dT + + @pytest.mark.unit + def test_expression_exponential(self, model): + # Use O2 parameters just to check results + data = {100: 729628.576, + 110: 1033154.739, + 120: 1460907.321, + 130: 2069823.39, + 140: 2948110.91, + 150: 4241040.16} + + for T, Psat in data.items(): + expr = cforms.expression_exponential( + model.params, + "pressure_sat", + T*pyunits.K, + model.params.pressure_crit) + + assert value(expr) == pytest.approx(Psat, rel=1e-8) + assert_units_equivalent(expr, pyunits.Pa) + + @pytest.mark.unit + def test_expression_nonexponential(self, model): + # Manufacture results using O2 paramters for Psat + data = {100: -4706232.585007142, + 110: -2952092.3626429834, + 120: -1204991.804869883, + 130: 552017.5795317084, + 140: 2335733.299130539, + 150: 4169589.797731975} + + for T, Psat in data.items(): + expr = cforms.expression_nonexponential( + model.params, + "pressure_sat", + T*pyunits.K, + model.params.pressure_crit) + + assert value(expr) == pytest.approx(Psat, rel=1e-8) + assert_units_equivalent(expr, pyunits.Pa) + + @pytest.mark.unit + def test_polynomial_parameters(self, model): + # Using parameters for O2 saturation pressure as test case + cforms.parameters_polynomial( + model.params, + "enth_mol_liq_comp", + pyunits.J/pyunits.mol, + [-9025.64288846283, + -35.0797393389397, + 4.78790950986069, + -8.95948250127742e-2, + 9.20005110933356e-4, + -5.57355242293296E-06, + 1.85555931770993E-08, + -2.63480658094238E-11], + [1, + -6.39773824359397e-3]) + + assert isinstance(model.params.enth_mol_liq_comp_coeff_A0, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A1, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A2, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A3, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A4, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A5, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A6, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_A7, Var) + + assert isinstance(model.params.enth_mol_liq_comp_coeff_B0, Var) + assert isinstance(model.params.enth_mol_liq_comp_coeff_B1, Var) + + assert model.params.enth_mol_liq_comp_coeff_A0.value == \ + -9025.64288846283 + assert model.params.enth_mol_liq_comp_coeff_A0.fixed + assert model.params.enth_mol_liq_comp_coeff_A1.value == \ + -35.0797393389397 + assert model.params.enth_mol_liq_comp_coeff_A1.fixed + assert model.params.enth_mol_liq_comp_coeff_A2.value == \ + 4.78790950986069 + assert model.params.enth_mol_liq_comp_coeff_A2.fixed + assert model.params.enth_mol_liq_comp_coeff_A3.value == \ + -8.95948250127742e-2 + assert model.params.enth_mol_liq_comp_coeff_A3.fixed + assert model.params.enth_mol_liq_comp_coeff_A4.value == \ + 9.20005110933356e-4 + assert model.params.enth_mol_liq_comp_coeff_A4.fixed + assert model.params.enth_mol_liq_comp_coeff_A5.value == \ + -5.57355242293296E-06 + assert model.params.enth_mol_liq_comp_coeff_A5.fixed + assert model.params.enth_mol_liq_comp_coeff_A6.value == \ + 1.85555931770993E-08 + assert model.params.enth_mol_liq_comp_coeff_A6.fixed + assert model.params.enth_mol_liq_comp_coeff_A7.value == \ + -2.63480658094238E-11 + assert model.params.enth_mol_liq_comp_coeff_A7.fixed + + assert model.params.enth_mol_liq_comp_coeff_B0.value == 1 + assert model.params.enth_mol_liq_comp_coeff_B0.fixed + assert model.params.enth_mol_liq_comp_coeff_B1.value == \ + -6.39773824359397e-3 + assert model.params.enth_mol_liq_comp_coeff_B1.fixed + + @pytest.mark.unit + def test_expression_polynomial(self, model): + # Use O2 parameters just to check results + data = {100: -3726.193393, + 110: -3156.212331, + 120: -2557.035333, + 130: -1909.402314, + 140: -1173.118775, + 150: -217.8380819} + h_anchor = 2002.355546 + + for T, hL in data.items(): + expr = cforms.expression_polynomial( + model.params, + "enth_mol_liq_comp", + T*pyunits.K) + + assert value(expr) == pytest.approx(hL-h_anchor, rel=1e-8) + assert_units_equivalent(expr, pyunits.J/pyunits.mol) diff --git a/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_wrapper.py b/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_wrapper.py new file mode 100644 index 0000000000..e4fb4775d0 --- /dev/null +++ b/idaes/generic_models/properties/core/coolprop/tests/test_coolprop_wrapper.py @@ -0,0 +1,938 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES), and is copyright (c) 2018-2021 +# by the software owners: The Regents of the University of California, through +# Lawrence Berkeley National Laboratory, National Technology & Engineering +# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia University +# Research Corporation, et al. All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and +# license information. +################################################################################# +""" +Tests for methods from CoolProp wrapper + +Authors: Andrew Lee +""" + +import pytest +from numpy import arange + +from pyomo.environ import ( + Block, + ConcreteModel, + Param, + SolverStatus, + TerminationCondition, + units as pyunits, + value, + Var) +from pyomo.util.check_units import assert_units_equivalent +from pyomo.common.dependencies import attempt_import + +from idaes.core import FlowsheetBlock, Component, LiquidPhase, VaporPhase +from idaes.generic_models.properties.core.generic.generic_property import ( + GenericParameterBlock) +from idaes.generic_models.properties.core.state_definitions import FTPx +from idaes.generic_models.properties.core.eos.ceos import Cubic, CubicType +from idaes.core.util import get_solver +from idaes.generic_models.properties.core.pure.ConstantProperties import \ + Constant + + +from idaes.generic_models.properties.core.coolprop.coolprop_wrapper import ( + CoolPropWrapper, + CoolPropExpressionError, + CoolPropPropertyError) + +CoolProp, coolprop_available = attempt_import('CoolProp.CoolProp') +solver = get_solver() + + +@pytest.mark.skipif(not coolprop_available, reason="CoolProp not installed") +class TestWrapper: + @pytest.mark.unit + def test_load_component(self): + # Clear cached components to be sure + CoolPropWrapper._cached_components = {} + + # Load parameters for oxygen + prop_dict = CoolPropWrapper._load_component_data("Oxygen") + + assert prop_dict is not None + assert "Oxygen" in CoolPropWrapper._cached_components + assert CoolPropWrapper._cached_components["Oxygen"] is prop_dict + assert isinstance(prop_dict, dict) + for k in ["STATES", "ANCILLARIES"]: + assert k in prop_dict + + @pytest.mark.unit + def test_load_component_invalid(self): + with pytest.raises(RuntimeError, + match="Failed to find component foo in CoolProp " + "JSON database."): + CoolPropWrapper._load_component_data("foo") + + @pytest.mark.unit + def test_get_component(self): + prop_dict = CoolPropWrapper._get_component_data("Oxygen") + + assert CoolPropWrapper._cached_components["Oxygen"] is prop_dict + + @pytest.mark.unit + def test_get_component_alias(self): + # Load oxygen properties using aliases + prop_dict = CoolPropWrapper._get_component_data("R732") + + assert CoolPropWrapper._cached_components["Oxygen"] is prop_dict + assert "R732" in CoolPropWrapper._cached_components + assert CoolPropWrapper._cached_components["R732"] is \ + CoolPropWrapper._cached_components["Oxygen"] + + @pytest.mark.unit + def test_load_component_by_alias(self): + # Load CO2 data using one of its aliases + prop_dict = CoolPropWrapper._load_component_data("R744") + + assert CoolPropWrapper._cached_components["R744"] is prop_dict + assert "R744" in CoolPropWrapper._cached_components + + # Retrieve CO2 data using its normal name + prop_dict2 = CoolPropWrapper._get_component_data("CO2") + + assert prop_dict2 is prop_dict + assert "CO2" in CoolPropWrapper._cached_components + assert CoolPropWrapper._cached_components["CO2"] is \ + CoolPropWrapper._cached_components["R744"] + + @pytest.mark.unit + def test_get_component_invalid(self): + with pytest.raises(RuntimeError, + match="Failed to find component foo in CoolProp " + "JSON database."): + CoolPropWrapper._get_component_data("foo") + + @pytest.mark.unit + def test_flush_cached_components(self): + CoolPropWrapper.flush_cached_components() + + assert CoolPropWrapper._cached_components == {} + + @pytest.mark.unit + def test_get_critical_properties(self): + Tc = CoolPropWrapper._get_critical_property( + "Acetone", "T") + assert Tc == (508.1, pyunits.K) + + Pc = CoolPropWrapper._get_critical_property( + "Acetone", "p") + assert Pc == (4700000.0, pyunits.Pa) + + rhoc = CoolPropWrapper._get_critical_property( + "Acetone", "rhomolar") + assert rhoc[0] == 4699.999999999999 + assert_units_equivalent(rhoc[1], pyunits.mol/pyunits.m**3) + + hc = CoolPropWrapper._get_critical_property( + "Acetone", "hmolar") + assert hc[0] == 31614.73051047263 + assert_units_equivalent(hc[1], pyunits.J/pyunits.mol) + + sc = CoolPropWrapper._get_critical_property( + "Acetone", "smolar") + assert sc[0] == 72.97112978635582 + assert_units_equivalent(sc[1], pyunits.J/pyunits.mol/pyunits.K) + + @pytest.mark.unit + def test_get_eos_properties(self): + omega = CoolPropWrapper._get_eos_property("Acetone", "acentric") + assert omega == (0.3071, pyunits.dimensionless) + + mw = CoolPropWrapper._get_eos_property("Acetone", "molar_mass") + assert mw[0] == 0.05807914 + assert_units_equivalent(mw[1], pyunits.kg/pyunits.mol) + + @pytest.mark.unit + def test_get_parameter_value(self): + Tc = CoolPropWrapper.get_parameter_value( + "Acetone", "temperature_crit") + assert Tc == (508.1, pyunits.K) + + Pc = CoolPropWrapper.get_parameter_value( + "Acetone", "pressure_crit") + assert Pc == (4700000.0, pyunits.Pa) + + rhoc = CoolPropWrapper.get_parameter_value( + "Acetone", "dens_mol_crit") + assert rhoc[0] == 4699.999999999999 + assert_units_equivalent(rhoc[1], pyunits.mol/pyunits.m**3) + + hc = CoolPropWrapper.get_parameter_value( + "Acetone", "enth_mol_crit") + assert hc[0] == 31614.73051047263 + assert_units_equivalent(hc[1], pyunits.J/pyunits.mol) + + sc = CoolPropWrapper.get_parameter_value( + "Acetone", "entr_mol_crit") + assert sc[0] == 72.97112978635582 + assert_units_equivalent(sc[1], pyunits.J/pyunits.mol/pyunits.K) + + omega = CoolPropWrapper.get_parameter_value("Acetone", "omega") + assert omega == (0.3071, pyunits.dimensionless) + + mw = CoolPropWrapper.get_parameter_value("Acetone", "mw") + assert mw[0] == 0.05807914 + assert_units_equivalent(mw[1], pyunits.kg/pyunits.mol) + + @pytest.mark.unit + def test_pressure_sat_no_params(self): + m = ConcreteModel() + # Dummy a Component object with the name of a property which does not + # have data for saturation pressure + m.Air = Block() + + with pytest.raises(CoolPropPropertyError, + match="Could not retrieve parameters for " + "pressure_sat of component Air from CoolProp. This " + "likely indicates that CoolProp does not have " + "values for the necessary parameters."): + CoolPropWrapper.pressure_sat_comp.build_parameters(m.Air) + + @pytest.mark.unit + def test_pressure_sat_unrecognised_form(self): + m = ConcreteModel() + # First, add a dummy component to the cached components which uses + # unsupoorted form + CoolPropWrapper._cached_components["TestComp"] = { + "ANCILLARIES": { + "pS": {"type": "foo", + "using_tau_r": True}}, + "INFO": {"ALIASES": ["testcomp"], + "NAME": "TestComp"}} + m.TestComp = Block() + + with pytest.raises(CoolPropExpressionError, + match="Found unsupported expression form for " + "pressure_sat of component TestComp. This likely " + "occured due to changes in CoolProp and the " + "interface should be updated."): + CoolPropWrapper.pressure_sat_comp.build_parameters(m.TestComp) + + # Pressure_sat uses has two parts to form. Set type to supported form + # and using_tau_r to False (unsupported) + CoolPropWrapper._cached_components[ + "TestComp"]["ANCILLARIES"]["pS"]["type"] = "pL" + CoolPropWrapper._cached_components[ + "TestComp"]["ANCILLARIES"]["pS"]["using_tau_r"] = False + with pytest.raises(CoolPropExpressionError, + match="Found unsupported expression form for " + "pressure_sat of component TestComp. This likely " + "occured due to changes in CoolProp and the " + "interface should be updated."): + CoolPropWrapper.pressure_sat_comp.build_parameters(m.TestComp) + + +@pytest.mark.skipif(not coolprop_available, reason="CoolProp not installed") +class TestCoolPropProperties(object): + @pytest.fixture(scope="class") + def m(self): + # Clear cached components to ensure clean slate + CoolPropWrapper.flush_cached_components() + + m = ConcreteModel() + + m.fs = FlowsheetBlock(default={'dynamic': False}) + + configuration = { + # Specifying components + "components": { + 'benzene': {"type": Component, + "dens_mol_liq_comp": CoolPropWrapper, + "enth_mol_liq_comp": CoolPropWrapper, + "enth_mol_ig_comp": CoolPropWrapper, + "entr_mol_liq_comp": CoolPropWrapper, + "entr_mol_ig_comp": CoolPropWrapper, + "pressure_sat_comp": CoolPropWrapper, + "parameter_data": { + "mw": CoolPropWrapper, + "dens_mol_crit": CoolPropWrapper, + "pressure_crit": CoolPropWrapper, + "temperature_crit": CoolPropWrapper, + "omega": CoolPropWrapper}}}, + # Specifying phases + "phases": {'Liq': {"type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": { + "type": CubicType.PR}}}, + + # Set base units of measurement + "base_units": {"time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K}, + + # Specifying state definition + "state_definition": FTPx, + "state_bounds": {"flow_mol": (0, 100, 1000, pyunits.mol/pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa)}, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + + "parameter_data": {"PR_kappa": {("benzene", "benzene"): 0.000}}} + + m.fs.props = GenericParameterBlock(default=configuration) + + m.fs.state = m.fs.props.build_state_block( + [0], default={"defined_state": True}) + + m.fs.state[0].flow_mol.fix(1) + m.fs.state[0].pressure.fix(101325) + m.fs.state[0].temperature.fix(300) + m.fs.state[0].mole_frac_comp["benzene"].fix(1) + + return m + + @pytest.mark.unit + def test_physical_constants(self, m): + # Benzene parameters + assert isinstance(m.fs.props.benzene.temperature_crit, Var) + assert m.fs.props.benzene.temperature_crit.fixed + assert value(m.fs.props.benzene.temperature_crit) == CoolProp.PropsSI( + "TCRIT", "Benzene") + + assert isinstance(m.fs.props.benzene.pressure_crit, Var) + assert m.fs.props.benzene.pressure_crit.fixed + assert value(m.fs.props.benzene.pressure_crit) == CoolProp.PropsSI( + "PCRIT", "Benzene") + + assert isinstance(m.fs.props.benzene.dens_mol_crit, Var) + assert m.fs.props.benzene.dens_mol_crit.fixed + assert value(m.fs.props.benzene.dens_mol_crit) == CoolProp.PropsSI( + "RHOMOLAR_CRITICAL", "Benzene") + + assert isinstance(m.fs.props.benzene.mw, Param) + assert value(m.fs.props.benzene.mw) == CoolProp.PropsSI( + "molarmass", "Benzene") + + assert isinstance(m.fs.props.benzene.omega, Var) + assert m.fs.props.benzene.omega.fixed + assert value(m.fs.props.benzene.omega) == CoolProp.PropsSI( + "acentric", "Benzene") + + @pytest.mark.unit + def test_dens_mol_liq_comp(self, m): + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n1, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n2, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n3, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n4, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n5, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_n6, Var) + + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t1, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t2, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t3, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t4, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t5, Var) + assert isinstance(m.fs.props.benzene.dens_mol_liq_comp_coeff_t6, Var) + + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n1.value == \ + pytest.approx(2.852587673922022, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n1.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n2.value == \ + pytest.approx(-0.5596547795188646, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n2.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n3.value == \ + pytest.approx(14.872052666571532, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n3.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n4.value == \ + pytest.approx(-66.42959110979461, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n4.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n5.value == \ + pytest.approx(1158.1329856052375, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n5.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n6.value == \ + pytest.approx(-3128.774352224071, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_n6.fixed + + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t1.value == \ + pytest.approx(0.407, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t1.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t2.value == \ + pytest.approx(0.565, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t2.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t3.value == \ + pytest.approx(4.029, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t3.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t4.value == \ + pytest.approx(5.699, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t4.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t5.value == \ + pytest.approx(9.989, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t5.fixed + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t6.value == \ + pytest.approx(12.299, rel=1e-10) + assert m.fs.props.benzene.dens_mol_liq_comp_coeff_t6.fixed + + # CoolProp results are non-ideal, which results in deviations + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "DMOLAR", "T", T, "Q", 0, "benzene"), rel=5e-3) == value( + CoolPropWrapper.dens_mol_liq_comp.return_expression( + m.fs.state[0], m.fs.props.benzene, T*pyunits.K)) + + @pytest.mark.unit + def test_enth_mol_liq(self, m): + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A0, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A1, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A2, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A3, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A4, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A5, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A6, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_A7, Var) + + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_B0, Var) + assert isinstance(m.fs.props.benzene.enth_mol_liq_comp_coeff_B1, Var) + + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A0.value == \ + pytest.approx(-59920.08975429371, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A0.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A1.value == \ + pytest.approx(-435.3004255527287, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A1.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A2.value == \ + pytest.approx(5.686269274827653, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A2.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A3.value == \ + pytest.approx(-0.026551688405615677, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A3.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A4.value == \ + pytest.approx(7.158706209734695e-05, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A4.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A5.value == \ + pytest.approx(-1.1450245890590754e-07, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A5.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A6.value == \ + pytest.approx(1.0020646255928023e-10, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A6.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A7.value == \ + pytest.approx(-3.714155743547148e-14, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_A7.fixed + + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_B0.value == \ + pytest.approx(1, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_B0.fixed + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_B1.value == \ + pytest.approx(-0.0017636924804910123, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_coeff_B1.fixed + + assert m.fs.props.benzene.enth_mol_liq_comp_anchor.value == \ + pytest.approx(54228.40275249632, rel=1e-10) + assert m.fs.props.benzene.enth_mol_liq_comp_anchor.fixed + + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "Hmolar", "T", T, "Q", 0, "benzene"), rel=5e-4) == value( + CoolPropWrapper.enth_mol_liq_comp.return_expression( + m.fs.state[0], m.fs.props.benzene, T*pyunits.K)) + + @pytest.mark.unit + def test_enth_mol_ig(self, m): + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A0, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A1, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A2, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A3, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A4, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A5, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A6, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_A7, Var) + + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_B0, Var) + assert isinstance(m.fs.props.benzene.enth_mol_ig_comp_coeff_B1, Var) + + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A0.value == \ + pytest.approx(-72325.6503937073, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A0.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A1.value == \ + pytest.approx(2230.356360119971, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A1.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A2.value == \ + pytest.approx(-19.220224456239638, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A2.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A3.value == \ + pytest.approx(0.08579743322690633, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A3.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A4.value == \ + pytest.approx(-0.00022509135257932873, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A4.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A5.value == \ + pytest.approx(3.4964051978046773e-07, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A5.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A6.value == \ + pytest.approx(-2.9861697218437715e-10, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A6.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A7.value == \ + pytest.approx(1.0849858354638855e-13, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_A7.fixed + + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_B0.value == \ + pytest.approx(1, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_B0.fixed + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_B1.value == \ + pytest.approx(-0.0017620752364379978, rel=1e-10) + assert m.fs.props.benzene.enth_mol_ig_comp_coeff_B1.fixed + + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "Hmolar", "T", T, "Q", 1, "benzene"), rel=5e-4) == value( + CoolPropWrapper.enth_mol_ig_comp.return_expression( + m.fs.state[0], m.fs.props.benzene, T*pyunits.K)) + + @pytest.mark.unit + def test_entr_mol_liq(self, m): + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A0, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A1, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A2, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A3, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A4, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A5, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A6, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_A7, Var) + + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_B0, Var) + assert isinstance(m.fs.props.benzene.entr_mol_liq_comp_coeff_B1, Var) + + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A0.value == \ + pytest.approx(-367.28736141903704, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A0.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A1.value == \ + pytest.approx(1.9742974534821924, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A1.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A2.value == \ + pytest.approx(-0.0044315658996985936, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A2.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A3.value == \ + pytest.approx(1.3891733323299664e-06, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A3.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A4.value == \ + pytest.approx(2.2861650201840276e-08, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A4.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A5.value == \ + pytest.approx(-6.533821280556607e-11, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A5.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A6.value == \ + pytest.approx(7.560967575590022e-14, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A6.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A7.value == \ + pytest.approx(-3.327152569338899e-17, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_A7.fixed + + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_B0.value == \ + pytest.approx(1, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_B0.fixed + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_B1.value == \ + pytest.approx(-0.0017637184112409214, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_coeff_B1.fixed + + assert m.fs.props.benzene.entr_mol_liq_comp_anchor.value == \ + pytest.approx(108.66827371730366, rel=1e-10) + assert m.fs.props.benzene.entr_mol_liq_comp_anchor.fixed + + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "Smolar", "T", T, "Q", 0, "benzene"), rel=5e-4) == value( + CoolPropWrapper.entr_mol_liq_comp.return_expression( + m.fs.state[0], m.fs.props.benzene, T*pyunits.K)) + + @pytest.mark.unit + def test_entr_mol_ig(self, m): + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A0, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A1, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A2, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A3, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A4, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A5, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A6, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_A7, Var) + + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_B0, Var) + assert isinstance(m.fs.props.benzene.entr_mol_ig_comp_coeff_B1, Var) + + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A0.value == \ + pytest.approx(805.7903359684354, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A0.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A1.value == \ + pytest.approx(-7.054023595110686, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A1.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A2.value == \ + pytest.approx(0.026948845504297693, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A2.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A3.value == \ + pytest.approx(-4.872969272569453e-05, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A3.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A4.value == \ + pytest.approx(1.4263950361676005e-08, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A4.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A5.value == \ + pytest.approx(9.666423804354728e-11, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A5.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A6.value == \ + pytest.approx(-1.5555968001310246e-13, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A6.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A7.value == \ + pytest.approx(7.656482336490157e-17, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_A7.fixed + + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_B0.value == \ + pytest.approx(1, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_B0.fixed + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_B1.value == \ + pytest.approx(-0.0017620273519612887, rel=1e-10) + assert m.fs.props.benzene.entr_mol_ig_comp_coeff_B1.fixed + + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "Smolar", "T", T, "Q", 1, "benzene"), rel=5e-4) == value( + CoolPropWrapper.entr_mol_ig_comp.return_expression( + m.fs.state[0], m.fs.props.benzene, T*pyunits.K)) + + @pytest.mark.unit + def test_psat(self, m): + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n1, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n2, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n3, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n4, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n5, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_n6, Var) + + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t1, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t2, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t3, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t4, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t5, Var) + assert isinstance(m.fs.props.benzene.pressure_sat_coeff_t6, Var) + + assert m.fs.props.benzene.pressure_sat_coeff_n1.value == \ + pytest.approx(0.005561906558935796, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n1.fixed + assert m.fs.props.benzene.pressure_sat_coeff_n2.value == \ + pytest.approx(-0.08662136922915314, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n2.fixed + assert m.fs.props.benzene.pressure_sat_coeff_n3.value == \ + pytest.approx(-6.964182734154488, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n3.fixed + assert m.fs.props.benzene.pressure_sat_coeff_n4.value == \ + pytest.approx(1.1249288132278856, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n4.fixed + assert m.fs.props.benzene.pressure_sat_coeff_n5.value == \ + pytest.approx(-3.961859460597414, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n5.fixed + assert m.fs.props.benzene.pressure_sat_coeff_n6.value == \ + pytest.approx(-13.106880507410812, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_n6.fixed + + assert m.fs.props.benzene.pressure_sat_coeff_t1.value == \ + pytest.approx(0.037, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t1.fixed + assert m.fs.props.benzene.pressure_sat_coeff_t2.value == \ + pytest.approx(0.505, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t2.fixed + assert m.fs.props.benzene.pressure_sat_coeff_t3.value == \ + pytest.approx(1.014, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t3.fixed + assert m.fs.props.benzene.pressure_sat_coeff_t4.value == \ + pytest.approx(1.469, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t4.fixed + assert m.fs.props.benzene.pressure_sat_coeff_t5.value == \ + pytest.approx(3.711, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t5.fixed + assert m.fs.props.benzene.pressure_sat_coeff_t6.value == \ + pytest.approx(12.647, rel=1e-10) + assert m.fs.props.benzene.pressure_sat_coeff_t6.fixed + + for T in range(300, 401, 10): + m.fs.state[0].temperature.fix(T) + assert pytest.approx( + CoolProp.PropsSI( + "P", "T", T, "Q", 0.5, "benzene"), rel=5e-4) == value( + m.fs.state[0].pressure_sat_comp["benzene"]) + + +@pytest.mark.skipif(not coolprop_available, reason="CoolProp not installed") +class TestVerifyExcessLiq(object): + @pytest.fixture(scope="class") + def m(self): + # Clear cached components to ensure clean slate + CoolPropWrapper.flush_cached_components() + + m = ConcreteModel() + + m.fs = FlowsheetBlock(default={'dynamic': False}) + + configuration = { + # Specifying components + "components": { + 'benzene': {"type": Component, + "elemental_composition": {'H': 6, 'C': 6}, + "dens_mol_liq_comp": CoolPropWrapper, + "enth_mol_ig_comp": Constant, + "entr_mol_ig_comp": Constant, + "parameter_data": { + "mw": CoolPropWrapper, + "dens_mol_crit": CoolPropWrapper, + "pressure_crit": CoolPropWrapper, + "temperature_crit": CoolPropWrapper, + "omega": CoolPropWrapper, + "cp_mol_ig_comp_coeff": 0, + "enth_mol_form_ig_comp_ref": 0, + "entr_mol_form_ig_comp_ref": 0}}}, + # Specifying phases + "phases": {'Liq': {"type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": { + "type": CubicType.PR}}}, + + # Set base units of measurement + "base_units": {"time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K}, + + # Specifying state definition + "state_definition": FTPx, + "state_bounds": {"flow_mol": (0, 100, 1000, pyunits.mol/pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa)}, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + + "parameter_data": {"PR_kappa": {("benzene", "benzene"): 0.000}}} + + m.fs.props = GenericParameterBlock(default=configuration) + + m.fs.state = m.fs.props.build_state_block( + [0], default={"defined_state": True}) + + m.fs.state[0].flow_mol.fix(1) + m.fs.state[0].mole_frac_comp["benzene"].fix(1) + + return m + + @pytest.mark.integration + def test_cubic_liquid(self, m): + for P in range(1, 11): + Td = CoolProp.PropsSI("T", "P", P*1e5, "Q", 0.5, "PR::benzene") + Tmin = CoolProp.PropsSI("TMIN", "benzene") + + m.fs.state[0].pressure.fix(P*1e5) + m.fs.state[0].temperature.fix(Tmin) + + m.fs.state.initialize() + + for T in arange(Tmin, Td, 10): + print(P, T) + m.fs.state[0].temperature.fix(T) + + results = solver.solve(m.fs) + + assert results.solver.termination_condition == \ + TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Check results + assert pytest.approx( + CoolProp.PropsSI("Z", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-8) == value( + m.fs.state[0].compress_fact_phase["Liq"]) + + assert pytest.approx( + CoolProp.PropsSI( + "DMOLAR", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-6) == value( + m.fs.state[0].dens_mol_phase["Liq"]) + + assert pytest.approx( + CoolProp.PropsSI( + "HMOLAR_RESIDUAL", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-6) == value( + m.fs.state[0].enth_mol_phase["Liq"]) + + @pytest.mark.integration + def test_cubic_liquid_entr(self, m): + Td = CoolProp.PropsSI("T", "P", 101325, "Q", 0.5, "PR::benzene") + Tmin = CoolProp.PropsSI("TMIN", "benzene") + + for T in arange(Tmin, Td, 10): + m.fs.state[0].pressure.fix(101325) + m.fs.state[0].temperature.fix(T) + + m.fs.state.initialize() + + S0_CP = CoolProp.PropsSI( + "SMOLAR", "T", T, "P", 101325, "PR::benzene") + S0_I = value(m.fs.state[0].entr_mol_phase["Liq"]) + + for P in range(1, 11): + print(T, P) + m.fs.state[0].pressure.fix(P*1e5) + + results = solver.solve(m.fs) + + assert results.solver.termination_condition == \ + TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + assert pytest.approx(CoolProp.PropsSI( + "SMOLAR", "T", T, "P", P*1e5, "PR::benzene") - S0_CP, + rel=1e-4) == value( + m.fs.state[0].entr_mol_phase["Liq"] - S0_I) + + +@pytest.mark.skipif(not coolprop_available, reason="CoolProp not installed") +class TestVerifyExcessVap(object): + @pytest.fixture(scope="class") + def m(self): + # Clear cached components to ensure clean slate + CoolPropWrapper.flush_cached_components() + + m = ConcreteModel() + + m.fs = FlowsheetBlock(default={'dynamic': False}) + + # NBP reference state: h=0, s=0 for saturated liquid at 1 atmosphere + CoolProp.set_reference_state('benzene', 'NBP') + + # Get Tsat for benezene at 1 atm + Tref = CoolProp.PropsSI("T", "P", 101325, "Q", 0.5, "PR::benzene") + + configuration = { + # Specifying components + "components": { + 'benzene': {"type": Component, + "elemental_composition": {'H': 6, 'C': 6}, + "dens_mol_liq_comp": CoolPropWrapper, + "enth_mol_ig_comp": Constant, + "entr_mol_ig_comp": Constant, + "parameter_data": { + "mw": CoolPropWrapper, + "dens_mol_crit": CoolPropWrapper, + "pressure_crit": CoolPropWrapper, + "temperature_crit": CoolPropWrapper, + "omega": CoolPropWrapper, + "cp_mol_ig_comp_coeff": 0, + "enth_mol_form_ig_comp_ref": 0, + "entr_mol_form_ig_comp_ref": 0}}}, + # Specifying phases + "phases": {'Vap': {"type": VaporPhase, + "equation_of_state": Cubic, + "equation_of_state_options": { + "type": CubicType.PR}}}, + + # Set base units of measurement + "base_units": {"time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K}, + + # Specifying state definition + "state_definition": FTPx, + "state_bounds": {"flow_mol": (0, 100, 1000, pyunits.mol/pyunits.s), + "temperature": (273.15, 300, 730, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa)}, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (Tref, pyunits.K), + + "parameter_data": {"PR_kappa": {("benzene", "benzene"): 0.000}}} + + m.fs.props = GenericParameterBlock(default=configuration) + + m.fs.state = m.fs.props.build_state_block( + [0], default={"defined_state": True}) + + m.fs.state[0].flow_mol.fix(1) + m.fs.state[0].mole_frac_comp["benzene"].fix(1) + + return m + + @pytest.mark.integration + def test_cubic_vapor(self, m): + for P in range(1, 11): + Tb = CoolProp.PropsSI("T", "P", P*1e5, "Q", 0.5, "PR::benzene") + Tmax = CoolProp.PropsSI("TMAX", "benzene") + + m.fs.state[0].pressure.fix(P*1e5) + m.fs.state[0].temperature.fix(Tb+1) + + m.fs.state.initialize() + + for T in arange(Tb+1, Tmax, 10): + m.fs.state[0].temperature.fix(T) + + results = solver.solve(m.fs) + + assert results.solver.termination_condition == \ + TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + # Check results + assert pytest.approx( + CoolProp.PropsSI("Z", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-8) == value( + m.fs.state[0].compress_fact_phase["Vap"]) + + assert pytest.approx( + CoolProp.PropsSI( + "DMOLAR", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-6) == value( + m.fs.state[0].dens_mol_phase["Vap"]) + + assert pytest.approx( + CoolProp.PropsSI( + "HMOLAR_RESIDUAL", "T", T, "P", P*1e5, "PR::benzene"), + rel=1e-6) == value( + m.fs.state[0].enth_mol_phase["Vap"]) + + @pytest.mark.integration + def test_cubic_vapor_entr(self, m): + Tb = CoolProp.PropsSI("T", "P", 1e6, "Q", 0.5, "PR::benzene") + Tmax = CoolProp.PropsSI("TMAX", "benzene") + + for T in arange(Tb+1, Tmax, 10): + m.fs.state[0].pressure.fix(101325) + m.fs.state[0].temperature.fix(T) + + m.fs.state.initialize() + + S0_CP = CoolProp.PropsSI( + "SMOLAR", "T", T, "P", 101325, "PR::benzene") + S0_I = value(m.fs.state[0].entr_mol_phase["Vap"]) + + for P in range(1, 11): + print(T, P) + m.fs.state[0].pressure.fix(P*1e5) + + results = solver.solve(m.fs) + + assert results.solver.termination_condition == \ + TerminationCondition.optimal + assert results.solver.status == SolverStatus.ok + + assert pytest.approx(CoolProp.PropsSI( + "SMOLAR", "T", T, "P", P*1e5, "PR::benzene") - S0_CP, + rel=1e-4) == value( + m.fs.state[0].entr_mol_phase["Vap"] - S0_I) diff --git a/idaes/generic_models/properties/core/eos/tests/test_ceos_CO2_H2O.py b/idaes/generic_models/properties/core/eos/tests/test_ceos_CO2_H2O.py index 5e3618581e..0810549b7b 100644 --- a/idaes/generic_models/properties/core/eos/tests/test_ceos_CO2_H2O.py +++ b/idaes/generic_models/properties/core/eos/tests/test_ceos_CO2_H2O.py @@ -20,16 +20,15 @@ from sys import modules import os -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Expression, Param, Constraint, value, Var, - units as pyunits, - TerminationCondition, - SolverStatus) + units as pyunits) from idaes.core import (declare_process_block_class, LiquidPhase, VaporPhase, SolidPhase) @@ -93,9 +92,7 @@ def build_model(): m.props.initialize(state_vars_fixed=True) results = get_solver(options={"bound_push": 1e-8}).solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) return m diff --git a/idaes/generic_models/properties/core/eos/tests/test_ceos_PR.py b/idaes/generic_models/properties/core/eos/tests/test_ceos_PR.py index 7a4536f14d..2e7b569848 100755 --- a/idaes/generic_models/properties/core/eos/tests/test_ceos_PR.py +++ b/idaes/generic_models/properties/core/eos/tests/test_ceos_PR.py @@ -156,6 +156,33 @@ def m(): # Set a distinct value for _teq so it can be distinguished from temperature m.props[1]._teq[("Vap", "Liq")].value = 100 + + + m.props[1].energy_internal_mol_phase_comp = Var( + m.params.phase_list, m.params.component_list, initialize=1) + m.props[1].enth_mol_phase_comp = Var(m.params.phase_list, + m.params.component_list) + m.props[1].vol_mol_phase_comp = Var(m.params.phase_list, + m.params.component_list) + m.props[1].entr_mol_phase_comp = Var(m.params.phase_list, + m.params.component_list, + initialize=1) + + m.props[1].enth_mol_phase = Var(m.params.phase_list) + m.props[1].entr_mol_phase = Var(m.params.phase_list) + + m.props[1].dens_mol_phase = Var(m.params.phase_list) + m.props[1].mw_phase = Var(m.params.phase_list) + + + for j in m.params.component_list: + m.params.config.include_enthalpy_of_formation = False + m.params.get_component(j).config.enth_mol_liq_comp = dummy_call + m.params.get_component(j).config.enth_mol_ig_comp = dummy_call + + m.params.get_component(j).config.entr_mol_liq_comp = dummy_call + m.params.get_component(j).config.entr_mol_ig_comp = dummy_call + return m @@ -457,16 +484,19 @@ def test_common(m): assert len(m.props[1].PR_daij_dT) == len(m.params.component_list)**2 for i in m.params.component_list: for j in m.params.component_list: - assert pytest.approx(value(m.props[1].PR_daij_dT[i,j]) - == (1-m.params.PR_kappa[i, j]) - * (m.props[1].PR_fw[j] - * sqrt(m.props[1].PR_a[i] - * m.params.get_component(j).temperature_crit - / m.params.get_component(j).pressure_crit) - + m.props[1].PR_fw[i] - * sqrt(m.props[1].PR_a[j] - * m.params.get_component(i).temperature_crit - / m.params.get_component(i).pressure_crit))) + assert value(m.props[1].PR_daij_dT[i,j]) == pytest.approx( + value(-(const.gas_constant/2)*sqrt(0.45724) + * (1-m.params.PR_kappa[i, j]) + /sqrt(m.props[1].temperature) + * (m.props[1].PR_fw[j] + * sqrt(m.props[1].PR_a[i] + * m.params.get_component(j).temperature_crit + / m.params.get_component(j).pressure_crit) + + m.props[1].PR_fw[i] + * sqrt(m.props[1].PR_a[j] + * m.params.get_component(i).temperature_crit + / m.params.get_component(i).pressure_crit))) + ) assert isinstance(m.props[1].PR_dam_dT, Expression) assert len(m.props[1].PR_dam_dT) == len(m.params.phase_list) @@ -602,12 +632,8 @@ def test_compress_fact_phase_Vap(m): assert pytest.approx(value( Cubic.compress_fact_phase(m.props[1], "Vap")), rel=1e-5) == Zv - @pytest.mark.unit def test_dens_mass_phase(m): - m.props[1].dens_mol_phase = Var(m.params.phase_list) - m.props[1].mw_phase = Var(m.params.phase_list) - for p in m.params.phase_list: assert str(Cubic.dens_mass_phase(m.props[1], p)) == str( m.props[1].dens_mol_phase[p]*m.props[1].mw_phase[p]) @@ -620,30 +646,15 @@ def test_dens_mol_phase(m): assert value(Cubic.dens_mol_phase(m.props[1], "Liq")) == pytest.approx( 41.157, rel=1e-3) - @pytest.mark.unit def test_energy_internal_mol_phase(m): - for j in m.params.component_list: - m.params.config.include_enthalpy_of_formation = False - m.params.get_component(j).config.enth_mol_liq_comp = dummy_call - m.params.get_component(j).config.enth_mol_ig_comp = dummy_call - - m.props[1].energy_internal_mol_phase_comp = Var( - m.params.phase_list, m.params.component_list, initialize=1) - assert pytest.approx(Uv, rel=1e-4) == value( Cubic.energy_internal_mol_phase(m.props[1], "Vap")) assert pytest.approx(Ul, rel=1e-4) == value( Cubic.energy_internal_mol_phase(m.props[1], "Liq")) - @pytest.mark.unit def test_energy_internal_mol_phase_comp(m): - m.props[1].enth_mol_phase_comp = Var(m.params.phase_list, - m.params.component_list) - m.props[1].vol_mol_phase_comp = Var(m.params.phase_list, - m.params.component_list) - for p in m.params.phase_list: for j in m.params.component_list: assert (str(Cubic.energy_internal_mol_phase_comp(m.props[1], p, j)) @@ -656,14 +667,6 @@ def test_energy_internal_mol_phase_comp(m): @pytest.mark.unit def test_enth_mol_phase(m): - for j in m.params.component_list: - m.params.get_component(j).config.enth_mol_liq_comp = dummy_call - m.params.get_component(j).config.enth_mol_ig_comp = dummy_call - - m.props[1].enth_mol_phase_comp = Var(m.params.phase_list, - m.params.component_list, - initialize=1) - assert pytest.approx(value( Cubic.enth_mol_phase(m.props[1], "Vap")), rel=1e-5) == Hv assert pytest.approx(value( @@ -679,9 +682,6 @@ def test_enth_mol_phase_comp(m): ("Liq", "c"): -757.0346, ("Vap", "c"): -1052.4697} for j in m.params.component_list: - m.params.get_component(j).config.enth_mol_liq_comp = dummy_call - m.params.get_component(j).config.enth_mol_ig_comp = dummy_call - assert pytest.approx(enth["Liq",j], rel=1e-5) == value( Cubic.enth_mol_phase_comp(m.props[1], "Liq", j)) assert pytest.approx(enth["Vap",j], rel=1e-5) == value( @@ -700,14 +700,6 @@ def test_enth_mol_phase_comp(m): @pytest.mark.unit def test_entr_mol_phase(m): - for j in m.params.component_list: - m.params.get_component(j).config.entr_mol_liq_comp = dummy_call - m.params.get_component(j).config.entr_mol_ig_comp = dummy_call - - m.props[1].entr_mol_phase_comp = Var(m.params.phase_list, - m.params.component_list, - initialize=1) - assert pytest.approx(value( Cubic.entr_mol_phase(m.props[1], "Vap")), rel=1e-5) == 46.58858 assert pytest.approx(value( @@ -723,9 +715,6 @@ def test_entr_mol_phase_comp(m): ("Liq", "c"): 59.1236, ("Vap", "c"): 42.2799} for j in m.params.component_list: - m.params.get_component(j).config.entr_mol_liq_comp = dummy_call - m.params.get_component(j).config.entr_mol_ig_comp = dummy_call - assert pytest.approx(entr[("Liq", j)], rel=1e-5) == value( Cubic.entr_mol_phase_comp(m.props[1], "Liq", j)) assert pytest.approx(entr[("Vap", j)], rel=1e-5) == value( @@ -809,9 +798,6 @@ def test_fug_coeff_phase_comp_eq_Vap(m): @pytest.mark.unit def test_gibbs_mol_phase(m): - m.props[1].enth_mol_phase = Var(m.params.phase_list) - m.props[1].entr_mol_phase = Var(m.params.phase_list) - for p in m.params.phase_list: assert str(Cubic.gibbs_mol_phase(m.props[1], p)) == str( m.props[1].enth_mol_phase[p] - @@ -820,10 +806,6 @@ def test_gibbs_mol_phase(m): @pytest.mark.unit def test_gibbs_mol_phase_comp(m): - for j in m.params.component_list: - m.params.get_component(j).config.entr_mol_ig_comp = dummy_call - m.params.get_component(j).config.enth_mol_ig_comp = dummy_call - for p in m.params.phase_list: for j in m.params.component_list: assert (pytest.approx( @@ -871,7 +853,7 @@ def test_vol_mol_phase_comp(m): for j in m.params.component_list)) ) -if __name__ == "__main__": - # mod = m() - # test_gibbs_mol_phase_comp(mod) - pass \ No newline at end of file +# if __name__ == "__main__": +# mod = m() +# test_common(mod) +# pass \ No newline at end of file diff --git a/idaes/generic_models/properties/core/eos/tests/test_haber_bosch_validation_PR.py b/idaes/generic_models/properties/core/eos/tests/test_haber_bosch_validation_PR.py index 7193207d61..8243ea4147 100644 --- a/idaes/generic_models/properties/core/eos/tests/test_haber_bosch_validation_PR.py +++ b/idaes/generic_models/properties/core/eos/tests/test_haber_bosch_validation_PR.py @@ -42,7 +42,8 @@ import pytest from pytest import approx -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Expression, Constraint, ExternalFunction, @@ -50,8 +51,6 @@ Param, Reals, SolverFactory, - SolverStatus, - TerminationCondition, sqrt, value, Var, @@ -280,8 +279,7 @@ def rule_S_ref(blk,j): solver = SolverFactory('ipopt') results = solver.solve(m) - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (value(m.S_ref) == approx(value(m.props.entr_mol_phase_comp["Vap",comp]), rel=1E-12)) @@ -443,8 +441,7 @@ def rule_V(blk,j): prop.pressure.fix(row["P"]*1E6) tester.set_pressure(row["P"]*1E6) results = solver.solve(m) - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Make sure that log mole fractions have been created and validated for comp in tester.component_list: diff --git a/idaes/generic_models/properties/core/examples/BT_PR.py b/idaes/generic_models/properties/core/examples/BT_PR.py index 771020d0f9..7e2144d821 100755 --- a/idaes/generic_models/properties/core/examples/BT_PR.py +++ b/idaes/generic_models/properties/core/examples/BT_PR.py @@ -11,13 +11,14 @@ # license information. ################################################################################# """ -Benzene-Toluene phase equilibrium package using ideal liquid and vapor. +Benzene-Toluene phase equilibrium package for liquid and vapor using the +Peng-Robinson equation of state. Example property package using the Generic Property Package Framework. This exmample shows how to set up a property package to do benzene-toluene -phase equilibrium in the generic framework using ideal liquid and vapor -assumptions along with methods drawn from the pre-built IDAES property -libraries. +phase equilibrium in the generic framework using the Peng-Robinson equation of +state for liquid and vapor phases along with methods drawn from the pre-built +IDAES property libraries. """ # Import Python libraries import logging diff --git a/idaes/generic_models/properties/core/examples/reactions/tests/test_reaction_example.py b/idaes/generic_models/properties/core/examples/reactions/tests/test_reaction_example.py index 1c9bdd414f..e0b0e5eda7 100755 --- a/idaes/generic_models/properties/core/examples/reactions/tests/test_reaction_example.py +++ b/idaes/generic_models/properties/core/examples/reactions/tests/test_reaction_example.py @@ -14,11 +14,10 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (Block, +from pyomo.environ import (check_optimal_termination, + Block, ConcreteModel, Set, - SolverStatus, - TerminationCondition, value) from pyomo.util.check_units import assert_units_consistent @@ -153,9 +152,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.initialize @pytest.mark.solver diff --git a/idaes/generic_models/properties/core/examples/tests/test_ASU_PR.py b/idaes/generic_models/properties/core/examples/tests/test_ASU_PR.py index 77098c6eeb..3a25891528 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_ASU_PR.py +++ b/idaes/generic_models/properties/core/examples/tests/test_ASU_PR.py @@ -14,10 +14,9 @@ Author: Andrew Lee, Alejandro Garciadiego """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -239,9 +238,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_ASU_PR_Dowling_2015.py b/idaes/generic_models/properties/core/examples/tests/test_ASU_PR_Dowling_2015.py index 07b502abde..b7faf7798d 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_ASU_PR_Dowling_2015.py +++ b/idaes/generic_models/properties/core/examples/tests/test_ASU_PR_Dowling_2015.py @@ -14,10 +14,9 @@ Author: Andrew Lee, Alejandro Garciadiego """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -243,9 +242,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): @@ -284,9 +281,7 @@ def test_SF0(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert model.props[1].mole_frac_phase_comp["Vap", "nitrogen"].value == \ @@ -317,9 +312,7 @@ def test_SFIL3(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert model.props[1].mole_frac_phase_comp["Liq", "nitrogen"].value == \ diff --git a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal.py b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal.py index 6a596a9eef..162dc732ed 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal.py +++ b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal.py @@ -14,10 +14,9 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -284,9 +283,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FPhx.py b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FPhx.py index d8f633e820..d9ec533347 100755 --- a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FPhx.py +++ b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FPhx.py @@ -14,10 +14,9 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -376,9 +375,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcPh.py b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcPh.py index a586ba86ad..f483548b34 100755 --- a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcPh.py +++ b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcPh.py @@ -14,11 +14,10 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Expression, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -377,9 +376,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcTP.py b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcTP.py index dc188a3d5d..e2d3005fc3 100755 --- a/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcTP.py +++ b/idaes/generic_models/properties/core/examples/tests/test_BTIdeal_FcTP.py @@ -14,11 +14,10 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Expression, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -364,9 +363,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_BT_PR.py b/idaes/generic_models/properties/core/examples/tests/test_BT_PR.py index e5aa31fbab..bfd47b9607 100755 --- a/idaes/generic_models/properties/core/examples/tests/test_BT_PR.py +++ b/idaes/generic_models/properties/core/examples/tests/test_BT_PR.py @@ -25,10 +25,9 @@ GenericParameterBlock) from pyomo.util.check_units import assert_units_consistent -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Objective, - SolverStatus, - TerminationCondition, value) from idaes.core.util import get_solver @@ -101,8 +100,7 @@ def test_T_sweep(self, m): results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) assert m.fs.state[1].flow_mol_phase["Liq"].value <= 1e-5 @pytest.mark.integration @@ -118,15 +116,13 @@ def test_P_sweep(self, m): results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) while m.fs.state[1].pressure.value <= 1e6: m.fs.state[1].pressure.value = ( m.fs.state[1].pressure.value + 1e5) results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) print(T, m.fs.state[1].pressure.value) @pytest.mark.component @@ -146,9 +142,7 @@ def test_T350_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), abs=1e-1) == 365 @@ -208,9 +202,7 @@ def test_T350_P5_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 431.47 @@ -270,9 +262,7 @@ def test_T450_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 371.4 @@ -332,9 +322,7 @@ def test_T450_P5_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 436.93 @@ -394,9 +382,7 @@ def test_T368_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 368 @@ -460,9 +446,7 @@ def test_T376_P1_x2(self, m): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(value( m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 376 diff --git a/idaes/generic_models/properties/core/examples/tests/test_CO2_H2O_Ideal_VLE.py b/idaes/generic_models/properties/core/examples/tests/test_CO2_H2O_Ideal_VLE.py index f59a9d37b4..49c0497812 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_CO2_H2O_Ideal_VLE.py +++ b/idaes/generic_models/properties/core/examples/tests/test_CO2_H2O_Ideal_VLE.py @@ -16,10 +16,9 @@ import pytest import numpy as np -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -224,9 +223,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_CO2_bmimPF6_PR.py b/idaes/generic_models/properties/core/examples/tests/test_CO2_bmimPF6_PR.py index de1670246b..1253a3c0b3 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_CO2_bmimPF6_PR.py +++ b/idaes/generic_models/properties/core/examples/tests/test_CO2_bmimPF6_PR.py @@ -14,10 +14,9 @@ Author: Andrew Lee, Alejandro Garciadiego """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -291,9 +290,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_HC_PR.py b/idaes/generic_models/properties/core/examples/tests/test_HC_PR.py index e438b6cb54..c7cf4804a3 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_HC_PR.py +++ b/idaes/generic_models/properties/core/examples/tests/test_HC_PR.py @@ -15,10 +15,9 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -325,9 +324,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.integration def test_solution(self, model): diff --git a/idaes/generic_models/properties/core/examples/tests/test_HC_PR_vap.py b/idaes/generic_models/properties/core/examples/tests/test_HC_PR_vap.py index e39be07738..17bc04447c 100644 --- a/idaes/generic_models/properties/core/examples/tests/test_HC_PR_vap.py +++ b/idaes/generic_models/properties/core/examples/tests/test_HC_PR_vap.py @@ -15,11 +15,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Set, - SolverStatus, - TerminationCondition, value, Var, units as pyunits) @@ -345,9 +344,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/properties/core/generic/generic_property.py b/idaes/generic_models/properties/core/generic/generic_property.py index fda63aa2bd..32c83fcccb 100644 --- a/idaes/generic_models/properties/core/generic/generic_property.py +++ b/idaes/generic_models/properties/core/generic/generic_property.py @@ -18,6 +18,7 @@ # Import Pyomo libraries from pyomo.environ import (Block, + check_optimal_termination, Constraint, exp, Expression, @@ -27,9 +28,7 @@ value, Var, units as pyunits, - Reference, - TerminationCondition, - SolverStatus) + Reference) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.util.calc_var_value import calculate_variable_from_constraint @@ -207,7 +206,6 @@ def build(self): # Set base units of measurement self.get_metadata().add_default_units(self.config.base_units) - units_meta = self.get_metadata().default_units # Call configure method to set construction arguments self.configure() @@ -686,7 +684,7 @@ def build(self): f"type argument was not an instance of " f"HenryType.") elif (self.config.phases_in_equilibrium is not None and - d["type"] != HenryType.Kpx): + d["type"] != HenryType.Kpx): raise PropertyNotSupportedError( f"{self.name} currently only Kpx type Henry's " f"constants are supported with full phase " @@ -716,19 +714,18 @@ def build(self): # First validate that p is a phase if p not in self.phase_list: raise ConfigurationError( - f"{self.name} property {prop} definition contained" - f" unrecognised phase {p}.") + f"{self.name} property {prop} definition " + f"contained unrecognised phase {p}.") else: try: meth.build_parameters(cobj, p) except AttributeError: - # Method provided has no build_parameters method + # No build_parameters method # Assume it is not needed and continue continue for p in self.phase_list: pobj = self.get_phase(p) - # pobj.config.equation_of_state.build_parameters(pobj) for a, v in pobj.config.items(): # Check to see if v has an attribute build_parameters @@ -749,7 +746,7 @@ def build(self): except KeyError: raise ConfigurationError( "{} values were not defined for parameter {} in " - "phase{}. Please check the parameter_data " + "phase {}. Please check the parameter_data " "argument to ensure values are provided." .format(self.name, a, p)) @@ -849,12 +846,12 @@ def build(self): for v in self.component_objects(Var, descend_into=True): for i in v: if v[i].value is None: - if i is None: # Scalar Var + if i is None: # Scalar Var raise ConfigurationError( "{} parameter {} was not assigned" " a value. Please check your configuration " "arguments.".format(self.name, v.local_name)) - else: # Indexed Var + else: # Indexed Var raise ConfigurationError( "{} parameter {}[{}] was not assigned" " a value. Please check your configuration " @@ -969,8 +966,10 @@ def define_metadata(cls, obj): 'gibbs_mol': {'method': '_gibbs_mol'}, 'gibbs_mol_phase': {'method': '_gibbs_mol_phase'}, 'gibbs_mol_phase_comp': {'method': '_gibbs_mol_phase_comp'}, - 'isentropic_speed_sound_phase': {'method': '_isentropic_speed_sound_phase'}, - 'isothermal_speed_sound_phase': {'method': '_isothermal_speed_sound_phase'}, + 'isentropic_speed_sound_phase': { + 'method': '_isentropic_speed_sound_phase'}, + 'isothermal_speed_sound_phase': { + 'method': '_isothermal_speed_sound_phase'}, 'henry': {'method': '_henry'}, 'mass_frac_phase_comp': {'method': '_mass_frac_phase_comp'}, 'mass_frac_phase_comp_apparent': { @@ -1144,7 +1143,7 @@ def initialize(blk, state_args=None, state_vars_fixed=False, solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="properties") init_log.info('Starting initialization') - + res = None for k in blk.keys(): @@ -1307,7 +1306,8 @@ def initialize(blk, state_args=None, state_vars_fixed=False, sum_flow) lb = blk[k].mole_frac_phase_comp_true[p, j].lb if lb is not None and x <= lb: - blk[k].mole_frac_phase_comp_true[p, j].set_value(lb) + blk[k].mole_frac_phase_comp_true[p, j].set_value( + lb) else: blk[k].mole_frac_phase_comp_true[p, j].set_value(x) @@ -1340,8 +1340,8 @@ def initialize(blk, state_args=None, state_vars_fixed=False, c.activate() for p, j in blk[k].params._phase_component_set: calculate_variable_from_constraint( - blk[k].log_mole_frac_phase_comp[p,j], - blk[k].log_mole_frac_phase_comp_eqn[p,j]) + blk[k].log_mole_frac_phase_comp[p, j], + blk[k].log_mole_frac_phase_comp_eqn[p, j]) for pp in blk[k].params._pe_pairs: # Activate formulation specific constraints blk[k].params.config.phase_equilibrium_state[pp] \ @@ -1386,8 +1386,8 @@ def initialize(blk, state_args=None, state_vars_fixed=False, "molality_phase_comp", "molality_phase_comp_apparent", "molality_phase_comp_true", - "mole_frac_comp", # Might have already been initialized - "mole_frac_phase_comp", # Might have already been initialized + "mole_frac_comp", # Might have already been initialized + "mole_frac_phase_comp", # Might have already been initialized "mole_frac_phase_comp_apparent", "mole_frac_phase_comp_true", "pressure_phase_comp", @@ -1429,10 +1429,7 @@ def initialize(blk, state_args=None, state_vars_fixed=False, .state_definition.do_not_initialize): c.activate() - if (res is not None and ( - res.solver.termination_condition != - TerminationCondition.optimal or - res.solver.status != SolverStatus.ok)): + if res is not None and not check_optimal_termination(res): raise InitializationError( f"{blk.name} failed to initialize successfully. Please check " f"the output logs for more information.") @@ -1545,7 +1542,7 @@ def _init_Tbub(self, blk, T_units): if blk.is_property_constructed("log_mole_frac_tbub"): blk.log_mole_frac_tbub[pp, j].value = value( log(blk._mole_frac_tbub[pp, j])) - + for j in henry_comps: blk._mole_frac_tbub[pp, j].value = value( blk.mole_frac_comp[j] * @@ -1897,8 +1894,10 @@ def calculate_scaling_factors(self): self, k[0], k[1], k[2]) iscale.constraint_scaling_transform( - self.equilibrium_constraint[k], sf_fug, overwrite=False) - except KeyError: # component not in phase + self.equilibrium_constraint[k], + sf_fug, + overwrite=False) + except KeyError: # component not in phase pass # Inherent reactions @@ -1924,11 +1923,10 @@ def calculate_scaling_factors(self): # Add scaling for additional Vars and Constraints # Bubble and dew points - def bubble_dew_scaling(b, pt_var): # Ditch the m.fs.unit.control_volume... short_name = pt_var.name.split(".")[-1] - + if short_name.startswith("temperature"): abbrv = "t" sf_pt = sf_T @@ -1937,26 +1935,26 @@ def bubble_dew_scaling(b, pt_var): sf_pt = sf_P else: _raise_dev_burnt_toast() - + if short_name.endswith("bubble"): phase = VaporPhase abbrv += "bub" elif short_name.endswith("dew"): phase = LiquidPhase abbrv += "dew" - - x_var = getattr(b,"_mole_frac_"+abbrv) + + x_var = getattr(b, "_mole_frac_"+abbrv) if b.is_property_constructed("log_mole_frac_"+abbrv): - log_eq = getattr(b,"log_mole_frac_"+abbrv+"_eqn") + log_eq = getattr(b, "log_mole_frac_"+abbrv+"_eqn") else: log_eq = None - + # Directly scale the bubble/dew temperature/pressure variable for v in pt_var.values(): if iscale.get_scaling_factor(v) is None: iscale.set_scaling_factor(v, sf_pt) - + # Scale mole fractions for bubble/dew calcs for i, v in x_var.items(): if iscale.get_scaling_factor(v) is None: @@ -1980,27 +1978,26 @@ def bubble_dew_scaling(b, pt_var): # component i[2] is not in the new phase, so this # variable is likely unused and scale doesn't matter iscale.set_scaling_factor(v, 1) - + scaling_method = getattr(b.params.config.bubble_dew_method, "scale_"+short_name) scaling_method(b, overwrite=False) - + return - + if self.is_property_constructed("temperature_bubble"): bubble_dew_scaling(self, self.temperature_bubble) if self.is_property_constructed("temperature_dew"): - bubble_dew_scaling(self, self.temperature_dew) - + bubble_dew_scaling(self, self.temperature_dew) + if self.is_property_constructed("pressure_bubble"): bubble_dew_scaling(self, self.pressure_bubble) - + if self.is_property_constructed("pressure_dew"): bubble_dew_scaling(self, self.pressure_dew) # Scale log form constraints - if self.is_property_constructed("log_mole_frac_comp"): for j, v in self.log_mole_frac_comp_eqn.items(): sf_x = iscale.get_scaling_factor( @@ -2010,8 +2007,8 @@ def bubble_dew_scaling(b, pt_var): hint="for log_mole_frac_comp") iscale.constraint_scaling_transform( v, sf_x, overwrite=False) - - #TODO: determine if this is lazy copy and pasting + + # Activity is generally of similar order to mole fractions if self.is_property_constructed("log_act_phase_comp"): for (p, j), v in self.log_act_phase_comp_eq.items(): sf_x = iscale.get_scaling_factor( @@ -2042,7 +2039,8 @@ def bubble_dew_scaling(b, pt_var): iscale.constraint_scaling_transform( v, sf_x, overwrite=False) - if self.is_property_constructed("log_act_phase_solvents") and len(self.params.solvent_set) > 1: + if (self.is_property_constructed("log_act_phase_solvents") and + len(self.params.solvent_set) > 1): for p, v in self.log_act_phase_solvents_eq.items(): iscale.constraint_scaling_transform( v, 1e-3, overwrite=False) @@ -2224,29 +2222,29 @@ def get_mole_frac(self, phase=None): # Bubble and Dew Points def _temperature_bubble(b): - _temperature_pressure_bubble_dew(b,"temperature_bubble") - + _temperature_pressure_bubble_dew(b, "temperature_bubble") + def _log_mole_frac_tbub(b): - _log_mole_frac_bubble_dew(b,"log_mole_frac_tbub") - + _log_mole_frac_bubble_dew(b, "log_mole_frac_tbub") + def _temperature_dew(b): - _temperature_pressure_bubble_dew(b,"temperature_dew") - + _temperature_pressure_bubble_dew(b, "temperature_dew") + def _log_mole_frac_tdew(b): - _log_mole_frac_bubble_dew(b,"log_mole_frac_tdew") + _log_mole_frac_bubble_dew(b, "log_mole_frac_tdew") def _pressure_bubble(b): - _temperature_pressure_bubble_dew(b,"pressure_bubble") + _temperature_pressure_bubble_dew(b, "pressure_bubble") def _log_mole_frac_pbub(b): - _log_mole_frac_bubble_dew(b,"log_mole_frac_pbub") + _log_mole_frac_bubble_dew(b, "log_mole_frac_pbub") def _pressure_dew(b): - _temperature_pressure_bubble_dew(b,"pressure_dew") - + _temperature_pressure_bubble_dew(b, "pressure_dew") + def _log_mole_frac_pdew(b): - _log_mole_frac_bubble_dew(b,"log_mole_frac_pdew") - + _log_mole_frac_bubble_dew(b, "log_mole_frac_pdew") + # ------------------------------------------------------------------------- # Property Methods def _act_phase_comp(self): @@ -2947,7 +2945,8 @@ def _isentropic_speed_sound_phase(self): try: def rule_isentropic_speed_sound_phase(b, p): p_config = b.params.get_phase(p).config - return p_config.equation_of_state.isentropic_speed_sound_phase(b, p) + return p_config.equation_of_state.isentropic_speed_sound_phase( + b, p) self.isentropic_speed_sound_phase = Expression( self.phase_list, doc="Isentropic speed of sound in each phase", @@ -2960,7 +2959,8 @@ def _isothermal_speed_sound_phase(self): try: def rule_isothermal_speed_sound_phase(b, p): p_config = b.params.get_phase(p).config - return p_config.equation_of_state.isothermal_speed_sound_phase(b, p) + return p_config.equation_of_state.isothermal_speed_sound_phase( + b, p) self.isothermal_speed_sound_phase = Expression( self.phase_list, doc="Isothermal speed of sound in each phase", @@ -2968,7 +2968,7 @@ def rule_isothermal_speed_sound_phase(b, p): except AttributeError: self.del_component(self.isothermal_speed_sound_phase) raise - + def _henry(self): try: def henry_rule(b, p, j): @@ -3049,17 +3049,6 @@ def _mw(self): self.del_component(self.mw) raise - def _mw_comp(self): - try: - def rule_mw_comp(b, j): - return b.params.get_component(j).mw - self.mw_comp = Expression(self.component_list, - doc="Component molecular weight", - rule=rule_mw_comp) - except AttributeError: - self.del_component(self.mw_comp) - raise - def _molality_phase_comp(self): try: def rule_molality(b, p, j): @@ -3351,7 +3340,9 @@ def rule_log_act_phase_comp(b, p, j): def _log_act_phase_solvents(self): if len(self.params.solvent_set) == 1: self.log_act_phase_solvents = \ - Reference(self.log_act_phase_comp[:, self.params.solvent_set.first()]) + Reference( + self.log_act_phase_comp[ + :, self.params.solvent_set.first()]) else: try: self.log_act_phase_solvents = Var( @@ -3368,12 +3359,13 @@ def rule_log_act_phase_solvents(b, p): return Expression.Skip else: return exp(b.log_act_phase_solvents[p]) == \ - sum(p_config.equation_of_state.act_phase_comp(b, p, j) + sum(p_config.equation_of_state.act_phase_comp( + b, p, j) for j in b.params.solvent_set) self.log_act_phase_solvents_eq = Constraint( self.phase_list, - doc="Natural log of summed solvent activity in each phase", + doc="Natural log of solvent activity in each phase", rule=rule_log_act_phase_solvents) except AttributeError: self.del_component(self.log_act_phase_solvents) @@ -3778,10 +3770,12 @@ def rule_log_k_eq(b, r): self.del_component(self.log_k_eq_constraint) raise + def _raise_dev_burnt_toast(): raise BurntToast("Users shouldn't be calling this function. " "If you're a dev, you know what you did.") + def _valid_VL_component_list(blk, pp): raoult_comps = [] henry_comps = [] @@ -3805,7 +3799,7 @@ def _valid_VL_component_list(blk, pp): return raoult_comps, henry_comps - + def _temperature_pressure_bubble_dew(b, name): # temperature/pressure bubble/dew splt = name.split("_") @@ -3821,7 +3815,7 @@ def _temperature_pressure_bubble_dew(b, name): docstring_mole_frac = "Vapor"+docstring_mole_frac+"at bubble " else: _raise_dev_burnt_toast() - + if splt[0] == "temperature": abbrv = "t"+abbrv bounds = (b.temperature.lb, b.temperature.ub) @@ -3833,11 +3827,10 @@ def _temperature_pressure_bubble_dew(b, name): units = units_meta["pressure"] else: _raise_dev_burnt_toast() - - + docstring_var += splt[0] docstring_mole_frac += splt[0] - + if b.params.config.bubble_dew_method is None: raise GenericPropertyPackageError(b, name) @@ -3847,91 +3840,93 @@ def _temperature_pressure_bubble_dew(b, name): doc=docstring_var, bounds=bounds, units=units)) - setattr(b,"_mole_frac_"+abbrv, Var( - b.params._pe_pairs, - b.component_list, - initialize=1/len(b.component_list), - bounds=(0, None), - doc=docstring_mole_frac, - units=None)) - + setattr(b, "_mole_frac_"+abbrv, Var( + b.params._pe_pairs, + b.component_list, + initialize=1/len(b.component_list), + bounds=(0, None), + doc=docstring_mole_frac, + units=None)) + tmp = getattr(b.params.config.bubble_dew_method, name) tmp(b) + except AttributeError: - if hasattr(b,name): - var = getattr(b,name) + if hasattr(b, name): + var = getattr(b, name) b.del_component(var) - if hasattr(b,"_mole_frac_"+abbrv): - mole_frac = getattr(b,"_mole_frac_"+abbrv) + if hasattr(b, "_mole_frac_"+abbrv): + mole_frac = getattr(b, "_mole_frac_"+abbrv) b.del_component(mole_frac) raise + def _log_mole_frac_bubble_dew(b, name): abbrv = name.split("_")[-1] docstring_var = " log mole fractions at " docstring_eqn = "Constraint for log of " - + if abbrv.endswith("dew"): docstring_var = "Liquid"+docstring_var+"dew " - docstring_eqn += "dew mole fractions" + docstring_eqn += "dew mole fractions" elif abbrv.endswith("bub"): docstring_var = "Vapor"+docstring_var+"bubble " - docstring_eqn += "bubble mole fractions" + docstring_eqn += "bubble mole fractions" else: _raise_dev_burnt_toast() - + if abbrv.startswith("t"): docstring_var += "temperature" elif abbrv.startswith("p"): docstring_var += "pressure" else: _raise_dev_burnt_toast() - + try: - setattr(b,"log_mole_frac_"+abbrv, Var( - b.params._pe_pairs, - b.component_list, - initialize=log(1/len(b.component_list)), - bounds=(None, 0), - doc=docstring_var, - units=None) - ) - + setattr(b, "log_mole_frac_"+abbrv, Var( + b.params._pe_pairs, + b.component_list, + initialize=log(1/len(b.component_list)), + bounds=(None, 0), + doc=docstring_var, + units=None)) + def rule_log_mole_frac(b, p1, p2, j): (l_phase, v_phase, vl_comps, henry_comps, l_only_comps, - v_only_comps) = bub_dew_VL_comp_list(b,(p1,p2)) - + v_only_comps) = bub_dew_VL_comp_list(b, (p1, p2)) + if l_phase is None or v_phase is None: # Not a VLE pair - return Constraint.Skip - elif not j in set(vl_comps+henry_comps): return Constraint.Skip - + elif j not in set(vl_comps+henry_comps): + return Constraint.Skip + if abbrv.endswith("dew") and (l_only_comps != []): # Non-vaporisables present, no dew point return Constraint.Skip elif abbrv.endswith("bub") and (v_only_comps != []): # Non-condensables present, no bubble point return Constraint.Skip - log_mole_frac = getattr(b,"log_mole_frac_"+abbrv) - mole_frac = getattr(b,"_mole_frac_"+abbrv) - + log_mole_frac = getattr(b, "log_mole_frac_"+abbrv) + mole_frac = getattr(b, "_mole_frac_"+abbrv) + return exp(log_mole_frac[p1, p2, j]) == mole_frac[p1, p2, j] + setattr(b, "log_mole_frac_"+abbrv+"_eqn", Constraint( b.params._pe_pairs, b.component_list, rule=rule_log_mole_frac, doc=docstring_eqn )) - except: - if hasattr(b,"log_mole_frac_"+abbrv): - var = getattr(b,"log_mole_frac_"+abbrv) + except AttributeError: + if hasattr(b, "log_mole_frac_"+abbrv): + var = getattr(b, "log_mole_frac_"+abbrv) b.del_component(var) - if hasattr(b,"log_mole_frac_"+abbrv+"_eqn"): - eqn = getattr(b,"log_mole_frac_"+abbrv+"_eqn") + if hasattr(b, "log_mole_frac_"+abbrv+"_eqn"): + eqn = getattr(b, "log_mole_frac_"+abbrv+"_eqn") b.del_component(eqn) - raise \ No newline at end of file + raise diff --git a/idaes/generic_models/properties/core/generic/tests/test_generic_property_integration.py b/idaes/generic_models/properties/core/generic/tests/test_generic_property_integration.py index 6cda72c695..a8bae47337 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_generic_property_integration.py +++ b/idaes/generic_models/properties/core/generic/tests/test_generic_property_integration.py @@ -19,8 +19,8 @@ # Import Pyomo units from pyomo.environ import ( - ConcreteModel, Constraint, Expression, SolverStatus, - TerminationCondition, units as pyunits, value) + check_optimal_termination, ConcreteModel, Constraint, Expression, + units as pyunits, value) # Import IDAES cores from idaes.core import ( @@ -178,9 +178,7 @@ def test_heater_w_inherent_rxns_comp_phase(self, frame): results = solver.solve(frame) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert value( frame.fs.H101.control_volume.properties_out[0].k_eq["e1"]) == 2 @@ -228,9 +226,7 @@ def test_heater_w_inherent_rxns_comp_total(self, frame): results = solver.solve(frame) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert value( frame.fs.H101.control_volume.properties_out[0].k_eq["e1"]) == 2 @@ -292,9 +288,7 @@ def test_CV1D_w_inherent_rxns_comp_phase(self, frame): results = solver.solve(frame) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(2, rel=1e-8) == value( frame.fs.cv.properties[0, 1].k_eq["e1"]) @@ -357,9 +351,7 @@ def test_CV1D_w_inherent_rxns_comp_total(self, frame): results = solver.solve(frame) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(2, rel=1e-8) == value( frame.fs.cv.properties[0, 1].k_eq["e1"]) diff --git a/idaes/generic_models/properties/core/generic/tests/test_noncondense.py b/idaes/generic_models/properties/core/generic/tests/test_noncondense.py index b0a7d0b4d6..acb309ef5b 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_noncondense.py +++ b/idaes/generic_models/properties/core/generic/tests/test_noncondense.py @@ -19,10 +19,9 @@ import pytest # Import Pyomo components -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, units as pyunits) @@ -263,9 +262,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver @@ -375,9 +372,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver diff --git a/idaes/generic_models/properties/core/generic/tests/test_noncondense_PR.py b/idaes/generic_models/properties/core/generic/tests/test_noncondense_PR.py index 64e50bc4ae..99e596aedc 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_noncondense_PR.py +++ b/idaes/generic_models/properties/core/generic/tests/test_noncondense_PR.py @@ -19,10 +19,9 @@ import pytest # Import Pyomo components -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, units as pyunits) @@ -254,9 +253,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver @@ -365,9 +362,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver diff --git a/idaes/generic_models/properties/core/generic/tests/test_nonvap.py b/idaes/generic_models/properties/core/generic/tests/test_nonvap.py index e2f3bce1ee..77bbf07cf1 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_nonvap.py +++ b/idaes/generic_models/properties/core/generic/tests/test_nonvap.py @@ -19,10 +19,9 @@ import pytest # Import Pyomo components -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, units as pyunits) @@ -264,9 +263,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver @@ -375,9 +372,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver diff --git a/idaes/generic_models/properties/core/generic/tests/test_nonvap_PR.py b/idaes/generic_models/properties/core/generic/tests/test_nonvap_PR.py index 9760fee3b6..20838c9229 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_nonvap_PR.py +++ b/idaes/generic_models/properties/core/generic/tests/test_nonvap_PR.py @@ -19,10 +19,9 @@ import pytest # Import Pyomo components -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, units as pyunits) @@ -253,9 +252,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver @@ -364,9 +361,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.component @pytest.mark.solver diff --git a/idaes/generic_models/properties/core/generic/tests/test_vle.py b/idaes/generic_models/properties/core/generic/tests/test_vle.py index b14e46dc39..93e6f71422 100755 --- a/idaes/generic_models/properties/core/generic/tests/test_vle.py +++ b/idaes/generic_models/properties/core/generic/tests/test_vle.py @@ -19,10 +19,9 @@ import pytest # Import Pyomo components -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Set, - SolverStatus, - TerminationCondition, value, units as pyunits) @@ -241,9 +240,7 @@ def test_solve_vle(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(365.35, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")]) @@ -491,9 +488,7 @@ def test_solve_vle(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(365.35, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")]) @@ -648,9 +643,7 @@ def test_solve_vle(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(361.50, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")]) diff --git a/idaes/generic_models/properties/core/reactions/tests/test_solubility_product_verification.py b/idaes/generic_models/properties/core/reactions/tests/test_solubility_product_verification.py index 200736de1e..ca32ade5af 100755 --- a/idaes/generic_models/properties/core/reactions/tests/test_solubility_product_verification.py +++ b/idaes/generic_models/properties/core/reactions/tests/test_solubility_product_verification.py @@ -17,7 +17,7 @@ import pytest from pyomo.environ import \ - ConcreteModel, units as pyunits, value, SolverStatus, TerminationCondition + check_optimal_termination, ConcreteModel, units as pyunits, value # Import IDAES cores from idaes.generic_models.properties.core.generic.generic_property import \ @@ -138,9 +138,7 @@ def test_saturated(self, model): results = solver.solve(model) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(8.235e-3, abs=1e-8) == value( model.state[0].mole_frac_phase_comp["Liq", "Na+"] * @@ -175,9 +173,7 @@ def test_subsaturated(self, model): results = solver.solve(model) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(0, abs=1e-5) == value( model.state[0].flow_mol_phase_comp["Sol", "NaCl"]) @@ -225,9 +221,7 @@ def test_subsaturated(self, model): results = solver.solve(model) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(0, abs=1.1e-6) == value( model.fs.R101.outlet.flow_mol_phase_comp[0, "Sol", "NaCl"]) @@ -242,9 +236,7 @@ def test_saturated(self, model): results = solver.solve(model) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(0, abs=1.1e-3) == value( model.fs.R101.outlet.flow_mol_phase_comp[0, "Sol", "NaCl"]) @@ -260,9 +252,7 @@ def test_supersaturated(self, model): results = solver.solve(model) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert pytest.approx(i, rel=1e-5) == value( model.fs.R101.outlet.flow_mol_phase_comp[0, "Sol", "NaCl"] + diff --git a/idaes/generic_models/properties/core/state_definitions/tests/test_FPhx_electrolyte.py b/idaes/generic_models/properties/core/state_definitions/tests/test_FPhx_electrolyte.py index 835081e27c..2debd586ec 100755 --- a/idaes/generic_models/properties/core/state_definitions/tests/test_FPhx_electrolyte.py +++ b/idaes/generic_models/properties/core/state_definitions/tests/test_FPhx_electrolyte.py @@ -17,11 +17,10 @@ import pytest # Import Pyomo units -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -216,9 +215,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs, tee=True) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates assert (value(m.fs.state[1].flow_mol_phase_comp_true["Vap", "H2O"]) == @@ -465,9 +462,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -900,9 +895,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) assert m.fs.state[1].temperature.value == 350 diff --git a/idaes/generic_models/properties/core/state_definitions/tests/test_FTPx_electrolyte.py b/idaes/generic_models/properties/core/state_definitions/tests/test_FTPx_electrolyte.py index 257047b29e..55929ac776 100755 --- a/idaes/generic_models/properties/core/state_definitions/tests/test_FTPx_electrolyte.py +++ b/idaes/generic_models/properties/core/state_definitions/tests/test_FTPx_electrolyte.py @@ -17,11 +17,10 @@ import pytest # Import Pyomo units -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -207,9 +206,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs, tee=True) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates assert (value(m.fs.state[1].flow_mol_phase_comp_true["Vap", "H2O"]) == @@ -454,9 +451,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -882,9 +877,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -1164,9 +1157,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -1457,9 +1448,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates for j in m.fs.state[1].mole_frac_comp: diff --git a/idaes/generic_models/properties/core/state_definitions/tests/test_FcPh_electrolyte.py b/idaes/generic_models/properties/core/state_definitions/tests/test_FcPh_electrolyte.py index 2e7325f767..cc43edaadc 100755 --- a/idaes/generic_models/properties/core/state_definitions/tests/test_FcPh_electrolyte.py +++ b/idaes/generic_models/properties/core/state_definitions/tests/test_FcPh_electrolyte.py @@ -17,11 +17,10 @@ import pytest # Import Pyomo units -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -215,9 +214,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs, tee=True) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates assert (value(m.fs.state[1].flow_mol_phase_comp_true["Vap", "H2O"]) == @@ -463,9 +460,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -897,9 +892,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) assert m.fs.state[1].temperature.value == 350 diff --git a/idaes/generic_models/properties/core/state_definitions/tests/test_FcTP_electrolyte.py b/idaes/generic_models/properties/core/state_definitions/tests/test_FcTP_electrolyte.py index ec4b8500fe..069a3c9552 100755 --- a/idaes/generic_models/properties/core/state_definitions/tests/test_FcTP_electrolyte.py +++ b/idaes/generic_models/properties/core/state_definitions/tests/test_FcTP_electrolyte.py @@ -17,12 +17,11 @@ import pytest # Import Pyomo units -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Expression, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -208,9 +207,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs, tee=True) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates assert (value(m.fs.state[1].flow_mol_phase_comp_true["Vap", "H2O"]) == @@ -454,9 +451,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -881,9 +876,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates for j in m.fs.state[1].mole_frac_comp: diff --git a/idaes/generic_models/properties/core/state_definitions/tests/test_FpcTP_electrolyte.py b/idaes/generic_models/properties/core/state_definitions/tests/test_FpcTP_electrolyte.py index 82eff853c8..f84799763b 100755 --- a/idaes/generic_models/properties/core/state_definitions/tests/test_FpcTP_electrolyte.py +++ b/idaes/generic_models/properties/core/state_definitions/tests/test_FpcTP_electrolyte.py @@ -17,12 +17,11 @@ import pytest # Import Pyomo units -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Expression, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -197,9 +196,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs, tee=True) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates assert (value(m.fs.state[1].flow_mol_phase_comp_true["Vap", "H2O"]) == @@ -434,9 +431,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check apparent species flowrates for j in m.fs.state[1].mole_frac_comp: @@ -845,9 +840,7 @@ def test_solve_for_true_species(self, frame): res = solver.solve(m.fs) # Check for optimal solution - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert check_optimal_termination(res) # Check true species flowrates for j in m.fs.state[1].mole_frac_comp: diff --git a/idaes/generic_models/properties/cubic_eos/cubic_prop_pack.py b/idaes/generic_models/properties/cubic_eos/cubic_prop_pack.py index 916833e2f6..2c9be206dd 100644 --- a/idaes/generic_models/properties/cubic_eos/cubic_prop_pack.py +++ b/idaes/generic_models/properties/cubic_eos/cubic_prop_pack.py @@ -29,7 +29,8 @@ from enum import Enum # Import Pyomo libraries -from pyomo.environ import (Constraint, +from pyomo.environ import (check_optimal_termination, + Constraint, exp, Expression, ExternalFunction, @@ -40,9 +41,7 @@ PositiveReals, value, Var, - units as pyunits, - SolverStatus, - TerminationCondition) + units as pyunits) from pyomo.common.config import ConfigValue, In # Import IDAES cores @@ -558,9 +557,7 @@ def antoine_P(b, j, T): ) # --------------------------------------------------------------------- - if (results.solver.termination_condition != - TerminationCondition.optimal or - results.solver.status != SolverStatus.ok): + if not check_optimal_termination(results): raise InitializationError( f"{blk.name} failed to initialize successfully. Please check " f"the output logs for more information.") diff --git a/idaes/generic_models/properties/cubic_eos/tests/test_BT_example.py b/idaes/generic_models/properties/cubic_eos/tests/test_BT_example.py index 2b40d0d6ab..85075b9681 100644 --- a/idaes/generic_models/properties/cubic_eos/tests/test_BT_example.py +++ b/idaes/generic_models/properties/cubic_eos/tests/test_BT_example.py @@ -18,10 +18,10 @@ from idaes.generic_models.properties.cubic_eos import BT_PR from idaes.core.util.exceptions import InitializationError -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Objective, - TerminationCondition, value) from pyomo.util.check_units import assert_units_consistent @@ -146,8 +146,7 @@ def test_T_sweep(self): results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) assert m.fs.state.flow_mol_phase["Liq"].value <= 1e-5 @pytest.mark.integration @@ -173,15 +172,13 @@ def test_P_sweep(self): results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) while m.fs.state.pressure.value <= 1e6: m.fs.state.pressure.value = m.fs.state.pressure.value + 1e5 results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) print(T, m.fs.state.pressure.value) @pytest.mark.component diff --git a/idaes/generic_models/properties/tests/test_harness.py b/idaes/generic_models/properties/tests/test_harness.py index e3521e7928..c420713544 100644 --- a/idaes/generic_models/properties/tests/test_harness.py +++ b/idaes/generic_models/properties/tests/test_harness.py @@ -138,8 +138,7 @@ def test_state_block_mole_frac_phase_comp(self, frame): "mole_frac_phase_comp is not a Pyomo Var or Expression.") err = False if len(frame.fs.props[1].mole_frac_phase_comp) != ( - len(frame.fs.params.component_list) * - len(frame.fs.params.phase_list)): + len(frame.fs.params._phase_component_set)): err = True for k in frame.fs.props[1].mole_frac_phase_comp: @@ -153,11 +152,10 @@ def test_state_block_mole_frac_phase_comp(self, frame): def test_get_material_flow_terms(self, frame): try: - for p in frame.fs.params.phase_list: - for j in frame.fs.params.component_list: - term = frame.fs.props[1].get_material_flow_terms(p, j) - # Assert that the term can be assigned a scale factor - assert isinstance(term, _scalable) + for p, j in frame.fs.params._phase_component_set: + term = frame.fs.props[1].get_material_flow_terms(p, j) + # Assert that the term can be assigned a scale factor + assert isinstance(term, _scalable) except KeyError: raise KeyError( "get_material_flow_terms method is not indexed by phase and " diff --git a/idaes/generic_models/unit_models/column_models/condenser.py b/idaes/generic_models/unit_models/column_models/condenser.py index fa7612bcd1..2e7f4c60f6 100644 --- a/idaes/generic_models/unit_models/column_models/condenser.py +++ b/idaes/generic_models/unit_models/column_models/condenser.py @@ -26,8 +26,14 @@ # Import Pyomo libraries from pyomo.common.config import ConfigBlock, ConfigValue, In from pyomo.network import Port -from pyomo.environ import \ - Reference, Expression, Var, Constraint, value, Set, SolverFactory +from pyomo.environ import ( + Reference, + Expression, + Var, + Constraint, + value, + Set, + check_optimal_termination) # Import IDAES cores import idaes.logger as idaeslog @@ -41,7 +47,7 @@ from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import PropertyPackageError, \ - ConfigurationError, PropertyNotSupportedError + ConfigurationError, PropertyNotSupportedError, InitializationError from idaes.core.util import get_solver _log = idaeslog.getLogger(__name__) @@ -747,6 +753,10 @@ def initialize(self, state_args=None, solver=None, optarg=None, init_log.info( "Initialization Complete, {}.".format(idaeslog.condition(res)) ) + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") self.control_volume.release_state(flags=flags) diff --git a/idaes/generic_models/unit_models/column_models/reboiler.py b/idaes/generic_models/unit_models/column_models/reboiler.py index 1647a25a01..5768cab5c0 100644 --- a/idaes/generic_models/unit_models/column_models/reboiler.py +++ b/idaes/generic_models/unit_models/column_models/reboiler.py @@ -25,7 +25,14 @@ # Import Pyomo libraries from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.network import Port -from pyomo.environ import Reference, Expression, Var, Constraint, value, Set +from pyomo.environ import ( + check_optimal_termination, + Reference, + Expression, + Var, + Constraint, + value, + Set) # Import IDAES cores import idaes.logger as idaeslog @@ -38,7 +45,7 @@ useDefault) from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import PropertyPackageError, \ - PropertyNotSupportedError, ConfigurationError + PropertyNotSupportedError, ConfigurationError, InitializationError from idaes.core.util import get_solver from idaes.core.util.model_statistics import degrees_of_freedom @@ -634,6 +641,11 @@ def initialize(self, state_args=None, solver=None, optarg=None, "initialization. Please ensure that the boilup_ratio " "or the outlet temperature is fixed.") + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + self.control_volume.properties_in.\ release_state(flags=flags, outlvl=outlvl) diff --git a/idaes/generic_models/unit_models/column_models/solvent_column.py b/idaes/generic_models/unit_models/column_models/solvent_column.py index ac69ce7507..5632f8fb78 100644 --- a/idaes/generic_models/unit_models/column_models/solvent_column.py +++ b/idaes/generic_models/unit_models/column_models/solvent_column.py @@ -20,8 +20,8 @@ # Import Pyomo libraries from pyomo.environ import ( - Constraint, Expression, Param, Reals, NonNegativeReals, value, Var, exp, - SolverStatus, TerminationCondition, units as pyunits) + check_optimal_termination, Constraint, Expression, Param, Reals, + NonNegativeReals, value, Var, exp, units as pyunits, SolverStatus) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool # Import IDAES Libraries @@ -768,34 +768,32 @@ def initialize(blk, hold_state=True) init_log.info("Step 2: Steady-State isothermal mass balance") - + blk.vapor_phase.properties.release_state(flags=vflag) - + blk.liquid_phase.properties.release_state(flags=lflag) - + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Step 2: {}.".format(idaeslog.condition(res))) - - assert res.solver.termination_condition == \ - TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok - + + assert check_optimal_termination(res) + # --------------------------------------------------------------------- init_log.info('Step 3: Interface equilibrium') - - # Activate interface pressure constraint - + + # Activate interface pressure constraint + blk.pressure_equil.unfix() blk.pressure_at_interface.activate() - + # ---------------------------------------------------------------------- with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Step 3 complete: {}.".format(idaeslog.condition(res))) - + # --------------------------------------------------------------------- init_log.info('Step 4: Isothermal chemical absoption') diff --git a/idaes/generic_models/unit_models/column_models/solvent_condenser.py b/idaes/generic_models/unit_models/column_models/solvent_condenser.py index 848c95b28c..5a67357521 100644 --- a/idaes/generic_models/unit_models/column_models/solvent_condenser.py +++ b/idaes/generic_models/unit_models/column_models/solvent_condenser.py @@ -26,7 +26,13 @@ __author__ = "Andrew Lee, Paul Akula" # Import Pyomo libraries -from pyomo.environ import Constraint, Param, Reference, units as pyunits, value +from pyomo.environ import ( + check_optimal_termination, + Constraint, + Param, + Reference, + units as pyunits, + value) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool # Import IDAES cores @@ -42,7 +48,7 @@ from idaes.core.util.config import is_physical_parameter_block from idaes.core.util import get_solver, scaling as iscale from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError _log = idaeslog.getIdaesLogger(__name__) @@ -439,7 +445,7 @@ def initialize(blk, liquid_state_args=None, vapor_state_args=None, # Check DOF if degrees_of_freedom(blk) != 0: - raise ConfigurationError( + raise InitializationError( f"{blk.name} degrees of freedom were not 0 at the beginning " f"of initialization. DoF = {degrees_of_freedom(blk)}") @@ -545,6 +551,12 @@ def initialize(blk, liquid_state_args=None, vapor_state_args=None, # Release Inlet state blk.vapor_phase.release_state(flags, outlvl) + # TODO : This fails in the current model + # if not check_optimal_termination(results): + # raise InitializationError( + # f"{blk.name} failed to initialize successfully. Please check " + # f"the output logs for more information.") + init_log.info('Initialization Complete: {}' .format(idaeslog.condition(results))) diff --git a/idaes/generic_models/unit_models/column_models/solvent_reboiler.py b/idaes/generic_models/unit_models/column_models/solvent_reboiler.py index 764286a278..98578846b4 100644 --- a/idaes/generic_models/unit_models/column_models/solvent_reboiler.py +++ b/idaes/generic_models/unit_models/column_models/solvent_reboiler.py @@ -26,7 +26,13 @@ __author__ = "Andrew Lee, Paul Akula" # Import Pyomo libraries -from pyomo.environ import Constraint, Param, Reference, units as pyunits, value +from pyomo.environ import ( + check_optimal_termination, + Constraint, + Param, + Reference, + units as pyunits, + value) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool # Import IDAES cores @@ -42,7 +48,7 @@ from idaes.core.util.config import is_physical_parameter_block from idaes.core.util import get_solver, scaling as iscale from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError _log = idaeslog.getIdaesLogger(__name__) @@ -438,7 +444,7 @@ def initialize(blk, liquid_state_args=None, vapor_state_args=None, # Check DOF if degrees_of_freedom(blk) != 0: - raise ConfigurationError( + raise InitializationError( f"{blk.name} degrees of freedom were not 0 at the beginning " f"of initialization. DoF = {degrees_of_freedom(blk)}") @@ -544,6 +550,11 @@ def initialize(blk, liquid_state_args=None, vapor_state_args=None, # Release Inlet state blk.liquid_phase.release_state(flags, outlvl) + if not check_optimal_termination(results): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info('Initialization Complete: {}' .format(idaeslog.condition(results))) diff --git a/idaes/generic_models/unit_models/column_models/tests/test_conventional_tray.py b/idaes/generic_models/unit_models/column_models/tests/test_conventional_tray.py index 89317f1b0f..9a6246abe3 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_conventional_tray.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_conventional_tray.py @@ -16,8 +16,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -246,16 +245,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_feed_tray.py b/idaes/generic_models/unit_models/column_models/tests/test_feed_tray.py index 1911e6c606..cd760c4424 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_feed_tray.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_feed_tray.py @@ -16,8 +16,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -269,16 +268,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_partial_condenser.py b/idaes/generic_models/unit_models/column_models/tests/test_partial_condenser.py index 80a354a622..414a98bf8c 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_partial_condenser.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_partial_condenser.py @@ -17,8 +17,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock, MaterialBalanceType, EnergyBalanceType @@ -209,16 +208,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_reboiler.py b/idaes/generic_models/unit_models/column_models/tests/test_reboiler.py index 1e743781d0..f099514e5d 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_reboiler.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_reboiler.py @@ -17,8 +17,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import \ @@ -188,16 +187,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_solvent_column.py b/idaes/generic_models/unit_models/column_models/tests/test_solvent_column.py index 49d3cebbf9..3e4c44dee6 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_solvent_column.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_solvent_column.py @@ -17,8 +17,8 @@ import pytest # Import Pyomo libraries -from pyomo.environ import ConcreteModel, value, Param, TransformationFactory,\ - SolverStatus, TerminationCondition, units as pyunits +from pyomo.environ import ConcreteModel, value, Param, TransformationFactory, \ + check_optimal_termination, units as pyunits # Import IDAES Libraries from idaes.core import FlowsheetBlock @@ -196,8 +196,7 @@ def test_solve_absorber(self, model_absorber_steady_state): results=solver.solve(model_absorber_steady_state) # Solver status and condition - assert results.solver.status == SolverStatus.ok - assert results.solver.termination_condition == TerminationCondition.optimal + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -386,8 +385,7 @@ def test_solve_stripper(self, model_stripper_steady_state): results=solver.solve(model_stripper_steady_state) # Solver status and condition - assert results.solver.status == SolverStatus.ok - assert results.solver.termination_condition == TerminationCondition.optimal + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_solvent_condenser.py b/idaes/generic_models/unit_models/column_models/tests/test_solvent_condenser.py index e519899586..1b06b1a090 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_solvent_condenser.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_solvent_condenser.py @@ -16,11 +16,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Param, - TerminationCondition, - SolverStatus, units, value) from pyomo.util.check_units import (assert_units_consistent, @@ -42,6 +41,7 @@ import configuration as aqueous_mea from idaes.power_generation.carbon_capture.mea_solvent_system.properties.MEA_vapor \ import wet_co2 +from idaes.core.util.exceptions import InitializationError # ----------------------------------------------------------------------------- @@ -70,13 +70,29 @@ def model(self): m.fs.unit.inlet.mole_frac_comp[0, "H2O"].fix(0.1183) m.fs.unit.reflux.flow_mol[0].fix(0.1083) - + + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.mass_transfer_term[0, "Vap", "CO2"], 1e4) + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.mass_transfer_term[0, "Vap", "H2O"], 10) + + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.properties_out[0].pressure, 1e-5) iscale.set_scaling_factor( m.fs.unit.vapor_phase.properties_out[0].fug_phase_comp[ "Vap", "CO2"], 1e-5) iscale.set_scaling_factor( m.fs.unit.vapor_phase.properties_out[0].fug_phase_comp[ "Vap", "H2O"], 1e-3) + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.properties_out[0].temperature, 1e-2) + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.properties_out[0].enth_mol_phase["Vap"], + 1e-3) + + iscale.set_scaling_factor( + m.fs.unit.vapor_phase.enthalpy_transfer[0], + 1e-3) iscale.calculate_scaling_factors(m.fs.unit) @@ -132,16 +148,14 @@ def test_dof(self, model): def test_initialize(self, model): initialization_tester(model) - # @pytest.mark.solver + @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -290,9 +304,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -390,3 +402,10 @@ def test_scaling(self, model): assert iscale.get_constraint_transform_applied_scaling_factor( model.fs.unit.unit_pressure_balance[0]) == 1e-5 + + @pytest.mark.component + def test_initialization_error_dof(self, model): + model.fs.unit.reflux.flow_mol[0].fix(100) + + with pytest.raises(InitializationError): + model.fs.unit.initialize() diff --git a/idaes/generic_models/unit_models/column_models/tests/test_solvent_reboiler.py b/idaes/generic_models/unit_models/column_models/tests/test_solvent_reboiler.py index 5c9228cf54..36f111736c 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_solvent_reboiler.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_solvent_reboiler.py @@ -16,11 +16,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Param, - TerminationCondition, - SolverStatus, units, value) from pyomo.util.check_units import (assert_units_consistent, @@ -42,6 +41,7 @@ import configuration as aqueous_mea from idaes.power_generation.carbon_capture.mea_solvent_system.properties.MEA_vapor \ import flue_gas, wet_co2 +from idaes.core.util.exceptions import InitializationError # ----------------------------------------------------------------------------- @@ -138,9 +138,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -303,9 +301,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -413,3 +409,10 @@ def test_scaling(self, model): assert iscale.get_constraint_transform_applied_scaling_factor( model.fs.unit.unit_pressure_balance[0]) == 1e-5 + + @pytest.mark.component + def test_initialization_error_dof(self, model): + model.fs.unit.bottoms.flow_mol[0].fix(100) + + with pytest.raises(InitializationError): + model.fs.unit.initialize() diff --git a/idaes/generic_models/unit_models/column_models/tests/test_total_condenser.py b/idaes/generic_models/unit_models/column_models/tests/test_total_condenser.py index 77fa1228c8..e232a0baa2 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_total_condenser.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_total_condenser.py @@ -17,8 +17,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock, MaterialBalanceType, EnergyBalanceType @@ -194,16 +193,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_tray_column.py b/idaes/generic_models/unit_models/column_models/tests/test_tray_column.py index 03dd19b05f..5d21627021 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_tray_column.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_tray_column.py @@ -16,8 +16,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -29,7 +28,7 @@ from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.testing import \ PhysicalParameterTestBlock, initialization_tester -from idaes.core.util import get_solver +from idaes.core.util import get_solver, scaling as iscale from idaes.generic_models.properties.core.generic.generic_property \ import GenericParameterBlock @@ -185,15 +184,11 @@ def test_initialize(self, btx_ftpz, btx_fctp): def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -290,6 +285,8 @@ def btx_ftpz_generic(self): m.fs.unit.reboiler.boilup_ratio.fix(1.3) + iscale.calculate_scaling_factors(m.fs.unit) + return m @pytest.mark.unit @@ -324,9 +321,7 @@ def test_initialize(self, btx_ftpz_generic): def test_solve(self, btx_ftpz_generic): results = solver.solve(btx_ftpz_generic) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_tray_side_liq_draw.py b/idaes/generic_models/unit_models/column_models/tests/test_tray_side_liq_draw.py index 13d4db9fc6..09595c1788 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_tray_side_liq_draw.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_tray_side_liq_draw.py @@ -16,8 +16,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import ( - ConcreteModel, TerminationCondition, SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -247,16 +246,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tests/test_tray_side_vap_draw.py b/idaes/generic_models/unit_models/column_models/tests/test_tray_side_vap_draw.py index cfc4b8dae7..ef464691c0 100644 --- a/idaes/generic_models/unit_models/column_models/tests/test_tray_side_vap_draw.py +++ b/idaes/generic_models/unit_models/column_models/tests/test_tray_side_vap_draw.py @@ -16,8 +16,7 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import ( - ConcreteModel, TerminationCondition, SolverStatus, value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -248,16 +247,12 @@ def test_solve(self, btx_ftpz, btx_fctp): results = solver.solve(btx_ftpz) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) results = solver.solve(btx_fctp) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/column_models/tray.py b/idaes/generic_models/unit_models/column_models/tray.py index 1989985a9e..aaf43b6f1e 100644 --- a/idaes/generic_models/unit_models/column_models/tray.py +++ b/idaes/generic_models/unit_models/column_models/tray.py @@ -29,7 +29,8 @@ # Import Pyomo libraries from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.network import Port -from pyomo.environ import Reference, Expression, Var, Set, value +from pyomo.environ import ( + Reference, Expression, Var, Set, value, check_optimal_termination) # Import IDAES cores from idaes.core import (declare_process_block_class, @@ -37,7 +38,7 @@ useDefault) from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.exceptions import ConfigurationError, \ - PropertyPackageError, PropertyNotSupportedError + PropertyPackageError, PropertyNotSupportedError, InitializationError from idaes.core.util import get_solver from idaes.core.util.model_statistics import degrees_of_freedom @@ -904,6 +905,11 @@ def initialize(self, state_args_feed=None, state_args_liq=None, "for tray block is not zero during " "initialization.") + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info( "Initialization complete, status {}.". format(idaeslog.condition(res))) diff --git a/idaes/generic_models/unit_models/column_models/tray_column.py b/idaes/generic_models/unit_models/column_models/tray_column.py index 431aa39cf9..410af37e37 100644 --- a/idaes/generic_models/unit_models/column_models/tray_column.py +++ b/idaes/generic_models/unit_models/column_models/tray_column.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool from pyomo.network import Arc, Port from pyomo.environ import value, Integers, RangeSet, TransformationFactory, \ - Block, Reference + Block, Reference, check_optimal_termination # Import IDAES cores from idaes.generic_models.unit_models.column_models import Tray, Condenser, \ @@ -32,7 +32,7 @@ from idaes.core import (declare_process_block_class, UnitModelBlockData, useDefault) -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError from idaes.core.util.config import is_physical_parameter_block from idaes.core.util import get_solver @@ -553,3 +553,9 @@ def initialize(self, state_args_feed=None, state_args_liq=None, # release feed tray state once initialization is complete self.feed_tray.properties_in_feed.\ release_state(flags=feed_flags, outlvl=outlvl) + + # TODO : This fails in the current model + # if not check_optimal_termination(res): + # raise InitializationError( + # f"{self.name} failed to initialize successfully. Please check " + # f"the output logs for more information.") diff --git a/idaes/generic_models/unit_models/heat_exchanger.py b/idaes/generic_models/unit_models/heat_exchanger.py index 525cce0c70..f2435afeae 100644 --- a/idaes/generic_models/unit_models/heat_exchanger.py +++ b/idaes/generic_models/unit_models/heat_exchanger.py @@ -21,17 +21,13 @@ # Import Pyomo libraries from pyomo.environ import ( Var, - Param, - Expression, log, Reference, PositiveReals, - SolverFactory, ExternalFunction, Block, units as pyunits, - NonNegativeReals, - value, + check_optimal_termination ) from pyomo.common.config import ConfigBlock, ConfigValue, In @@ -52,7 +48,7 @@ import idaes.core.util.unit_costing as costing from idaes.core.util.misc import add_object_reference from idaes.core.util import get_solver, scaling as iscale -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError _log = idaeslog.getLogger(__name__) @@ -236,7 +232,7 @@ def delta_temperature_underwood_callback(b): """ dT1 = b.delta_temperature_in dT2 = b.delta_temperature_out - temp_units = pyunits.get_units(dT1) + temp_units = pyunits.get_units(dT1[dT1.index_set().first()]) # external function that ruturns the real root, for the cuberoot of negitive # numbers, so it will return without error for positive and negitive dT. @@ -615,6 +611,11 @@ def initialize( self.costing.activate() costing.initialize(self.costing) + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + def _get_performance_contents(self, time_point=0): var_dict = { "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] diff --git a/idaes/generic_models/unit_models/heat_exchanger_1D.py b/idaes/generic_models/unit_models/heat_exchanger_1D.py index 33f4c28659..046fa98327 100644 --- a/idaes/generic_models/unit_models/heat_exchanger_1D.py +++ b/idaes/generic_models/unit_models/heat_exchanger_1D.py @@ -21,6 +21,7 @@ # Import Pyomo libraries from pyomo.environ import ( Var, + check_optimal_termination, Constraint, value, units as pyunits @@ -42,7 +43,7 @@ import HeatExchangerFlowPattern from idaes.core.util.config import is_physical_parameter_block, DefaultBool from idaes.core.util.misc import add_object_reference -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.constants import Constants as c from idaes.core.util import get_solver, scaling as iscale @@ -673,7 +674,7 @@ def initialize( # Solve unit # Wall 0D if self.config.has_wall_conduction == \ - WallConductionType.zero_dimensional: + WallConductionType.zero_dimensional: shell_units = self.config.shell_side.property_package.\ get_metadata().get_derived_units for t in self.flowsheet().time: @@ -717,10 +718,17 @@ def initialize( init_log.info_high( "Initialization Step 4 {}.".format(idaeslog.condition(res)) ) + else: + res = None self.shell.release_state(flags_shell) self.tube.release_state(flags_tube) + if res is not None and not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info("Initialization Complete.") def _get_performance_contents(self, time_point=0): diff --git a/idaes/generic_models/unit_models/heat_exchanger_ntu.py b/idaes/generic_models/unit_models/heat_exchanger_ntu.py index c1226abe13..3143fc01ac 100644 --- a/idaes/generic_models/unit_models/heat_exchanger_ntu.py +++ b/idaes/generic_models/unit_models/heat_exchanger_ntu.py @@ -20,6 +20,7 @@ # Import Pyomo libraries from pyomo.environ import (Block, + check_optimal_termination, Constraint, Expression, Param, @@ -41,6 +42,7 @@ from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.math import smooth_min, smooth_max from idaes.core.util import get_solver +from idaes.core.util.exceptions import InitializationError import idaes.core.util.unit_costing as costing import idaes.logger as idaeslog @@ -422,6 +424,11 @@ def initialize( self.costing.activate() costing.initialize(self.costing) + if not check_optimal_termination(res): + raise InitializationError( + f"{self.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { diff --git a/idaes/generic_models/unit_models/mixer.py b/idaes/generic_models/unit_models/mixer.py index d2aac5d2cc..27bc334ace 100644 --- a/idaes/generic_models/unit_models/mixer.py +++ b/idaes/generic_models/unit_models/mixer.py @@ -16,7 +16,7 @@ from enum import Enum from pyomo.environ import ( - Constraint, + check_optimal_termination, Param, PositiveReals, Reals, @@ -40,6 +40,7 @@ BurntToast, ConfigurationError, PropertyNotSupportedError, + InitializationError, ) from idaes.core.util.math import smooth_min from idaes.core.util.tables import create_stream_table_dataframe @@ -909,6 +910,7 @@ def initialize(blk, outlvl=idaeslog.NOTSET, optarg=None, hold_state=False, ) + res = None # Revert fixed status of variables to what they were before for t in blk.flowsheet().time: s_vars = mblock[t].define_state_vars() @@ -942,6 +944,11 @@ def initialize(blk, outlvl=idaeslog.NOTSET, optarg=None, else: init_log.info("Initialization Complete.") + if res is not None and not check_optimal_termination(res): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + if hold_state is True: return flags else: diff --git a/idaes/generic_models/unit_models/pressure_changer.py b/idaes/generic_models/unit_models/pressure_changer.py index 2400c84770..2b5164545b 100644 --- a/idaes/generic_models/unit_models/pressure_changer.py +++ b/idaes/generic_models/unit_models/pressure_changer.py @@ -19,7 +19,14 @@ from enum import Enum # Import Pyomo libraries -from pyomo.environ import value, Var, Block, Expression, Constraint, Reference +from pyomo.environ import ( + value, + Var, + Block, + Expression, + Constraint, + Reference, + check_optimal_termination) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool # Import IDAES cores @@ -33,7 +40,8 @@ UnitModelBlockData, useDefault, ) -from idaes.core.util.exceptions import PropertyNotSupportedError +from idaes.core.util.exceptions import ( + PropertyNotSupportedError, InitializationError) from idaes.core.util.config import is_physical_parameter_block import idaes.logger as idaeslog import idaes.core.util.unit_costing as costing @@ -69,8 +77,8 @@ class IsentropicPerformanceCurveData(ProcessBlockData): CONFIG.declare("build_head_expressions", ConfigValue( default=True, domain=bool, - doc="If true add expressions for 'head' and 'head_isentropic'." - " These expressions can be used in performance curve constraints.")) + doc="If true add expressions for 'head' and 'head_isentropic'. " + "These expressions can be used in performance curve constraints.")) def has_constraints(self): for o in self.component_data_objects(Constraint): @@ -82,31 +90,31 @@ def build(self): if self.config.build_head_expressions: try: @self.Expression(self.flowsheet().time) - def head_isentropic(b, t): # units are energy/mass + def head_isentropic(b, t): # units are energy/mass b = b.parent_block() if hasattr(b.control_volume.properties_in[t], "flow_mass"): return (b.work_isentropic[t] / - b.control_volume.properties_in[t].flow_mass) + b.control_volume.properties_in[t].flow_mass) else: return (b.work_isentropic[t] / - b.control_volume.properties_in[t].flow_mol / - b.control_volume.properties_in[t].mw) + b.control_volume.properties_in[t].flow_mol / + b.control_volume.properties_in[t].mw) @self.Expression(self.flowsheet().time) - def head(b, t): # units are energy/mass + def head(b, t): # units are energy/mass b = b.parent_block() if hasattr(b.control_volume.properties_in[t], "flow_mass"): return (b.work_mechanical[t] / - b.control_volume.properties_in[t].flow_mass) + b.control_volume.properties_in[t].flow_mass) else: return (b.work_mechanical[t] / - b.control_volume.properties_in[t].flow_mol / - b.control_volume.properties_in[t].mw) + b.control_volume.properties_in[t].flow_mol / + b.control_volume.properties_in[t].mw) except PropertyNotSupportedError: _log.exception( - "flow_mass or flow_mol and mw are not supported by the" - " property package but are required for isentropic pressure" + "flow_mass or flow_mol and mw are not supported by the " + "property package but are required for isentropic pressure" " changer head calculation") raise @@ -778,21 +786,22 @@ def init_adiabatic(blk, state_args, outlvl, solver, optarg): ) init_log.info_high("Initialization Step 1 Complete.") - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - init_log.info_high("Initialization Step 2 {}." - .format(idaeslog.condition(res))) - # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) - init_log.info_high("Initialization Step 3 {}." + init_log.info_high("Initialization Step 2 {}." .format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl) + + if not check_optimal_termination(res): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info(f"Initialization Complete: {idaeslog.condition(res)}") def init_isentropic(blk, state_args, outlvl, solver, optarg): @@ -838,21 +847,21 @@ def init_isentropic(blk, state_args, outlvl, solver, optarg): unfix_eff = {} unfix_ratioP = {} for t in blk.flowsheet().time: - if not (blk.ratioP[t].fixed or blk.deltaP[t].fixed or - cv.properties_out[t].pressure.fixed): + if not (blk.ratioP[t].fixed or blk.deltaP[t].fixed or + cv.properties_out[t].pressure.fixed): if blk.config.compressor: if not (value(blk.ratioP[t]) >= 1.01 and - value(blk.ratioP[t]) <= 50): + value(blk.ratioP[t]) <= 50): blk.ratioP[t] = 1.8 else: if not (value(blk.ratioP[t]) >= 0.01 and - value(blk.ratioP[t]) <= 0.999): + value(blk.ratioP[t]) <= 0.999): blk.ratioP[t] = 0.7 blk.ratioP[t].fix() unfix_ratioP[t] = True if not blk.efficiency_isentropic[t].fixed: if not (value(blk.efficiency_isentropic[t]) >= 0.05 and - value(blk.efficiency_isentropic[t]) <= 1.0): + value(blk.efficiency_isentropic[t]) <= 1.0): blk.efficiency_isentropic[t] = 0.8 blk.efficiency_isentropic[t].fix() unfix_eff[t] = True @@ -989,11 +998,18 @@ def tmp_rule(b, t): blk.ratioP[t].unfix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) - init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.") + init_log.info_high( + f"Initialization Step 5 {idaeslog.condition(res)}.") # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl) + + if not check_optimal_termination(res): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please check " + f"the output logs for more information.") + init_log.info(f"Initialization Complete: {idaeslog.condition(res)}") def _get_performance_contents(self, time_point=0): diff --git a/idaes/generic_models/unit_models/separator.py b/idaes/generic_models/unit_models/separator.py index f9f38daf0e..87d2443e42 100644 --- a/idaes/generic_models/unit_models/separator.py +++ b/idaes/generic_models/unit_models/separator.py @@ -19,6 +19,7 @@ from pyomo.environ import ( Block, + check_optimal_termination, Constraint, Param, Reals, @@ -35,7 +36,7 @@ UnitModelBlockData, useDefault, MaterialBalanceType, - MaterialFlowBasis + MaterialFlowBasis, ) from idaes.core.util.config import ( is_physical_parameter_block, @@ -45,6 +46,7 @@ BurntToast, ConfigurationError, PropertyNotSupportedError, + InitializationError, ) from idaes.core.util import get_solver from idaes.core.util.tables import create_stream_table_dataframe @@ -1574,6 +1576,12 @@ def initialize( if blk.config.mixed_state_block is None: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) + + if not check_optimal_termination(res): + raise InitializationError( + f"{blk.name} failed to initialize successfully. Please " + f"check the output logs for more information.") + init_log.info( "Initialization Step 2 Complete: {}" .format(idaeslog.condition(res)) diff --git a/idaes/generic_models/unit_models/tests/test_cstr.py b/idaes/generic_models/unit_models/tests/test_cstr.py index 6e97ae73a1..bf56d29f8e 100644 --- a/idaes/generic_models/unit_models/tests/test_cstr.py +++ b/idaes/generic_models/unit_models/tests/test_cstr.py @@ -16,9 +16,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, units, value, Var) @@ -164,9 +163,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -222,9 +219,7 @@ def test_costing(self, sapon): sapon.fs.unit.diameter.fix(2) results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (pytest.approx(29790.11975, abs=1e3) == value(sapon.fs.unit.costing.base_cost)) assert (pytest.approx(40012.2523, abs=1e3) == diff --git a/idaes/generic_models/unit_models/tests/test_equilibrium_reactor.py b/idaes/generic_models/unit_models/tests/test_equilibrium_reactor.py index 545f594ead..25929a071b 100644 --- a/idaes/generic_models/unit_models/tests/test_equilibrium_reactor.py +++ b/idaes/generic_models/unit_models/tests/test_equilibrium_reactor.py @@ -16,9 +16,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, units) from idaes.core import (FlowsheetBlock, @@ -42,6 +41,7 @@ from idaes.core.util import get_solver from pyomo.util.check_units import (assert_units_consistent, assert_units_equivalent) +from idaes.core.util.exceptions import InitializationError # ----------------------------------------------------------------------------- @@ -166,9 +166,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -208,3 +206,10 @@ def test_conservation(self, sapon): @pytest.mark.unit def test_report(self, sapon): sapon.fs.unit.report() + + @pytest.mark.component + def test_initialization_error(self, sapon): + sapon.fs.unit.outlet.pressure[0].fix(1) + + with pytest.raises(InitializationError): + sapon.fs.unit.initialize() diff --git a/idaes/generic_models/unit_models/tests/test_feed_flash.py b/idaes/generic_models/unit_models/tests/test_feed_flash.py index f1208a7cb3..8b96210695 100644 --- a/idaes/generic_models/unit_models/tests/test_feed_flash.py +++ b/idaes/generic_models/unit_models/tests/test_feed_flash.py @@ -16,10 +16,7 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from idaes.core import FlowsheetBlock, MaterialBalanceType from idaes.generic_models.unit_models.feed_flash import FeedFlash, FlashType from idaes.generic_models.properties import iapws95 @@ -127,9 +124,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -212,9 +207,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/generic_models/unit_models/tests/test_flash.py b/idaes/generic_models/unit_models/tests/test_flash.py index 7af95f8d36..4d7357f564 100644 --- a/idaes/generic_models/unit_models/tests/test_flash.py +++ b/idaes/generic_models/unit_models/tests/test_flash.py @@ -15,9 +15,8 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, units, Var) @@ -162,9 +161,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -280,9 +277,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -346,9 +341,7 @@ def test_costing(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (pytest.approx(63787.06525, abs=1e3) == value(iapws.fs.unit.costing.base_cost)) assert (pytest.approx(97660.6169, abs=1e3) == diff --git a/idaes/generic_models/unit_models/tests/test_gibbs.py b/idaes/generic_models/unit_models/tests/test_gibbs.py index a968f53e59..71f5addca0 100644 --- a/idaes/generic_models/unit_models/tests/test_gibbs.py +++ b/idaes/generic_models/unit_models/tests/test_gibbs.py @@ -17,10 +17,9 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, - SolverStatus, value, units) from pyomo.util.check_units import (assert_units_consistent, @@ -247,9 +246,7 @@ def test_solve_temperature(self, methane): results = solver.solve(methane) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -339,9 +336,7 @@ def test_solve_heat_duty(self, methane): results = solver.solve(methane, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/generic_models/unit_models/tests/test_heat_exchanger.py b/idaes/generic_models/unit_models/tests/test_heat_exchanger.py index 25f9762648..6a793e0114 100644 --- a/idaes/generic_models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/generic_models/unit_models/tests/test_heat_exchanger.py @@ -17,11 +17,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Expression, - TerminationCondition, - SolverStatus, value, Var, units as pyunits) @@ -59,7 +58,7 @@ from idaes.core.util.testing import (PhysicalParameterTestBlock, initialization_tester) from idaes.core.util import get_solver -from pyomo.util.calc_var_value import calculate_variable_from_constraint +from idaes.core.util.exceptions import InitializationError # Imports to assemble BT-PR with different units @@ -187,8 +186,7 @@ def test_costing(): results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) # Check Solution @@ -374,9 +372,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -525,9 +521,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -710,9 +704,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -727,9 +719,7 @@ def test_solve_underwood(self, iapws_underwood): results = solver.solve(iapws_underwood) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -883,9 +873,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1140,9 +1128,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1187,3 +1173,10 @@ def test_conservation(self, btx): @pytest.mark.unit def test_report(self, btx): btx.fs.unit.report() + + @pytest.mark.component + def test_initialization_error(self, btx): + btx.fs.unit.outlet_1.flow_mol[0].fix(20) + + with pytest.raises(InitializationError): + btx.fs.unit.initialize() diff --git a/idaes/generic_models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/generic_models/unit_models/tests/test_heat_exchanger_1D.py index c9a51b81a8..bcc646de2e 100644 --- a/idaes/generic_models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/generic_models/unit_models/tests/test_heat_exchanger_1D.py @@ -16,12 +16,15 @@ Author: Jaffer Ghouse """ import pytest -from pyomo.environ import (ConcreteModel, TerminationCondition, - SolverStatus, value, units as pyunits) +from pyomo.environ import (check_optimal_termination, + ConcreteModel, + value, + units as pyunits) from pyomo.common.config import ConfigBlock from pyomo.util.check_units import (assert_units_consistent, assert_units_equivalent) +import idaes from idaes.core import (FlowsheetBlock, MaterialBalanceType, EnergyBalanceType, MomentumBalanceType, useDefault) from idaes.generic_models.unit_models.heat_exchanger_1D import HeatExchanger1D as HX1D @@ -38,16 +41,14 @@ from idaes.generic_models.properties.examples.saponification_thermo import ( SaponificationParameterBlock) -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import ConfigurationError, InitializationError from idaes.core.util.model_statistics import (degrees_of_freedom, number_variables, number_total_constraints, number_unused_variables) from idaes.core.util.testing import (PhysicalParameterTestBlock, initialization_tester) -from idaes.core.util import get_solver -from idaes.core.util import scaling as iscale - +from idaes.core.util import get_solver, scaling as iscale # Imports to assemble BT-PR with different units from idaes.core import LiquidPhase, VaporPhase, Component @@ -270,9 +271,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -350,6 +349,8 @@ def btx(self): m.fs.unit.tube_inlet.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.tube_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + iscale.calculate_scaling_factors(m.fs.unit) + return m @pytest.mark.unit @@ -446,9 +447,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -608,9 +607,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -771,9 +768,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -942,9 +937,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -1132,9 +1125,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -1405,9 +1396,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.integration @@ -1449,3 +1438,11 @@ def test_conservation(self, btx): @pytest.mark.component def test_report(self, btx): btx.fs.unit.report() + + @pytest.mark.component + def test_initialization_error(self, btx): + btx.fs.unit.shell_outlet.flow_mol[0].fix(20) + + with idaes.temporary_config_ctx(): + with pytest.raises(InitializationError): + btx.fs.unit.initialize(optarg={"max_iter": 1}) diff --git a/idaes/generic_models/unit_models/tests/test_heater.py b/idaes/generic_models/unit_models/tests/test_heater.py index 0c167315c5..eb16c742ac 100644 --- a/idaes/generic_models/unit_models/tests/test_heater.py +++ b/idaes/generic_models/unit_models/tests/test_heater.py @@ -17,9 +17,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - SolverStatus, - TerminationCondition, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, units as pyunits) from pyomo.util.check_units import (assert_units_consistent, @@ -152,9 +151,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -261,9 +258,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -331,9 +326,7 @@ def test_verify(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert Tin == pytest.approx(value(prop_in.temperature), rel=1e-3) assert Tout == pytest.approx(value(prop_out.temperature), rel=1e-3) @@ -411,9 +404,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -532,9 +523,7 @@ def test_solve(self, btg): results = solver.solve(btg) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/generic_models/unit_models/tests/test_hx_ntu.py b/idaes/generic_models/unit_models/tests/test_hx_ntu.py index 367545b2f6..234b9ed08d 100644 --- a/idaes/generic_models/unit_models/tests/test_hx_ntu.py +++ b/idaes/generic_models/unit_models/tests/test_hx_ntu.py @@ -16,12 +16,11 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Expression, Param, - TerminationCondition, - SolverStatus, units as pyunits, value, Var) @@ -39,6 +38,7 @@ from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util import get_solver from idaes.core.util.testing import initialization_tester +from idaes.core.util.exceptions import InitializationError # ----------------------------------------------------------------------------- @@ -171,9 +171,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -260,3 +258,10 @@ def test_conservation(self, model): @pytest.mark.unit def test_report(self, model): model.fs.unit.report() + + @pytest.mark.component + def test_initialization_error(self, model): + model.fs.unit.hot_outlet.pressure[0].fix(1) + + with pytest.raises(InitializationError): + model.fs.unit.initialize() diff --git a/idaes/generic_models/unit_models/tests/test_mixer.py b/idaes/generic_models/unit_models/tests/test_mixer.py index 41d790dd1f..00befdbe6b 100644 --- a/idaes/generic_models/unit_models/tests/test_mixer.py +++ b/idaes/generic_models/unit_models/tests/test_mixer.py @@ -17,14 +17,13 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, Param, RangeSet, Set, Var, - TerminationCondition, - SolverStatus, value, units as pyunits) from pyomo.util.check_units import assert_units_consistent @@ -53,6 +52,7 @@ MomentumMixingType) from idaes.core.util.exceptions import (BurntToast, ConfigurationError, + InitializationError, PropertyNotSupportedError) from idaes.core.util.model_statistics import (degrees_of_freedom, number_variables, @@ -784,9 +784,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1020,9 +1018,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1138,9 +1134,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1204,3 +1198,19 @@ def test_construction_component_not_in_phase(): "inlet_list":["in1", "in2"], "momentum_mixing_type":MomentumMixingType.none}) iscale.calculate_scaling_factors(m) + + +@pytest.mark.unit +def test_initialization_error(): + m = ConcreteModel() + m.fs = FlowsheetBlock(default={"dynamic": False}) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.mix = Mixer(default={"property_package": m.fs.pp}) + + m.fs.mix.inlet_1_state[0].material_flow_mol.fix(10) + m.fs.mix.inlet_2_state[0].material_flow_mol.fix(10) + m.fs.mix.mixed_state[0].material_flow_mol.fix(100) + + with pytest.raises(InitializationError): + m.fs.mix.initialize() diff --git a/idaes/generic_models/unit_models/tests/test_pfr.py b/idaes/generic_models/unit_models/tests/test_pfr.py index 4d05a15a56..91a5881587 100644 --- a/idaes/generic_models/unit_models/tests/test_pfr.py +++ b/idaes/generic_models/unit_models/tests/test_pfr.py @@ -17,9 +17,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, Var, units) @@ -180,9 +179,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -243,9 +240,7 @@ def test_costing(self, sapon): assert isinstance(sapon.fs.unit.costing.purchase_cost, Var) results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (pytest.approx(9869.51230, abs=1e3) == value(sapon.fs.unit.costing.base_cost)) assert (pytest.approx(16025.42623, abs=1e3) == diff --git a/idaes/generic_models/unit_models/tests/test_pressure_changer.py b/idaes/generic_models/unit_models/tests/test_pressure_changer.py index d15e40a62d..dc42feea23 100644 --- a/idaes/generic_models/unit_models/tests/test_pressure_changer.py +++ b/idaes/generic_models/unit_models/tests/test_pressure_changer.py @@ -17,10 +17,9 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, - SolverStatus, units, value, Var) @@ -54,7 +53,8 @@ number_unused_variables) from idaes.core.util.testing import (PhysicalParameterTestBlock, initialization_tester) -from idaes.core.util.exceptions import BalanceTypeNotSupportedError +from idaes.core.util.exceptions import ( + BalanceTypeNotSupportedError, InitializationError) from idaes.core.util import get_solver, scaling as iscale @@ -280,9 +280,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -413,9 +411,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -503,9 +499,7 @@ def test_verify(self, iapws_turb): iapws.fs.unit.initialize(optarg={'tol': 1e-6}) results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) Tout = pytest.approx(cases["Tout"][i], rel=1e-2) Pout = pytest.approx(cases["Pout"][i]*1000, rel=1e-2) @@ -528,6 +522,13 @@ def test_verify(self, iapws_turb): def test_report(self, iapws): iapws.fs.unit.report() + @pytest.mark.component + def test_initialization_error(self, iapws): + iapws.fs.unit.outlet.flow_mol[0].fix(500) + + with pytest.raises(InitializationError): + iapws.fs.unit.initialize() + # ----------------------------------------------------------------------------- class TestSaponification(object): @@ -610,9 +611,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -815,9 +814,7 @@ def test_compressor(self): m.fs.unit.initialize() results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert value(m.fs.unit.control_volume.work[0]) == \ pytest.approx(101410.4, rel=1e-5) diff --git a/idaes/generic_models/unit_models/tests/test_product.py b/idaes/generic_models/unit_models/tests/test_product.py index 524371c372..adf481961a 100644 --- a/idaes/generic_models/unit_models/tests/test_product.py +++ b/idaes/generic_models/unit_models/tests/test_product.py @@ -16,10 +16,7 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -187,9 +184,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/generic_models/unit_models/tests/test_rstoic.py b/idaes/generic_models/unit_models/tests/test_rstoic.py index 32de7cc522..5380f89cf6 100644 --- a/idaes/generic_models/unit_models/tests/test_rstoic.py +++ b/idaes/generic_models/unit_models/tests/test_rstoic.py @@ -17,9 +17,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, units, Var) @@ -168,9 +167,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -227,9 +224,7 @@ def test_costing(self, sapon): sapon.fs.unit.length.fix(3) results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (pytest.approx(56327.5803, abs=1e3) == value(sapon.fs.unit.costing.base_cost)) assert (pytest.approx(85432.06008, abs=1e3) == diff --git a/idaes/generic_models/unit_models/tests/test_separator.py b/idaes/generic_models/unit_models/tests/test_separator.py index ae2dc55902..d54bddea95 100644 --- a/idaes/generic_models/unit_models/tests/test_separator.py +++ b/idaes/generic_models/unit_models/tests/test_separator.py @@ -17,11 +17,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Constraint, - TerminationCondition, Set, - SolverStatus, value, Var, units as pyunits) @@ -43,7 +42,8 @@ SplittingType, EnergySplittingType) from idaes.core.util.exceptions import (BurntToast, - ConfigurationError) + ConfigurationError, + InitializationError) from idaes.generic_models.properties.examples.saponification_thermo import ( SaponificationParameterBlock) @@ -871,9 +871,7 @@ def test_solve(self, sapon): results = solver.solve(sapon) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1090,9 +1088,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -1246,9 +1242,7 @@ def test_solve(self, iapws): results = solver.solve(iapws) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -2912,9 +2906,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") @@ -2972,3 +2964,21 @@ def test_conservation(self, btx): @pytest.mark.unit def test_report(self, btx): btx.fs.unit.report() + + +@pytest.mark.unit +def test_initialization_error(): + m = ConcreteModel() + m.fs = FlowsheetBlock(default={"dynamic": False}) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.sep = Separator(default={"property_package": m.fs.pp}) + + m.fs.sep.outlet_1_state[0].material_flow_mol.fix(10) + m.fs.sep.outlet_2_state[0].material_flow_mol.fix(10) + m.fs.sep.mixed_state[0].material_flow_mol.fix(100) + + m.fs.sep.split_fraction.fix() + + with pytest.raises(InitializationError): + m.fs.sep.initialize() diff --git a/idaes/generic_models/unit_models/tests/test_skeleton_unit_model.py b/idaes/generic_models/unit_models/tests/test_skeleton_unit_model.py index e5415083cf..dd3de1e88b 100644 --- a/idaes/generic_models/unit_models/tests/test_skeleton_unit_model.py +++ b/idaes/generic_models/unit_models/tests/test_skeleton_unit_model.py @@ -15,8 +15,8 @@ """ import pytest -from pyomo.environ import ConcreteModel, Constraint, Var, Set, value, \ - SolverStatus, TerminationCondition +from pyomo.environ import ( + check_optimal_termination, ConcreteModel, Constraint, Var, Set, value) from idaes.core import FlowsheetBlock from idaes.generic_models.unit_models import SkeletonUnitModel from idaes.core.util.model_statistics import degrees_of_freedom @@ -149,9 +149,7 @@ def test_default_initialize(self, skeleton_default): def test_solve(self, skeleton_default): results = solver.solve(skeleton_default) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -311,9 +309,7 @@ def my_initialize(unit, **kwargs): def test_solve(self, skeleton_custom): results = solver.solve(skeleton_custom) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/generic_models/unit_models/tests/test_statejunction.py b/idaes/generic_models/unit_models/tests/test_statejunction.py index acf7425d54..c5bf54a662 100644 --- a/idaes/generic_models/unit_models/tests/test_statejunction.py +++ b/idaes/generic_models/unit_models/tests/test_statejunction.py @@ -17,10 +17,7 @@ import pytest -from pyomo.environ import (ConcreteModel, - SolverStatus, - TerminationCondition, - value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -197,9 +194,7 @@ def test_solve(self, btx): results = solver.solve(btx) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/generic_models/unit_models/tests/test_valve.py b/idaes/generic_models/unit_models/tests/test_valve.py index 06a0614780..aa03af82ea 100644 --- a/idaes/generic_models/unit_models/tests/test_valve.py +++ b/idaes/generic_models/unit_models/tests/test_valve.py @@ -15,9 +15,8 @@ """ import math import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, units, value) from idaes.core import FlowsheetBlock @@ -48,13 +47,13 @@ def valve_model(self): m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = iapws95.Iapws95ParameterBlock() m.fs.valve = Valve(default={ - "valve_function_callback":self.type, + "valve_function_callback": self.type, "property_package": m.fs.properties}) - fin = 900 # mol/s - pin = 200000 # Pa - pout = 100000 # Pa - tin = 300 # K - hin = iapws95.htpx(T=tin*units.K, P=pin*units.Pa) # J/mol + fin = 1000 # mol/s + pin = 200000 # Pa + pout = 100000 # Pa + tin = 300 # K + hin = iapws95.htpx(T=tin*units.K, P=pin*units.Pa) # J/mol # Calculate the flow coefficient to give 1000 mol/s flow with given P if self.type == ValveFunctionType.linear: cv = 1000/math.sqrt(pin - pout)/0.5 @@ -65,9 +64,8 @@ def valve_model(self): # set inlet m.fs.valve.inlet.enth_mol[0].fix(hin) m.fs.valve.inlet.flow_mol[0].fix(fin) - m.fs.valve.inlet.flow_mol[0].unfix() m.fs.valve.inlet.pressure[0].fix(pin) - m.fs.valve.outlet.pressure[0].fix(pout) + m.fs.valve.outlet.pressure[0].set_value(pout) m.fs.valve.Cv.fix(cv) m.fs.valve.valve_opening.fix(0.5) iscale.calculate_scaling_factors(m) @@ -124,9 +122,7 @@ def test_solve(self, valve_model): results = solver.solve(valve_model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/power_generation/carbon_capture/compression_system/tests/test_compressor.py b/idaes/power_generation/carbon_capture/compression_system/tests/test_compressor.py index 1d28acb124..d415f2486c 100644 --- a/idaes/power_generation/carbon_capture/compression_system/tests/test_compressor.py +++ b/idaes/power_generation/carbon_capture/compression_system/tests/test_compressor.py @@ -107,9 +107,7 @@ def test_run(build_unit): # solve model results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert degrees_of_freedom(m) == 0 # energy balance diff --git a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/MEA_solvent.py b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/MEA_solvent.py index 8f685cbac9..f8c41fc8e1 100644 --- a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/MEA_solvent.py +++ b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/MEA_solvent.py @@ -680,7 +680,7 @@ def build_parameters(rblock, config): @staticmethod def return_expression(b, rblock, r_idx, T): - return exp(b.log_k_eq[r_idx]) + return exp(b.log_k_eq[r_idx])*((pyunits.m)**3/pyunits.mol) @staticmethod def return_log_expression(b, rblock, r_idx, T): diff --git a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_solvent.py b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_solvent.py index a3a96dae2c..83383824db 100644 --- a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_solvent.py +++ b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_solvent.py @@ -14,10 +14,7 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, - SolverStatus, - TerminationCondition, - value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core.util.model_statistics import (degrees_of_freedom, @@ -94,9 +91,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_vapor.py b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_vapor.py index c2a40cb1de..a12dc9ba83 100644 --- a/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_vapor.py +++ b/idaes/power_generation/carbon_capture/mea_solvent_system/properties/tests/test_mea_vapor.py @@ -14,10 +14,7 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import (ConcreteModel, - SolverStatus, - TerminationCondition, - value) +from pyomo.environ import check_optimal_termination, ConcreteModel, value from pyomo.util.check_units import assert_units_consistent from idaes.core.util.model_statistics import (degrees_of_freedom, @@ -96,9 +93,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component @@ -179,9 +174,7 @@ def test_solve(self, model): results = solver.solve(model) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.skipif(solver is None, reason="Solver not available") @pytest.mark.component diff --git a/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_column.py b/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_column.py index ebf0a573f7..c53819286c 100644 --- a/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_column.py +++ b/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_column.py @@ -18,7 +18,7 @@ # Import Pyomo libraries from pyomo.environ import ConcreteModel, value, Param, TransformationFactory,\ - TerminationCondition, units as pyunits + check_optimal_termination, units as pyunits # Import IDAES Libraries from idaes.core import FlowsheetBlock @@ -388,7 +388,7 @@ def test_steady_state_column(self, column_model_ss): res = solver.solve(m) # Solver status/condition - assert res.solver.termination_condition == TerminationCondition.optimal + assert check_optimal_termination(res) # Outlet Stream Condition Testing assert m.fs.unit.vapor_phase.properties[0, 1].temperature.value ==\ @@ -416,7 +416,7 @@ def test_dynamic_column(self, column_model_dyn): res = solver.solve(m) # Solver status/condition - assert res.solver.termination_condition == TerminationCondition.optimal + assert check_optimal_termination(res) # Performance Condition Testing at final time assert value(m.fs.unit.liquid_phase.properties[ diff --git a/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_plate_heat_exchanger.py b/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_plate_heat_exchanger.py index 6798418006..a02894127e 100644 --- a/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_plate_heat_exchanger.py +++ b/idaes/power_generation/carbon_capture/mea_solvent_system/unit_models/tests/test_plate_heat_exchanger.py @@ -16,11 +16,10 @@ """ import pytest -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Param, RangeSet, - SolverStatus, - TerminationCondition, units as pyunits, value) from idaes.core import FlowsheetBlock @@ -173,9 +172,7 @@ def test_solve(self, phe): results = solver.solve(phe) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) @pytest.mark.solver @pytest.mark.skipif(solver is None, reason="Solver not available") diff --git a/idaes/power_generation/costing/tests/test_NGCC_costing.py b/idaes/power_generation/costing/tests/test_NGCC_costing.py index 0f6d8d0d0c..2e3642083d 100644 --- a/idaes/power_generation/costing/tests/test_NGCC_costing.py +++ b/idaes/power_generation/costing/tests/test_NGCC_costing.py @@ -109,9 +109,7 @@ def test_units1_costing(build_costing): # Solve the model results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # Accounts with raw water withdrawal as reference parameter assert pytest.approx(26.435, abs=0.5) \ @@ -187,9 +185,7 @@ def test_units2_costing(build_costing): # Solve the model results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # Accounts with HRSG duty as reference parameter assert pytest.approx(90.794, abs=0.1) \ == sum(pyo.value(m.fs.b7.costing.total_plant_cost[ac]) @@ -309,9 +305,7 @@ def test_units3_costing(build_costing): # Solve the model results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # Accounts with condenser duty as reference parameter assert pytest.approx(14.27, abs=0.1) \ @@ -337,9 +331,7 @@ def test_flowsheet_costing(build_costing): # Solve the model results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # Verify total plant costs assert pytest.approx(574.85, abs=0.1) == pyo.value(m.fs.flowsheet_cost) diff --git a/idaes/power_generation/costing/tests/test_power_plant_costing.py b/idaes/power_generation/costing/tests/test_power_plant_costing.py index 35ea07ff17..c3989b7e41 100644 --- a/idaes/power_generation/costing/tests/test_power_plant_costing.py +++ b/idaes/power_generation/costing/tests/test_power_plant_costing.py @@ -122,8 +122,7 @@ def test_PP_costing(): solver = get_solver() results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal + assert pyo.check_optimal_termination(results) # all numbers come from the NETL excel file: # "201.001.001_BBR4 COE Spreadsheet_Rev0U_20190919_njk.xlsm" @@ -284,8 +283,7 @@ def test_power_plant_costing(): solver = get_solver() results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal + assert pyo.check_optimal_termination(results) # all numbers come from the NETL excel file # "201.001.001_BBR4 COE Spreadsheet_Rev0U_20190919_njk.xlsm" assert pytest.approx(pyo.value( @@ -483,8 +481,7 @@ def co2_cooler_UA_rule(b): solver = get_solver() results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal + assert pyo.check_optimal_termination(results) assert pytest.approx(pyo.value( m.fs.boiler.costing.equipment_cost), abs=1e-1) == 216300/1e3 @@ -531,8 +528,7 @@ def test_ASU_costing(): solver = get_solver() results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal + assert pyo.check_optimal_termination(results) m.fs.ASU.costing.bare_erected_cost.display() diff --git a/idaes/power_generation/flowsheets/NGFC/NGFC_flowsheet.py b/idaes/power_generation/flowsheets/NGFC/NGFC_flowsheet.py index c2f0fa50e0..d25bfb1795 100644 --- a/idaes/power_generation/flowsheets/NGFC/NGFC_flowsheet.py +++ b/idaes/power_generation/flowsheets/NGFC/NGFC_flowsheet.py @@ -47,6 +47,7 @@ from idaes.core.util.misc import svg_tag from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.exceptions import InitializationError import idaes.core.util.scaling as iscale @@ -190,9 +191,9 @@ def recycle_translator_P(b, t): def recycle_translator_x(b, t, j): return b.inlet.mole_frac_comp[t, j] == b.outlet.mole_frac_comp[t, j] - m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C2H6'].fix(0) - m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C3H8'].fix(0) - m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C4H10'].fix(0) + m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C2H6'].fix(1e-11) + m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C3H8'].fix(1e-11) + m.fs.recycle_translator.outlet.mole_frac_comp[0, 'C4H10'].fix(1e-11) # build cathode side units m.fs.air_blower = PressureChanger( @@ -219,10 +220,10 @@ def recycle_translator_x(b, t, j): "split_basis": SplittingType.componentFlow, "property_package": m.fs.air_props}) - m.fs.cathode.split_fraction[0, 'ion_outlet', 'H2O'].fix(0) - m.fs.cathode.split_fraction[0, 'ion_outlet', 'CO2'].fix(0) - m.fs.cathode.split_fraction[0, 'ion_outlet', 'N2'].fix(0) - m.fs.cathode.split_fraction[0, 'ion_outlet', 'Ar'].fix(0) + m.fs.cathode.split_fraction[0, 'ion_outlet', 'H2O'].fix(1e-11) + m.fs.cathode.split_fraction[0, 'ion_outlet', 'CO2'].fix(1e-11) + m.fs.cathode.split_fraction[0, 'ion_outlet', 'N2'].fix(1e-11) + m.fs.cathode.split_fraction[0, 'ion_outlet', 'Ar'].fix(1e-11) m.fs.cathode_translator = Translator( default={"outlet_state_defined": True, @@ -242,7 +243,7 @@ def cathode_translator_T(b, t): def cathode_translator_P(b, t): return b.inlet.pressure[t] == b.outlet.pressure[t] - m.fs.cathode_translator.outlet.mole_frac_comp.fix(0) + m.fs.cathode_translator.outlet.mole_frac_comp.fix(1e-11) m.fs.cathode_translator.outlet.mole_frac_comp[0, 'O2'].fix(1) m.fs.cathode_heat = Heater( @@ -299,7 +300,7 @@ def cathode_exhaust_translator_x(b, t, j): for j in m.fs.syn_props.component_list: if j not in m.fs.air_props.component_list: - m.fs.cathode_exhaust_translator.outlet.mole_frac_comp[0, j].fix(0) + m.fs.cathode_exhaust_translator.outlet.mole_frac_comp[0, j].fix(1e-11) m.fs.combustor_mix = Mixer( default={"inlet_list": ["anode_inlet", "cathode_inlet"], @@ -454,7 +455,7 @@ def set_power_island_inputs(m): m.fs.anode_mix.feed.mole_frac_comp[0, 'H2'].fix(0.2752) m.fs.anode_mix.feed.mole_frac_comp[0, 'H2O'].fix(0.1118) m.fs.anode_mix.feed.mole_frac_comp[0, 'N2'].fix(0.2879) - m.fs.anode_mix.feed.mole_frac_comp[0, 'O2'].fix(0.0000) + m.fs.anode_mix.feed.mole_frac_comp[0, 'O2'].fix(0) m.fs.anode_mix.feed.mole_frac_comp[0, 'Ar'].fix(0.0034) # anode heat exchanger @@ -525,9 +526,9 @@ def set_power_island_inputs(m): m.fs.cathode_HRSG.deltaP.fix(-1379) # Pa, equal to -0.2 psi m.fs.combustor.deltaP.fix(-6895) # equal to -1 psi - m.fs.combustor.outlet.mole_frac_comp[0, "H2"].fix(0) - m.fs.combustor.outlet.mole_frac_comp[0, "CO"].fix(0) - m.fs.combustor.outlet.mole_frac_comp[0, "CH4"].fix(0) + m.fs.combustor.outlet.mole_frac_comp[0, "H2"].fix(1e-11) + m.fs.combustor.outlet.mole_frac_comp[0, "CO"].fix(1e-11) + m.fs.combustor.outlet.mole_frac_comp[0, "CH4"].fix(1e-11) m.fs.combustor_expander.deltaP.fix(-6.9) # Pa, equal to -0.001 psi m.fs.combustor_expander.efficiency_isentropic.fix(0.95) @@ -660,13 +661,13 @@ def set_reformer_inputs(m): m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'C2H6'].fix(0.032) m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'C3H8'].fix(0.007) m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'C4H10'].fix(0.004) - m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'CO'].fix(0) + m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'CO'].fix(1e-11) m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'CO2'].fix(0.01) - m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'H2'].fix(0) - m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'H2O'].fix(0) + m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'H2'].fix(1e-11) + m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'H2O'].fix(1e-11) m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'N2'].fix(0.016) - m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'O2'].fix(0) - m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'Ar'].fix(0) + m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'O2'].fix(1e-11) + m.fs.reformer_recuperator.tube_inlet.mole_frac_comp[0, 'Ar'].fix(1e-11) # recuperator conditions m.fs.reformer_recuperator.tube_outlet.temperature.fix(1010.93) @@ -683,7 +684,7 @@ def set_reformer_inputs(m): m.fs.air_compressor_s1.inlet.flow_mol[0] == 1332.9 # mol/s m.fs.air_compressor_s1.inlet.temperature.fix(288.15) # K m.fs.air_compressor_s1.inlet.pressure.fix(101353) # Pa, equal to 14.7 psia - m.fs.air_compressor_s1.inlet.mole_frac_comp.fix(0) + m.fs.air_compressor_s1.inlet.mole_frac_comp.fix(1e-11) m.fs.air_compressor_s1.inlet.mole_frac_comp[0, 'CO2'].fix(0.0003) m.fs.air_compressor_s1.inlet.mole_frac_comp[0, 'H2O'].fix(0.0104) m.fs.air_compressor_s1.inlet.mole_frac_comp[0, 'N2'].fix(0.7722) @@ -707,7 +708,7 @@ def set_reformer_inputs(m): m.fs.reformer_mix.steam_inlet.flow_mol[0] == 464.77 # mol/s m.fs.reformer_mix.steam_inlet.temperature.fix(422) # K m.fs.reformer_mix.steam_inlet.pressure.fix(206843) # Pa, equal to 30 psia - m.fs.reformer_mix.steam_inlet.mole_frac_comp.fix(0) + m.fs.reformer_mix.steam_inlet.mole_frac_comp.fix(1e-11) m.fs.reformer_mix.steam_inlet.mole_frac_comp[0, 'H2O'].fix(1) # reformer outlet pressure @@ -868,7 +869,11 @@ def initialize_power_island(m): m.fs.anode.outlet.mole_frac_comp[0, "O2"] = 0 - m.fs.anode.initialize(outlvl=logging.INFO) + # This initialization step fails to converge, but is sufficent to continue + try: + m.fs.anode.initialize(outlvl=logging.INFO) + except InitializationError: + pass copy_port_values( m.fs.anode_recycle.inlet, m.fs.anode.outlet) @@ -1008,7 +1013,11 @@ def initialize_reformer(m): m.fs.reformer.outlet.mole_frac_comp[0, 'C3H8'] = 0 m.fs.reformer.outlet.mole_frac_comp[0, 'C4H10'] = 0 - m.fs.reformer.initialize() + # This initialization step fails to converge, but is sufficent to continue + try: + m.fs.reformer.initialize() + except InitializationError: + pass # reformer recuperator copy_port_values( diff --git a/idaes/power_generation/flowsheets/gas_turbine/tests/test_gas_turbine.py b/idaes/power_generation/flowsheets/gas_turbine/tests/test_gas_turbine.py index cf43cb3435..9e1c94be25 100644 --- a/idaes/power_generation/flowsheets/gas_turbine/tests/test_gas_turbine.py +++ b/idaes/power_generation/flowsheets/gas_turbine/tests/test_gas_turbine.py @@ -99,4 +99,4 @@ def test_initialize(): ng_comp=ng_comp, initialize=True) res = run_full_load(m, solver) - assert res.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(res) diff --git a/idaes/power_generation/flowsheets/subcritical_power_plant/subcritical_boiler.py b/idaes/power_generation/flowsheets/subcritical_power_plant/subcritical_boiler.py index c7e2e521ad..1681930846 100644 --- a/idaes/power_generation/flowsheets/subcritical_power_plant/subcritical_boiler.py +++ b/idaes/power_generation/flowsheets/subcritical_power_plant/subcritical_boiler.py @@ -382,8 +382,7 @@ def run_sensitivity(): properties_out[0].vapor_frac)) flow.append(pyo.value(m.fs.drum.feedwater_inlet.flow_mol[0])) - if results.solver.termination_condition == \ - pyo.TerminationCondition.optimal: + if pyo.check_optimal_termination(results): print('iter '+str(count)+ ' = EXIT- Optimal Solution Found.') else: print('iter '+str(count) + ' = infeasible') diff --git a/idaes/power_generation/flowsheets/test/test_scpc_plant.py b/idaes/power_generation/flowsheets/test/test_scpc_plant.py index 69a2b000d3..dc3f9fbb5a 100644 --- a/idaes/power_generation/flowsheets/test/test_scpc_plant.py +++ b/idaes/power_generation/flowsheets/test/test_scpc_plant.py @@ -18,7 +18,7 @@ import pytest from pyomo.environ import ( - SolverFactory, SolverStatus, TerminationCondition, value) + check_optimal_termination, SolverFactory, value) from pyomo.util.check_units import assert_units_consistent import idaes.power_generation.flowsheets.\ @@ -58,9 +58,7 @@ def test_boiler(boiler): boiler.fs.ATMP1.outlet.enth_mol[0].fix(62710.01) boiler.fs.ATMP1.SprayWater.flow_mol[0].unfix() result = boiler.solver.solve(boiler, tee=False) - assert result.solver.termination_condition == \ - TerminationCondition.optimal - assert result.solver.status == SolverStatus.ok + assert check_optimal_termination(result) assert value(boiler.fs.ECON.side_1.properties_out[0].temperature) == \ pytest.approx(521.009, 1) @@ -69,4 +67,4 @@ def test_boiler(boiler): def test_power_plant(): # SCPC.main imports and solves the SCPC Power Plant Flowsheet m, results = SCPC.main() - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) diff --git a/idaes/power_generation/flowsheets/test/test_subcritical_boiler.py b/idaes/power_generation/flowsheets/test/test_subcritical_boiler.py index 28d3ff6293..740d09cbe5 100644 --- a/idaes/power_generation/flowsheets/test/test_subcritical_boiler.py +++ b/idaes/power_generation/flowsheets/test/test_subcritical_boiler.py @@ -73,9 +73,7 @@ def test_init(model): model.fs.Waterwalls[i].heat_flux_conv[0], 1e-5) iscale.calculate_scaling_factors(model) results = solver.solve(model, tee=True) - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert (pyo.value(model.fs.downcomer.deltaP[0]) > 0) diff --git a/idaes/power_generation/properties/flue_gas_ideal.py b/idaes/power_generation/properties/flue_gas_ideal.py index 4861628cad..edc98ebef8 100644 --- a/idaes/power_generation/properties/flue_gas_ideal.py +++ b/idaes/power_generation/properties/flue_gas_ideal.py @@ -18,9 +18,21 @@ - components in flue gas: O2, N2, NO, CO2, H2O, SO2 """ # Import Pyomo libraries -from pyomo.environ import (Constraint, Param, PositiveReals, Reals, value, log, - sqrt, Var, Expression, units as pyunits) +from pyomo.environ import ( + Constraint, + Param, + PositiveReals, + Reals, + value, + log, + sqrt, + Var, + Expression, + check_optimal_termination, + units as pyunits, +) from pyomo.opt import SolverFactory +from pyomo.common.config import ConfigValue # Import IDAES cores from idaes.core import ( @@ -29,15 +41,17 @@ StateBlockData, StateBlock, Component, - VaporPhase + VaporPhase, ) from idaes.core.util.model_statistics import ( - degrees_of_freedom, number_activated_constraints) -from idaes.core import ( - MaterialBalanceType, EnergyBalanceType, MaterialFlowBasis) + degrees_of_freedom, + number_activated_constraints, +) +from idaes.core import MaterialBalanceType, EnergyBalanceType, MaterialFlowBasis from idaes.core.util.initialization import fix_state_vars, revert_state_vars from idaes.core.util import constants, get_solver import idaes.core.util.scaling as iscale +from idaes.core.util.exceptions import ConfigurationError, InitializationError # Import Python libraries import idaes.logger as idaeslog @@ -48,7 +62,7 @@ __version__ = "3" # Set up logger -_log = idaeslog.getLogger('idaes.unit_model.properties') +_log = idaeslog.getLogger("idaes.unit_model.properties") @declare_process_block_class("FlueGasParameterBlock") @@ -61,191 +75,300 @@ class FlueGasParameterData(PhysicalParameterBlock): """ + CONFIG = PhysicalParameterBlock.CONFIG() + CONFIG.declare( + "components", + ConfigValue( + default=["N2", "O2", "NO", "CO2", "H2O", "SO2"], + domain=list, + description="Components to include", + ), + ) + def build(self): - """ Add contents to the block.""" - super(FlueGasParameterData, self).build() + """Add contents to the block.""" + super().build() self._state_block_class = FlueGasStateBlock + _valid_comps = ["N2", "O2", "NO", "CO2", "H2O", "SO2"] - # Create Component objects - self.N2 = Component() - self.O2 = Component() - self.NO = Component() - self.CO2 = Component() - self.H2O = Component() - self.SO2 = Component() + for j in self.config.components: + if j not in _valid_comps: + raise ConfigurationError(f"Component '{j}' is not supported") + self.add_component(j, Component()) # Create Phase object self.Vap = VaporPhase() # Molecular weight - self.mw_comp = Param(self.component_list, - initialize={'O2': 0.0319988, - 'N2': 0.0280134, - 'NO': 0.0300061, - 'CO2': 0.0440095, - 'H2O': 0.0180153, - 'SO2': 0.064064}, - doc='Molecular Weight [kg/mol]', - units=pyunits.kg/pyunits.mol) + self.mw_comp = Param( + self.component_list, + initialize={ + k: v + for k, v in { + "O2": 0.0319988, + "N2": 0.0280134, + "NO": 0.0300061, + "CO2": 0.0440095, + "H2O": 0.0180153, + "SO2": 0.064064, + }.items() + if k in self.component_list + }, + doc="Molecular Weight [kg/mol]", + units=pyunits.kg / pyunits.mol, + ) # Thermodynamic reference state - self.pressure_ref = Param(within=PositiveReals, - default=1.01325e5, - doc='Reference pressure [Pa]', - units=pyunits.Pa) + self.pressure_ref = Param( + within=PositiveReals, + default=1.01325e5, + doc="Reference pressure [Pa]", + units=pyunits.Pa, + ) - self.temperature_ref = Param(within=PositiveReals, - default=298.15, - doc='Reference temperature [K]', - units=pyunits.K) + self.temperature_ref = Param( + within=PositiveReals, + default=298.15, + doc="Reference temperature [K]", + units=pyunits.K, + ) # Critical Properties - self.pressure_crit = Param(self.component_list, - within=PositiveReals, - initialize={'O2': 50.45985e5, - 'N2': 33.943875e5, - 'NO': 64.85e5, - 'CO2': 73.8e5, - 'H2O': 220.64e5, - 'SO2': 7.883e6}, - doc='Critical pressure [Pa]', - units=pyunits.Pa) - - self.temperature_crit = Param(self.component_list, - within=PositiveReals, - initialize={'O2': 154.58, - 'N2': 126.19, - 'NO': 180.0, - 'CO2': 304.18, - 'H2O': 647, - 'SO2': 430.8}, - doc='Critical temperature [K]', - units=pyunits.K) + self.pressure_crit = Param( + self.component_list, + within=PositiveReals, + initialize={ + k: v + for k, v in { + "O2": 50.45985e5, + "N2": 33.943875e5, + "NO": 64.85e5, + "CO2": 73.8e5, + "H2O": 220.64e5, + "SO2": 7.883e6, + }.items() + if k in self.component_list + }, + doc="Critical pressure [Pa]", + units=pyunits.Pa, + ) + + self.temperature_crit = Param( + self.component_list, + within=PositiveReals, + initialize={ + k: v + for k, v in { + "O2": 154.58, + "N2": 126.19, + "NO": 180.0, + "CO2": 304.18, + "H2O": 647, + "SO2": 430.8, + }.items() + if k in self.component_list + }, + doc="Critical temperature [K]", + units=pyunits.K, + ) # Constants for specific heat capacity, enthalpy, and entropy # calculations for ideal gas (from NIST 01/08/2020 # https://webbook.nist.gov/cgi/cbook.cgi?ID=C7727379&Units=SI&Mask=1#Thermo-Gas) - cp_mol_ig_comp_coeff_parameter_A = {'N2': 19.50583, - 'O2': 30.03235, - 'CO2': 24.99735, - 'H2O': 30.092, - 'NO': 23.83491, - 'SO2': 21.43049} - cp_mol_ig_comp_coeff_parameter_B = {'N2': 19.88705, - 'O2': 8.772972, - 'CO2': 55.18696, - 'H2O': 6.832514, - 'NO': 12.58878, - 'SO2': 74.35094} - cp_mol_ig_comp_coeff_parameter_C = {'N2': -8.598535, - 'O2': -3.98813, - 'CO2': -33.69137, - 'H2O': 6.793435, - 'NO': -1.139011, - 'SO2': -57.75217} - cp_mol_ig_comp_coeff_parameter_D = {'N2': 1.369784, - 'O2': 0.788313, - 'CO2': 7.948387, - 'H2O': -2.53448, - 'NO': -1.497459, - 'SO2': 16.35534} - cp_mol_ig_comp_coeff_parameter_E = {'N2': 0.527601, - 'O2': -0.7416, - 'CO2': -0.136638, - 'H2O': 0.082139, - 'NO': 0.214194, - 'SO2': 0.086731} - cp_mol_ig_comp_coeff_parameter_F = {'N2': -4.935202, - 'O2': -11.3247, - 'CO2': -403.6075, - 'H2O': -250.881, - 'NO': 83.35783, - 'SO2': -305.7688} - cp_mol_ig_comp_coeff_parameter_G = {'N2': 212.39, - 'O2': 236.1663, - 'CO2': 228.2431, - 'H2O': 223.3967, - 'NO': 237.1219, - 'SO2': 254.8872} - cp_mol_ig_comp_coeff_parameter_H = {'N2': 0, - 'O2': 0, - 'CO2': -393.5224, - 'H2O': -241.8264, - 'NO': 90.29114, - 'SO2': -296.8422} + cp_mol_ig_comp_coeff_parameter_A = { + k: v + for k, v in { + "N2": 19.50583, + "O2": 30.03235, + "CO2": 24.99735, + "H2O": 30.092, + "NO": 23.83491, + "SO2": 21.43049, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_B = { + k: v + for k, v in { + "N2": 19.88705, + "O2": 8.772972, + "CO2": 55.18696, + "H2O": 6.832514, + "NO": 12.58878, + "SO2": 74.35094, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_C = { + k: v + for k, v in { + "N2": -8.598535, + "O2": -3.98813, + "CO2": -33.69137, + "H2O": 6.793435, + "NO": -1.139011, + "SO2": -57.75217, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_D = { + k: v + for k, v in { + "N2": 1.369784, + "O2": 0.788313, + "CO2": 7.948387, + "H2O": -2.53448, + "NO": -1.497459, + "SO2": 16.35534, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_E = { + k: v + for k, v in { + "N2": 0.527601, + "O2": -0.7416, + "CO2": -0.136638, + "H2O": 0.082139, + "NO": 0.214194, + "SO2": 0.086731, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_F = { + k: v + for k, v in { + "N2": -4.935202, + "O2": -11.3247, + "CO2": -403.6075, + "H2O": -250.881, + "NO": 83.35783, + "SO2": -305.7688, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_G = { + k: v + for k, v in { + "N2": 212.39, + "O2": 236.1663, + "CO2": 228.2431, + "H2O": 223.3967, + "NO": 237.1219, + "SO2": 254.8872, + }.items() + if k in self.component_list + } + cp_mol_ig_comp_coeff_parameter_H = { + k: v + for k, v in { + "N2": 0, + "O2": 0, + "CO2": -393.5224, + "H2O": -241.8264, + "NO": 90.29114, + "SO2": -296.8422, + }.items() + if k in self.component_list + } self.cp_mol_ig_comp_coeff_A = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_A, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K, + ) self.cp_mol_ig_comp_coeff_B = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_B, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK, + ) self.cp_mol_ig_comp_coeff_C = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_C, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**2) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK ** 2, + ) self.cp_mol_ig_comp_coeff_D = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_D, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**3) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK ** 3, + ) self.cp_mol_ig_comp_coeff_E = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_E, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K*pyunits.kK**2) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K * pyunits.kK ** 2, + ) self.cp_mol_ig_comp_coeff_F = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_F, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.kJ/pyunits.mol) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.kJ / pyunits.mol, + ) self.cp_mol_ig_comp_coeff_G = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_G, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.J/pyunits.mol/pyunits.K) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.J / pyunits.mol / pyunits.K, + ) self.cp_mol_ig_comp_coeff_H = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_H, - doc='Constants for spec. heat capacity for ideal gas', - units=pyunits.kJ/pyunits.mol) + doc="Constants for spec. heat capacity for ideal gas", + units=pyunits.kJ / pyunits.mol, + ) # Viscosity and thermal conductivity parameters self.ce_param = Param( initialize=2.6693e-5, - units=(pyunits.g**0.5*pyunits.mol**0.5*pyunits.angstrom**2 * - pyunits.K**-0.5*pyunits.cm**-1*pyunits.s**-1), - doc="Parameter for the Chapman-Enskog viscosity correlation") + units=( + pyunits.g ** 0.5 + * pyunits.mol ** 0.5 + * pyunits.angstrom ** 2 + * pyunits.K ** -0.5 + * pyunits.cm ** -1 + * pyunits.s ** -1 + ), + doc="Parameter for the Chapman-Enskog viscosity correlation", + ) self.sigma = Param( self.component_list, initialize={ - 'O2': 3.458, - 'N2': 3.621, - 'NO': 3.47, - 'CO2': 3.763, - 'H2O': 2.605, - 'SO2': 4.29}, - doc='collision diameter in Angstrom (10e-10 m)', - units=pyunits.angstrom + k: v + for k, v in { + "O2": 3.458, + "N2": 3.621, + "NO": 3.47, + "CO2": 3.763, + "H2O": 2.605, + "SO2": 4.29, + }.items() + if k in self.component_list + }, + doc="collision diameter", + units=pyunits.angstrom, ) self.ep_Kappa = Param( self.component_list, initialize={ - 'O2': 107.4, - 'N2': 97.53, - 'NO': 119.0, - 'CO2': 244.0, - 'H2O': 572.4, - 'SO2': 252.0}, - doc="characteristic energy of interaction between pair of " - "molecules, K = Boltzmann constant in Kelvin", - units=pyunits.K) + k: v + for k, v in { + "O2": 107.4, + "N2": 97.53, + "NO": 119.0, + "CO2": 244.0, + "H2O": 572.4, + "SO2": 252.0, + }.items() + if k in self.component_list + }, + doc="Boltzmann constant divided by characteristic Lennard-Jones energy", + units=pyunits.K, + ) self.set_default_scaling("flow_mol", 1e-4) self.set_default_scaling("flow_mass", 1e-3) @@ -283,36 +406,41 @@ def build(self): @classmethod def define_metadata(cls, obj): - obj.add_properties({ - 'flow_mol_comp': {'method': None, 'units': 'mol/s'}, - 'pressure': {'method': None, 'units': 'Pa'}, - 'temperature': {'method': None, 'units': 'K'}, - 'pressure_crit': {'method': None, 'units': 'Pa'}, - 'temperature_crit': {'method': None, 'units': 'K'}, - 'pressure_red': {'method': None, 'units': None}, - 'temperature_red': {'method': None, 'units': None}, - 'enth_mol_phase': {'method': '_enthalpy_calc', 'units': 'J/mol'}, - 'entr_mol_phase': {'method': '_entropy_calc', 'units': 'J/mol/K'}, - 'enth_mol': {'method': '_enthalpy_calc', 'units': 'J/mol'}, - 'entr_mol': {'method': '_entropy_calc', 'units': 'J/mol.K'}, - 'cp_mol': {'method': '_heat_cap_calc', 'units': 'J/mol.K'}, - 'cp_mol_phase': {'method': '_heat_cap_calc', 'units': 'J/mol/K'}, - 'compress_fact': {'method': '_compress_fact', 'units': None}, - 'dens_mol_phase': {'method': '_dens_mol_phase', - 'units': 'mol/m^3'}, - 'pressure_sat': {'method': '_vapor_pressure', 'units': 'Pa'}, - 'flow_vol': {'method': '_flow_volume', 'units': 'm^3/s'}, - 'visc_d': {'method': '_therm_cond', 'units': 'kg/m-s'}, - 'therm_cond': {'method': '_therm_cond', 'units': 'W/m-K'}, - 'mw_comp': {'method': None, 'units': 'kg/mol'}, - 'mw': {'method': None, 'units': 'kg/mol'}, - }) - - obj.add_default_units({'time': pyunits.s, - 'length': pyunits.m, - 'mass': pyunits.kg, - 'amount': pyunits.mol, - 'temperature': pyunits.K}) + obj.add_properties( + { + "flow_mol_comp": {"method": None, "units": "mol/s"}, + "pressure": {"method": None, "units": "Pa"}, + "temperature": {"method": None, "units": "K"}, + "pressure_crit": {"method": None, "units": "Pa"}, + "temperature_crit": {"method": None, "units": "K"}, + "pressure_red": {"method": None, "units": None}, + "temperature_red": {"method": None, "units": None}, + "enth_mol_phase": {"method": "_enthalpy_calc", "units": "J/mol"}, + "entr_mol_phase": {"method": "_entropy_calc", "units": "J/mol/K"}, + "enth_mol": {"method": "_enthalpy_calc", "units": "J/mol"}, + "entr_mol": {"method": "_entropy_calc", "units": "J/mol.K"}, + "cp_mol": {"method": "_heat_cap_calc", "units": "J/mol.K"}, + "cp_mol_phase": {"method": "_heat_cap_calc", "units": "J/mol/K"}, + "compress_fact": {"method": "_compress_fact", "units": None}, + "dens_mol_phase": {"method": "_dens_mol_phase", "units": "mol/m^3"}, + "pressure_sat": {"method": "_vapor_pressure", "units": "Pa"}, + "flow_vol": {"method": "_flow_volume", "units": "m^3/s"}, + "visc_d": {"method": "_therm_cond", "units": "kg/m-s"}, + "therm_cond": {"method": "_therm_cond", "units": "W/m-K"}, + "mw_comp": {"method": None, "units": "kg/mol"}, + "mw": {"method": None, "units": "kg/mol"}, + } + ) + + obj.add_default_units( + { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + } + ) class _FlueGasStateBlock(StateBlock): @@ -328,7 +456,7 @@ def initialize( state_vars_fixed=False, outlvl=idaeslog.NOTSET, solver=None, - optarg=None + optarg=None, ): """Initialisation routine for property package. @@ -366,34 +494,38 @@ def initialize( which states were fixed during initialization. """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") - solve_log = idaeslog.getSolveLogger( - self.name, outlvl, tag="properties") + solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="properties") # Create solver opt = get_solver(solver, optarg) if state_args is None: - state_args = {"flow_mol_comp": {"N2": 1.0, - "CO2": 1.0, - "NO": 1.0, - "O2": 1.0, - "H2O": 1.0, - "SO2": 1.0}, - "pressure": 1e5, - "temperature": 495.0} + state_args = { + "flow_mol_comp": { + "N2": 1.0, + "CO2": 1.0, + "NO": 1.0, + "O2": 1.0, + "H2O": 1.0, + "SO2": 1.0, + }, + "pressure": 1e5, + "temperature": 495.0, + } if state_vars_fixed is False: flags = fix_state_vars(self, state_args) # Check when the state vars are fixed already result in dof 0 for b in self.values(): if degrees_of_freedom(b) != 0: - raise Exception(f"{self.name} initializtion error: State vars " - "fixed but degrees of freedom not equal to 0") + raise InitializationError( + f"{self.name} State vars fixed but degrees of freedom not 0" + ) # --------------------------------------------------------------------- # Solve 1st stage for k, b in self.items(): deactivate_list = [] - if hasattr(b, 'enthalpy_correlation'): + if hasattr(b, "enthalpy_correlation"): deactivate_list.append(b.enthalpy_correlation) if hasattr(b, "volumetric_flow_calculation"): deactivate_list.append(b.volumetric_flow_calculation) @@ -408,7 +540,8 @@ def initialize( else: res = "skipped" init_log.info_high( - "Initialization Step 1 {}.".format(idaeslog.condition(res))) + "Initialization Step 1 {}.".format(idaeslog.condition(res)) + ) for c in deactivate_list: c.activate() @@ -419,11 +552,13 @@ def initialize( else: res = "skipped" init_log.info_high( - "Initialization Step 2 {}.".format(idaeslog.condition(res))) + "Initialization Step 2 {}.".format(idaeslog.condition(res)) + ) + + if not res == "skipped" and not check_optimal_termination(res): + raise InitializationError(f"Solve failed {res}.") - init_log.info( - 'Initialisation Complete, {}.'.format( - idaeslog.condition(res))) + init_log.info(f"Initialisation Complete, {idaeslog.condition(res)}.") # --------------------------------------------------------------------- # If input block, return flags, else release state if state_vars_fixed is False: @@ -433,7 +568,7 @@ def initialize( self.release_state(flags) def release_state(self, flags, outlvl=idaeslog.NOTSET): - ''' + """ Method to relase state variables fixed during initialisation. Keyword Arguments: @@ -442,15 +577,14 @@ def release_state(self, flags, outlvl=idaeslog.NOTSET): unfixed. This dict is returned by initialize if hold_state=True. outlvl : sets output level of of logging - ''' + """ # Unfix state variables init_log = idaeslog.getInitLogger(self.name, outlvl, tag="properties") revert_state_vars(self, flags) - init_log.info('{} State Released.'.format(self.name)) + init_log.info("{} State Released.".format(self.name)) -@declare_process_block_class("FlueGasStateBlock", - block_class=_FlueGasStateBlock) +@declare_process_block_class("FlueGasStateBlock", block_class=_FlueGasStateBlock) class FlueGasStateBlockData(StateBlockData): """ This is an example of a property package for calculating the thermophysical @@ -469,131 +603,148 @@ def build(self): domain=Reals, initialize=1.0, bounds=(0, 1e6), - doc='Component molar flowrate [mol/s]', - units=pyunits.mol/pyunits.s + doc="Component molar flowrate [mol/s]", + units=pyunits.mol / pyunits.s, ) self.pressure = Var( domain=Reals, initialize=1.01325e5, bounds=(1, 5e7), - doc='State pressure [Pa]', - units=pyunits.Pa + doc="State pressure [Pa]", + units=pyunits.Pa, ) self.temperature = Var( domain=Reals, initialize=500, bounds=(200, 1500), - doc='State temperature [K]', - units=pyunits.K + doc="State temperature [K]", + units=pyunits.K, ) # Add expressions for some basic oft-used quantiies - self.flow_mol = Expression( - expr=sum(self.flow_mol_comp[j] for j in comps)) + self.flow_mol = Expression(expr=sum(self.flow_mol_comp[j] for j in comps)) def rule_mole_frac(b, c): return b.flow_mol_comp[c] / b.flow_mol + self.mole_frac_comp = Expression( - comps, - rule=rule_mole_frac, - doc='mole fraction of component i' + comps, rule=rule_mole_frac, doc="mole fraction of component i" ) self.flow_mass = Expression( - expr=sum(self.flow_mol_comp[j] * self.params.mw_comp[j] - for j in comps), - doc='total mass flow') + expr=sum(self.flow_mol_comp[j] * self.params.mw_comp[j] for j in comps), + doc="total mass flow", + ) def rule_mw_comp(b, j): return b.params.mw_comp[j] + self.mw_comp = Expression(comps, rule=rule_mw_comp) def rule_mw(b): return sum(b.mw_comp[j] * b.mole_frac_comp[j] for j in comps) + self.mw = Expression(rule=rule_mw) self.pressure_crit = Expression( expr=sum( - self.params.pressure_crit[j] * - self.mole_frac_comp[j] for j in comps)) + self.params.pressure_crit[j] * self.mole_frac_comp[j] for j in comps + ) + ) self.temperature_crit = Expression( expr=sum( - self.params.temperature_crit[j] * - self.mole_frac_comp[j] for j in comps)) - self.pressure_red = Expression( - expr=self.pressure / self.pressure_crit) - self.temperature_red = Expression( - expr=self.temperature / self.temperature_crit) + self.params.temperature_crit[j] * self.mole_frac_comp[j] for j in comps + ) + ) + self.pressure_red = Expression(expr=self.pressure / self.pressure_crit) + self.temperature_red = Expression(expr=self.temperature / self.temperature_crit) - self.compress_fact = Expression( - expr=1.0, doc='Vapor Compressibility Factor') + self.compress_fact = Expression(expr=1.0, doc="Vapor Compressibility Factor") def rule_dens_mol_phase(b, p): - return b.pressure / b.compress_fact / \ - constants.Constants.gas_constant / b.temperature + return ( + b.pressure + / b.compress_fact + / constants.Constants.gas_constant + / b.temperature + ) + self.dens_mol_phase = Expression( - self.params.phase_list, - rule=rule_dens_mol_phase, - doc='Molar Density') + self.params.phase_list, rule=rule_dens_mol_phase, doc="Molar Density" + ) self.flow_vol = Expression( - doc='Volumetric Flowrate', - expr=self.flow_mol / self.dens_mol_phase["Vap"]) + doc="Volumetric Flowrate", expr=self.flow_mol / self.dens_mol_phase["Vap"] + ) def _heat_cap_calc(self): # heat capacity J/mol-K - self.cp_mol = Var(initialize=1000, - doc='heat capacity [J/mol-K]', - units=pyunits.J/pyunits.mol/pyunits.K) + self.cp_mol = Var( + initialize=1000, + doc="heat capacity [J/mol-K]", + units=pyunits.J / pyunits.mol / pyunits.K, + ) def rule_cp_phase(b, p): # This property module only has one phase return self.cp_mol - self.cp_mol_phase = Expression(self.params.phase_list, - rule=rule_cp_phase) + + self.cp_mol_phase = Expression(self.params.phase_list, rule=rule_cp_phase) try: ft = sum(self.flow_mol_comp[j] for j in self.params.component_list) t = pyunits.convert(self.temperature, to_units=pyunits.kK) - self.heat_cap_correlation = Constraint(expr=( - self.cp_mol * ft == - sum(self.flow_mol_comp[j] * ( - self.params.cp_mol_ig_comp_coeff_A[j] + - self.params.cp_mol_ig_comp_coeff_B[j] * t + - self.params.cp_mol_ig_comp_coeff_C[j] * t**2 + - self.params.cp_mol_ig_comp_coeff_D[j] * t**3 + - self.params.cp_mol_ig_comp_coeff_E[j] / t**2) - for j in self.params.component_list))) + self.heat_cap_correlation = Constraint( + expr=( + self.cp_mol * ft + == sum( + self.flow_mol_comp[j] + * ( + self.params.cp_mol_ig_comp_coeff_A[j] + + self.params.cp_mol_ig_comp_coeff_B[j] * t + + self.params.cp_mol_ig_comp_coeff_C[j] * t ** 2 + + self.params.cp_mol_ig_comp_coeff_D[j] * t ** 3 + + self.params.cp_mol_ig_comp_coeff_E[j] / t ** 2 + ) + for j in self.params.component_list + ) + ) + ) except AttributeError: self.del_component(self.cp_mol) self.del_component(self.heat_cap_correlation) def _enthalpy_calc(self): - self.enth_mol = Var(doc='Specific Enthalpy [J/mol]', - units=pyunits.J/pyunits.mol) + self.enth_mol = Var( + doc="Specific Enthalpy [J/mol]", units=pyunits.J / pyunits.mol + ) def rule_enth_phase(b, p): # This property module only has one phase return self.enth_mol - self.enth_mol_phase = Expression( - self.params.phase_list, rule=rule_enth_phase) + + self.enth_mol_phase = Expression(self.params.phase_list, rule=rule_enth_phase) def enthalpy_correlation(b): ft = sum(self.flow_mol_comp[j] for j in self.params.component_list) t = pyunits.convert(self.temperature, to_units=pyunits.kK) return self.enth_mol * ft == sum( - self.flow_mol_comp[j] * pyunits.convert( - self.params.cp_mol_ig_comp_coeff_A[j] * t + - self.params.cp_mol_ig_comp_coeff_B[j] * t**2 / 2 + - self.params.cp_mol_ig_comp_coeff_C[j] * t**3 / 3 + - self.params.cp_mol_ig_comp_coeff_D[j] * t**4 / 4 - - self.params.cp_mol_ig_comp_coeff_E[j] / t + - self.params.cp_mol_ig_comp_coeff_F[j], - to_units=pyunits.J/pyunits.mol) - for j in self.params.component_list) + self.flow_mol_comp[j] + * pyunits.convert( + self.params.cp_mol_ig_comp_coeff_A[j] * t + + self.params.cp_mol_ig_comp_coeff_B[j] * t ** 2 / 2 + + self.params.cp_mol_ig_comp_coeff_C[j] * t ** 3 / 3 + + self.params.cp_mol_ig_comp_coeff_D[j] * t ** 4 / 4 + - self.params.cp_mol_ig_comp_coeff_E[j] / t + + self.params.cp_mol_ig_comp_coeff_F[j], + to_units=pyunits.J / pyunits.mol, + ) + for j in self.params.component_list + ) # NOTE: the H term (from the Shomate Equation) is not # included here so that the reference state enthalpy is the # enthalpy of formation (not 0). + try: self.enthalpy_correlation = Constraint(rule=enthalpy_correlation) except AttributeError: @@ -602,15 +753,16 @@ def enthalpy_correlation(b): self.del_component(self.enthalpy_correlation) def _entropy_calc(self): - self.entr_mol = Var(doc='Specific Entropy [J/mol/K]', - units=pyunits.J/pyunits.mol/pyunits.K) + self.entr_mol = Var( + doc="Specific Entropy [J/mol/K]", units=pyunits.J / pyunits.mol / pyunits.K + ) # Specific Entropy def rule_entr_phase(b, p): # This property module only has one phase return self.entr_mol - self.entr_mol_phase = Expression( - self.params.phase_list, rule=rule_entr_phase) + + self.entr_mol_phase = Expression(self.params.phase_list, rule=rule_entr_phase) def entropy_correlation(b): ft = sum(self.flow_mol_comp[j] for j in self.params.component_list) @@ -619,15 +771,19 @@ def entropy_correlation(b): x = self.mole_frac_comp p = self.pressure r_gas = constants.Constants.gas_constant - return (self.entr_mol + r_gas * log(p/1e5)) * ft == \ - sum(n[j] * ( - self.params.cp_mol_ig_comp_coeff_A[j]*log(t) + - self.params.cp_mol_ig_comp_coeff_B[j]*t + - self.params.cp_mol_ig_comp_coeff_C[j]*t**2 / 2 + - self.params.cp_mol_ig_comp_coeff_D[j]*t**3 / 3 - - self.params.cp_mol_ig_comp_coeff_E[j]/t**2 / 2 + - self.params.cp_mol_ig_comp_coeff_G[j] + - r_gas * log(x[j])) for j in self.params.component_list) + return (self.entr_mol + r_gas * log(p / 1e5)) * ft == sum( + n[j] + * ( + self.params.cp_mol_ig_comp_coeff_A[j] * log(t) + + self.params.cp_mol_ig_comp_coeff_B[j] * t + + self.params.cp_mol_ig_comp_coeff_C[j] * t ** 2 / 2 + + self.params.cp_mol_ig_comp_coeff_D[j] * t ** 3 / 3 + - self.params.cp_mol_ig_comp_coeff_E[j] / t ** 2 / 2 + + self.params.cp_mol_ig_comp_coeff_G[j] + + r_gas * log(x[j]) + ) + for j in self.params.component_list + ) try: self.entropy_correlation = Constraint(rule=entropy_correlation) @@ -640,79 +796,110 @@ def _therm_cond(self): self.therm_cond_comp = Var( comps, initialize=0.05, - doc='thermal conductivity J/m-K-s', - units=pyunits.J/pyunits.m/pyunits.K/pyunits.s) + doc="thermal conductivity J/m-K-s", + units=pyunits.J / pyunits.m / pyunits.K / pyunits.s, + ) self.therm_cond = Var( initialize=0.05, - doc='thermal conductivity of gas mixture J/m-K-s', - units=pyunits.J/pyunits.m/pyunits.K/pyunits.s) + doc="thermal conductivity of gas mixture J/m-K-s", + units=pyunits.J / pyunits.m / pyunits.K / pyunits.s, + ) self.visc_d_comp = Var( comps, initialize=2e-5, - doc='dynamic viscocity of pure gas species', - units=pyunits.kg/pyunits.m/pyunits.s) + doc="dynamic viscocity of pure gas species", + units=pyunits.kg / pyunits.m / pyunits.s, + ) self.visc_d = Var( - initialize=2e-5, doc='viscosity of gas mixture kg/m-s', - units=pyunits.kg/pyunits.m/pyunits.s) + initialize=2e-5, + doc="viscosity of gas mixture kg/m-s", + units=pyunits.kg / pyunits.m / pyunits.s, + ) try: + def rule_therm_cond(b, c): t = pyunits.convert(b.temperature, to_units=pyunits.kK) - return b.therm_cond_comp[c] == ( - ((b.params.cp_mol_ig_comp_coeff_A[c] + - b.params.cp_mol_ig_comp_coeff_B[c]*t + - b.params.cp_mol_ig_comp_coeff_C[c]*t**2 + - b.params.cp_mol_ig_comp_coeff_D[c]*t**3 + - b.params.cp_mol_ig_comp_coeff_E[c]/t**2) / - b.params.mw_comp[c]) + - 1.25 * - (constants.Constants.gas_constant / - b.params.mw_comp[c])) * b.visc_d_comp[c] + return ( + b.therm_cond_comp[c] + == ( + ( + ( + b.params.cp_mol_ig_comp_coeff_A[c] + + b.params.cp_mol_ig_comp_coeff_B[c] * t + + b.params.cp_mol_ig_comp_coeff_C[c] * t ** 2 + + b.params.cp_mol_ig_comp_coeff_D[c] * t ** 3 + + b.params.cp_mol_ig_comp_coeff_E[c] / t ** 2 + ) + / b.params.mw_comp[c] + ) + + 1.25 + * (constants.Constants.gas_constant / b.params.mw_comp[c]) + ) + * b.visc_d_comp[c] + ) + self.therm_cond_con = Constraint(comps, rule=rule_therm_cond) def rule_theta(b, c): return b.temperature / b.params.ep_Kappa[c] + self.theta = Expression(comps, rule=rule_theta) def rule_omega(b, c): - return (1.5794145 - + 0.00635771 * b.theta[c] - - 0.7314 * log(b.theta[c]) - + 0.2417357 * log(b.theta[c])**2 - - 0.0347045 * log(b.theta[c])**3) + return ( + 1.5794145 + + 0.00635771 * b.theta[c] + - 0.7314 * log(b.theta[c]) + + 0.2417357 * log(b.theta[c]) ** 2 + - 0.0347045 * log(b.theta[c]) ** 3 + ) + self.omega = Expression(comps, rule=rule_omega) # Pure gas viscocity - from Chapman-Enskog theory def rule_visc_d(b, c): - return (pyunits.convert( - b.visc_d_comp[c], - to_units=pyunits.g/pyunits.cm/pyunits.s) * - b.params.sigma[c]**2 * b.omega[c] == - b.params.ce_param * sqrt( - pyunits.convert(b.params.mw_comp[c], - to_units=pyunits.g/pyunits.mol) * - b.temperature)) + return ( + pyunits.convert( + b.visc_d_comp[c], to_units=pyunits.g / pyunits.cm / pyunits.s + ) + == b.params.ce_param + * sqrt( + pyunits.convert( + b.params.mw_comp[c], to_units=pyunits.g / pyunits.mol + ) + * b.temperature + ) + / b.params.sigma[c] ** 2 + / b.omega[c] + ) + self.visc_d_con = Constraint(comps, rule=rule_visc_d) # section to calculate viscosity of gas mixture def rule_phi(b, i, j): return ( - 1/2.8284 * - (1 + (b.params.mw_comp[i] / b.params.mw_comp[j]))**(-0.5) * - (1 + sqrt(b.visc_d_comp[i] / b.visc_d_comp[j]) * - (b.params.mw_comp[j] / b.params.mw_comp[i])**0.25)**2) - self.phi_ij = Expression( - comps, - comps, - rule=rule_phi - ) + 1 + / 2.8284 + * (1 + (b.params.mw_comp[i] / b.params.mw_comp[j])) ** (-0.5) + * ( + 1 + + sqrt(b.visc_d_comp[i] / b.visc_d_comp[j]) + * (b.params.mw_comp[j] / b.params.mw_comp[i]) ** 0.25 + ) + ** 2 + ) + + self.phi_ij = Expression(comps, comps, rule=rule_phi) # viscosity of Gas mixture kg/m-s def rule_visc_d_mix(b): return b.visc_d == sum( (b.mole_frac_comp[i] * b.visc_d_comp[i]) / sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps) - for i in comps) + for i in comps + ) + self.vis_d_mix_con = Constraint(rule=rule_visc_d_mix) # thermal conductivity of gas mixture in kg/m-s @@ -720,7 +907,9 @@ def rule_therm_mix(b): return b.therm_cond == sum( (b.mole_frac_comp[i] * b.therm_cond_comp[i]) / sum(b.mole_frac_comp[j] * b.phi_ij[i, j] for j in comps) - for i in comps) + for i in comps + ) + self.therm_mix_con = Constraint(rule=rule_therm_mix) except AttributeError: @@ -754,11 +943,12 @@ def get_material_flow_basis(self): def get_enthalpy_flow_terms(self, p): if not self.is_property_constructed("enthalpy_flow_terms"): try: + def rule_enthalpy_flow_terms(b, p): return self.enth_mol_phase[p] * self.flow_mol + self.enthalpy_flow_terms = Expression( - self.params.phase_list, - rule=rule_enthalpy_flow_terms + self.params.phase_list, rule=rule_enthalpy_flow_terms ) except AttributeError: self.del_component(self.enthalpy_flow_terms) @@ -770,12 +960,14 @@ def get_material_density_terms(self, p, j): def get_energy_density_terms(self, p): if not self.is_property_constructed("energy_density_terms"): try: + def rule_energy_density_terms(b, p): - return self.enth_mol_phase[p] * \ - self.dens_mol_phase[p] - self.pressure + return ( + self.enth_mol_phase[p] * self.dens_mol_phase[p] - self.pressure + ) + self.energy_density_terms = Expression( - self.params.phase_list, - rule=rule_energy_density_terms + self.params.phase_list, rule=rule_energy_density_terms ) except AttributeError: self.del_component(self.energy_density_terms) @@ -785,7 +977,7 @@ def define_state_vars(self): return { "flow_mol_comp": self.flow_mol_comp, "temperature": self.temperature, - "pressure": self.pressure + "pressure": self.pressure, } def model_check(self): @@ -827,51 +1019,53 @@ def calculate_scaling_factors(self): if self.is_property_constructed("heat_cap_correlation"): iscale.constraint_scaling_transform( self.heat_cap_correlation, - iscale.get_scaling_factor(self.cp_mol) * - iscale.get_scaling_factor(self.flow_mol), - overwrite=False + iscale.get_scaling_factor(self.cp_mol) + * iscale.get_scaling_factor(self.flow_mol), + overwrite=False, ) if self.is_property_constructed("enthalpy_correlation"): for p, c in self.enthalpy_correlation.items(): iscale.constraint_scaling_transform( c, - iscale.get_scaling_factor(self.enth_mol) * - iscale.get_scaling_factor(self.flow_mol), - overwrite=False + iscale.get_scaling_factor(self.enth_mol) + * iscale.get_scaling_factor(self.flow_mol), + overwrite=False, ) if self.is_property_constructed("entropy_correlation"): iscale.constraint_scaling_transform( self.entropy_correlation, - iscale.get_scaling_factor(self.entr_mol) * - iscale.get_scaling_factor(self.flow_mol), - overwrite=False + iscale.get_scaling_factor(self.entr_mol) + * iscale.get_scaling_factor(self.flow_mol), + overwrite=False, ) if self.is_property_constructed("vapor_pressure_correlation"): iscale.constraint_scaling_transform( self.vapor_pressure_correlation, - log(iscale.get_scaling_factor(self.pressure_sat)) * - iscale.get_scaling_factor(self.flow_mol), - overwrite=False + log(iscale.get_scaling_factor(self.pressure_sat)) + * iscale.get_scaling_factor(self.flow_mol), + overwrite=False, ) if self.is_property_constructed("therm_cond_con"): for i, c in self.therm_cond_con.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.therm_cond_comp[i]), - overwrite=False) + overwrite=False, + ) if self.is_property_constructed("therm_mix_con"): iscale.constraint_scaling_transform( self.therm_mix_con, iscale.get_scaling_factor(self.therm_cond), - overwrite=False) + overwrite=False, + ) if self.is_property_constructed("visc_d_con"): for i, c in self.visc_d_con.items(): iscale.constraint_scaling_transform( - c, - iscale.get_scaling_factor(self.visc_d_comp[i]), - overwrite=False) - if self.is_property_constructed("visc_d_mix_con"): - iscale.constraint_scaling_transform( - self.visc_d_mix_con, - iscale.get_scaling_factor(self.visc_d), - overwrite=False) + c, iscale.get_scaling_factor(self.visc_d_comp[i]), overwrite=False + ) + for i, c in self.vis_d_mix_con.items(): + iscale.constraint_scaling_transform( + self.vis_d_mix_con, + iscale.get_scaling_factor(self.visc_d), + overwrite=False, + ) diff --git a/idaes/power_generation/properties/natural_gas_PR.py b/idaes/power_generation/properties/natural_gas_PR.py index da659c4993..7b6f70ce39 100644 --- a/idaes/power_generation/properties/natural_gas_PR.py +++ b/idaes/power_generation/properties/natural_gas_PR.py @@ -29,6 +29,7 @@ # Import Python libraries import logging import copy +import enum # Import Pyomo units import pyomo.environ as pyo @@ -39,10 +40,12 @@ from idaes.generic_models.properties.core.state_definitions import FTPx from idaes.generic_models.properties.core.eos.ceos import Cubic, CubicType -from idaes.generic_models.properties.core.phase_equil.forms import log_fugacity +from idaes.generic_models.properties.core.eos.ideal import Ideal +from idaes.generic_models.properties.core.phase_equil.forms import fugacity, log_fugacity from idaes.generic_models.properties.core.phase_equil import SmoothVLE - -from idaes.generic_models.properties.core.pure import NIST, RPP4, RPP5 +from idaes.generic_models.properties.core.phase_equil.bubble_dew import \ + IdealBubbleDew +from idaes.generic_models.properties.core.pure import NIST, RPP4, RPP5, Perrys from idaes.generic_models.properties.core.reactions.dh_rxn import \ constant_dh_rxn @@ -54,10 +57,16 @@ GenericReactionParameterBlock, ConcentrationForm) import idaes.generic_models.properties.core.reactions as rxn +from idaes.core.util.exceptions import ConfigurationError # Set up logger _log = logging.getLogger(__name__) + +class EosType(enum.Enum): + PR = 1 + IDEAL = 2 + # Property Sources # Source: NIST webbook @@ -69,7 +78,7 @@ # Properties: Critical temperatures and pressures. Omega. # Heat capacity coefficients for ethane, propane, and butane. -_phase_dicts = { +_phase_dicts_pr = { "Vap": { "type": VaporPhase, "equation_of_state": Cubic, @@ -82,6 +91,13 @@ }, } +_phase_dicts_ideal = { + "Vap": { + "type": VaporPhase, + "equation_of_state": Ideal, + }, +} + _component_params = { 'H2': { 'type': Component, @@ -402,7 +418,7 @@ # returns a configuration dictionary for the list of specified components -def get_prop(components=None, phases="Vap"): +def get_prop(components=None, phases="Vap", eos=EosType.PR): if components is None: components = list(_component_params.keys()) configuration = { @@ -420,7 +436,7 @@ def get_prop(components=None, phases="Vap"): "state_definition": FTPx, "state_bounds": {"flow_mol": (0, 8000, 50000, pyunits.mol/pyunits.s), "temperature": (273.15, 500, 2500, pyunits.K), - "pressure": (5e4, 1.3e5, 1e7, pyunits.Pa)}, + "pressure": (5e4, 1.3e5, 1e8, pyunits.Pa)}, "pressure_ref": (101325, pyunits.Pa), "temperature_ref": (298.15, pyunits.K), } @@ -431,7 +447,15 @@ def get_prop(components=None, phases="Vap"): if isinstance(phases, str): phases = [phases] for k in phases: - configuration["phases"][k] = copy.deepcopy(_phase_dicts[k]) + if eos==EosType.PR: + configuration["phases"][k] = copy.deepcopy(_phase_dicts_pr[k]) + elif eos==EosType.IDEAL: + if k == "Liq": + raise ConfigurationError( + "This parameter set does not support Ideal EOS with liquid") + configuration["phases"][k] = copy.deepcopy(_phase_dicts_ideal[k]) + else: + raise ValueError("Invalid EoS.") if len(phases) > 1: p = tuple(phases) configuration["phases_in_equilibrium"] = [p] diff --git a/idaes/power_generation/properties/tests/test_NG_PR.py b/idaes/power_generation/properties/tests/test_NG_PR.py index f5c71aa7da..a7a8b2dc23 100644 --- a/idaes/power_generation/properties/tests/test_NG_PR.py +++ b/idaes/power_generation/properties/tests/test_NG_PR.py @@ -35,11 +35,10 @@ from idaes.generic_models.unit_models import GibbsReactor from pyomo.util.check_units import assert_units_consistent -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Objective, SolverFactory, - SolverStatus, - TerminationCondition, value) from idaes.core.util import get_solver @@ -99,8 +98,7 @@ def test_T_sweep(self, m): results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) assert -93000 == pytest.approx(value( m.fs.state[1].enth_mol_phase['Vap']), 1) assert 250 == pytest.approx(value( @@ -128,16 +126,14 @@ def test_P_sweep(self, m): results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) while m.fs.state[1].pressure.value <= 1e6: m.fs.state[1].pressure.value = ( m.fs.state[1].pressure.value + 2e5) results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal + assert check_optimal_termination(results) assert -70000 >= value(m.fs.state[1].enth_mol_phase['Vap']) assert -105000 <= value(m.fs.state[1].enth_mol_phase['Vap']) @@ -169,15 +165,14 @@ def test_gibbs(self, m): m.fs.reactor.inlet.mole_frac_comp[0, "N2"].fix(0.05) m.fs.reactor.inlet.mole_frac_comp[0, "Ar"].fix(0.05) m.fs.reactor.inlet.mole_frac_comp[0, "CH4"].fix(0.4) - - constraint_scaling_transform(m.fs.reactor.control_volume.enthalpy_balances[0.0],1E-6) + + constraint_scaling_transform( + m.fs.reactor.control_volume.enthalpy_balances[0.0], 1E-6) m.fs.reactor.gibbs_scaling = 1E-6 - + results = solver.solve(m, tee=True) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert 0.017 == pytest.approx(value( m.fs.reactor.outlet.mole_frac_comp[0, 'H2']), 1e-1) diff --git a/idaes/power_generation/properties/tests/test_SOFC_ROM.py b/idaes/power_generation/properties/tests/test_SOFC_ROM.py index b92e4bb9e1..1ece69c2f2 100644 --- a/idaes/power_generation/properties/tests/test_SOFC_ROM.py +++ b/idaes/power_generation/properties/tests/test_SOFC_ROM.py @@ -38,10 +38,9 @@ number_unused_variables) from idaes.core.util import get_solver -from pyomo.environ import (ConcreteModel, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, Block, - SolverStatus, - TerminationCondition, value) from pyomo.util.check_units import assert_units_consistent @@ -115,9 +114,7 @@ def test_solve(self, m): results = solver.solve(m) - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert (pytest.approx(705.5, abs=1e-1) == value(m.SOFC.anode_outlet_temperature)) diff --git a/idaes/power_generation/unit_models/boiler_heat_exchanger.py b/idaes/power_generation/unit_models/boiler_heat_exchanger.py index 4a21ae49f1..77115ee1d1 100644 --- a/idaes/power_generation/unit_models/boiler_heat_exchanger.py +++ b/idaes/power_generation/unit_models/boiler_heat_exchanger.py @@ -33,35 +33,59 @@ - Pressure drop tube and shell side (friction factor calc.) """ + +__author__ = "Boiler subsystem team (J Ma, M Zamarripa)" +__version__ = "1.0.0" + # Import Python libraries import logging from enum import Enum # Import Pyomo libraries from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool + # Additional import for the unit operation -from pyomo.environ import value, Var, Param, exp, sqrt,\ - log, PositiveReals, NonNegativeReals, units as pyunits +from pyomo.environ import ( + value, + Var, + Param, + exp, + sqrt, + log, + PositiveReals, + NonNegativeReals, + ExternalFunction, + Reference, + units as pyunits, +) # Import IDAES cores -from idaes.core import (ControlVolume0DBlock, - declare_process_block_class, - MaterialBalanceType, - EnergyBalanceType, - MomentumBalanceType, - UnitModelBlockData, - useDefault) +from idaes.core import ( + ControlVolume0DBlock, + declare_process_block_class, + MaterialBalanceType, + EnergyBalanceType, + MomentumBalanceType, + UnitModelBlockData, + useDefault, +) from idaes.core.util.config import is_physical_parameter_block, DefaultBool from idaes.core.util.misc import add_object_reference from idaes.core.util.constants import Constants as c from idaes.core.util import get_solver - +from idaes.core.util.functions import functions_lib +import idaes.core.util.scaling as iscale import idaes.logger as idaeslog +from idaes.generic_models.unit_models.heat_exchanger import ( + delta_temperature_lmtd_callback, + delta_temperature_lmtd2_callback, + delta_temperature_lmtd3_callback, + delta_temperature_amtd_callback, + delta_temperature_underwood_callback, +) -__author__ = "Boiler subsystem team (J Ma, M Zamarripa)" -__version__ = "1.0.0" # Set up logger _log = logging.getLogger(__name__) @@ -77,135 +101,225 @@ class DeltaTMethod(Enum): coCurrent = 1 +def delta_temperature_underwood_tune_callback(b): + """ + This is a callback for a temperature difference expression to calculate + :math:`\Delta T` in the heat exchanger model using log-mean temperature + difference (LMTD) approximation given by Underwood (1970). It can be + supplied to "delta_temperature_callback" HeatExchanger configuration option. + This uses a cube root function that works with negative numbers returning + the real negative root. This should always evaluate successfully. + """ + temp_units = pyunits.get_units(b.deltaT_1) + dT1 = b.deltaT_1 + dT2 = b.deltaT_2 + b.lmtd_param_c1 = Var(initialize=0.3241) + b.lmtd_param_c2 = Var(initialize=1.99996) + b.lmtd_param_c1.fix() + b.lmtd_param_c2.fix() + c1 = b.lmtd_param_c1 + c2 = b.lmtd_param_c2 + + @b.Expression(b.flowsheet().time) + def delta_temperature(b, t): + return (((dT1[t] / temp_units) ** c1 + (dT2[t] / temp_units) ** c1) / c2) ** ( + 1 / c1 + ) * temp_units + + @declare_process_block_class("BoilerHeatExchanger") class BoilerHeatExchangerData(UnitModelBlockData): """ Standard Heat Exchanger Unit Model Class """ + CONFIG = ConfigBlock() - CONFIG.declare("dynamic", ConfigValue( - domain=DefaultBool, - default=useDefault, - description="Dynamic model flag", - doc="""Indicates whether this model will be dynamic or not, + CONFIG.declare( + "dynamic", + ConfigValue( + domain=DefaultBool, + default=useDefault, + description="Dynamic model flag", + doc="""Indicates whether this model will be dynamic or not, **default** = useDefault. **Valid values:** { **useDefault** - get flag from parent (default = False), **True** - set as a dynamic model, -**False** - set as a steady-state model.}""")) - CONFIG.declare("has_holdup", ConfigValue( - default=useDefault, - domain=DefaultBool, - description="Holdup construction flag", - doc="""Indicates whether holdup terms should be constructed or not. +**False** - set as a steady-state model.}""", + ), + ) + CONFIG.declare( + "has_holdup", + ConfigValue( + default=useDefault, + domain=DefaultBool, + description="Holdup construction flag", + doc="""Indicates whether holdup terms should be constructed or not. Must be True if dynamic = True, **default** - False. **Valid values:** { **True** - construct holdup terms, -**False** - do not construct holdup terms}""")) - CONFIG.declare("side_1_property_package", ConfigValue( - default=useDefault, - domain=is_physical_parameter_block, - description="Property package to use for control volume", - doc="""Property parameter object used to define property calculations, +**False** - do not construct holdup terms}""", + ), + ) + CONFIG.declare( + "side_1_property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, -**PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) - CONFIG.declare("side_1_property_package_args", ConfigBlock( - implicit=True, - description="Arguments to use for constructing property packages", - doc="""A ConfigBlock with arguments to be passed to a property block(s) +**PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + CONFIG.declare( + "side_1_property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { -see property package for documentation.}""")) - CONFIG.declare("side_2_property_package", ConfigValue( - default=useDefault, - domain=is_physical_parameter_block, - description="Property package to use for control volume", - doc="""Property parameter object used to define property calculations, +see property package for documentation.}""", + ), + ) + CONFIG.declare( + "side_2_property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for control volume", + doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, -**PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) - CONFIG.declare("side_2_property_package_args", ConfigBlock( - implicit=True, - description="Arguments to use for constructing property packages", - doc="""A ConfigBlock with arguments to be passed to a property block(s) +**PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + CONFIG.declare( + "side_2_property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { -see property package for documentation.}""")) - CONFIG.declare("material_balance_type", ConfigValue( - default=MaterialBalanceType.useDefault, - domain=In(MaterialBalanceType), - description="Material balance construction flag", - doc="""Indicates what type of material balance should be constructed, +see property package for documentation.}""", + ), + ) + CONFIG.declare( + "material_balance_type", + ConfigValue( + default=MaterialBalanceType.useDefault, + domain=In(MaterialBalanceType), + description="Material balance construction flag", + doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, -**MaterialBalanceType.total** - use total material balance.}""")) - CONFIG.declare("energy_balance_type", ConfigValue( - default=EnergyBalanceType.useDefault, - domain=In(EnergyBalanceType), - description="Energy balance construction flag", - doc="""Indicates what type of energy balance should be constructed, +**MaterialBalanceType.total** - use total material balance.}""", + ), + ) + CONFIG.declare( + "energy_balance_type", + ConfigValue( + default=EnergyBalanceType.useDefault, + domain=In(EnergyBalanceType), + description="Energy balance construction flag", + doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, -**EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) - CONFIG.declare("momentum_balance_type", ConfigValue( - default=MomentumBalanceType.pressureTotal, - domain=In(MomentumBalanceType), - description="Momentum balance construction flag", - doc="""Indicates what type of momentum balance should be constructed, +**EnergyBalanceType.energyPhase** - energy balances for each phase.}""", + ), + ) + CONFIG.declare( + "momentum_balance_type", + ConfigValue( + default=MomentumBalanceType.pressureTotal, + domain=In(MomentumBalanceType), + description="Momentum balance construction flag", + doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, -**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) - CONFIG.declare("has_pressure_change", ConfigValue( - default=False, - domain=Bool, - description="Pressure change term construction flag", - doc="""Indicates whether terms for pressure change should be +**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", + ), + ) + CONFIG.declare( + "has_pressure_change", + ConfigValue( + default=False, + domain=Bool, + description="Pressure change term construction flag", + doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, -**False** - exclude pressure change terms.}""")) - CONFIG.declare("delta_T_method", ConfigValue( - default=DeltaTMethod.counterCurrent, - domain=In(DeltaTMethod), - description="Flow configuration in unit to compute delta T", - doc="""Flag indicating type of flow arrangement to use for delta +**False** - exclude pressure change terms.}""", + ), + ) + CONFIG.declare( + "delta_T_method", + ConfigValue( + default=DeltaTMethod.counterCurrent, + domain=In(DeltaTMethod), + description="Flow configuration in unit to compute delta T", + doc="""Flag indicating type of flow arrangement to use for delta **default** - DeltaTMethod.counterCurrent **Valid values:** { -**DeltaTMethod.counterCurrent**}""")) - CONFIG.declare("tube_arrangement", ConfigValue( - default=TubeArrangement.inLine, - domain=In(TubeArrangement), - description='tube configuration', - doc='Tube arrangement could be in-line and staggered')) - CONFIG.declare("side_1_water_phase", ConfigValue( - default='Liq', - domain=In(['Liq', 'Vap']), - description='side 1 water phase', - doc='Define water phase for property calls')) - CONFIG.declare("has_radiation", ConfigValue( - default=False, - domain=In([False, True]), - description='Has side 2 gas radiation', - doc='Define if side 2 gas radiation is to be considered')) +**DeltaTMethod.counterCurrent**}""", + ), + ) + CONFIG.declare( + "tube_arrangement", + ConfigValue( + default=TubeArrangement.inLine, + domain=In(TubeArrangement), + description="tube configuration", + doc="Tube arrangement could be in-line and staggered", + ), + ) + CONFIG.declare( + "side_1_water_phase", + ConfigValue( + default="Liq", + domain=In(["Liq", "Vap"]), + description="side 1 water phase", + doc="Define water phase for property calls", + ), + ) + CONFIG.declare( + "has_radiation", + ConfigValue( + default=False, + domain=In([False, True]), + description="Has side 2 gas radiation", + doc="Define if side 2 gas radiation is to be considered", + ), + ) + CONFIG.declare( + "delta_temperature_callback", + ConfigValue( + default=delta_temperature_underwood_tune_callback, + description="Callback for for temperature difference calculations", + ), + ) def build(self): """ @@ -221,17 +335,23 @@ def build(self): super(BoilerHeatExchangerData, self).build() # Build ControlVolume Block - self.side_1 = ControlVolume0DBlock(default={ - "dynamic": self.config.dynamic, - "has_holdup": self.config.has_holdup, - "property_package": self.config.side_1_property_package, - "property_package_args": self.config.side_1_property_package_args}) - - self.side_2 = ControlVolume0DBlock(default={ - "dynamic": self.config.dynamic, - "has_holdup": self.config.has_holdup, - "property_package": self.config.side_2_property_package, - "property_package_args": self.config.side_2_property_package_args}) + self.side_1 = ControlVolume0DBlock( + default={ + "dynamic": self.config.dynamic, + "has_holdup": self.config.has_holdup, + "property_package": self.config.side_1_property_package, + "property_package_args": self.config.side_1_property_package_args, + } + ) + + self.side_2 = ControlVolume0DBlock( + default={ + "dynamic": self.config.dynamic, + "has_holdup": self.config.has_holdup, + "property_package": self.config.side_2_property_package, + "property_package_args": self.config.side_2_property_package_args, + } + ) # Add Geometry self.side_1.add_geometry() @@ -242,30 +362,34 @@ def build(self): # Add material balance self.side_1.add_material_balances( - balance_type=self.config.material_balance_type) + balance_type=self.config.material_balance_type + ) # add energy balance self.side_1.add_energy_balances( - balance_type=self.config.energy_balance_type, - has_heat_transfer=True) + balance_type=self.config.energy_balance_type, has_heat_transfer=True + ) # add momentum balance self.side_1.add_momentum_balances( balance_type=self.config.momentum_balance_type, - has_pressure_change=self.config.has_pressure_change) + has_pressure_change=self.config.has_pressure_change, + ) # Add state block self.side_2.add_state_blocks(has_phase_equilibrium=False) # Add material balance self.side_2.add_material_balances( - balance_type=self.config.material_balance_type) + balance_type=self.config.material_balance_type + ) # add energy balance self.side_2.add_energy_balances( - balance_type=self.config.energy_balance_type, - has_heat_transfer=True) + balance_type=self.config.energy_balance_type, has_heat_transfer=True + ) # add momentum balance self.side_2.add_momentum_balances( balance_type=self.config.momentum_balance_type, - has_pressure_change=self.config.has_pressure_change) + has_pressure_change=self.config.has_pressure_change, + ) # Set Unit Geometry and control volume self._set_geometry() @@ -300,78 +424,88 @@ def _set_geometry(self): self.delta_elevation = Var( initialize=0, within=NonNegativeReals, - doc='Elevation increase used for static pressure calculation - m', - units=pyunits.m) + doc="Elevation increase used for static pressure calculation", + units=pyunits.m, + ) # Number of tube columns in the cross section plane # perpendicular to shell side fluid flow (y direction) - self.tube_ncol = Var(initialize=10.0, - within=PositiveReals, - doc='Number of tube columns') + self.tube_ncol = Var( + initialize=10.0, within=PositiveReals, doc="Number of tube columns" + ) # Number of tube rows in the direction of shell side # fluid flow (x direction) - self.tube_nrow = Var(initialize=10.0, - within=PositiveReals, - doc='Number of tube rows') + self.tube_nrow = Var( + initialize=10.0, within=PositiveReals, doc="Number of tube rows" + ) # Number of inlet tube rows - self.nrow_inlet = Var(initialize=1, - within=PositiveReals, - doc='Number of inlet tube rows') + self.nrow_inlet = Var( + initialize=1, within=PositiveReals, doc="Number of inlet tube rows" + ) # Length of a tube in z direction for each path - self.tube_length = Var(initialize=5.0, - within=PositiveReals, - doc='Tube length - m', - units=pyunits.m) + self.tube_length = Var( + initialize=5.0, within=PositiveReals, doc="Tube length", units=pyunits.m + ) # Inner diameter of tubes - self.tube_di = Var(initialize=0.05, - within=PositiveReals, - doc='Inner diameter of tube - m', - units=pyunits.m) + self.tube_di = Var( + initialize=0.05, + within=PositiveReals, + doc="Inner diameter of tube", + units=pyunits.m, + ) # Thickness of tube - self.tube_thickness = Var(initialize=0.005, - within=PositiveReals, - doc='Tube thickness - m', - units=pyunits.m) + self.tube_thickness = Var( + initialize=0.005, + within=PositiveReals, + doc="Tube thickness", + units=pyunits.m, + ) # Pitch of tubes between two neighboring columns (in y direction). # Always greater than tube outside diameter - self.pitch_y = Var(initialize=0.1, - within=PositiveReals, - doc='Pitch between two neighboring columns - m', - units=pyunits.m) + self.pitch_y = Var( + initialize=0.1, + within=PositiveReals, + doc="Pitch between two neighboring columns", + units=pyunits.m, + ) # Pitch of tubes between two neighboring rows (in x direction). # Always greater than tube outside diameter - self.pitch_x = Var(initialize=0.1, - within=PositiveReals, - doc='Pitch between two neighboring rows - m', - units=pyunits.m) + self.pitch_x = Var( + initialize=0.1, + within=PositiveReals, + doc="Pitch between two neighboring rows", + units=pyunits.m, + ) # Tube outside diameter - @self.Expression(doc="Outside diameter of tube - m") + @self.Expression(doc="Outside diameter of tube") def do_tube(b): return b.tube_di + b.tube_thickness * 2.0 if self.config.has_radiation is True: # Mean beam length for radiation - @self.Expression(doc="Mean beam length - m") + @self.Expression(doc="Mean beam length") def mbl(b): - return 3.6*(b.pitch_x*b.pitch_y/c.pi/b.do_tube - b.do_tube/4.0) + return 3.6 * ( + b.pitch_x * b.pitch_y / c.pi / b.do_tube - b.do_tube / 4.0 + ) # Mean beam length for radiation divided by sqrt(2) - @self.Expression(doc="Mean beam length - m") + @self.Expression(doc="Mean beam length") def mbl_div2(b): - return b.mbl/sqrt(2.0) + return b.mbl / sqrt(2.0) # Mean beam length for radiation multiplied by sqrt(2) - @self.Expression(doc="Mean beam length - m") + @self.Expression(doc="Mean beam length") def mbl_mul2(b): - return b.mbl*sqrt(2.0) + return b.mbl * sqrt(2.0) # Number of 180 degree bends for the tube @self.Expression(doc="Nbend_tube") @@ -379,30 +513,27 @@ def nbend_tube(b): return b.tube_nrow / b.nrow_inlet # Total flow area on tube side - @self.Expression(doc="Total flow area on tube side - m2") + @self.Expression(doc="Total flow area on tube side") def area_flow_tube(b): - return 0.25 * c.pi * b.tube_di**2.0 * b.tube_ncol * b.nrow_inlet + return 0.25 * c.pi * b.tube_di ** 2.0 * b.tube_ncol * b.nrow_inlet # Total flow area on shell side - @self.Expression(doc="Total flow area on shell side - m2") + @self.Expression(doc="Total flow area on shell side") def area_flow_shell(b): return b.tube_length * (b.pitch_y - b.do_tube) * b.tube_ncol # Total heat transfer area based on outside diameter - @self.Expression(doc="Total heat transfer " - "area based on tube outside diamer - m2") + @self.Expression(doc="Total heat transfer " "area based on tube outside diamer") def area_heat_transfer(b): return c.pi * b.do_tube * b.tube_length * b.tube_ncol * b.tube_nrow # Ratio of pitch_x/do_tube - @self.Expression(doc="Ratio of pitch in x " - "direction to tube outside diamer") + @self.Expression(doc="Ratio of pitch in x " "direction to tube outside diamer") def pitch_x_to_do(b): return b.pitch_x / b.do_tube # Ratio of pitch_y/do_tube - @self.Expression(doc="Ratio of pitch in y " - "direction to tube outside diamer") + @self.Expression(doc="Ratio of pitch in y " "direction to tube outside diamer") def pitch_y_to_do(b): return b.pitch_y / b.do_tube @@ -414,16 +545,28 @@ def pitch_y_to_do(b): def volume_side_1_eqn(b): return b.volumne_side_1 == ( - 0.25 * c.pi * b.tube_di**2.0 * b.tube_length - * b.tube_ncol * b.tube_nrow) + 0.25 + * c.pi + * b.tube_di ** 2.0 + * b.tube_length + * b.tube_ncol + * b.tube_nrow + ) + # Total shell side valume self.Constraint(doc="Total shell side volume") def volume_side_2_eqn(b): - return b.volumne_side_2 == \ - b.tube_ncol * b.pitch_y * b.tube_length \ - * b.tube_nrow * b.pitch_x - 0.25 * c.pi * b.do_tube**2.0 \ - * b.tube_length * b.tube_ncol * b.tube_nrow + return ( + b.volumne_side_2 + == b.tube_ncol * b.pitch_y * b.tube_length * b.tube_nrow * b.pitch_x + - 0.25 + * c.pi + * b.do_tube ** 2.0 + * b.tube_length + * b.tube_ncol + * b.tube_nrow + ) def _make_performance(self): """ @@ -446,15 +589,18 @@ def _make_performance(self): self.therm_cond_wall = Param( initialize=43.0, within=PositiveReals, - doc="Thermal conductivity of the wall - W/(m K)", - units=pyunits.W/pyunits.m/pyunits.K) + doc="Thermal conductivity of the wall", + units=pyunits.W / pyunits.m / pyunits.K, + ) # Loss coefficient for a 180 degree bend (u-turn), # usually related to radius to inside diameter ratio - self.k_loss_uturn = Param(initialize=0.5, - within=PositiveReals, - mutable=True, - doc='Loss coefficient of a tube u-turn') + self.k_loss_uturn = Param( + initialize=0.5, + within=PositiveReals, + mutable=True, + doc="Loss coefficient of a tube u-turn", + ) # Heat transfer resistance due to the fouling on tube side # (typical boiler hx) @@ -462,72 +608,71 @@ def _make_performance(self): initialize=0.00017, within=NonNegativeReals, mutable=True, - doc="Fouling resistance on tube side - K m2 / W", - units=pyunits.K*pyunits.m**2*pyunits.W**-1) + doc="Fouling resistance on tube side", + units=pyunits.K * pyunits.m ** 2 * pyunits.W ** -1, + ) # Heat transfer resistance due to the fouling on shell side self.shell_r_fouling = Param( initialize=0.0008, within=NonNegativeReals, mutable=True, - doc="Fouling resistance on tube side - K m2 / W", - units=pyunits.K*pyunits.m**2*pyunits.W**-1) + doc="Fouling resistance on tube side", + units=pyunits.K * pyunits.m ** 2 * pyunits.W ** -1, + ) # Correction factor for overall heat transfer coefficient - self.fcorrection_htc = Var(initialize=1.0, - within=NonNegativeReals, - doc="Correction factor for HTC") + self.fcorrection_htc = Var( + initialize=1.0, within=NonNegativeReals, doc="Correction factor for HTC" + ) # Correction factor for tube side pressure drop due to friction self.fcorrection_dp_tube = Var( - initialize=1.0, - doc="Correction factor for tube side pressure drop") + initialize=1.0, doc="Correction factor for tube side pressure drop" + ) # Correction factor for shell side pressure drop due to friction self.fcorrection_dp_shell = Var( - initialize=1.0, - doc="Correction factor for shell side pressure drop") - - # Temperature driving force - self.temperature_driving_force = Var( - self.flowsheet().time, - initialize=1.0, - doc="Mean driving force for heat exchange - K", - units=pyunits.K) + initialize=1.0, doc="Correction factor for shell side pressure drop" + ) if self.config.has_radiation is True: # Shell side wall emissivity, converted from parameter to variable - self.emissivity_wall = Var(initialize=0.7, - doc='Shell side wall emissivity') + self.emissivity_wall = Var(initialize=0.7, doc="Shell side wall emissivity") # Gas emissivity at mbl self.gas_emissivity = Var( self.flowsheet().time, initialize=0.5, - doc="Emissivity at given mean beam length") + doc="Emissivity at given mean beam length", + ) # Gas emissivity at mbl/sqrt(2) self.gas_emissivity_div2 = Var( self.flowsheet().time, initialize=0.4, - doc="Emissivity at mean beam length divided by sqrt of 2") + doc="Emissivity at mean beam length divided by sqrt of 2", + ) # Gas emissivity at mbl*sqrt(2) self.gas_emissivity_mul2 = Var( self.flowsheet().time, initialize=0.6, - doc="Emissivity at mean beam length multiplied by sqrt of 2") + doc="Emissivity at mean beam length multiplied by sqrt of 2", + ) # Gray fraction of gas in entire spectrum self.gas_gray_fraction = Var( self.flowsheet().time, initialize=0.5, - doc="Gray fraction of gas in entire spectrum") + doc="Gray fraction of gas in entire spectrum", + ) # Gas-surface radiation exchange factor for shell side wall - self.frad_gas_shell = Var(self.flowsheet().time, - initialize=0.5, - doc="Gas-surface radiation exchange " - "factor for shell side wall") + self.frad_gas_shell = Var( + self.flowsheet().time, + initialize=0.5, + doc="Gas-surface radiation exchange " "factor for shell side wall", + ) # Shell side equivalent convective heat transfer coefficient # due to radiation @@ -535,39 +680,50 @@ def _make_performance(self): self.flowsheet().time, initialize=100.0, doc="Shell convective heat transfer coefficient due to radiation", - units=pyunits.W/pyunits.m**2/pyunits.K) + units=pyunits.W / pyunits.m ** 2 / pyunits.K, + ) # Temperature difference at side 1 inlet - self.deltaT_1 = Var(self.flowsheet().time, - initialize=1.0, - doc="Temperature difference at side 1 inlet - K", - units=pyunits.K) + self.deltaT_1 = Var( + self.flowsheet().time, + initialize=1.0, + doc="Temperature difference at side 1 inlet", + units=pyunits.K, + ) # Temperature difference at side 1 outlet - self.deltaT_2 = Var(self.flowsheet().time, - initialize=1.0, - doc="Temperature difference at side 1 outlet - K", - units=pyunits.K) + self.deltaT_2 = Var( + self.flowsheet().time, + initialize=1.0, + doc="Temperature difference at side 1 outlet", + units=pyunits.K, + ) + + self.delta_temperature_in = Reference(self.deltaT_1) + self.delta_temperature_out = Reference(self.deltaT_2) # Overall heat transfer coefficient self.overall_heat_transfer_coefficient = Var( self.flowsheet().time, initialize=1.0, - units=pyunits.W/pyunits.m**2/pyunits.K) + units=pyunits.W / pyunits.m ** 2 / pyunits.K, + ) # Tube side convective heat transfer coefficient self.hconv_tube = Var( self.flowsheet().time, initialize=100.0, - doc="Tube side convective heat transfer coefficient - W / (m2 K)", - units=pyunits.W/pyunits.m**2/pyunits.K) + doc="Tube side convective heat transfer coefficient", + units=pyunits.W / pyunits.m ** 2 / pyunits.K, + ) # Shell side convective heat transfer coefficient due to convection self.hconv_shell_conv = Var( self.flowsheet().time, initialize=100.0, doc="Shell side convective heat transfer coefficient due to convection", - units=pyunits.W/pyunits.m**2/pyunits.K) + units=pyunits.W / pyunits.m ** 2 / pyunits.K, + ) # Total shell side convective heat transfer coefficient # including convection and radiation @@ -575,26 +731,34 @@ def _make_performance(self): self.flowsheet().time, initialize=150.0, doc="Total shell side convective heat transfer coefficient", - units=pyunits.W/pyunits.m**2/pyunits.K) + units=pyunits.W / pyunits.m ** 2 / pyunits.K, + ) # Heat conduction resistance of tube wall self.rcond_wall = Var( initialize=1.0, - doc="Heat conduction resistance of wall - K m2 / W", - units=pyunits.m**2*pyunits.K/pyunits.W) + doc="Heat conduction resistance of wall", + units=pyunits.m ** 2 * pyunits.K / pyunits.W, + ) if self.config.has_radiation is True: # Constraints for gas emissivity @self.Constraint(self.flowsheet().time, doc="Gas emissivity") def gas_emissivity_eqn(b, t): # This is a surrogate model, so need to do units manually - X1 = (b.side_2.properties_in[t].temperature - + b.side_2.properties_out[t].temperature)/2/pyunits.K - X2 = b.mbl/pyunits.m - X3 = b.side_2.properties_in[t].pressure/pyunits.Pa - X4 = b.side_2.properties_in[t].mole_frac_comp['CO2'] - X5 = b.side_2.properties_in[t].mole_frac_comp['H2O'] - X6 = b.side_2.properties_in[t].mole_frac_comp['O2'] + X1 = ( + ( + b.side_2.properties_in[t].temperature + + b.side_2.properties_out[t].temperature + ) + / 2 + / pyunits.K + ) + X2 = b.mbl / pyunits.m + X3 = b.side_2.properties_in[t].pressure / pyunits.Pa + X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] + X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] + X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. - 500 samples # Wide operating range: @@ -605,44 +769,52 @@ def gas_emissivity_eqn(b, t): # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) - return b.gas_emissivity[t] == \ - (- 0.116916606892E-003 * X1 - - 0.29111124038936179309056E-001 * X2 - + 0.50509651230704191577346E-006 * X3 - + 1.1844222822155641150488 * X4 - - 0.64720757767102773949652E-001 * X5 - - 0.35853593221454795048064E-001 * X6 - + 0.12227919099126832724878 * log(X1) - + 0.45102118316418124410738E-001 * log(X2) - + 0.33111863480179408447679E-001 * log(X3) - + 0.17674928397780117345084E-001 * log(X5) - - 0.12541139396423576016226E-001 * exp(X2) - - 0.90251708836308952577099 * exp(X4) - + 0.32447078857791738538963E-002 * X2**2 - - 0.31332075610864829615706E-004 * X1*X2 - - 0.54639645449809960433102E-009 * X1*X3 - - 0.19721467902854980460033E-003 * X1*X5 - + 0.45275517692290622763507E-004 * X1*X6 - + 0.75458754990630776904396E-006 * X2*X3 - + 0.39691751689931338564765E-001 * X2*X4 - + 0.73169514231974708273754 * X2*X5 - - 0.35852614507684822664491E-001 * X2*X6 - + 0.39743672195685803976177E-005 * X3*X5 - + 0.58802879141883679897383E-008 * (X1*X2)**2 - - 1.2994610452829884472692 * (X2*X5)**2) + return b.gas_emissivity[t] == ( + -0.116916606892e-003 * X1 + - 0.29111124038936179309056e-001 * X2 + + 0.50509651230704191577346e-006 * X3 + + 1.1844222822155641150488 * X4 + - 0.64720757767102773949652e-001 * X5 + - 0.35853593221454795048064e-001 * X6 + + 0.12227919099126832724878 * log(X1) + + 0.45102118316418124410738e-001 * log(X2) + + 0.33111863480179408447679e-001 * log(X3) + + 0.17674928397780117345084e-001 * log(X5) + - 0.12541139396423576016226e-001 * exp(X2) + - 0.90251708836308952577099 * exp(X4) + + 0.32447078857791738538963e-002 * X2 ** 2 + - 0.31332075610864829615706e-004 * X1 * X2 + - 0.54639645449809960433102e-009 * X1 * X3 + - 0.19721467902854980460033e-003 * X1 * X5 + + 0.45275517692290622763507e-004 * X1 * X6 + + 0.75458754990630776904396e-006 * X2 * X3 + + 0.39691751689931338564765e-001 * X2 * X4 + + 0.73169514231974708273754 * X2 * X5 + - 0.35852614507684822664491e-001 * X2 * X6 + + 0.39743672195685803976177e-005 * X3 * X5 + + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 + - 1.2994610452829884472692 * (X2 * X5) ** 2 + ) # Constraints for gas emissivity at mbl/sqrt(2) - @self.Constraint(self.flowsheet().time, - doc="Gas emissivity at a lower mean beam length") + @self.Constraint( + self.flowsheet().time, doc="Gas emissivity at a lower mean beam length" + ) def gas_emissivity_div2_eqn(b, t): # This is a surrogate model, so need to do units manually - X1 = (b.side_2.properties_in[t].temperature - + b.side_2.properties_out[t].temperature)/2/pyunits.K - X2 = b.mbl_div2/pyunits.m - X3 = b.side_2.properties_in[t].pressure/pyunits.Pa - X4 = b.side_2.properties_in[t].mole_frac_comp['CO2'] - X5 = b.side_2.properties_in[t].mole_frac_comp['H2O'] - X6 = b.side_2.properties_in[t].mole_frac_comp['O2'] + X1 = ( + ( + b.side_2.properties_in[t].temperature + + b.side_2.properties_out[t].temperature + ) + / 2 + / pyunits.K + ) + X2 = b.mbl_div2 / pyunits.m + X3 = b.side_2.properties_in[t].pressure / pyunits.Pa + X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] + X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] + X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. - 500 samples # Wide operating range: @@ -652,44 +824,52 @@ def gas_emissivity_div2_eqn(b, t): # X4: 0.12-0.16 (mol frac CO2) # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) - return b.gas_emissivity_div2[t] == \ - (- 0.116916606892E-003 * X1 - - 0.29111124038936179309056E-001 * X2 - + 0.50509651230704191577346E-006 * X3 - + 1.1844222822155641150488 * X4 - - 0.64720757767102773949652E-001 * X5 - - 0.35853593221454795048064E-001 * X6 - + 0.12227919099126832724878 * log(X1) - + 0.45102118316418124410738E-001 * log(X2) - + 0.33111863480179408447679E-001 * log(X3) - + 0.17674928397780117345084E-001 * log(X5) - - 0.12541139396423576016226E-001 * exp(X2) - - 0.90251708836308952577099 * exp(X4) - + 0.32447078857791738538963E-002 * X2**2 - - 0.31332075610864829615706E-004 * X1*X2 - - 0.54639645449809960433102E-009 * X1*X3 - - 0.19721467902854980460033E-003 * X1*X5 - + 0.45275517692290622763507E-004 * X1*X6 - + 0.75458754990630776904396E-006 * X2*X3 - + 0.39691751689931338564765E-001 * X2*X4 - + 0.73169514231974708273754 * X2*X5 - - 0.35852614507684822664491E-001 * X2*X6 - + 0.39743672195685803976177E-005 * X3*X5 - + 0.58802879141883679897383E-008 * (X1*X2)**2 - - 1.2994610452829884472692 * (X2*X5)**2) + return b.gas_emissivity_div2[t] == ( + -0.116916606892e-003 * X1 + - 0.29111124038936179309056e-001 * X2 + + 0.50509651230704191577346e-006 * X3 + + 1.1844222822155641150488 * X4 + - 0.64720757767102773949652e-001 * X5 + - 0.35853593221454795048064e-001 * X6 + + 0.12227919099126832724878 * log(X1) + + 0.45102118316418124410738e-001 * log(X2) + + 0.33111863480179408447679e-001 * log(X3) + + 0.17674928397780117345084e-001 * log(X5) + - 0.12541139396423576016226e-001 * exp(X2) + - 0.90251708836308952577099 * exp(X4) + + 0.32447078857791738538963e-002 * X2 ** 2 + - 0.31332075610864829615706e-004 * X1 * X2 + - 0.54639645449809960433102e-009 * X1 * X3 + - 0.19721467902854980460033e-003 * X1 * X5 + + 0.45275517692290622763507e-004 * X1 * X6 + + 0.75458754990630776904396e-006 * X2 * X3 + + 0.39691751689931338564765e-001 * X2 * X4 + + 0.73169514231974708273754 * X2 * X5 + - 0.35852614507684822664491e-001 * X2 * X6 + + 0.39743672195685803976177e-005 * X3 * X5 + + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 + - 1.2994610452829884472692 * (X2 * X5) ** 2 + ) # Constraints for gas emissivity at mbl*sqrt(2) - @self.Constraint(self.flowsheet().time, - doc="Gas emissivity at a higher mean beam length") + @self.Constraint( + self.flowsheet().time, doc="Gas emissivity at a higher mean beam length" + ) def gas_emissivity_mul2_eqn(b, t): # This is a surrogate model, so need to do units manually - X1 = (b.side_2.properties_in[t].temperature - + b.side_2.properties_out[t].temperature)/2/pyunits.K - X2 = b.mbl_mul2/pyunits.m - X3 = b.side_2.properties_in[t].pressure/pyunits.Pa - X4 = b.side_2.properties_in[t].mole_frac_comp['CO2'] - X5 = b.side_2.properties_in[t].mole_frac_comp['H2O'] - X6 = b.side_2.properties_in[t].mole_frac_comp['O2'] + X1 = ( + ( + b.side_2.properties_in[t].temperature + + b.side_2.properties_out[t].temperature + ) + / 2 + / pyunits.K + ) + X2 = b.mbl_mul2 / pyunits.m + X3 = b.side_2.properties_in[t].pressure / pyunits.Pa + X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] + X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] + X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. 500 samples # Wide operating range: @@ -699,349 +879,452 @@ def gas_emissivity_mul2_eqn(b, t): # X4: 0.12-0.16 (mol frac CO2) # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) - return b.gas_emissivity_mul2[t] == \ - (- 0.116916606892E-003 * X1 - - 0.29111124038936179309056E-001 * X2 - + 0.50509651230704191577346E-006 * X3 - + 1.1844222822155641150488 * X4 - - 0.64720757767102773949652E-001 * X5 - - 0.35853593221454795048064E-001 * X6 - + 0.12227919099126832724878 * log(X1) - + 0.45102118316418124410738E-001 * log(X2) - + 0.33111863480179408447679E-001 * log(X3) - + 0.17674928397780117345084E-001 * log(X5) - - 0.12541139396423576016226E-001 * exp(X2) - - 0.90251708836308952577099 * exp(X4) - + 0.32447078857791738538963E-002 * X2**2 - - 0.31332075610864829615706E-004 * X1*X2 - - 0.54639645449809960433102E-009 * X1*X3 - - 0.19721467902854980460033E-003 * X1*X5 - + 0.45275517692290622763507E-004 * X1*X6 - + 0.75458754990630776904396E-006 * X2*X3 - + 0.39691751689931338564765E-001 * X2*X4 - + 0.73169514231974708273754 * X2*X5 - - 0.35852614507684822664491E-001 * X2*X6 - + 0.39743672195685803976177E-005 * X3*X5 - + 0.58802879141883679897383E-008 * (X1*X2)**2 - - 1.2994610452829884472692 * (X2*X5)**2) + return b.gas_emissivity_mul2[t] == ( + -0.116916606892e-003 * X1 + - 0.29111124038936179309056e-001 * X2 + + 0.50509651230704191577346e-006 * X3 + + 1.1844222822155641150488 * X4 + - 0.64720757767102773949652e-001 * X5 + - 0.35853593221454795048064e-001 * X6 + + 0.12227919099126832724878 * log(X1) + + 0.45102118316418124410738e-001 * log(X2) + + 0.33111863480179408447679e-001 * log(X3) + + 0.17674928397780117345084e-001 * log(X5) + - 0.12541139396423576016226e-001 * exp(X2) + - 0.90251708836308952577099 * exp(X4) + + 0.32447078857791738538963e-002 * X2 ** 2 + - 0.31332075610864829615706e-004 * X1 * X2 + - 0.54639645449809960433102e-009 * X1 * X3 + - 0.19721467902854980460033e-003 * X1 * X5 + + 0.45275517692290622763507e-004 * X1 * X6 + + 0.75458754990630776904396e-006 * X2 * X3 + + 0.39691751689931338564765e-001 * X2 * X4 + + 0.73169514231974708273754 * X2 * X5 + - 0.35852614507684822664491e-001 * X2 * X6 + + 0.39743672195685803976177e-005 * X3 * X5 + + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 + - 1.2994610452829884472692 * (X2 * X5) ** 2 + ) # fraction of gray gas spectrum - @self.Constraint(self.flowsheet().time, - doc="Fraction of gray gas spectrum") + @self.Constraint(self.flowsheet().time, doc="Fraction of gray gas spectrum") def gas_gray_fraction_eqn(b, t): - return (b.gas_gray_fraction[t]*(2*b.gas_emissivity_div2[t] - - b.gas_emissivity_mul2[t]) == - b.gas_emissivity_div2[t]**2) + return ( + b.gas_gray_fraction[t] + * (2 * b.gas_emissivity_div2[t] - b.gas_emissivity_mul2[t]) + == b.gas_emissivity_div2[t] ** 2 + ) # gas-surface radiation exchange factor # between gas and shell side wall - @self.Constraint(self.flowsheet().time, - doc="Gas-surface radiation exchange " - "factor between gas and shell side wall") + @self.Constraint( + self.flowsheet().time, + doc="Gas-surface radiation exchange " + "factor between gas and shell side wall", + ) def frad_gas_shell_eqn(b, t): - return (b.frad_gas_shell[t] * - ((1/b.emissivity_wall-1)*b.gas_emissivity[t] + - b.gas_gray_fraction[t]) == - b.gas_gray_fraction[t]*b.gas_emissivity[t]) + return ( + b.frad_gas_shell[t] + * ( + (1 / b.emissivity_wall - 1) * b.gas_emissivity[t] + + b.gas_gray_fraction[t] + ) + == b.gas_gray_fraction[t] * b.gas_emissivity[t] + ) # equivalent convective heat transfer coefficent due to radiation - @self.Constraint(self.flowsheet().time, - doc="Equivalent convective heat transfer " - "coefficent due to radiation") + @self.Constraint( + self.flowsheet().time, + doc="Equivalent convective heat transfer " + "coefficent due to radiation", + ) def hconv_shell_rad_eqn(b, t): - return b.hconv_shell_rad[t] == \ - c.stefan_constant * b.frad_gas_shell[t] * \ - ((b.side_2.properties_in[t].temperature + - b.side_2.properties_out[t].temperature)/2 - + b.side_1.properties_in[t].temperature) * \ - (((b.side_2.properties_in[t].temperature - + b.side_2.properties_out[t].temperature)/2)**2 + - b.side_1.properties_in[t].temperature**2) + return b.hconv_shell_rad[t] == c.stefan_constant * b.frad_gas_shell[ + t + ] * ( + ( + b.side_2.properties_in[t].temperature + + b.side_2.properties_out[t].temperature + ) + / 2 + + b.side_1.properties_in[t].temperature + ) * ( + ( + ( + b.side_2.properties_in[t].temperature + + b.side_2.properties_out[t].temperature + ) + / 2 + ) + ** 2 + + b.side_1.properties_in[t].temperature ** 2 + ) # Energy balance equation - @self.Constraint(self.flowsheet().time, - doc="Energy balance between two sides") + @self.Constraint(self.flowsheet().time, doc="Energy balance between two sides") def energy_balance(b, t): - return b.side_1.heat[t] / 1e6 == -b.side_2.heat[t] / 1e6 - - # Heat transfer correlation - @self.Constraint(self.flowsheet().time, - doc="Heat transfer correlation") - def heat_transfer_correlation(b, t): - return b.heat_duty[t] / 1e6 == \ - (b.overall_heat_transfer_coefficient[t] * - b.area_heat_transfer * - b.temperature_driving_force[t]) / 1e6 + return b.side_1.heat[t] == -b.side_2.heat[t] # Driving force - @self.Constraint(self.flowsheet().time, - doc="Simplified Log mean temperature " - "difference calculation") + self.config.delta_temperature_callback(self) + + @self.Expression(self.flowsheet().time) def LMTD(b, t): - return b.temperature_driving_force[t] == \ - ((b.deltaT_1[t]**0.3241 + - b.deltaT_2[t]**0.3241)/1.99996)**(1/0.3241) + return b.delta_temperature[t] + + # Heat transfer correlation + @self.Constraint(self.flowsheet().time, doc="Heat transfer correlation") + def heat_transfer_correlation(b, t): + u = b.overall_heat_transfer_coefficient[t] + a = b.area_heat_transfer + deltaT = b.delta_temperature[t] + return b.heat_duty[t] == deltaT * u * a # Tube side heat transfer coefficient and pressure drop # ----------------------------------------------------- # Velocity on tube side - self.v_tube = Var(self.flowsheet().time, - initialize=1.0, - doc="Velocity on tube side - m/s", - units=pyunits.m/pyunits.s) + self.v_tube = Var( + self.flowsheet().time, + initialize=1.0, + doc="Velocity on tube side", + units=pyunits.m / pyunits.s, + ) # Reynalds number on tube side - self.N_Re_tube = Var(self.flowsheet().time, - initialize=10000.0, - doc="Reynolds number on tube side") + self.N_Re_tube = Var( + self.flowsheet().time, + initialize=10000.0, + doc="Reynolds number on tube side", + ) if self.config.has_pressure_change is True: # Friction factor on tube side - self.friction_factor_tube = Var(self.flowsheet().time, - initialize=1.0, - doc='Friction factor on tube side') + self.friction_factor_tube = Var( + self.flowsheet().time, + initialize=1.0, + doc="Friction factor on tube side", + ) # Pressure drop due to friction on tube side self.deltaP_tube_friction = Var( self.flowsheet().time, initialize=-10.0, - doc="Pressure drop due to friction on tube side - Pa", - units=pyunits.Pa) + doc="Pressure drop due to friction on tube side", + units=pyunits.Pa, + ) # Pressure drop due to 180 degree turn on tube side self.deltaP_tube_uturn = Var( self.flowsheet().time, initialize=-10.0, - doc="Pressure drop due to u-turn on tube side - Pa", - units=pyunits.Pa) + doc="Pressure drop due to u-turn on tube side", + units=pyunits.Pa, + ) # Prandtl number on tube side - self.N_Pr_tube = Var(self.flowsheet().time, initialize=1, - doc="Prandtl number on tube side") + self.N_Pr_tube = Var( + self.flowsheet().time, initialize=1, doc="Prandtl number on tube side" + ) # Nusselt number on tube side - self.N_Nu_tube = Var(self.flowsheet().time, initialize=1, - doc="Nusselts number on tube side") + self.N_Nu_tube = Var( + self.flowsheet().time, initialize=1, doc="Nusselts number on tube side" + ) # Velocity equation - @self.Constraint(self.flowsheet().time, - doc="Tube side velocity equation - m/s") + @self.Constraint(self.flowsheet().time, doc="Tube side velocity equation") def v_tube_eqn(b, t): - return (b.v_tube[t] * b.area_flow_tube * - b.side_1.properties_in[t].dens_mol_phase[ - self.side_1_fluid_phase] == - b.side_1.properties_in[t].flow_mol) + return ( + b.v_tube[t] + * b.area_flow_tube + * b.side_1.properties_in[t].dens_mol_phase[self.side_1_fluid_phase] + == b.side_1.properties_in[t].flow_mol + ) # Reynolds number - @self.Constraint(self.flowsheet().time, - doc="Reynolds number equation on tube side") + @self.Constraint( + self.flowsheet().time, doc="Reynolds number equation on tube side" + ) def N_Re_tube_eqn(b, t): - return (b.N_Re_tube[t] * - b.side_1.properties_in[t].visc_d_phase[ - self.side_1_fluid_phase] == - b.tube_di * b.v_tube[t] * - b.side_1.properties_in[t].dens_mass_phase[ - self.side_1_fluid_phase]) + return ( + b.N_Re_tube[t] + * b.side_1.properties_in[t].visc_d_phase[self.side_1_fluid_phase] + == b.tube_di + * b.v_tube[t] + * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] + ) if self.config.has_pressure_change is True: # Friction factor - @self.Constraint(self.flowsheet().time, - doc="Darcy friction factor on tube side") + @self.Constraint( + self.flowsheet().time, doc="Darcy friction factor on tube side" + ) def friction_factor_tube_eqn(b, t): - return b.friction_factor_tube[t]*b.N_Re_tube[t]**0.25 == \ - 0.3164*b.fcorrection_dp_tube + return ( + b.friction_factor_tube[t] * b.N_Re_tube[t] ** 0.25 + == 0.3164 * b.fcorrection_dp_tube + ) # Pressure drop due to friction - @self.Constraint(self.flowsheet().time, - doc="Pressure drop due to friction on tube side") + @self.Constraint( + self.flowsheet().time, doc="Pressure drop due to friction on tube side" + ) def deltaP_tube_friction_eqn(b, t): - return (b.deltaP_tube_friction[t]*b.tube_di*b.nrow_inlet == - -0.5 * b.side_1.properties_in[t].dens_mass_phase[ - self.side_1_fluid_phase] * - b.v_tube[t]**2 * b.friction_factor_tube[t] * - b.tube_length * b.tube_nrow) + return ( + b.deltaP_tube_friction[t] + == -0.5 + * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] + * b.v_tube[t] ** 2 + * b.friction_factor_tube[t] + * b.tube_length + * b.tube_nrow + / b.tube_di + / b.nrow_inlet + ) # Pressure drop due to u-turn - @self.Constraint(self.flowsheet().time, - doc="Pressure drop due to u-turn on tube side") + @self.Constraint( + self.flowsheet().time, doc="Pressure drop due to u-turn on tube side" + ) def deltaP_tube_uturn_eqn(b, t): - return (b.deltaP_tube_uturn[t] == - -0.5 * b.side_1.properties_in[t].dens_mass_phase[ - self.side_1_fluid_phase] * - b.v_tube[t]**2 * b.k_loss_uturn) + return ( + b.deltaP_tube_uturn[t] + == -0.5 + * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] + * b.v_tube[t] ** 2 + * b.k_loss_uturn + ) # Total pressure drop on tube side - @self.Constraint(self.flowsheet().time, - doc="Total pressure drop on tube side") + @self.Constraint( + self.flowsheet().time, doc="Total pressure drop on tube side" + ) def deltaP_tube_eqn(b, t): - return (b.deltaP_tube[t] == - b.deltaP_tube_friction[t] + b.deltaP_tube_uturn[t] - - b.delta_elevation * c.acceleration_gravity * - (b.side_1.properties_in[t].dens_mass_phase[ - self.side_1_fluid_phase] + - b.side_1.properties_out[t].dens_mass_phase[ - self.side_1_fluid_phase]) / 2.0) + return ( + b.deltaP_tube[t] + == b.deltaP_tube_friction[t] + + b.deltaP_tube_uturn[t] + - b.delta_elevation + * c.acceleration_gravity + * ( + b.side_1.properties_in[t].dens_mass_phase[ + self.side_1_fluid_phase + ] + + b.side_1.properties_out[t].dens_mass_phase[ + self.side_1_fluid_phase + ] + ) + / 2.0 + ) # Prandtl number - @self.Constraint(self.flowsheet().time, - doc="Prandtl number equation on tube side") + @self.Constraint( + self.flowsheet().time, doc="Prandtl number equation on tube side" + ) def N_Pr_tube_eqn(b, t): - return (b.N_Pr_tube[t] * - b.side_1.properties_in[t].therm_cond_phase[ - self.side_1_fluid_phase] * - b.side_1.properties_in[t].mw == - b.side_1.properties_in[t].cp_mol_phase[ - self.side_1_fluid_phase] * - b.side_1.properties_in[t].visc_d_phase[ - self.side_1_fluid_phase]) + return ( + b.N_Pr_tube[t] + * b.side_1.properties_in[t].therm_cond_phase[self.side_1_fluid_phase] + * b.side_1.properties_in[t].mw + == b.side_1.properties_in[t].cp_mol_phase[self.side_1_fluid_phase] + * b.side_1.properties_in[t].visc_d_phase[self.side_1_fluid_phase] + ) # Nusselts number - @self.Constraint(self.flowsheet().time, - doc="Nusselts number equation on tube side") + @self.Constraint( + self.flowsheet().time, doc="Nusselts number equation on tube side" + ) def N_Nu_tube_eqn(b, t): - return b.N_Nu_tube[t] == \ - 0.023 * b.N_Re_tube[t]**0.8 * b.N_Pr_tube[t]**0.4 + return ( + b.N_Nu_tube[t] + == 0.023 * b.N_Re_tube[t] ** 0.8 * abs(b.N_Pr_tube[t]) ** 0.4 + ) # Heat transfer coefficient - @self.Constraint(self.flowsheet().time, - doc="Convective heat transfer " - "coefficient equation on tube side") + @self.Constraint( + self.flowsheet().time, + doc="Convective heat transfer " "coefficient equation on tube side", + ) def hconv_tube_eqn(b, t): - return (b.hconv_tube[t]*self.tube_di/1000 == - b.N_Nu_tube[t] * - b.side_1.properties_in[t].therm_cond_phase[ - self.side_1_fluid_phase]/1000) + return ( + b.hconv_tube[t] * self.tube_di / 1000 + == b.N_Nu_tube[t] + * b.side_1.properties_in[t].therm_cond_phase[self.side_1_fluid_phase] + / 1000 + ) # Pressure drop and heat transfer coefficient on shell side # ---------------------------------------------------------- # Tube arrangement factor if self.config.tube_arrangement == TubeArrangement.inLine: - self.f_arrangement = Param(initialize=0.788, - doc="In-line tube arrangement factor") + self.f_arrangement = Param( + initialize=0.788, doc="In-line tube arrangement factor" + ) elif self.config.tube_arrangement == TubeArrangement.staggered: - self.f_arrangement = Param(initialize=1.0, - doc="Staggered tube arrangement factor") + self.f_arrangement = Param( + initialize=1.0, doc="Staggered tube arrangement factor" + ) else: - raise Exception('tube arrangement type not supported') + raise Exception("tube arrangement type not supported") # Velocity on shell side - self.v_shell = Var(self.flowsheet().time, - initialize=1.0, - doc="Velocity on shell side - m/s", - units=pyunits.m/pyunits.s) + self.v_shell = Var( + self.flowsheet().time, + initialize=1.0, + doc="Velocity on shell side", + units=pyunits.m / pyunits.s, + ) # Reynalds number on shell side - self.N_Re_shell = Var(self.flowsheet().time, - initialize=10000.0, - doc="Reynolds number on shell side") + self.N_Re_shell = Var( + self.flowsheet().time, + initialize=10000.0, + doc="Reynolds number on shell side", + ) # Friction factor on shell side - self.friction_factor_shell = Var(self.flowsheet().time, - initialize=1.0, - doc='Friction factor on shell side') + self.friction_factor_shell = Var( + self.flowsheet().time, initialize=1.0, doc="Friction factor on shell side" + ) # Prandtl number on shell side - self.N_Pr_shell = Var(self.flowsheet().time, - initialize=1, - doc="Prandtl number on shell side") + self.N_Pr_shell = Var( + self.flowsheet().time, initialize=1, doc="Prandtl number on shell side" + ) # Nusselt number on shell side - self.N_Nu_shell = Var(self.flowsheet().time, - initialize=1, - doc="Nusselts number on shell side") + self.N_Nu_shell = Var( + self.flowsheet().time, initialize=1, doc="Nusselts number on shell side" + ) # Velocity equation on shell side @self.Constraint(self.flowsheet().time, doc="Velocity on shell side") def v_shell_eqn(b, t): - return b.v_shell[t] * \ - b.side_2.properties_in[t].dens_mol_phase["Vap"] * \ - b.area_flow_shell == \ - sum(b.side_2.properties_in[t].flow_mol_comp[j] - for j in b.side_2.properties_in[t].params.component_list) + return b.v_shell[t] * b.side_2.properties_in[t].dens_mol_phase[ + "Vap" + ] * b.area_flow_shell == sum( + b.side_2.properties_in[t].flow_mol_comp[j] + for j in b.side_2.properties_in[t].params.component_list + ) # Reynolds number - @self.Constraint(self.flowsheet().time, - doc="Reynolds number equation on shell side") + @self.Constraint( + self.flowsheet().time, doc="Reynolds number equation on shell side" + ) def N_Re_shell_eqn(b, t): - return b.N_Re_shell[t] * b.side_2.properties_in[t].visc_d == \ - b.do_tube * b.v_shell[t] \ - * b.side_2.properties_in[t].dens_mol_phase["Vap"] *\ - sum(b.side_2.properties_in[t].mw_comp[c] - * b.side_2.properties_in[t].mole_frac_comp[c] - for c in b.side_2.properties_in[t]. - params.component_list) + return b.N_Re_shell[t] * b.side_2.properties_in[ + t + ].visc_d == b.do_tube * b.v_shell[t] * b.side_2.properties_in[ + t + ].dens_mol_phase[ + "Vap" + ] * sum( + b.side_2.properties_in[t].mw_comp[c] + * b.side_2.properties_in[t].mole_frac_comp[c] + for c in b.side_2.properties_in[t].params.component_list + ) if self.config.has_pressure_change is True: # Friction factor on shell side if self.config.tube_arrangement == TubeArrangement.inLine: - @self.Constraint(self.flowsheet().time, - doc="In-line friction factor on shell side") + + @self.Constraint( + self.flowsheet().time, doc="In-line friction factor on shell side" + ) def friction_factor_shell_eqn(b, t): - return b.friction_factor_shell[t] \ - * b.N_Re_shell[t]**0.15 == \ - (0.044 + 0.08 * b.pitch_x_to_do - / (b.pitch_y_to_do - 1.0)**(0.43 + 1.13 - / b.pitch_x_to_do) - ) * b.fcorrection_dp_shell + return ( + b.friction_factor_shell[t] * b.N_Re_shell[t] ** 0.15 + == ( + 0.044 + + 0.08 + * b.pitch_x_to_do + / (b.pitch_y_to_do - 1.0) ** (0.43 + 1.13 / b.pitch_x_to_do) + ) + * b.fcorrection_dp_shell + ) elif self.config.tube_arrangement == TubeArrangement.staggered: - @self.Constraint(self.flowsheet().time, - doc="Staggered friction factor on shell side") + + @self.Constraint( + self.flowsheet().time, doc="Staggered friction factor on shell side" + ) def friction_factor_shell_eqn(b, t): - return b.friction_factor_shell[t] \ - * b.N_Re_shell[t]**0.16 == \ - (0.25 + 0.118 / (b.pitch_y_to_do - 1.0)**1.08) \ + return ( + b.friction_factor_shell[t] * b.N_Re_shell[t] ** 0.16 + == (0.25 + 0.118 / (b.pitch_y_to_do - 1.0) ** 1.08) * b.fcorrection_dp_shell + ) + else: - raise Exception('tube arrangement type not supported') + raise Exception("tube arrangement type not supported") # Pressure drop on shell side - @self.Constraint(self.flowsheet().time, - doc="Pressure change on shell side") + @self.Constraint(self.flowsheet().time, doc="Pressure change on shell side") def deltaP_shell_eqn(b, t): return ( - b.deltaP_shell[t] == - -1.4 * b.friction_factor_shell[t] * b.tube_nrow * - b.side_2.properties_in[t].dens_mol_phase["Vap"] * - sum(b.side_2.properties_in[t].mw_comp[c] * - b.side_2.properties_in[t].mole_frac_comp[c] for c - in b.side_2.properties_in[t].params.component_list) * - b.v_shell[t]**2) + b.deltaP_shell[t] + == -1.4 + * b.friction_factor_shell[t] + * b.tube_nrow + * b.side_2.properties_in[t].dens_mol_phase["Vap"] + * sum( + b.side_2.properties_in[t].mw_comp[c] + * b.side_2.properties_in[t].mole_frac_comp[c] + for c in b.side_2.properties_in[t].params.component_list + ) + * b.v_shell[t] ** 2 + ) # Prandtl number - @self.Constraint(self.flowsheet().time, - doc="Prandtl number equation on shell side") + @self.Constraint( + self.flowsheet().time, doc="Prandtl number equation on shell side" + ) def N_Pr_shell_eqn(b, t): - return b.N_Pr_shell[t] * b.side_2.properties_in[t].therm_cond \ - * sum(b.side_2.properties_in[t].mw_comp[c] - * b.side_2.properties_in[t].mole_frac_comp[c] - for c in b.side_2.properties_in[t]. - params.component_list) == \ - b.side_2.properties_in[t].cp_mol * \ - b.side_2.properties_in[t].visc_d + return ( + b.N_Pr_shell[t] + * b.side_2.properties_in[t].therm_cond + * sum( + b.side_2.properties_in[t].mw_comp[c] + * b.side_2.properties_in[t].mole_frac_comp[c] + for c in b.side_2.properties_in[t].params.component_list + ) + == b.side_2.properties_in[t].cp_mol * b.side_2.properties_in[t].visc_d + ) # Nusselt number, currently assume Re>300 - @self.Constraint(self.flowsheet().time, - doc="Nusselts number equation on shell side") + @self.Constraint( + self.flowsheet().time, doc="Nusselts number equation on shell side" + ) def N_Nu_shell_eqn(b, t): - return b.N_Nu_shell[t] == b.f_arrangement * 0.33 \ - * b.N_Re_shell[t]**0.6 * b.N_Pr_shell[t]**0.333333 + return ( + b.N_Nu_shell[t] + == b.f_arrangement + * 0.33 + * b.N_Re_shell[t] ** 0.6 + * b.N_Pr_shell[t] ** 0.333333 + ) # Convective heat transfer coefficient on shell side due to convection - @self.Constraint(self.flowsheet().time, - doc="Convective heat transfer coefficient equation" - "on shell side due to convection") + @self.Constraint( + self.flowsheet().time, + doc="Convective heat transfer coefficient equation" + "on shell side due to convection", + ) def hconv_shell_conv_eqn(b, t): - return b.hconv_shell_conv[t] * b.do_tube / 1000 == \ - b.N_Nu_shell[t] * b.side_2.properties_in[t].therm_cond\ - / 1000 + return ( + b.hconv_shell_conv[t] * b.do_tube / 1000 + == b.N_Nu_shell[t] * b.side_2.properties_in[t].therm_cond / 1000 + ) # Total convective heat transfer coefficient on shell side - @self.Constraint(self.flowsheet().time, - doc="Total convective heat transfer " - "coefficient equation on shell side") + @self.Constraint( + self.flowsheet().time, + doc="Total convective heat transfer " "coefficient equation on shell side", + ) def hconv_shell_total_eqn(b, t): if self.config.has_radiation is True: - return b.hconv_shell_total[t] == \ - b.hconv_shell_conv[t] + b.hconv_shell_rad[t] + return ( + b.hconv_shell_total[t] + == b.hconv_shell_conv[t] + b.hconv_shell_rad[t] + ) else: return b.hconv_shell_total[t] == b.hconv_shell_conv[t] @@ -1049,18 +1332,26 @@ def hconv_shell_total_eqn(b, t): # based on outside surface area @self.Constraint(doc="Wall conduction heat transfer resistance") def rcond_wall_eqn(b): - return b.rcond_wall * b.therm_cond_wall == \ - 0.5 * b.do_tube * log(b.do_tube / b.tube_di) + return b.rcond_wall * b.therm_cond_wall == 0.5 * b.do_tube * log( + b.do_tube / b.tube_di + ) # Overall heat transfer coefficient - @self.Constraint(self.flowsheet().time, - doc="Wall conduction heat transfer resistance") + @self.Constraint( + self.flowsheet().time, doc="Wall conduction heat transfer resistance" + ) def overall_heat_transfer_coefficient_eqn(b, t): - return b.overall_heat_transfer_coefficient[t] \ - * (b.rcond_wall + b.tube_r_fouling + b.shell_r_fouling + - 1.0 / b.hconv_shell_total[t] - + b.do_tube / b.hconv_tube[t] / b.tube_di) == \ - b.fcorrection_htc + return ( + b.overall_heat_transfer_coefficient[t] + * ( + b.rcond_wall + + b.tube_r_fouling + + b.shell_r_fouling + + 1.0 / b.hconv_shell_total[t] + + b.do_tube / b.hconv_tube[t] / b.tube_di + ) + == b.fcorrection_htc + ) def _make_co_current(self): """ @@ -1073,19 +1364,23 @@ def _make_co_current(self): None """ # Temperature Differences - @self.Constraint(self.flowsheet().time, - doc="Side 1 inlet temperature difference") + @self.Constraint( + self.flowsheet().time, doc="Side 1 inlet temperature difference" + ) def temperature_difference_1(b, t): return b.deltaT_1[t] == ( - b.side_2.properties_in[t].temperature - - b.side_1.properties_in[t].temperature) + b.side_2.properties_in[t].temperature + - b.side_1.properties_in[t].temperature + ) - @self.Constraint(self.flowsheet().time, - doc="Side 1 outlet temperature difference") + @self.Constraint( + self.flowsheet().time, doc="Side 1 outlet temperature difference" + ) def temperature_difference_2(b, t): return b.deltaT_2[t] == ( - b.side_2.properties_out[t].temperature - - b.side_1.properties_out[t].temperature) + b.side_2.properties_out[t].temperature + - b.side_1.properties_out[t].temperature + ) def _make_counter_current(self): """ @@ -1098,19 +1393,23 @@ def _make_counter_current(self): None """ # Temperature Differences - @self.Constraint(self.flowsheet().time, - doc="Side 1 inlet temperature difference") + @self.Constraint( + self.flowsheet().time, doc="Side 1 inlet temperature difference" + ) def temperature_difference_1(b, t): return b.deltaT_1[t] == ( - b.side_2.properties_out[t].temperature - - b.side_1.properties_in[t].temperature) + b.side_2.properties_out[t].temperature + - b.side_1.properties_in[t].temperature + ) - @self.Constraint(self.flowsheet().time, - doc="Side 1 outlet temperature difference") + @self.Constraint( + self.flowsheet().time, doc="Side 1 outlet temperature difference" + ) def temperature_difference_2(b, t): return b.deltaT_2[t] == ( - b.side_2.properties_in[t].temperature - - b.side_1.properties_out[t].temperature) + b.side_2.properties_in[t].temperature + - b.side_1.properties_out[t].temperature + ) def model_check(blk): """ @@ -1127,9 +1426,15 @@ def model_check(blk): blk.side_1.model_check() blk.side_2.model_check() - def initialize(blk, state_args_1=None, state_args_2=None, - outlvl=idaeslog.NOTSET, solver=None, optarg=None): - ''' + def initialize( + blk, + state_args_1=None, + state_args_2=None, + outlvl=idaeslog.NOTSET, + solver=None, + optarg=None, + ): + """ General Heat Exchanger initialisation routine. Keyword Arguments: @@ -1151,7 +1456,7 @@ def initialize(blk, state_args_1=None, state_args_2=None, Returns: None - ''' + """ # Set solver options init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") @@ -1161,16 +1466,14 @@ def initialize(blk, state_args_1=None, state_args_2=None, # --------------------------------------------------------------------- # Initialize inlet property blocks - flags1 = blk.side_1.initialize(outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_1) + flags1 = blk.side_1.initialize( + outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1 + ) - flags2 = blk.side_2.initialize(outlvl=outlvl, - optarg=optarg, - solver=solver, - state_args=state_args_2) - init_log.info('{} Initialisation Step 1 Complete.'.format(blk.name)) + flags2 = blk.side_2.initialize( + outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2 + ) + init_log.info("{} Initialisation Step 1 Complete.".format(blk.name)) # --------------------------------------------------------------------- # Initialize temperature differentials @@ -1180,30 +1483,37 @@ def initialize(blk, state_args_1=None, state_args_2=None, t2_flags = {} for t in blk.flowsheet().time: p1_flags[t] = blk.side_1.properties_out[t].pressure.fixed - if not blk.side_1.properties_out[t].pressure.fixed \ - and blk.config.has_pressure_change: + if ( + not blk.side_1.properties_out[t].pressure.fixed + and blk.config.has_pressure_change + ): blk.side_1.properties_out[t].pressure.fix( - value(blk.side_1.properties_in[t].pressure)) + value(blk.side_1.properties_in[t].pressure) + ) p2_flags[t] = blk.side_2.properties_out[t].pressure.fixed - if not blk.side_2.properties_out[t].pressure.fixed \ - and blk.config.has_pressure_change: + if ( + not blk.side_2.properties_out[t].pressure.fixed + and blk.config.has_pressure_change + ): blk.side_2.properties_out[t].pressure.fix( - value(blk.side_2.properties_in[t].pressure)) + value(blk.side_2.properties_in[t].pressure) + ) h1_flags[t] = blk.side_1.properties_out[t].enth_mol.fixed if not blk.side_1.properties_out[t].enth_mol.fixed: blk.side_1.properties_out[t].enth_mol.fix( - value(blk.side_1.properties_in[t].enth_mol)+100.0) + value(blk.side_1.properties_in[t].enth_mol) + 100.0 + ) t2_flags[t] = blk.side_2.properties_out[t].temperature.fixed if not blk.side_2.properties_out[t].temperature.fixed: blk.side_2.properties_out[t].temperature.fix( - value(blk.side_2.properties_in[t].temperature)-5.0) + value(blk.side_2.properties_in[t].temperature) - 5.0 + ) # assuming Delta T min approach # Deactivate Constraints blk.heat_transfer_correlation.deactivate() - blk.LMTD.deactivate() blk.energy_balance.deactivate() if blk.config.has_pressure_change: blk.deltaP_tube_eqn.deactivate() @@ -1224,7 +1534,6 @@ def initialize(blk, state_args_1=None, state_args_2=None, if not t2_flags[t]: blk.side_2.properties_out[t].temperature.unfix() blk.heat_transfer_correlation.activate() - blk.LMTD.activate() blk.energy_balance.activate() if blk.config.has_pressure_change: @@ -1240,4 +1549,125 @@ def initialize(blk, state_args_1=None, state_args_2=None, blk.side_1.release_state(flags1, outlvl) blk.side_2.release_state(flags2, outlvl) - init_log.info('{} Initialisation Complete.'.format(blk.name)) + init_log.info("{} Initialisation Complete.".format(blk.name)) + + def calculate_scaling_factors(self): + super().calculate_scaling_factors() + + # We have a pretty good idea that the delta Ts will be between about + # 1 and 100 regardless of process of temperature units, so a default + # should be fine, so don't warn. Guessing a typical delta t around 10 + # the default scaling factor is set to 0.1 + sf_dT1 = dict( + zip( + self.deltaT_1.keys(), + [ + iscale.get_scaling_factor(v, default=0.1) + for v in self.deltaT_1.values() + ], + ) + ) + sf_dT2 = dict( + zip( + self.deltaT_2.keys(), + [ + iscale.get_scaling_factor(v, default=0.1) + for v in self.deltaT_2.values() + ], + ) + ) + + # U depends a lot on the process and units of measure so user should set + # this one. + sf_u = dict( + zip( + self.overall_heat_transfer_coefficient.keys(), + [ + iscale.get_scaling_factor(v, default=0.01, warning=True) + for v in self.overall_heat_transfer_coefficient.values() + ], + ) + ) + + # Since this depends on the process size this is another scaling factor + # the user should always set. + sf_a = iscale.get_scaling_factor( + self.area_heat_transfer, default=1e-4, warning=True + ) + + for t, c in self.heat_transfer_correlation.items(): + iscale.constraint_scaling_transform( + c, sf_dT1[t] * sf_u[t] * sf_a, overwrite=False + ) + + for t, c in self.energy_balance.items(): + iscale.constraint_scaling_transform( + c, sf_dT1[t] * sf_u[t] * sf_a, overwrite=False + ) + + for t, c in self.temperature_difference_1.items(): + iscale.constraint_scaling_transform(c, sf_dT1[t], overwrite=False) + + for t, c in self.temperature_difference_2.items(): + iscale.constraint_scaling_transform(c, sf_dT2[t], overwrite=False) + + for t, c in self.v_shell_eqn.items(): + s = iscale.min_scaling_factor( + self.side_2.properties_in[t].flow_mol_comp, + default=0, + warning=False, + hint=None, + ) + if s == 0: + s = iscale.get_scaling_factor( + self.side_2.properties_in[t].flow_mol, default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + for t, c in self.v_tube_eqn.items(): + s = iscale.min_scaling_factor( + self.side_1.properties_in[t].flow_mol_comp, + default=0, + warning=False, + hint=None, + ) + if s == 0: + s = iscale.get_scaling_factor( + self.side_1.properties_in[t].flow_mol, default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + for t, c in self.N_Nu_tube_eqn.items(): + s = iscale.get_scaling_factor(self.N_Nu_tube[t], default=1, warning=True) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + for t, c in self.N_Nu_shell_eqn.items(): + s = iscale.get_scaling_factor(self.N_Nu_shell[t], default=1, warning=True) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + for t, c in self.hconv_shell_total_eqn.items(): + s = iscale.get_scaling_factor( + self.hconv_shell_total[t], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + if hasattr(self, "deltaP_shell_eqn"): + for t, c in self.deltaP_shell_eqn.items(): + s = iscale.get_scaling_factor( + self.deltaP_shell[t], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + if hasattr(self, "deltaP_tube_eqn"): + for t, c in self.deltaP_tube_eqn.items(): + s = iscale.get_scaling_factor( + self.deltaP_tube[t], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) + + if hasattr(self, "deltaP_tube_friction_eqn"): + for t, c in self.deltaP_tube_friction_eqn.items(): + s = iscale.get_scaling_factor( + self.deltaP_tube_friction[t], default=1, warning=True + ) + iscale.constraint_scaling_transform(c, s, overwrite=False) diff --git a/idaes/power_generation/unit_models/feedwater_heater_0D_dynamic.py b/idaes/power_generation/unit_models/feedwater_heater_0D_dynamic.py index 27b51c620d..86593d38ca 100644 --- a/idaes/power_generation/unit_models/feedwater_heater_0D_dynamic.py +++ b/idaes/power_generation/unit_models/feedwater_heater_0D_dynamic.py @@ -52,7 +52,8 @@ from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale from idaes.power_generation.unit_models.helm import HelmMixer as Mixer -from idaes.core.util import get_solver, copy_port_values as _set_port +from idaes.core.util import get_solver +from idaes.core.util.initialization import propagate_state _log = idaeslog.getLogger(__name__) @@ -363,15 +364,15 @@ def initialize(self, *args, **kwargs): # initialize desuperheat if any if config.has_desuperheat: if config.has_drain_cooling: - _set_port(self.desuperheat.inlet_2, self.cooling.inlet_2) + propagate_state(self.desuperheat.inlet_2, self.cooling.inlet_2) else: - _set_port(self.desuperheat.inlet_2, self.condense.inlet_2) + propagate_state(self.desuperheat.inlet_2, self.condense.inlet_2) self.desuperheat.initialize(*args, **kwargs) self.desuperheat.inlet_1.flow_mol.unfix() if config.has_drain_mixer: - _set_port(self.drain_mix.steam, self.desuperheat.outlet_1) + propagate_state(self.drain_mix.steam, self.desuperheat.outlet_1) else: - _set_port(self.condense.inlet_1, self.desuperheat.outlet_1) + propagate_state(self.condense.inlet_1, self.desuperheat.outlet_1) # fix the steam and fwh inlet for init self.desuperheat.inlet_1.fix() self.desuperheat.inlet_1.flow_mol.unfix() # unfix for extract calc @@ -381,14 +382,14 @@ def initialize(self, *args, **kwargs): self.drain_mix.drain.fix() self.drain_mix.outlet.unfix() self.drain_mix.initialize(*args, **kwargs) - _set_port(self.condense.inlet_1, self.drain_mix.outlet) + propagate_state(self.condense.inlet_1, self.drain_mix.outlet) if config.has_desuperheat: self.drain_mix.steam.unfix() else: self.drain_mix.steam.flow_mol.unfix() # Initialize condense section if config.has_drain_cooling: - _set_port(self.condense.inlet_2, self.cooling.inlet_2) + propagate_state(self.condense.inlet_2, self.cooling.inlet_2) self.cooling.inlet_2.fix() else: self.condense.inlet_2.fix() @@ -407,7 +408,7 @@ def initialize(self, *args, **kwargs): self.condense.initialize() # Initialize drain cooling if included if config.has_drain_cooling: - _set_port(self.cooling.inlet_1, self.condense.outlet_1) + propagate_state(self.cooling.inlet_1, self.condense.outlet_1) self.cooling.initialize(*args, **kwargs) # Create solver diff --git a/idaes/power_generation/unit_models/helm/turbine_multistage.py b/idaes/power_generation/unit_models/helm/turbine_multistage.py index 6f542c9241..a041439053 100644 --- a/idaes/power_generation/unit_models/helm/turbine_multistage.py +++ b/idaes/power_generation/unit_models/helm/turbine_multistage.py @@ -37,7 +37,7 @@ from idaes.core.util.config import is_physical_parameter_block from idaes.core.util import from_json, to_json, StoreSpec -from idaes.core.util.misc import copy_port_values as copy_port +from idaes.core.util.initialization import propagate_state import idaes.core.util.scaling as iscale import idaes.logger as idaeslog @@ -495,7 +495,11 @@ def _rule(b, i, j): ) self.power = pyo.Var( - self.flowsheet().time, initialize=-1e8, doc="power (W)") + self.flowsheet().time, + initialize=-1e8, + doc="total turbine power", + units=pyo.units.W + ) @self.Constraint(self.flowsheet().time) def power_eqn(b, t): return (b.power[t] == @@ -598,12 +602,12 @@ def _init_section( """ Reuse the initializtion for HP, IP and, LP sections. """ if 0 in splits: - copy_port(splits[0].inlet, prev_port) + propagate_state(splits[0].inlet, prev_port) splits[0].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = splits[0].outlet_1 for i in stages: if i - 1 not in disconnects: - copy_port(stages[i].inlet, prev_port) + propagate_state(stages[i].inlet, prev_port) else: if copy_disconneted_flow: for t in stages[i].inlet.flow_mol: @@ -614,7 +618,7 @@ def _init_section( stages[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = stages[i].outlet if i in splits: - copy_port(splits[i].inlet, prev_port) + propagate_state(splits[i].inlet, prev_port) splits[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = splits[i].outlet_1 return prev_port @@ -685,7 +689,7 @@ def initialize( # Initialize valves for i in self.inlet_stage_idx: u = self.throttle_valve[i] - copy_port(u.inlet, getattr( + propagate_state(u.inlet, getattr( self.inlet_split, "outlet_{}".format(i))) u.initialize( outlvl=outlvl, solver=solver, optarg=optarg) @@ -693,7 +697,7 @@ def initialize( # Initialize turbine for i in self.inlet_stage_idx: u = self.inlet_stage[i] - copy_port(u.inlet, self.throttle_valve[i].outlet) + propagate_state(u.inlet, self.throttle_valve[i].outlet) u.initialize( outlvl=outlvl, solver=solver, @@ -704,7 +708,7 @@ def initialize( # Initialize Mixer self.inlet_mix.use_minimum_inlet_pressure_constraint() for i in self.inlet_stage_idx: - copy_port( + propagate_state( getattr(self.inlet_mix, "inlet_{}".format(i)), self.inlet_stage[i].outlet, ) @@ -754,7 +758,7 @@ def initialize( copy_disconneted_pressure=copy_disconneted_pressure, ) - copy_port(self.outlet_stage.inlet, prev_port) + propagate_state(self.outlet_stage.inlet, prev_port) self.outlet_stage.initialize( outlvl=outlvl, solver=solver, diff --git a/idaes/power_generation/unit_models/helm/turbine_stage.py b/idaes/power_generation/unit_models/helm/turbine_stage.py index 9a04c207dc..fa7bd122c8 100644 --- a/idaes/power_generation/unit_models/helm/turbine_stage.py +++ b/idaes/power_generation/unit_models/helm/turbine_stage.py @@ -21,8 +21,7 @@ """ __Author__ = "John Eslick" -from pyomo.environ import Var, SolverFactory, value, units as pyunits -from pyomo.opt import TerminationCondition +from pyomo.environ import Var, units as pyunits from idaes.core import declare_process_block_class from idaes.power_generation.unit_models.helm.turbine import HelmIsentropicTurbineData diff --git a/idaes/power_generation/unit_models/tests/test_boiler_heat_exchanger.py b/idaes/power_generation/unit_models/tests/test_boiler_heat_exchanger.py index 6d29288de4..414a1ca9bd 100644 --- a/idaes/power_generation/unit_models/tests/test_boiler_heat_exchanger.py +++ b/idaes/power_generation/unit_models/tests/test_boiler_heat_exchanger.py @@ -17,11 +17,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, - value, - units as pyunits) +from pyomo.environ import ( + check_optimal_termination, ConcreteModel, value, units as pyunits) from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock @@ -32,11 +29,16 @@ from idaes.power_generation.properties import FlueGasParameterBlock # Import Power Plant HX Unit Model from idaes.power_generation.unit_models.boiler_heat_exchanger import ( - BoilerHeatExchanger, TubeArrangement, DeltaTMethod) + BoilerHeatExchanger, TubeArrangement, DeltaTMethod, + delta_temperature_lmtd_callback, delta_temperature_amtd_callback, + delta_temperature_underwood_callback, + delta_temperature_underwood_tune_callback) from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.util.testing import PhysicalParameterTestBlock from idaes.core.util import get_solver +import idaes.core.util.scaling as iscale + # ----------------------------------------------------------------------------- # Get default solver for testing @@ -44,9 +46,8 @@ # ----------------------------------------------------------------------------- - -@pytest.mark.unit -def test_config(): +def tc( + delta_temperature_callback=delta_temperature_underwood_tune_callback): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) @@ -55,6 +56,7 @@ def test_config(): m.fs.prop_fluegas = FlueGasParameterBlock() m.fs.unit = BoilerHeatExchanger(default={ + "delta_temperature_callback": delta_temperature_callback, "side_1_property_package": m.fs.prop_steam, "side_2_property_package": m.fs.prop_fluegas, "has_pressure_change": True, @@ -75,11 +77,10 @@ def test_config(): DeltaTMethod.counterCurrent -@pytest.mark.skipif(not iapws95.iapws95_available(), - reason="IAPWS not available") -@pytest.mark.skipif(solver is None, reason="Solver not available") -@pytest.mark.component -def test_boiler_hx(): +def th( + delta_temperature_callback=delta_temperature_underwood_tune_callback, + tout_1=809.55, + tout_2=788.53): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) @@ -88,6 +89,7 @@ def test_boiler_hx(): m.fs.prop_fluegas = FlueGasParameterBlock() m.fs.unit = BoilerHeatExchanger(default={ + "delta_temperature_callback": delta_temperature_callback, "side_1_property_package": m.fs.prop_steam, "side_2_property_package": m.fs.prop_fluegas, "has_pressure_change": True, @@ -99,7 +101,6 @@ def test_boiler_hx(): # Set inputs h = value(iapws95.htpx(773.15*pyunits.K, 2.5449e7*pyunits.Pa)) - print(h) m.fs.unit.side_1_inlet.flow_mol[0].fix(24678.26) # mol/s m.fs.unit.side_1_inlet.enth_mol[0].fix(h) # J/mol m.fs.unit.side_1_inlet.pressure[0].fix(2.5449e7) # Pascals @@ -140,24 +141,21 @@ def test_boiler_hx(): m.fs.unit.fcorrection_dp_shell.fix(1.0) assert degrees_of_freedom(m) == 0 - + iscale.calculate_scaling_factors(m) m.fs.unit.initialize() results = solver.solve(m) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert value(m.fs.unit.side_1.properties_out[0].temperature) == \ - pytest.approx(588.07, 1) + pytest.approx(tout_1, abs=0.5) assert value(m.fs.unit.side_2.properties_out[0].temperature) == \ - pytest.approx(573.07, 1) + pytest.approx(tout_2, abs=0.5) -@pytest.mark.skipif(not iapws95.iapws95_available(), - reason="IAPWS not available") -@pytest.mark.integration -def test_units(): + +def tu( + delta_temperature_callback=delta_temperature_underwood_tune_callback): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) @@ -166,6 +164,7 @@ def test_units(): m.fs.prop_fluegas = FlueGasParameterBlock() m.fs.unit = BoilerHeatExchanger(default={ + "delta_temperature_callback": delta_temperature_callback, "side_1_property_package": m.fs.prop_steam, "side_2_property_package": m.fs.prop_fluegas, "has_pressure_change": True, @@ -176,3 +175,75 @@ def test_units(): "has_radiation": True}) assert_units_consistent(m) + + + +@pytest.mark.unit +def test_config_am(): + tc(delta_temperature_amtd_callback) + +@pytest.mark.unit +def test_config_lm(): + tc(delta_temperature_lmtd_callback) + +@pytest.mark.unit +def test_config_uw(): + tc(delta_temperature_underwood_callback) + +@pytest.mark.unit +def test_config_uwt(): + tc(delta_temperature_underwood_tune_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_boiler_hx_am(): + # arithmetic mean is pretty far off + th(delta_temperature_amtd_callback, tout_1=817.7, tout_2=720) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_boiler_hx_lm(): + th(delta_temperature_lmtd_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_boiler_hx_uw(): + th(delta_temperature_underwood_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.skipif(solver is None, reason="Solver not available") +@pytest.mark.component +def test_boiler_hx_uwt(): + th(delta_temperature_underwood_tune_callback) + + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.integration +def test_units_am(): + tu(delta_temperature_amtd_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.integration +def test_units_lm(): + tu(delta_temperature_lmtd_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.integration +def test_units_uw(): + tu(delta_temperature_underwood_callback) + +@pytest.mark.skipif(not iapws95.iapws95_available(), + reason="IAPWS not available") +@pytest.mark.integration +def test_units_uwt(): + tu(delta_temperature_underwood_tune_callback) diff --git a/idaes/power_generation/unit_models/tests/test_boilerfireside.py b/idaes/power_generation/unit_models/tests/test_boilerfireside.py index 6d0399f3b1..f80a4d13d6 100644 --- a/idaes/power_generation/unit_models/tests/test_boilerfireside.py +++ b/idaes/power_generation/unit_models/tests/test_boilerfireside.py @@ -153,9 +153,7 @@ def test_solve_unit(build_unit): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # flue gas outlet temperature assert (pytest.approx(1236.780228, abs=1e-3) == pyo.value(m.fs.unit. diff --git a/idaes/power_generation/unit_models/tests/test_downcomer.py b/idaes/power_generation/unit_models/tests/test_downcomer.py index 5326fef15a..a5fe8e18fb 100644 --- a/idaes/power_generation/unit_models/tests/test_downcomer.py +++ b/idaes/power_generation/unit_models/tests/test_downcomer.py @@ -111,9 +111,7 @@ def test_solve_unit(build_downcomer): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # check material balance assert (pytest.approx(pyo.value(m.fs.unit.control_volume.properties_in[0]. diff --git a/idaes/power_generation/unit_models/tests/test_drum.py b/idaes/power_generation/unit_models/tests/test_drum.py index 92a3689f71..93977357cb 100644 --- a/idaes/power_generation/unit_models/tests/test_drum.py +++ b/idaes/power_generation/unit_models/tests/test_drum.py @@ -142,9 +142,7 @@ def test_run_drum(build_drum): # solve model results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert degrees_of_freedom(m) == 0 assert (pytest.approx(0.6, abs=1e-3) == pyo.value(m.fs.unit.drum_level[0])) diff --git a/idaes/power_generation/unit_models/tests/test_drum1D.py b/idaes/power_generation/unit_models/tests/test_drum1D.py index 7819d62809..0f97b456ea 100644 --- a/idaes/power_generation/unit_models/tests/test_drum1D.py +++ b/idaes/power_generation/unit_models/tests/test_drum1D.py @@ -135,9 +135,7 @@ def test_run_drum1D(build_drum1D): # solve model results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert degrees_of_freedom(m) == 0 assert (pytest.approx(0.6, abs=1e-3) == pyo.value(m.fs.unit.level[0])) diff --git a/idaes/power_generation/unit_models/tests/test_heat_exchanger2D.py b/idaes/power_generation/unit_models/tests/test_heat_exchanger2D.py index 50f2f245d7..f4c3fbb83f 100644 --- a/idaes/power_generation/unit_models/tests/test_heat_exchanger2D.py +++ b/idaes/power_generation/unit_models/tests/test_heat_exchanger2D.py @@ -162,9 +162,7 @@ def test_run_unit(build_unit): # solve model results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # energy balance assert (pytest.approx(0, abs=1e-3) == diff --git a/idaes/power_generation/unit_models/tests/test_heat_exchanger_3streams.py b/idaes/power_generation/unit_models/tests/test_heat_exchanger_3streams.py index bf1e63fcee..9a55822dee 100644 --- a/idaes/power_generation/unit_models/tests/test_heat_exchanger_3streams.py +++ b/idaes/power_generation/unit_models/tests/test_heat_exchanger_3streams.py @@ -17,9 +17,8 @@ """ import pytest -from pyomo.environ import (ConcreteModel, - TerminationCondition, - SolverStatus, +from pyomo.environ import (check_optimal_termination, + ConcreteModel, value, Param) from idaes.core import FlowsheetBlock @@ -139,9 +138,7 @@ def test_run_unit(build_unit): # solve model results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - TerminationCondition.optimal - assert results.solver.status == SolverStatus.ok + assert check_optimal_termination(results) assert degrees_of_freedom(m) == 0 assert (pytest.approx(434.650, abs=1e-3) == value(m.fs.unit.side_1_outlet.temperature[0])) diff --git a/idaes/power_generation/unit_models/tests/test_steamheater.py b/idaes/power_generation/unit_models/tests/test_steamheater.py index 927fdb024f..7dc6c17098 100644 --- a/idaes/power_generation/unit_models/tests/test_steamheater.py +++ b/idaes/power_generation/unit_models/tests/test_steamheater.py @@ -115,9 +115,7 @@ def test_solve_unit(build_unit): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # check material balance assert (pytest.approx(pyo.value(m.fs.unit.control_volume.properties_in[0]. diff --git a/idaes/power_generation/unit_models/tests/test_waterpipe.py b/idaes/power_generation/unit_models/tests/test_waterpipe.py index 91c0f30b71..a09846e103 100644 --- a/idaes/power_generation/unit_models/tests/test_waterpipe.py +++ b/idaes/power_generation/unit_models/tests/test_waterpipe.py @@ -100,9 +100,7 @@ def test_solve_unit(build_unit): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # check material balance assert (pytest.approx(pyo.value(m.fs.unit.control_volume.properties_in[0]. @@ -156,9 +154,7 @@ def test_pipe_expansion(): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # check material balance assert (pytest.approx(pyo.value(m.fs.unit.control_volume.properties_in[0]. @@ -211,9 +207,7 @@ def test_pipe_noexpansion(): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) # check material balance assert (pytest.approx(pyo.value(m.fs.unit.control_volume.properties_in[0]. @@ -266,9 +260,7 @@ def test_pipe_vaporphase(): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert pyo.value(m.fs.unit.control_volume.properties_in[0].vapor_frac) == 1 diff --git a/idaes/power_generation/unit_models/tests/test_watertank.py b/idaes/power_generation/unit_models/tests/test_watertank.py index 996dc681d9..17ccf6058f 100644 --- a/idaes/power_generation/unit_models/tests/test_watertank.py +++ b/idaes/power_generation/unit_models/tests/test_watertank.py @@ -191,9 +191,7 @@ def test_run_watertank(tank_models): results = solver.solve(m, tee=True) # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) assert degrees_of_freedom(m) == 0 assert (pytest.approx(0.6, abs=1e-3) == pyo.value(m.fs.unit.tank_level[0])) diff --git a/idaes/power_generation/unit_models/tests/test_waterwall.py b/idaes/power_generation/unit_models/tests/test_waterwall.py index 801eb53d06..ff7c4f1dd2 100644 --- a/idaes/power_generation/unit_models/tests/test_waterwall.py +++ b/idaes/power_generation/unit_models/tests/test_waterwall.py @@ -27,7 +27,6 @@ # Import Pyomo libraries import pyomo.environ as pyo from pyomo.network import Arc -from pyomo.util.check_units import assert_units_consistent # Import IDAES core from idaes.core import FlowsheetBlock @@ -209,6 +208,4 @@ def test_waterwall(model): properties_out[0].enth_mol)) assert degrees_of_freedom(model) == 0 # Check for optimal solution - assert results.solver.termination_condition == \ - pyo.TerminationCondition.optimal - assert results.solver.status == pyo.SolverStatus.ok + assert pyo.check_optimal_termination(results) diff --git a/idaes/surrogate/pysmo/radial_basis_function.py b/idaes/surrogate/pysmo/radial_basis_function.py index b8b97dae32..65e5815bad 100644 --- a/idaes/surrogate/pysmo/radial_basis_function.py +++ b/idaes/surrogate/pysmo/radial_basis_function.py @@ -657,7 +657,7 @@ def pyomo_optimization(x, y): """ model = ConcreteModel() - pd.set_option('precision', 64) + pd.set_option('display.precision', 64) x_data = pd.DataFrame(x) y_data = pd.DataFrame(y) diff --git a/idaes/surrogate/sampling/scaling.py b/idaes/surrogate/sampling/scaling.py index 3a1a714ded..c24f0793a9 100644 --- a/idaes/surrogate/sampling/scaling.py +++ b/idaes/surrogate/sampling/scaling.py @@ -13,6 +13,22 @@ import pandas as pd class OffsetScaler(object): + + @staticmethod + def create_normalizing_scaler(dataframe): + """ + Creates a scaling object that normalizes the data between 0 and 1 + + Args: + dataframe: pandas DataFrame + The dataframe containing the data (usually the training data) + that will be used to compute the scaling factor and offset + """ + expected_columns = list(dataframe.columns) + offset = dataframe.min() + factor = dataframe.max()-dataframe.min() + return OffsetScaler(expected_columns, offset, factor) + @staticmethod def create_from_mean_std(dataframe): """ diff --git a/idaes/surrogate/sampling/tests/test_scaling.py b/idaes/surrogate/sampling/tests/test_scaling.py index 4c3efe0f78..9e95c44cef 100755 --- a/idaes/surrogate/sampling/tests/test_scaling.py +++ b/idaes/surrogate/sampling/tests/test_scaling.py @@ -56,6 +56,28 @@ def test_OffsetScaler(self): 'OffsetScaler was passed a factor series with an index that' \ ' does not match expected_columns. Please make sure these labels match.' + @pytest.mark.unit + def test_OffsetScaler_normalize(self): + df = pd.DataFrame({'A': [1.0,2.0,3.0,4.0,5.0], 'B':[2.0,5.0,6.0,10.0,12.0]}) + + scaler = OffsetScaler.create_normalizing_scaler(df) + pd.testing.assert_series_equal(scaler._offset, pd.Series({'A': 1.0, 'B': 2.0})) + pd.testing.assert_series_equal(scaler._factor, pd.Series({'A': 4.0, 'B': 10.0})) + + scaled_df = scaler.scale(df) + expected_scaled_df = pd.DataFrame({'A': [0.0, 0.25, 0.5, 0.75, 1.0], + 'B': [0.0, 0.3, 0.4, 0.8, 1.0]}) + pd.testing.assert_frame_equal(scaled_df, expected_scaled_df) + + df = pd.DataFrame({'A': [0.0,6.0], 'B':[3.0,7.0]}) + scaled_df = scaler.scale(df) + expected_scaled_df = pd.DataFrame({'A': [-0.25, 1.25], + 'B': [0.1, 0.5]}) + pd.testing.assert_frame_equal(scaled_df, expected_scaled_df) + + unscaled_df = scaler.unscale(scaled_df) + pd.testing.assert_frame_equal(df, unscaled_df) + @pytest.mark.unit def test_OffsetScaler_serialization(self): diff --git a/idaes/tests/test_version.py b/idaes/tests/test_version.py index 963c2e0769..724bf799eb 100644 --- a/idaes/tests/test_version.py +++ b/idaes/tests/test_version.py @@ -29,8 +29,16 @@ def test_idaes_version(): def test_ver_class(): v = ver.Version(1, 2, 3) assert str(v) == '1.2.3' + seq = [1, 2, 3] + for i, n in enumerate(v): + print("i:", i, 'n:', n) + assert n == seq[i] v = ver.Version(1, 2, 3, 'beta', 1) assert str(v) == '1.2.3.b1' + v = ver.Version(1, 2, 3, 'beta', 2, 'test') + assert str(v) == '1.2.3.b2+test' + v = ver.Version(1, 2, 3, 'beta', label='test') + assert str(v) == '1.2.3.b+test' v = ver.Version(1, 2, 3, 'development') assert str(v) == '1.2.3.dev' pytest.raises(ValueError, ver.Version, 1, 2, 3, 'howdy') diff --git a/idaes/ui/flowsheet.py b/idaes/ui/flowsheet.py index d91e1ab812..d5f494efee 100644 --- a/idaes/ui/flowsheet.py +++ b/idaes/ui/flowsheet.py @@ -635,33 +635,30 @@ def adjust_image_position(x_pos, y_pos, y_starting_pos): # Add routing config if edge/link has source or destination elements # that has routing specifications. e.g. If destination element requires # the link to connect horizontally from the left side. - if src_unit_icon.routing_config and src_port in src_unit_icon.routing_config: - if link_name not in self._out_json["routing_config"]: - self._out_json["routing_config"][link_name] = { - 'cell_index': link_index + if link_name not in self._out_json["routing_config"]: + self._out_json["routing_config"][link_name] = { + 'cell_index': link_index, + 'cell_config': { + 'gap': { + 'source': { + 'x': 0, + 'y': 0 + }, + 'destination': { + 'x': 0, + 'y': 0 + } + } } + } + cell_config_gap = self._out_json["routing_config"][link_name]["cell_config"]["gap"] + if src_unit_icon.routing_config and src_port in src_unit_icon.routing_config: # The port group has to be specified in the routing config - self._out_json["routing_config"][link_name]["source"] = src_unit_icon.routing_config[src_port] + cell_config_gap["source"] = src_unit_icon.routing_config[src_port]["gap"] if dest_unit_icon.routing_config and dest_port in dest_unit_icon.routing_config: - if link_name not in self._out_json["routing_config"]: - self._out_json["routing_config"][link_name] = { - 'cell_index': link_index - } # The port group has to be specified in the routing config - self._out_json["routing_config"][link_name]["destination"] = dest_unit_icon.routing_config[dest_port] - - # Make sure that all registered Unit Models are created - for _, unit_attrs in self.unit_models.items(): - unit_name = unit_attrs['name'] - unit_type = unit_attrs['type'] - unit_icon = UnitModelIcon(unit_type) - if unit_name in track_jointjs_elements: - # skip if unit is already added to the list of created cells - continue - cell_index = create_jointjs_image(unit_icon, unit_name, unit_type, x_pos, y_pos) - x_pos, y_pos, y_starting_pos = adjust_image_position(x_pos, y_pos, y_starting_pos) - track_jointjs_elements[unit_name] = cell_index + cell_config_gap["destination"] = dest_unit_icon.routing_config[dest_port]["gap"] def _create_image_jointjs_json(self, x_pos, y_pos, name, image, title, port_groups): # Create the jointjs for a given image @@ -706,6 +703,12 @@ def _create_link_jointjs_json( "router": {"name": "manhattan", "padding": padding}, "connector": {"name": "jumpover", "attrs": {"line": {"stroke": "#5c9adb"}}}, "id": name, + "attrs": { + "line": { + "stroke": '#979797', + "stroke-width": 2 + } + }, "labels": [ # This label MUST be first or the show/hide will fail { diff --git a/idaes/ui/fsvis/static/css/main.css b/idaes/ui/fsvis/static/css/main.css index a9b2302e11..7e449144cb 100644 --- a/idaes/ui/fsvis/static/css/main.css +++ b/idaes/ui/fsvis/static/css/main.css @@ -76,7 +76,6 @@ hr { .idaes-container { min-width: 800px; - min-height: 800px; width: 90%; height: 50%; flex-grow: 1; @@ -86,6 +85,8 @@ hr { border-radius: 5px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.23); border: solid 1px #e0e0e0; + resize: vertical; + overflow: auto; } #idaes-page-contents { @@ -288,11 +289,6 @@ input:checked + .slider:before { font-weight: bold; } -.joint-type-standard-link path { - stroke: #979797; - stroke-width: 2; -} - .joint-type-standard-link tspan { fill: #8d8d8d; } @@ -346,4 +342,24 @@ input:checked + .slider:before { .streamtable-units { opacity: 0.6; float: right; -} \ No newline at end of file +} + +/* Styling the stream table column header that corresponds to the link mouseover event */ +.link-streamtable-hover-columnheader { + border-top: 5px solid #0B79BD ; + border-left: 5px solid #0B79BD; + border-right: 5px solid #0B79BD; +} + +/* Styling the last row in the stream table column that corresponds to the link mouseover event */ +.link-streamtable-hover-lastrow { + border-bottom: 5px solid #0B79BD; + border-left: 5px solid #0B79BD; + border-right: 5px solid #0B79BD; +} + +/* Styling the grid cell in the stream table column that corresponds to the link mouseover event */ +.link-streamtable-hover { + border-left: 5px solid #0B79BD; + border-right: 5px solid #0B79BD; +} diff --git a/idaes/ui/fsvis/static/js/cell_config.js b/idaes/ui/fsvis/static/js/cell_config.js index 1765097396..d396728b12 100644 --- a/idaes/ui/fsvis/static/js/cell_config.js +++ b/idaes/ui/fsvis/static/js/cell_config.js @@ -29,86 +29,59 @@ export class JointJsCellConfig { this._model = model; } - // TODO: Implement a function that find the orthogonal path from a - // source to a target. - // Orthogonal Connector Routing: - // https://link.springer.com/content/pdf/10.1007%2F978-3-642-11805-0_22.pdf - // - // routerGapFnFactory(linkEnds, direction, minGap) { - // var router_fn = null; - // switch(direction) { - // case "left": - // router_fn = (vertices, opt, linkView) => { - // const a = linkView.getEndAnchor('source'); - // const b = linkView.getEndAnchor('target'); - // const minGap = link.destination.gap.distance; - // const x1 = Math.min(b.x - minGap, (a.x + b.x) / 2); - // const p1 = { - // x: x1, - // y: a.y - // }; - // const p2 = { - // x: x1, - // y: b.y - // }; - // return [p1, ...vertices, p2]; - // } - // break; - // default: - // throw Error('Unknown direction for Link routing'); - // } - - // return router_fn; - // } + /** + * Finding the correct cell index based on the given cell name 'cellName' + */ + findCellIndex(cellName, cellType) { + for (let i = 0; i < this.model['cells'].length; i++) { + const cell = this.model['cells'][i]; + if (cell.id == cellName && cell.type == cellType) { + return i; + } + } + // If an index is not returned, that means the link was not found + throw new Error(`Link with linkName: ${cellName} was not found`); + } /** * Generate a custom function that handles the router 'gap' option. - * The 'gap' option is specified by the users to choose the paths that the - * link will take to connect unit models. + * The 'gap' option is specified by the users to choose the two vertices + * that the link will take to connect the unit models (elements). */ - routerGapFnFactory(direction, gap) { - var router_fn = null; - switch(direction) { - case "left": - router_fn = (vertices, opt, linkView) => { - const a = linkView.getEndAnchor('source'); - const b = linkView.getEndAnchor('target'); - const minGap = gap - const x1 = Math.min(b.x - minGap, (a.x + b.x) / 2); - const p1 = { - x: x1, - y: a.y - }; - const p2 = { - x: x1, - y: b.y - }; - return [p1, ...vertices, p2]; - } - break; - default: - throw Error('Unsupported direction for Link routing'); + routerGapFnFactory(gap) { + var router_fn = (vertices, opt, linkView) => { + const a = linkView.getEndAnchor('source'); + const b = linkView.getEndAnchor('target'); + const p1 = { + x: a.x + gap.source.x, + y: a.y + + gap.source.y + }; + const p2 = { + x: b.x + gap.destination.x, + y: b.y + gap.destination.y + }; + + // TODO: Research if there's a better pre-implemented router + // function than the manhattan function. + return joint.routers.manhattan([p1, ...vertices, p2], opt, linkView); } return router_fn; } processRoutingConfig() { + const src = "source"; + const dest = "destination"; + var routing_config = this._model['routing_config']; - for (var link in routing_config) { - var routing_fn = null; - // TODO: Implement for source as well - if ('destination' in routing_config[link]) { - routing_fn = this.routerGapFnFactory( - routing_config[link].destination.gap.direction, - routing_config[link].destination.gap.distance - ); + for (var linkName in routing_config) { + var routing_fn = this.routerGapFnFactory( + routing_config[linkName].cell_config.gap + ); - this._model['cells'][routing_config[link].cell_index].router = routing_fn; - } + const cell_index = this.findCellIndex(linkName, "standard.Link"); + this._model['cells'][cell_index].router = routing_fn; } return this._model; } - - }; - \ No newline at end of file +}; diff --git a/idaes/ui/fsvis/static/js/main.js b/idaes/ui/fsvis/static/js/main.js index 33a8349da7..6c082ae64e 100644 --- a/idaes/ui/fsvis/static/js/main.js +++ b/idaes/ui/fsvis/static/js/main.js @@ -47,7 +47,7 @@ export class App { $('#idaes-fs-name').text(model.model.id); // set flowsheet name var jjCellConfig = new JointJsCellConfig(model); var processed_model = jjCellConfig.processRoutingConfig(); - this.paper.graph.fromJSON(processed_model); + this.paper.setup(processed_model); } /** diff --git a/idaes/ui/fsvis/static/js/paper.js b/idaes/ui/fsvis/static/js/paper.js index 850b9a47ed..c6bac68d9f 100644 --- a/idaes/ui/fsvis/static/js/paper.js +++ b/idaes/ui/fsvis/static/js/paper.js @@ -7,6 +7,12 @@ export class Paper { var height = 800; var gridSize = 1; + // Default values for the highlighting events + this._originalLinkStroke = "#979797"; + this._originalLinkStrokeWidth = 2; + this._highlightLinkStroke = "#0B79BD"; + this._highlightLinkStrokeWidth = 4; + this._graph = new joint.dia.Graph([], { cellNamespace: { standard } }); this._paper = new joint.dia.Paper({ model: this._graph, @@ -35,11 +41,10 @@ export class Paper { // We want all of the elements to be the same width so set the width equal to the // stream table let stream_table = document.getElementById("stream-table"); - $('#idaes-canvas').css({ width: stream_table.offsetWidth, height: stream_table.offsetHeight }); + $('#idaes-canvas').css({ height: stream_table.offsetHeight }); $("#idaes-canvas")[0].append(self._paperScroller.render().el); - self.setupEvents(); - + self.preSetupRegisterEvents(); } get graph() { @@ -64,16 +69,46 @@ export class Paper { return angle_translation[angle]; } - setupEvents() { + /** + * Register Events before the graph model is loaded + */ + preSetupRegisterEvents() { let model_id = $("#idaes-fs-name").data("flowsheetId"); let url = "/fs?id=".concat(model_id); + // Getting the main elements for the idaes canvas and the stream table + // to be able to dispatch highlighting events to the streams existing + // on paper and in the stream table + let idaesCanvas = document.querySelector('#idaes-canvas'); + let streamTable = document.querySelector('#stream-table-data'); + // Setup paper resize on window resize window.onresize = function() { let stream_table = document.getElementById("stream-table"); - $('#idaes-canvas').css({ width: stream_table.offsetWidth, height: stream_table.offsetHeight }); + $('#idaes-canvas').css({ height: stream_table.offsetHeight }); } + // Registering listeners to idaes-canvas to highlight the correct + // streams in the paper + idaesCanvas.addEventListener('HighlightStream', (event) => { + const relatedLinkElement = idaesCanvas.querySelector( + `[model-id=${event.detail.streamId}]` + ); + if (relatedLinkElement) { + relatedLinkElement.dispatchEvent(new Event('HighlightStream')); + } + }); + // Registering listeners to idaes-canvas to remove the highlight from + // the correct streams in the paper + idaesCanvas.addEventListener('RemoveHighlightStream', (event) => { + const relatedLinkElement = idaesCanvas.querySelector( + `[model-id=${event.detail.streamId}]` + ); + if (relatedLinkElement) { + relatedLinkElement.dispatchEvent(new Event('RemoveHighlightStream')); + } + }); + // /images/icons rotate 90 degrees on right click. Replaces browser // context menu self._paper.on("element:contextmenu", function(cellView, evt) { @@ -92,9 +127,10 @@ export class Paper { } }); - // Adds link tools (adding vertices, moving segments) to links when your - // mouse over - self._paper.on("link:mouseover", function(cellView, evt) { + // Setup event when a link in the paper is hovered upon + self._paper.on("link:mouseenter", function(linkView) { + // Adds link tools (adding vertices, moving segments) to links when your + // mouse over var verticesTool = new joint.linkTools.Vertices({ focusOpacity: 0.5, redundancyRemoval: true, @@ -108,13 +144,39 @@ export class Paper { verticesTool, segmentsTool ] }); - cellView.addTools(toolsView) - cellView.showTools() + linkView.addTools(toolsView); + linkView.showTools(); + + // Highlight the corresponding Link and the column in the Stream Table + const highlightStreamEvent = new CustomEvent( + 'HighlightStream', + { + detail: { + streamId: linkView.model.id + } + } + ); + streamTable.dispatchEvent(highlightStreamEvent); + idaesCanvas.dispatchEvent(highlightStreamEvent); }); - // Removes the link tools when you leave the link - self._paper.on("link:mouseout", function(cellView, evt) { - cellView.hideTools() + // Setup event when the hovering over link ends + self._paper.on("link:mouseleave", function(linkView) { + // Removes the link tools when you leave the link + linkView.hideTools(); + + // Remove the highlight from the link and the column in the + // Stream Table when the hovering ends + const removeHighlightStreamEvent = new CustomEvent( + 'RemoveHighlightStream', + { + detail: { + streamId: linkView.model.id + } + } + ); + streamTable.dispatchEvent(removeHighlightStreamEvent); + idaesCanvas.dispatchEvent(removeHighlightStreamEvent); }); // Send a post request to the server with the new this._graph @@ -160,5 +222,40 @@ export class Paper { } }); } + + /** + * Register Events after the graph model is loaded + */ + postSetupRegisterEvents() { + // Setup event listeners for the links in Paper/Graph + this._graph.getLinks().forEach((link) => { + let linkView = link.findView(this._paper); + linkView.el.addEventListener('HighlightStream', () => { + linkView.model.attr({ + line: { + stroke: this._highlightLinkStroke, + 'stroke-width': this._highlightLinkStrokeWidth + } + }); + }); + + linkView.el.addEventListener('RemoveHighlightStream', () => { + linkView.model.attr({ + line: { + stroke: this._originalLinkStroke, + 'stroke-width': this._originalLinkStrokeWidth + } + }); + }); + }); + } + + /** + * Setup the graph model + */ + setup(model) { + this._graph.fromJSON(model); + this.postSetupRegisterEvents(); + } }; diff --git a/idaes/ui/fsvis/static/js/stream_table.js b/idaes/ui/fsvis/static/js/stream_table.js index 5d8aeeecab..462fe5ecab 100644 --- a/idaes/ui/fsvis/static/js/stream_table.js +++ b/idaes/ui/fsvis/static/js/stream_table.js @@ -33,7 +33,7 @@ export class StreamTable { // There is an empty column because of the way that the pandas dataframe was oriented so // only add the columns that don't have an empty column header // Also ignore the "Units" column header - let column_header = columns[col] + let column_header = columns[col]; if (column_header !== "" && column_header !== "Units") { // If the column_header is Variable then we don't want the column to be right-aligned and we want the column to be pinned to the left so when the user scrolls the column scrolls with them if (column_header === "Variable") { @@ -51,7 +51,14 @@ export class StreamTable { } // If the column header isn't "Variable" then we assume that the contents of the column are numbers so they should be right aligned else { - column_defs.push({headerName: column_header, field: column_header, filter: 'agTextColumnFilter', sortable: true, resizable: true, cellStyle: {"text-align": "right"}}); + column_defs.push({ + headerName: column_header, + field: column_header, + filter: 'agTextColumnFilter', + sortable: true, + resizable: true, + cellStyle: {"text-align": "right"} + }); } let list_item = document.createElement("li"); let checkbox_item = document.createElement("div"); @@ -73,7 +80,6 @@ export class StreamTable { let data = data_arrays[var_index]; for (let col_index in columns) { if (columns[col_index] === "Units") { - console.log("data[col_index]:", data[col_index]); if (data[col_index] && data[col_index] !== 'None') { row_object[variable_col] = row_object[variable_col] + '' + data[col_index].html + ''; } @@ -128,5 +134,77 @@ export class StreamTable { }; }); }); + + // Getting the main elements for the idaes canvas and the stream table + // to be able to dispatch highlighting events to the streams existing + // on paper and in the stream table + let streamTable = document.querySelector('#stream-table-data'); + let idaesCanvas = document.querySelector('#idaes-canvas'); + + // Registering listeners to the stream table to highlight the correct + // streams in the stream table + streamTable.addEventListener('HighlightStream', (event) => { + var streamGridCells = streamTable.querySelectorAll( + `[col-id=${event.detail.streamId}]` + ); + streamGridCells.forEach((gridCell, index) => { + if (gridCell.getAttribute('role') == 'columnheader') { + gridCell.classList.add('link-streamtable-hover-columnheader'); + } + else if (index == streamGridCells.length - 1) { + gridCell.classList.add('link-streamtable-hover-lastrow'); + } + else { + gridCell.classList.add('link-streamtable-hover'); + } + }); + }); + + // Registering listeners to idaes-canvas to remove the highlight from + // the correct streams in the stream table + streamTable.addEventListener('RemoveHighlightStream', (event) => { + var streamGridCells = streamTable.querySelectorAll( + `[col-id=${event.detail.streamId}]` + ); + streamGridCells.forEach((gridCell) => { + gridCell.classList.remove('link-streamtable-hover-columnheader'); + gridCell.classList.remove('link-streamtable-hover-lastrow'); + gridCell.classList.remove('link-streamtable-hover'); + }); + }); + + let streamGridCells = document.querySelectorAll('[col-id]'); + streamGridCells.forEach((gridCell) => { + // When the mouse hovers over a grid cell, the link as well as the + // stream column that represents the correct stream will be highlighted. + gridCell.addEventListener('mouseenter', function(event) { + const highlightStreamEvent = new CustomEvent( + 'HighlightStream', + { + detail: { + streamId: event.target.attributes['col-id'].value + } + } + ); + streamTable.dispatchEvent(highlightStreamEvent); + idaesCanvas.dispatchEvent(highlightStreamEvent); + }); + + // When the mouse leaves a grid cell, the link as well as the + // stream column that represents the correct stream will remove + // the highlighting feature. + gridCell.addEventListener('mouseleave', function(event) { + const removeHighlightStreamEvent = new CustomEvent( + 'RemoveHighlightStream', + { + detail: { + streamId: event.target.attributes['col-id'].value + } + } + ); + streamTable.dispatchEvent(removeHighlightStreamEvent); + idaesCanvas.dispatchEvent(removeHighlightStreamEvent); + }); + }); }; }; diff --git a/idaes/ui/fsvis/tests/test_fsvis.py b/idaes/ui/fsvis/tests/test_fsvis.py index a8f513d09f..a7ed892da0 100644 --- a/idaes/ui/fsvis/tests/test_fsvis.py +++ b/idaes/ui/fsvis/tests/test_fsvis.py @@ -265,16 +265,23 @@ def test_visualize_save_loadfromsaved(flash_model, save_files_prefix): name = "flash_tvslfs" save_dir = Path(save_files_prefix).parent # save initial - result = fsvis.visualize(flowsheet, name, save_dir=save_dir) + result = fsvis.visualize( + flowsheet, name, save_dir=save_dir, browser=False + ) path_base = save_dir / (name + ".json") assert path_base.exists() # this time, should use loaded one # there should still be only one file - result = fsvis.visualize(flowsheet, name, save_dir=save_dir) + result = fsvis.visualize( + flowsheet, name, save_dir=save_dir, browser=False + ) path_v1 = save_dir / (name + "-1.json") assert not path_v1.exists() # same behavior with explicit flag - result = fsvis.visualize(flowsheet, name, save_dir=save_dir, load_from_saved=True) + result = fsvis.visualize( + flowsheet, name, save_dir=save_dir, browser=False, + load_from_saved=True + ) assert not path_v1.exists() diff --git a/idaes/ui/icons.py b/idaes/ui/icons.py index 5f616488d2..0727db4be3 100644 --- a/idaes/ui/icons.py +++ b/idaes/ui/icons.py @@ -10,6 +10,8 @@ # Please see the files COPYRIGHT.md and LICENSE.md for full copyright and # license information. ################################################################################# +import os +import json from typing import Dict @@ -31,14 +33,21 @@ def __init__(self, unit_model: str = None, default: str = DEFAULT): KeyError if unit_model name is not found and `default` arg is falsy (e.g. None or "") """ if unit_model is None: - unit_model = self.DEFAULT + unit_model = default self._model = unit_model + + # Loading the Unit Models mappings + dir_path = os.path.dirname(os.path.realpath(__file__)) + mappings_file = os.path.join(dir_path, "mappings", "mappings.json") + with open(mappings_file, 'r') as mappings_f: + self._mapping = json.load(mappings_f) + try: self._model_details = self._mapping[unit_model] except KeyError: - if not default: - raise - self._model_details = self._mapping[self.DEFAULT] + if not default or default not in self._mapping: + raise ValueError("Specified unit model doesn't exist, and the default model is not set.") + self._model_details = self._mapping[default] self._pos = self._build_link_positions() @property @@ -114,10 +123,7 @@ def routing_config(self) -> Dict: Returns: The routing configuration (see example result) """ - if "routing_config" in self._model_details: - return self._model_details["routing_config"] - else: - return {} + return self._model_details["routing_config"] def _build_link_positions(self) -> Dict: """Fill in boilerplate based on raw info and place built value in class cache. @@ -150,550 +156,4 @@ def _build_link_positions(self) -> Dict: # - Use 'cstr' as your template for new entries # - Do not remove in/out entries in existing entries, or arcs won't connect # TODO: Move this mapping to its own directory/files. - _mapping = { - "cstr": { - "image": "reactor_c.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 15, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "flash": { - "image": "flash.svg", - "port_groups": { - "bottom": { - "position": { - "name": "bottom", - "args": { - "x": 25, - "y": 50, - "dx": 1, - "dy": 1 - } - } - }, - "in": { - "position": { - "name": "left", - "args": { - "x": 8, - "y": 25, - "dx": 1, - "dy": 1 - } - } - }, - "top": { - "position": { - "name": "top", - "args": { - "x": 25, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - # added by AR - "out": { - "position": { - "name": "left", - "args": { - "x": 45, - "y": 45, - "dx": 1, - "dy": 1 - } - } - }, - #end - } - }, - "gibbs_reactor": { - "image": "reactor_g.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 5, - "y": 10, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 45, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "heat_exchanger": { - "image": "heat_exchanger_1.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "heater": { - "image": "heater_2.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 6, - "y": 25, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 43, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "heat_exchanger_1D": { - "image": "heat_exchanger_1.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 15, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "mixer": { - "image": "mixer.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": {} - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - }, - "routing_config": { - "in": { - "gap": { - "direction": "left", - "distance": 30 - } - } - } - }, - "plug_flow_reactor": { - "image": "reactor_pfr.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 15, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "pressure_changer": { - "image": "compressor.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "separator": { - "image": "splitter.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "right", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "stoichiometric_reactor": { - "image": "reactor_s.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 5, - "y": 10, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 45, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "equilibrium_reactor": { - "image": "reactor_e.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 5, - "y": 10, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 45, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "feed": { - "image": "feed.svg", - "port_groups": { - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "product": { - "image": "product.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "feed_flash": { - "image": "feed.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 25, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 25, - "y": 50, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "statejunction": { - "image": "NONE", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 15, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "translator": { - "image": "NONE", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 15, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 45, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "packed_column": { - "image": "packed_column_1.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 10, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 40, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "tray_column": { - "image": "tray_column_1.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 10, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 40, - "dx": 1, - "dy": 1 - } - } - } - } - }, - "default": { - "image": "default.svg", - "port_groups": { - "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 0, - "dx": 1, - "dy": 1 - } - } - }, - "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 50, - "dx": 1, - "dy": 1 - } - } - } - } - }, - } + _mapping = {} diff --git a/idaes/ui/mappings/mappings.json b/idaes/ui/mappings/mappings.json new file mode 100644 index 0000000000..b884e98d42 --- /dev/null +++ b/idaes/ui/mappings/mappings.json @@ -0,0 +1,791 @@ +{ + "cstr": { + "image": "reactor_c.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 15, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "flash": { + "image": "flash.svg", + "port_groups": { + "bottom": { + "position": { + "name": "bottom", + "args": { + "x": 25, + "y": 50, + "dx": 1, + "dy": 1 + } + } + }, + "in": { + "position": { + "name": "left", + "args": { + "dx": 8, + "dy": 0 + } + } + }, + "top": { + "position": { + "name": "top", + "args": { + "x": 25, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "right", + "args": { + "dx": -8, + "dy": 0 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "gibbs_reactor": { + "image": "reactor_g.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 5, + "y": 10, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 45, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "heat_exchanger": { + "image": "heat_exchanger_1.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 2, + "y": 25, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "heater": { + "image": "heater_2.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 6, + "y": 25, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 43, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "heat_exchanger_1D": { + "image": "heat_exchanger_1.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 15, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "mixer": { + "image": "mixer.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": {} + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "plug_flow_reactor": { + "image": "reactor_pfr.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 15, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "pressure_changer": { + "image": "compressor.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": {} + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 50, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "separator": { + "image": "splitter.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 2, + "y": 25, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "right", + "args": {} + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "stoichiometric_reactor": { + "image": "reactor_s.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 5, + "y": 10, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 45, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "equilibrium_reactor": { + "image": "reactor_e.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 5, + "y": 10, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 45, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "feed": { + "image": "feed.svg", + "port_groups": { + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "product": { + "image": "product.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 2, + "y": 25, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + } + } + }, + "feed_flash": { + "image": "feed.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 25, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 25, + "y": 50, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "statejunction": { + "image": "NONE", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 15, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "translator": { + "image": "NONE", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 15, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 45, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "packed_column": { + "image": "packed_column_1.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 10, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 40, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "tray_column": { + "image": "tray_column_1.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 10, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 40, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + }, + "default": { + "image": "default.svg", + "port_groups": { + "in": { + "position": { + "name": "left", + "args": { + "x": 2, + "y": 0, + "dx": 1, + "dy": 1 + } + } + }, + "out": { + "position": { + "name": "left", + "args": { + "x": 48, + "y": 50, + "dx": 1, + "dy": 1 + } + } + } + }, + "routing_config": { + "in": { + "gap": { + "x": -30, + "y": 0 + } + }, + "out": { + "gap": { + "x": 30, + "y": 0 + } + } + } + } +} \ No newline at end of file diff --git a/idaes/ui/tests/demo_flowsheet.json b/idaes/ui/tests/demo_flowsheet.json index b1e06a77ac..ca8516ad8d 100644 --- a/idaes/ui/tests/demo_flowsheet.json +++ b/idaes/ui/tests/demo_flowsheet.json @@ -158,6 +158,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -259,10 +265,8 @@ "markup": "", "position": { "args": { - "dx": 1, - "dy": 1, - "x": 8, - "y": 25 + "dx": 8, + "dy": 0 }, "name": "left" } @@ -279,12 +283,10 @@ "markup": "", "position": { "args": { - "dx": 1, - "dy": 1, - "x": 45, - "y": 45 + "dx": -8, + "dy": 0 }, - "name": "left" + "name": "right" } }, "top": { @@ -335,6 +337,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -444,6 +452,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -553,6 +567,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -741,6 +761,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -850,6 +876,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1038,6 +1070,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1147,6 +1185,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1335,6 +1379,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1444,6 +1494,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1553,6 +1609,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1662,6 +1724,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1850,6 +1918,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1959,6 +2033,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2094,12 +2174,7 @@ }, "markup": "", "position": { - "args": { - "dx": 1, - "dy": 1, - "x": 2, - "y": 25 - }, + "args": {}, "name": "left" } }, @@ -2117,7 +2192,7 @@ "args": { "dx": 1, "dy": 1, - "x": 48, + "x": 50, "y": 25 }, "name": "left" @@ -2147,6 +2222,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2256,6 +2337,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2391,12 +2478,7 @@ }, "markup": "", "position": { - "args": { - "dx": 1, - "dy": 1, - "x": 2, - "y": 25 - }, + "args": {}, "name": "left" } }, @@ -2414,7 +2496,7 @@ "args": { "dx": 1, "dy": 1, - "x": 48, + "x": 50, "y": 25 }, "name": "left" @@ -2444,6 +2526,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2553,6 +2641,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2741,6 +2835,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2850,6 +2950,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2985,12 +3091,7 @@ }, "markup": "", "position": { - "args": { - "dx": 1, - "dy": 1, - "x": 2, - "y": 25 - }, + "args": {}, "name": "left" } }, @@ -3008,7 +3109,7 @@ "args": { "dx": 1, "dy": 1, - "x": 48, + "x": 50, "y": 25 }, "name": "left" @@ -3038,6 +3139,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -3147,6 +3254,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -3996,23 +4109,335 @@ } }, "routing_config": { + "s01": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 2 + }, + "s02": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 4 + }, + "s_inlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 11 + }, "s_inlet_1_1": { - "cell_index": 25, - "destination": { + "cell_config": { "gap": { - "direction": "left", - "distance": 30 + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } } - } + }, + "cell_index": 25 + }, + "s_inlet_2": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 16 }, "s_inlet_2_1": { - "cell_index": 27, - "destination": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 27 + }, + "s_inlet_3": { + "cell_config": { "gap": { - "direction": "left", - "distance": 30 + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } } - } + }, + "cell_index": 21 + }, + "s_inlet_4": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 30 + }, + "s_inlet_5": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 35 + }, + "s_inlet_6": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 40 + }, + "s_inlet_7": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 45 + }, + "s_inlet_8": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 50 + }, + "s_liq_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 6 + }, + "s_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 13 + }, + "s_outlet_2": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 18 + }, + "s_outlet_3": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 23 + }, + "s_outlet_4": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 32 + }, + "s_outlet_5": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 37 + }, + "s_outlet_6": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 42 + }, + "s_outlet_7": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 47 + }, + "s_outlet_8": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 52 + }, + "s_vap_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 8 } } } \ No newline at end of file diff --git a/idaes/ui/tests/flash_flowsheet.json b/idaes/ui/tests/flash_flowsheet.json index 01be1cef08..f148d0bf8f 100644 --- a/idaes/ui/tests/flash_flowsheet.json +++ b/idaes/ui/tests/flash_flowsheet.json @@ -1,128 +1,40 @@ { - "model": { - "stream_table": { - "index": [], - "columns": [ - "", - "Variable" - ], - "data": [] - }, - "id": "demo", - "unit_models": { - "flash": { - "type": "flash", - "image": "/images/icons/flash.svg", - "performance_contents": { - "0": { - "Variable": "Heat Duty", - "Value": 0 - }, - "1": { - "Variable": "Pressure Change", - "Value": 0 - } - }, - "stream_contents": { - "0": { - "Variable": "flow_mol", - "Inlet": "-Inf", - "Vapor Outlet": 0.5, - "Liquid Outlet": 0.5 - }, - "1": { - "Variable": "mole_frac_comp benzene", - "Inlet": 0.5, - "Vapor Outlet": 0.5, - "Liquid Outlet": 0.5 - }, - "2": { - "Variable": "mole_frac_comp toluene", - "Inlet": 0.5, - "Vapor Outlet": 0.5, - "Liquid Outlet": 0.5 - }, - "3": { - "Variable": "temperature", - "Inlet": "Inf", - "Vapor Outlet": 298.15, - "Liquid Outlet": 298.15 - }, - "4": { - "Variable": "pressure", - "Inlet": "NaN", - "Vapor Outlet": 101325.0, - "Liquid Outlet": 101325.0 - } - } - }, - "inlet_1": { - "type": "feed", - "image": "/images/icons/feed.svg" - }, - "liq_outlet_1": { - "type": "product", - "image": "/images/icons/product.svg" - }, - "vap_outlet_1": { - "type": "product", - "image": "/images/icons/product.svg" - } - }, - "arcs": { - "s_inlet_1": { - "source": "inlet_1", - "dest": "flash", - "label": "feed info" - }, - "s_liq_outlet_1": { - "source": "flash", - "dest": "liq_outlet_1", - "label": "product info" - }, - "s_vap_outlet_1": { - "source": "flash", - "dest": "vap_outlet_1", - "label": "product info" - } - } - }, - "routing_config": {}, "cells": [ { - "type": "standard.Image", - "position": { - "x": 10, - "y": 10 - }, - "size": { - "width": 50, - "height": 50 - }, "angle": 0, + "attrs": { + "image": { + "xlinkHref": "/images/icons/feed.svg" + }, + "label": { + "text": "inlet_1" + }, + "root": { + "title": "feed" + } + }, "id": "inlet_1", - "z": 1, "ports": { "groups": { "out": { - "position": { - "name": "left", - "args": { - "x": 48, - "y": 25, - "dx": 1, - "dy": 1 - } - }, "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" + "markup": "", + "position": { + "args": { + "dx": 1, + "dy": 1, + "x": 48, + "y": 25 + }, + "name": "left" + } } }, "items": [ @@ -132,112 +44,108 @@ } ] }, + "position": { + "x": 10, + "y": 10 + }, + "size": { + "height": 50, + "width": 50 + }, + "type": "standard.Image", + "z": 1 + }, + { + "angle": 0, "attrs": { "image": { - "xlinkHref": "/images/icons/feed.svg" + "xlinkHref": "/images/icons/flash.svg" }, "label": { - "text": "inlet_1" + "text": "flash" }, "root": { - "title": "feed" + "title": "flash" } - } - }, - { - "type": "standard.Image", - "position": { - "x": 110, - "y": 110 }, - "size": { - "width": 50, - "height": 50 - }, - "angle": 0, "id": "flash", - "z": 1, "ports": { "groups": { "bottom": { - "position": { - "name": "bottom", - "args": { - "x": 25, - "y": 50, - "dx": 1, - "dy": 1 - } - }, "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" - }, - "in": { + "markup": "", "position": { - "name": "left", "args": { - "x": 8, - "y": 25, "dx": 1, - "dy": 1 - } - }, + "dy": 1, + "x": 25, + "y": 50 + }, + "name": "bottom" + } + }, + "in": { "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" - }, - "top": { + "markup": "", "position": { - "name": "top", "args": { - "x": 25, - "y": 0, - "dx": 1, - "dy": 1 - } - }, + "dx": 8, + "dy": 0 + }, + "name": "left" + } + }, + "out": { "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" - }, - "out": { + "markup": "", "position": { - "name": "left", "args": { - "x": 45, - "y": 45, - "dx": 1, - "dy": 1 - } - }, + "dx": -8, + "dy": 0 + }, + "name": "right" + } + }, + "top": { "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" + "markup": "", + "position": { + "args": { + "dx": 1, + "dy": 1, + "x": 25, + "y": 0 + }, + "name": "top" + } } }, "items": [ @@ -255,39 +163,31 @@ } ] }, - "attrs": { - "image": { - "xlinkHref": "/images/icons/flash.svg" - }, - "label": { - "text": "flash" - }, - "root": { - "title": "flash" - } - } - }, - { - "type": "standard.Link", - "source": { - "id": "inlet_1", - "port": 0 + "position": { + "x": 110, + "y": 110 }, - "target": { - "id": "flash", - "port": 1 + "size": { + "height": 50, + "width": 50 }, - "router": { - "name": "manhattan", - "padding": 10 + "type": "standard.Image", + "z": 1 + }, + { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } }, "connector": { - "name": "jumpover", "attrs": { "line": { "stroke": "#5c9adb" } - } + }, + "name": "jumpover" }, "id": "s_inlet_1", "labels": [ @@ -295,15 +195,15 @@ "attrs": { "rect": { "fill": "#d7dce0", + "fill-opacity": 0, "stroke": "white", - "stroke-width": 0, - "fill-opacity": 0 + "stroke-width": 0 }, "text": { - "text": "feed info", + "display": "none", "fill": "black", - "text-anchor": "left", - "display": "none" + "text": "feed info", + "text-anchor": "left" } }, "position": { @@ -319,42 +219,56 @@ } } ], + "router": { + "name": "manhattan", + "padding": 10 + }, + "source": { + "id": "inlet_1", + "port": 0 + }, + "target": { + "id": "flash", + "port": 1 + }, + "type": "standard.Link", "z": 2 }, { - "type": "standard.Image", - "position": { - "x": 210, - "y": 210 - }, - "size": { - "width": 50, - "height": 50 - }, "angle": 0, + "attrs": { + "image": { + "xlinkHref": "/images/icons/product.svg" + }, + "label": { + "text": "liq_outlet_1" + }, + "root": { + "title": "product" + } + }, "id": "liq_outlet_1", - "z": 1, "ports": { "groups": { "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - }, "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" + "markup": "", + "position": { + "args": { + "dx": 1, + "dy": 1, + "x": 2, + "y": 25 + }, + "name": "left" + } } }, "items": [ @@ -364,39 +278,31 @@ } ] }, - "attrs": { - "image": { - "xlinkHref": "/images/icons/product.svg" - }, - "label": { - "text": "liq_outlet_1" - }, - "root": { - "title": "product" - } - } - }, - { - "type": "standard.Link", - "source": { - "id": "flash", - "port": 2 + "position": { + "x": 210, + "y": 210 }, - "target": { - "id": "liq_outlet_1", - "port": 3 + "size": { + "height": 50, + "width": 50 }, - "router": { - "name": "manhattan", - "padding": 10 + "type": "standard.Image", + "z": 1 + }, + { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } }, "connector": { - "name": "jumpover", "attrs": { "line": { "stroke": "#5c9adb" } - } + }, + "name": "jumpover" }, "id": "s_liq_outlet_1", "labels": [ @@ -404,15 +310,15 @@ "attrs": { "rect": { "fill": "#d7dce0", + "fill-opacity": 0, "stroke": "white", - "stroke-width": 0, - "fill-opacity": 0 + "stroke-width": 0 }, "text": { - "text": "product info", + "display": "none", "fill": "black", - "text-anchor": "left", - "display": "none" + "text": "product info", + "text-anchor": "left" } }, "position": { @@ -428,42 +334,56 @@ } } ], + "router": { + "name": "manhattan", + "padding": 10 + }, + "source": { + "id": "flash", + "port": 2 + }, + "target": { + "id": "liq_outlet_1", + "port": 3 + }, + "type": "standard.Link", "z": 2 }, { - "type": "standard.Image", - "position": { - "x": 310, - "y": 310 - }, - "size": { - "width": 50, - "height": 50 - }, "angle": 0, + "attrs": { + "image": { + "xlinkHref": "/images/icons/product.svg" + }, + "label": { + "text": "vap_outlet_1" + }, + "root": { + "title": "product" + } + }, "id": "vap_outlet_1", - "z": 1, "ports": { "groups": { "in": { - "position": { - "name": "left", - "args": { - "x": 2, - "y": 25, - "dx": 1, - "dy": 1 - } - }, "attrs": { "rect": { + "height": 0, "stroke": "#000000", "stroke-width": 0, - "width": 0, - "height": 0 + "width": 0 } }, - "markup": "" + "markup": "", + "position": { + "args": { + "dx": 1, + "dy": 1, + "x": 2, + "y": 25 + }, + "name": "left" + } } }, "items": [ @@ -473,39 +393,31 @@ } ] }, - "attrs": { - "image": { - "xlinkHref": "/images/icons/product.svg" - }, - "label": { - "text": "vap_outlet_1" - }, - "root": { - "title": "product" - } - } - }, - { - "type": "standard.Link", - "source": { - "id": "flash", - "port": 4 + "position": { + "x": 310, + "y": 310 }, - "target": { - "id": "vap_outlet_1", - "port": 5 + "size": { + "height": 50, + "width": 50 }, - "router": { - "name": "manhattan", - "padding": 10 + "type": "standard.Image", + "z": 1 + }, + { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } }, "connector": { - "name": "jumpover", "attrs": { "line": { "stroke": "#5c9adb" } - } + }, + "name": "jumpover" }, "id": "s_vap_outlet_1", "labels": [ @@ -513,15 +425,15 @@ "attrs": { "rect": { "fill": "#d7dce0", + "fill-opacity": 0, "stroke": "white", - "stroke-width": 0, - "fill-opacity": 0 + "stroke-width": 0 }, "text": { - "text": "product info", + "display": "none", "fill": "black", - "text-anchor": "left", - "display": "none" + "text": "product info", + "text-anchor": "left" } }, "position": { @@ -537,7 +449,155 @@ } } ], + "router": { + "name": "manhattan", + "padding": 10 + }, + "source": { + "id": "flash", + "port": 4 + }, + "target": { + "id": "vap_outlet_1", + "port": 5 + }, + "type": "standard.Link", "z": 2 } - ] + ], + "model": { + "arcs": { + "s_inlet_1": { + "dest": "flash", + "label": "feed info", + "source": "inlet_1" + }, + "s_liq_outlet_1": { + "dest": "liq_outlet_1", + "label": "product info", + "source": "flash" + }, + "s_vap_outlet_1": { + "dest": "vap_outlet_1", + "label": "product info", + "source": "flash" + } + }, + "id": "demo", + "stream_table": { + "columns": [ + "", + "Variable" + ], + "data": [], + "index": [] + }, + "unit_models": { + "flash": { + "image": "/images/icons/flash.svg", + "performance_contents": { + "0": { + "Value": 0, + "Variable": "Heat Duty" + }, + "1": { + "Value": 0, + "Variable": "Pressure Change" + } + }, + "stream_contents": { + "0": { + "Inlet": "-Inf", + "Liquid Outlet": 0.5, + "Vapor Outlet": 0.5, + "Variable": "flow_mol" + }, + "1": { + "Inlet": 0.5, + "Liquid Outlet": 0.5, + "Vapor Outlet": 0.5, + "Variable": "mole_frac_comp benzene" + }, + "2": { + "Inlet": 0.5, + "Liquid Outlet": 0.5, + "Vapor Outlet": 0.5, + "Variable": "mole_frac_comp toluene" + }, + "3": { + "Inlet": "Inf", + "Liquid Outlet": 298.15, + "Vapor Outlet": 298.15, + "Variable": "temperature" + }, + "4": { + "Inlet": "NaN", + "Liquid Outlet": 101325.0, + "Vapor Outlet": 101325.0, + "Variable": "pressure" + } + }, + "type": "flash" + }, + "inlet_1": { + "image": "/images/icons/feed.svg", + "type": "feed" + }, + "liq_outlet_1": { + "image": "/images/icons/product.svg", + "type": "product" + }, + "vap_outlet_1": { + "image": "/images/icons/product.svg", + "type": "product" + } + } + }, + "routing_config": { + "s_inlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 2 + }, + "s_liq_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 4 + }, + "s_vap_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 6 + } + } } \ No newline at end of file diff --git a/idaes/ui/tests/serialized_boiler_flowsheet.json b/idaes/ui/tests/serialized_boiler_flowsheet.json index 8a86174992..506fcfa84b 100644 --- a/idaes/ui/tests/serialized_boiler_flowsheet.json +++ b/idaes/ui/tests/serialized_boiler_flowsheet.json @@ -167,6 +167,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -308,6 +314,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -441,6 +453,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -582,6 +600,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -710,6 +734,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -810,12 +840,7 @@ }, "markup": "", "position": { - "args": { - "dx": 1, - "dy": 1, - "x": 48, - "y": 25 - }, + "args": {}, "name": "right" } } @@ -847,6 +872,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -988,6 +1019,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1042,6 +1079,12 @@ "z": 2 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1174,6 +1217,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1228,6 +1277,12 @@ "z": 2 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1282,6 +1337,12 @@ "z": 2 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1391,6 +1452,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1500,6 +1567,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1609,6 +1682,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1718,6 +1797,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1827,6 +1912,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -1936,6 +2027,12 @@ "z": 1 }, { + "attrs": { + "line": { + "stroke": "#979797", + "stroke-width": 2 + } + }, "connector": { "attrs": { "line": { @@ -2707,31 +2804,259 @@ }, "routing_config": { "FSHtoATMP1": { - "cell_index": 10, - "destination": { + "cell_config": { "gap": { - "direction": "left", - "distance": 30 + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } } - } + }, + "cell_index": 10 + }, + "econ2ww": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 2 + }, + "fg_fsh2PrSH": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 15 + }, + "fg_fsh2_separator": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 12 + }, + "fg_fsh2rh": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 14 + }, + "fg_mix2econ": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 19 }, "fg_prsh2mix": { - "cell_index": 18, - "destination": { + "cell_config": { "gap": { - "direction": "left", - "distance": 30 + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } } - } + }, + "cell_index": 18 }, "fg_rhtomix": { - "cell_index": 17, - "destination": { + "cell_config": { "gap": { - "direction": "left", - "distance": 30 + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } } - } + }, + "cell_index": 17 + }, + "plsh2fsh": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 8 + }, + "prsh2plsh": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 6 + }, + "s_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 21 + }, + "s_side_1_inlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 23 + }, + "s_side_1_inlet_2": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 29 + }, + "s_side_1_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 31 + }, + "s_side_2_inlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 27 + }, + "s_side_2_outlet_1": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 25 + }, + "ww2prsh": { + "cell_config": { + "gap": { + "destination": { + "x": -30, + "y": 0 + }, + "source": { + "x": 30, + "y": 0 + } + } + }, + "cell_index": 4 } } } \ No newline at end of file diff --git a/idaes/ui/tests/test_icons.py b/idaes/ui/tests/test_icons.py index cbdf6bf8c0..bc10f2de6a 100644 --- a/idaes/ui/tests/test_icons.py +++ b/idaes/ui/tests/test_icons.py @@ -17,7 +17,8 @@ @pytest.mark.parametrize( "test_input,expected", - [("cstr", "reactor_c.svg"), + [("default", "default.svg"), + ("cstr", "reactor_c.svg"), ("equilibrium_reactor", "reactor_e.svg"), ("gibbs_reactor", "reactor_g.svg"), ("plug_flow_reactor", "reactor_pfr.svg"), @@ -40,6 +41,8 @@ @pytest.mark.unit def test_icon_mapping(test_input, expected): assert UnitModelIcon(test_input).icon == expected + with pytest.raises(ValueError): + UnitModelIcon("unregistered_model", "not_default") @pytest.mark.parametrize( "model_name,expected", diff --git a/idaes/util/download_bin.py b/idaes/util/download_bin.py index 9ca3a8ae35..abc739b723 100644 --- a/idaes/util/download_bin.py +++ b/idaes/util/download_bin.py @@ -15,6 +15,7 @@ import idaes.logger as idaeslog import tarfile import idaes +from idaes.config import extra_binaries from shutil import copyfile from pyomo.common.download import FileDownloader import urllib @@ -182,6 +183,14 @@ def _add_pack(name): furl.append("/".join([url, f])) for e in extra: + if e not in extra_binaries: + _log.warning(f"Unknown extra package {e}, not installed.") + continue + if platform not in extra_binaries[e]: + _log.warning( + f"Extra package {e} not available for {platform}, not installed." + ) + continue _add_pack(e) # you have to explicitly ask for extras so assume you want if not extras_only: _add_pack("lib") diff --git a/setup.py b/setup.py index ca5a3b6942..1c172f738d 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def rglob(path, glob): # idaes core / dmf "backports.shutil_get_terminal_size", "bunch", - "click<=7.1.2", # problems with 8.x + "click", "colorama", "flask", # for ui/fsvis "flask-cors", @@ -93,7 +93,9 @@ def rglob(path, glob): "tensorflow", # idaes.surrogate.keras_surrogate # FIXME update requirement once PyPI distribution is updated # (8fef0fa2ca is the commit on main where #128 was merged in) - "gridx-prescient @ https://github.com/grid-parity-exchange/Prescient/archive/8fef0fa2ca.zip" # idaes.tests.prescient + "gridx-prescient @ https://github.com/grid-parity-exchange/Prescient/archive/8fef0fa2ca.zip", # idaes.tests.prescient + # A Lee 11-Jan-22: no precompiled version of CoolProp available for Pyhton 3.9 + "coolprop; python_version < '3.9'", # idaes.generic_models.properties.general.coolprop ], }, package_data={