Beispiel #1
0
def list_of_floats(arg):
    '''Domain validator for lists of floats

    Args:
        arg : argument to be cast to list of floats and validated

    Returns:
        List of floats
    '''
    deprecation_warning(
        "The list_of_floats function is deprecated.  Use the ListOf(float) "
        "validator from pyomo.common.config instead.", version='1.11',
        remove_in='1.13')

    return ListOf(float)(arg)
Beispiel #2
0
def list_of_phase_types(arg):
    '''Domain validator for lists of PhaseTypes

    Args:
        arg : argument to be cast to list of PhaseTypes and validated

    Returns:
        List of PhaseTypes
    '''
    deprecation_warning(
        "The list_of_phase_types function is deprecated.  Use the "
        "ListOf(PhaseType) validator from pyomo.common.config instead.",
        version='1.11', remove_in='1.13')

    return ListOf(PhaseType)(arg)
Beispiel #3
0
class MixerData(UnitModelBlockData):
    """
    This is a general purpose model for a Mixer block with the IDAES modeling
    framework. This block can be used either as a stand-alone Mixer unit
    operation, or as a sub-model within another unit operation.

    This model creates a number of StateBlocks to represent the incoming
    streams, then writes a set of phase-component material balances, an
    overall enthalpy balance and a momentum balance (2 options) linked to a
    mixed-state StateBlock. The mixed-state StateBlock can either be specified
    by the user (allowing use as a sub-model), or created by the Mixer.

    When being used as a sub-model, Mixer should only be used when a set
    of new StateBlocks are required for the streams to be mixed. It should not
    be used to mix streams from mutiple ControlVolumes in a single unit model -
    in these cases the unit model developer should write their own mixing
    equations.
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Indicates whether this model will be dynamic or not,
**default** = False. Mixer blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Mixer blocks do not contain holdup, thus this must be
False.""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            doc="""Property parameter object used to define property
calculations, **default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "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(
        "inlet_list",
        ConfigValue(
            domain=ListOf(str),
            description="List of inlet names",
            doc="""A list containing names of inlets,
**default** - None.
**Valid values:** {
**None** - use num_inlets argument,
**list** - a list of names to use for inlets.}""",
        ),
    )
    CONFIG.declare(
        "num_inlets",
        ConfigValue(
            domain=int,
            description="Number of inlets to unit",
            doc="""Argument indicating number (int) of inlets to construct, not
used if inlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use inlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of inlets to create (will be named with sequential integers
from 1 to num_inlets).}""",
        ),
    )
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**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(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Calculate phase equilibrium in mixed stream",
            doc="""Argument indicating whether phase equilibrium should be
calculated for the resulting mixed stream,
**default** - False.
**Valid values:** {
**True** - calculate phase equilibrium in mixed stream,
**False** - do not calculate equilibrium in mixed stream.}""",
        ),
    )
    CONFIG.declare(
        "energy_mixing_type",
        ConfigValue(
            default=MixingType.extensive,
            domain=MixingType,
            description="Method to use when mixing energy flows",
            doc="""Argument indicating what method to use when mixing energy
flows of incoming streams,
**default** - MixingType.extensive.
**Valid values:** {
**MixingType.none** - do not include energy mixing equations,
**MixingType.extensive** - mix total enthalpy flows of each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_mixing_type",
        ConfigValue(
            default=MomentumMixingType.minimize,
            domain=MomentumMixingType,
            description="Method to use when mixing momentum/pressure",
            doc="""Argument indicating what method to use when mixing momentum/
pressure of incoming streams,
**default** - MomentumMixingType.minimize.
**Valid values:** {
**MomentumMixingType.none** - do not include momentum mixing equations,
**MomentumMixingType.minimize** - mixed stream has pressure equal to the
minimimum pressure of the incoming streams (uses smoothMin operator),
**MomentumMixingType.equality** - enforces equality of pressure in mixed and
all incoming streams.,
**MomentumMixingType.minimize_and_equality** - add constraints for pressure
equal to the minimum pressure of the inlets and constraints for equality of
pressure in mixed and all incoming streams. When the model is initially built,
the equality constraints are deactivated.  This option is useful for switching
between flow and pressure driven simulations.}""",
        ),
    )
    CONFIG.declare(
        "mixed_state_block",
        ConfigValue(
            default=None,
            domain=is_state_block,
            description="Existing StateBlock to use as mixed stream",
            doc="""An existing state block to use as the outlet stream from the
Mixer block,
**default** - None.
**Valid values:** {
**None** - create a new StateBlock for the mixed stream,
**StateBlock** - a StateBock to use as the destination for the mixed stream.}
""",
        ),
    )
    CONFIG.declare(
        "construct_ports",
        ConfigValue(
            default=True,
            domain=Bool,
            description="Construct inlet and outlet Port objects",
            doc="""Argument indicating whether model should construct Port
objects linked to all inlet states and the mixed state,
**default** - True.
**Valid values:** {
**True** - construct Ports for all states,
**False** - do not construct Ports.""",
        ),
    )

    def build(self):
        """
        General build method for MixerData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of unit models.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        # Call super.build()
        super(MixerData, self).build()

        # Call setup methods from ControlVolumeBlockData
        self._get_property_package()
        self._get_indexing_sets()

        # Create list of inlet names
        inlet_list = self.create_inlet_list()

        # Build StateBlocks
        inlet_blocks = self.add_inlet_state_blocks(inlet_list)

        if self.config.mixed_state_block is None:
            mixed_block = self.add_mixed_state_block()
        else:
            mixed_block = self.get_mixed_state_block()

        mb_type = self.config.material_balance_type
        if mb_type == MaterialBalanceType.useDefault:
            t_ref = self.flowsheet().time.first()
            mb_type = mixed_block[t_ref].default_material_balance_type()

        if mb_type != MaterialBalanceType.none:
            self.add_material_mixing_equations(inlet_blocks=inlet_blocks,
                                               mixed_block=mixed_block,
                                               mb_type=mb_type)
        else:
            raise BurntToast("{} received unrecognised value for "
                             "material_mixing_type argument. This "
                             "should not occur, so please contact "
                             "the IDAES developers with this bug.".format(
                                 self.name))

        if self.config.energy_mixing_type == MixingType.extensive:
            self.add_energy_mixing_equations(inlet_blocks=inlet_blocks,
                                             mixed_block=mixed_block)
        elif self.config.energy_mixing_type == MixingType.none:
            pass
        else:
            raise ConfigurationError(
                "{} received unrecognised value for "
                "material_mixing_type argument. This "
                "should not occur, so please contact "
                "the IDAES developers with this bug.".format(self.name))

        # Add to try/expect to catch cases where pressure is not supported
        # by properties.
        try:
            if self.config.momentum_mixing_type == MomentumMixingType.minimize:
                self.add_pressure_minimization_equations(
                    inlet_blocks=inlet_blocks, mixed_block=mixed_block)
            elif (self.config.momentum_mixing_type ==
                  MomentumMixingType.equality):
                self.add_pressure_equality_equations(inlet_blocks=inlet_blocks,
                                                     mixed_block=mixed_block)
            elif (self.config.momentum_mixing_type ==
                  MomentumMixingType.minimize_and_equality):
                self.add_pressure_minimization_equations(
                    inlet_blocks=inlet_blocks, mixed_block=mixed_block)
                self.add_pressure_equality_equations(inlet_blocks=inlet_blocks,
                                                     mixed_block=mixed_block)
                self.pressure_equality_constraints.deactivate()
            elif self.config.momentum_mixing_type == MomentumMixingType.none:
                pass
            else:
                raise ConfigurationError(
                    "{} recieved unrecognised value for "
                    "momentum_mixing_type argument. This "
                    "should not occur, so please contact "
                    "the IDAES developers with this bug.".format(self.name))
        except PropertyNotSupportedError:
            raise PropertyNotSupportedError(
                "{} The property package supplied for this unit does not "
                "appear to support pressure, which is required for momentum "
                "mixing. Please set momentum_mixing_type to "
                "MomentumMixingType.none or provide a property package which "
                "supports pressure.".format(self.name))

        self.add_port_objects(inlet_list, inlet_blocks, mixed_block)

    def create_inlet_list(self):
        """
        Create list of inlet stream names based on config arguments.

        Returns:
            list of strings
        """
        if (self.config.inlet_list is not None
                and self.config.num_inlets is not None):
            # If both arguments provided and not consistent, raise Exception
            if len(self.config.inlet_list) != self.config.num_inlets:
                raise ConfigurationError(
                    "{} Mixer provided with both inlet_list and "
                    "num_inlets arguments, which were not consistent ("
                    "length of inlet_list was not equal to num_inlets). "
                    "PLease check your arguments for consistency, and "
                    "note that it is only necessary to provide one of "
                    "these arguments.".format(self.name))
        elif self.config.inlet_list is None and self.config.num_inlets is None:
            # If no arguments provided for inlets, default to num_inlets = 2
            self.config.num_inlets = 2

        # Create a list of names for inlet StateBlocks
        if self.config.inlet_list is not None:
            inlet_list = self.config.inlet_list
        else:
            inlet_list = [
                "inlet_" + str(n) for n in range(1, self.config.num_inlets + 1)
            ]

        return inlet_list

    def add_inlet_state_blocks(self, inlet_list):
        """
        Construct StateBlocks for all inlet streams.

        Args:
            list of strings to use as StateBlock names

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["defined_state"] = True

        # Create empty list to hold StateBlocks for return
        inlet_blocks = []

        # Create an instance of StateBlock for all inlets
        for i in inlet_list:
            i_obj = self.config.property_package.build_state_block(
                self.flowsheet().time,
                doc="Material properties at inlet",
                default=tmp_dict,
            )

            setattr(self, i + "_state", i_obj)

            inlet_blocks.append(getattr(self, i + "_state"))

        return inlet_blocks

    def add_mixed_state_block(self):
        """
        Constructs StateBlock to represent mixed stream.

        Returns:
            New StateBlock object
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["defined_state"] = False

        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().time,
            doc="Material properties of mixed stream",
            default=tmp_dict,
        )

        return self.mixed_state

    def get_mixed_state_block(self):
        """
        Validates StateBlock provided in user arguments for mixed stream.

        Returns:
            The user-provided StateBlock or an Exception
        """
        # Sanity check to make sure method is not called when arg missing
        if self.config.mixed_state_block is None:
            raise BurntToast("{} get_mixed_state_block method called when "
                             "mixed_state_block argument is None. This should "
                             "not happen.".format(self.name))

        # Check that the user-provided StateBlock uses the same prop pack
        if (self.config.mixed_state_block[self.flowsheet().time.first()].
                config.parameters != self.config.property_package):
            raise ConfigurationError(
                "{} StateBlock provided in mixed_state_block argument "
                "does not come from the same property package as "
                "provided in the property_package argument. All "
                "StateBlocks within a Mixer must use the same "
                "property package.".format(self.name))

        return self.config.mixed_state_block

    def add_material_mixing_equations(self, inlet_blocks, mixed_block,
                                      mb_type):
        """
        Add material mixing equations.
        """
        pp = self.config.property_package
        # Get phase component list(s)
        pc_set = mixed_block.phase_component_set

        # Get units metadata
        units = pp.get_metadata()

        flow_basis = mixed_block[
            self.flowsheet().time.first()].get_material_flow_basis()
        if flow_basis == MaterialFlowBasis.molar:
            flow_units = units.get_derived_units("flow_mole")
        elif flow_basis == MaterialFlowBasis.mass:
            flow_units = units.get_derived_units("flow_mass")
        else:
            # Let this pass for now with no units
            flow_units = None

        if mb_type == MaterialBalanceType.componentPhase:
            # Create equilibrium generation term and constraints if required
            if self.config.has_phase_equilibrium is True:
                try:
                    self.phase_equilibrium_generation = Var(
                        self.flowsheet().time,
                        pp.phase_equilibrium_idx,
                        domain=Reals,
                        doc="Amount of generation in unit by phase equilibria",
                        units=flow_units)
                except AttributeError:
                    raise PropertyNotSupportedError(
                        "{} Property package does not contain a list of phase "
                        "equilibrium reactions (phase_equilibrium_idx), "
                        "thus does not support phase equilibrium.".format(
                            self.name))

            # Define terms to use in mixing equation
            def phase_equilibrium_term(b, t, p, j):
                if self.config.has_phase_equilibrium:
                    sd = {}
                    for r in pp.phase_equilibrium_idx:
                        if pp.phase_equilibrium_list[r][0] == j:
                            if (pp.phase_equilibrium_list[r][1][0] == p):
                                sd[r] = 1
                            elif (pp.phase_equilibrium_list[r][1][1] == p):
                                sd[r] = -1
                            else:
                                sd[r] = 0
                        else:
                            sd[r] = 0

                    return sum(b.phase_equilibrium_generation[t, r] * sd[r]
                               for r in pp.phase_equilibrium_idx)
                else:
                    return 0

            # Write phase-component balances
            @self.Constraint(
                self.flowsheet().time,
                pc_set,
                doc="Material mixing equations",
            )
            def material_mixing_equations(b, t, p, j):
                return 0 == (
                    sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                        for i in range(len(inlet_blocks))) -
                    mixed_block[t].get_material_flow_terms(p, j) +
                    phase_equilibrium_term(b, t, p, j))

        elif mb_type == MaterialBalanceType.componentTotal:
            # Write phase-component balances
            @self.Constraint(
                self.flowsheet().time,
                mixed_block.component_list,
                doc="Material mixing equations",
            )
            def material_mixing_equations(b, t, j):
                return 0 == sum(
                    sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                        for i in range(len(inlet_blocks))) -
                    mixed_block[t].get_material_flow_terms(p, j)
                    for p in mixed_block.phase_list if (p, j) in pc_set)

        elif mb_type == MaterialBalanceType.total:
            # Write phase-component balances
            @self.Constraint(self.flowsheet().time,
                             doc="Material mixing equations")
            def material_mixing_equations(b, t):
                return 0 == sum(
                    sum(
                        sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                            for i in range(len(inlet_blocks))) -
                        mixed_block[t].get_material_flow_terms(p, j)
                        for j in mixed_block.component_list
                        if (p, j) in pc_set) for p in mixed_block.phase_list)

        elif mb_type == MaterialBalanceType.elementTotal:
            raise ConfigurationError("{} Mixers do not support elemental "
                                     "material balances.".format(self.name))
        elif mb_type == MaterialBalanceType.none:
            pass
        else:
            raise BurntToast(
                "{} Mixer received unrecognised value for "
                "material_balance_type. This should not happen, "
                "please report this bug to the IDAES developers.".format(
                    self.name))

    def add_energy_mixing_equations(self, inlet_blocks, mixed_block):
        """
        Add energy mixing equations (total enthalpy balance).
        """
        @self.Constraint(self.flowsheet().time, doc="Energy balances")
        def enthalpy_mixing_equations(b, t):
            return 0 == (sum(
                sum(inlet_blocks[i][t].get_enthalpy_flow_terms(p)
                    for p in mixed_block.phase_list)
                for i in range(len(inlet_blocks))) -
                         sum(mixed_block[t].get_enthalpy_flow_terms(p)
                             for p in mixed_block.phase_list))

    def add_pressure_minimization_equations(self, inlet_blocks, mixed_block):
        """
        Add pressure minimization equations. This is done by sequential
        comparisons of each inlet to the minimum pressure so far, using
        the IDAES smooth minimum fuction.
        """
        if not hasattr(self, "inlet_idx"):
            self.inlet_idx = RangeSet(len(inlet_blocks))

        # Get units metadata
        units = self.config.property_package.get_metadata()

        # Add variables
        self.minimum_pressure = Var(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Variable for calculating minimum inlet pressure",
            units=units.get_derived_units("pressure"))

        self.eps_pressure = Param(
            mutable=True,
            initialize=1e-3,
            domain=PositiveReals,
            doc="Smoothing term for minimum inlet pressure",
            units=units.get_derived_units("pressure"))

        # Calculate minimum inlet pressure
        @self.Constraint(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Calculation for minimum inlet pressure",
        )
        def minimum_pressure_constraint(b, t, i):
            if i == self.inlet_idx.first():
                return self.minimum_pressure[t, i] == (
                    inlet_blocks[i - 1][t].pressure)
            else:
                return self.minimum_pressure[t, i] == (smooth_min(
                    self.minimum_pressure[t, i - 1],
                    inlet_blocks[i - 1][t].pressure,
                    self.eps_pressure,
                ))

        # Set inlet pressure to minimum pressure
        @self.Constraint(self.flowsheet().time,
                         doc="Link pressure to control volume")
        def mixture_pressure(b, t):
            return mixed_block[t].pressure == (
                self.minimum_pressure[t, self.inlet_idx.last()])

    def add_pressure_equality_equations(self, inlet_blocks, mixed_block):
        """
        Add pressure equality equations. Note that this writes a number of
        constraints equal to the number of inlets, enforcing equality between
        all inlets and the mixed stream.
        """
        if not hasattr(self, "inlet_idx"):
            self.inlet_idx = RangeSet(len(inlet_blocks))

        # Create equality constraints
        @self.Constraint(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Calculation for minimum inlet pressure",
        )
        def pressure_equality_constraints(b, t, i):
            return mixed_block[t].pressure == inlet_blocks[i - 1][t].pressure

    def add_port_objects(self, inlet_list, inlet_blocks, mixed_block):
        """
        Adds Port objects if required.

        Args:
            a list of inlet StateBlock objects
            a mixed state StateBlock object

        Returns:
            None
        """
        if self.config.construct_ports is True:
            # Add ports
            for p in inlet_list:
                i_state = getattr(self, p + "_state")
                self.add_port(name=p, block=i_state, doc="Inlet Port")
            self.add_port(name="outlet", block=mixed_block, doc="Outlet Port")

    def model_check(blk):
        """
        This method executes the model_check methods on the associated state
        blocks (if they exist). This method is generally called by a unit model
        as part of the unit's model_check method.

        Args:
            None

        Returns:
            None
        """
        # Try property block model check
        for t in blk.flowsheet().time:
            try:
                inlet_list = blk.create_inlet_list()
                for i in inlet_list:
                    i_block = getattr(blk, i + "_state")
                    i_block[t].model_check()
            except AttributeError:
                _log.warning(
                    "{} Mixer inlet property block has no model "
                    "checks. To correct this, add a model_check "
                    "method to the associated StateBlock class.".format(
                        blk.name))
            try:
                if blk.config.mixed_state_block is None:
                    blk.mixed_state[t].model_check()
                else:
                    blk.config.mixed_state_block.model_check()
            except AttributeError:
                _log.warning("{} Mixer outlet property block has no "
                             "model checks. To correct this, add a "
                             "model_check method to the associated "
                             "StateBlock class.".format(blk.name))

    def use_minimum_inlet_pressure_constraint(self):
        """Activate the mixer pressure = mimimum inlet pressure constraint and
        deactivate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_minimum_inlet_pressure_constraint() can only be used
                when momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.activate()
        self.pressure_equality_constraints.deactivate()

    def use_equal_pressure_constraint(self):
        """Deactivate the mixer pressure = mimimum inlet pressure constraint
        and activate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_equal_pressure_constraint() can only be used when
                momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.deactivate()
        self.pressure_equality_constraints.activate()

    def initialize(blk,
                   outlvl=idaeslog.NOTSET,
                   optarg=None,
                   solver=None,
                   hold_state=False):
        """
        Initialization routine for mixer.

        Keyword Arguments:
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            hold_state : flag indicating whether the initialization routine
                     should unfix any state variables fixed during
                     initialization, **default** - False. **Valid values:**
                     **True** - states variables are not unfixed, and a dict of
                     returned containing flags for which states were fixed
                     during initialization, **False** - state variables are
                     unfixed after initialization by calling the release_state
                     method.

        Returns:
            If hold_states is True, returns a dict containing flags for which
            states were fixed during initialization.
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # Initialize inlet state blocks
        flags = {}
        inlet_list = blk.create_inlet_list()
        i_block_list = []
        for i in inlet_list:
            i_block = getattr(blk, i + "_state")
            i_block_list.append(i_block)
            flags[i] = {}
            flags[i] = i_block.initialize(
                outlvl=outlvl,
                optarg=optarg,
                solver=solver,
                hold_state=True,
            )

        # Initialize mixed state block
        if blk.config.mixed_state_block is None:
            mblock = blk.mixed_state
        else:
            mblock = blk.config.mixed_state_block

        o_flags = {}
        # Calculate initial guesses for mixed stream state
        for t in blk.flowsheet().time:
            # Iterate over state vars as defined by property package
            s_vars = mblock[t].define_state_vars()
            for s in s_vars:
                i_vars = []
                for k in s_vars[s]:
                    # Record whether variable was fixed or not
                    o_flags[t, s, k] = s_vars[s][k].fixed

                    # If fixed, use current value
                    # otherwise calculate guess from mixed state
                    if not s_vars[s][k].fixed:
                        for i in range(len(i_block_list)):
                            i_vars.append(
                                getattr(i_block_list[i][t],
                                        s_vars[s].local_name))

                        if s == "pressure":
                            # If pressure, use minimum as initial guess
                            mblock[t].pressure.value = min(
                                i_block_list[i][t].pressure.value
                                for i in range(len(i_block_list)))
                        elif "flow" in s:
                            # If a "flow" variable (i.e. extensive), sum inlets
                            for k in s_vars[s]:
                                s_vars[s][k].value = sum(
                                    i_vars[i][k].value
                                    for i in range(len(i_block_list)))
                        else:
                            # Otherwise use average of inlets
                            for k in s_vars[s]:
                                s_vars[s][k].value = sum(
                                    i_vars[i][k].value for i in range(
                                        len(i_block_list))) / len(i_block_list)

        mblock.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
        )

        # Revert fixed status of variables to what they were before
        for t in blk.flowsheet().time:
            s_vars = mblock[t].define_state_vars()
            for s in s_vars:
                for k in s_vars[s]:
                    s_vars[s][k].fixed = o_flags[t, s, k]

        if blk.config.mixed_state_block is None:
            if (hasattr(blk, "pressure_equality_constraints")
                    and blk.pressure_equality_constraints.active is True):
                blk.pressure_equality_constraints.deactivate()
                for t in blk.flowsheet().time:
                    sys_press = getattr(blk,
                                        blk.create_inlet_list()[0] +
                                        "_state")[t].pressure
                    blk.mixed_state[t].pressure.fix(sys_press.value)
                with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                    res = opt.solve(blk, tee=slc.tee)
                blk.pressure_equality_constraints.activate()
                for t in blk.flowsheet().time:
                    blk.mixed_state[t].pressure.unfix()
            else:
                with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                    res = opt.solve(blk, tee=slc.tee)

            init_log.info("Initialization Complete: {}".format(
                idaeslog.condition(res)))
        else:
            init_log.info("Initialization Complete.")

        if hold_state is True:
            return flags
        else:
            blk.release_state(flags, outlvl=outlvl)

    def release_state(blk, flags, outlvl=idaeslog.NOTSET):
        """
        Method to release state variables fixed during initialization.

        Keyword Arguments:
            flags : dict containing information of which state variables
                    were fixed during initialization, and should now be
                    unfixed. This dict is returned by initialize if
                    hold_state = True.
            outlvl : sets output level of logging

        Returns:
            None
        """
        inlet_list = blk.create_inlet_list()
        for i in inlet_list:
            i_block = getattr(blk, i + "_state")
            i_block.release_state(flags[i], outlvl=outlvl)

    def _get_stream_table_contents(self, time_point=0):
        io_dict = {}
        inlet_list = self.create_inlet_list()
        for i in inlet_list:
            io_dict[i] = getattr(self, i + "_state")
        if self.config.mixed_state_block is None:
            io_dict["Outlet"] = self.mixed_state
        else:
            io_dict["Outlet"] = self.config.mixed_state_block
        return create_stream_table_dataframe(io_dict, time_point=time_point)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        mb_type = self.config.material_balance_type
        if mb_type == MaterialBalanceType.useDefault:
            t_ref = self.flowsheet().time.first()
            mb_type = self.mixed_state[t_ref].default_material_balance_type()

        if hasattr(self, "pressure_equality_constraints"):
            for (t, i), c in self.pressure_equality_constraints.items():
                s = iscale.get_scaling_factor(self.mixed_state[t].pressure,
                                              default=1,
                                              warning=True)
                iscale.constraint_scaling_transform(c, s)

        if hasattr(self, "material_mixing_equations"):
            if mb_type == MaterialBalanceType.componentPhase:
                for (t, p, j), c in self.material_mixing_equations.items():
                    flow_term = self.mixed_state[t].get_material_flow_terms(
                        p, j)
                    s = iscale.get_scaling_factor(flow_term, default=1)
                    iscale.constraint_scaling_transform(c, s, overwrite=False)
            elif mb_type == MaterialBalanceType.componentTotal:
                for (t, j), c in self.material_mixing_equations.items():
                    for i, p in enumerate(self.mixed_state.phase_list):
                        try:
                            ft = self.mixed_state[t].get_material_flow_terms(
                                p, j)
                        except (KeyError, AttributeError):
                            continue  # component not in phase
                        if i == 0:
                            s = iscale.get_scaling_factor(ft, default=1)
                        else:
                            _s = iscale.get_scaling_factor(ft, default=1)
                            s = _s if _s < s else s
                    iscale.constraint_scaling_transform(c, s, overwrite=False)
            elif mb_type == MaterialBalanceType.total:
                pc_set = self.mixed_state.phase_component_set
                for t, c in self.material_mixing_equations.items():
                    for i, (p, j) in enumerate(pc_set):
                        ft = self.mixed_state[t].get_material_flow_terms(p, j)
                        if i == 0:
                            s = iscale.get_scaling_factor(ft, default=1)
                        else:
                            _s = iscale.get_scaling_factor(ft, default=1)
                            s = _s if _s < s else s
                    iscale.constraint_scaling_transform(c, s, overwrite=False)

        if hasattr(self, "enthalpy_mixing_equations"):
            for t, c in self.enthalpy_mixing_equations.items():

                def scale_gen():
                    for v in self.mixed_state[t].phase_list:
                        yield self.mixed_state[t].get_enthalpy_flow_terms(p)

                s = iscale.min_scaling_factor(scale_gen(), default=1)
                iscale.constraint_scaling_transform(c, s, overwrite=False)
class PFRData(UnitModelBlockData):
    """
    Standard Plug Flow Reactor Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**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,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy 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,
**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_equilibrium_reactions",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Equilibrium reaction construction flag",
            doc="""Indicates whether terms for equilibrium controlled reactions
should be constructed,
**default** - True.
**Valid values:** {
**True** - include equilibrium reaction terms,
**False** - exclude equilibrium reaction terms.}"""))
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed,
**default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    CONFIG.declare(
        "has_heat_of_reaction",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat of reaction term construction flag",
            doc="""Indicates whether terms for heat of reaction terms should be
constructed,
**default** - False.
**Valid values:** {
**True** - include heat of reaction terms,
**False** - exclude heat of reaction terms.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    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(
        "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,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare(
        "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(
        "reaction_package",
        ConfigValue(
            default=None,
            domain=is_reaction_parameter_block,
            description="Reaction package to use for control volume",
            doc=
            """Reaction parameter object used to define reaction calculations,
**default** - None.
**Valid values:** {
**None** - no reaction package,
**ReactionParameterBlock** - a ReactionParameterBlock object.}"""))
    CONFIG.declare(
        "reaction_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing reaction packages",
            doc=
            """A ConfigBlock with arguments to be passed to a reaction block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see reaction package for documentation.}"""))
    CONFIG.declare(
        "length_domain_set",
        ConfigValue(
            default=[0.0, 1.0],
            domain=ListOf(float),
            description="List of points to use to initialize length domain",
            doc=
            """A list of values to be used when constructing the length domain
of the reactor. Point must lie between 0.0 and 1.0,
**default** - [0.0, 1.0].
**Valid values:** {
a list of floats}"""))
    CONFIG.declare(
        "transformation_method",
        ConfigValue(
            default="dae.finite_difference",
            description="Method to use for DAE transformation",
            doc="""Method to use to transform domain. Must be a method recognised
by the Pyomo TransformationFactory,
**default** - "dae.finite_difference"."""))
    CONFIG.declare(
        "transformation_scheme",
        ConfigValue(default="BACKWARD",
                    description="Scheme to use for DAE transformation",
                    doc="""Scheme to use when transformating domain. See Pyomo
documentation for supported schemes,
**default** - "BACKWARD"."""))
    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            description=
            "Number of finite elements to use for DAE transformation",
            doc="""Number of finite elements to use when transforming length
domain,
**default** - 20."""))
    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=3,
            description="No. collocation points to use for DAE transformation",
            doc="""Number of collocation points to use when transforming length
domain,
**default** - 3."""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(PFRData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume1DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "reaction_package": self.config.reaction_package,
                "reaction_package_args": self.config.reaction_package_args,
                "transformation_method": self.config.transformation_method,
                "transformation_scheme": self.config.transformation_scheme,
                "finite_elements": self.config.finite_elements,
                "collocation_points": self.config.collocation_points
            })

        self.control_volume.add_geometry(
            length_domain_set=self.config.length_domain_set)

        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_reaction_blocks(
            has_equilibrium=self.config.has_equilibrium_reactions)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_rate_reactions=True,
            has_equilibrium_reactions=self.config.has_equilibrium_reactions,
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_of_reaction=self.config.has_heat_of_reaction,
            has_heat_transfer=self.config.has_heat_transfer)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        self.control_volume.apply_transformation()

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Add PFR performance equation
        @self.Constraint(self.flowsheet().time,
                         self.control_volume.length_domain,
                         self.config.reaction_package.rate_reaction_idx,
                         doc="PFR performance equation")
        def performance_eqn(b, t, x, r):
            return b.control_volume.rate_reaction_extent[t, x, r] == (
                b.control_volume.reactions[t, x].reaction_rate[r] *
                b.control_volume.area)

        # Set references to balance terms at unit level
        add_object_reference(self, "length", self.control_volume.length)
        add_object_reference(self, "area", self.control_volume.area)

        # Add volume variable for full reactor
        units = self.config.property_package.get_metadata()
        self.volume = Var(initialize=1,
                          doc="Reactor Volume",
                          units=units.get_derived_units("volume"))

        self.geometry = Constraint(expr=self.volume == self.area * self.length)

        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            self.heat_duty = Reference(self.control_volume.heat[...])
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.control_volume.deltaP[...])

    def _get_performance_contents(self, time_point=0):
        var_dict = {"Volume": self.volume}
        var_dict = {"Length": self.length}
        var_dict = {"Area": self.area}

        return {"vars": var_dict}

    def get_costing(self, year=None, module=costing, **kwargs):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing(year=year, module=module)

        self.costing = Block()
        units_meta = (
            self.config.property_package.get_metadata().get_derived_units)
        self.diameter = Var(initialize=1,
                            units=units_meta('length'),
                            doc='vessel diameter')
        self.diameter_eq = Constraint(
            expr=self.volume == (self.length * const.pi * self.diameter**2) /
            4)
        module.pfr_costing(self.costing, **kwargs)
Beispiel #5
0
class AlamoTrainer(SurrogateTrainer):
    """
    Standard SurrogateTrainer for ALAMO.

    This defines a set of configuration options for ALAMO along with
    methods to read and write the ALAMO input and output files and to call
    the ALAMO executable.

    Generally, options default to None to indicate that no entry will be
    written in the ALAMO input file. In this case, the default ALAMO settings
    will be used.
    """
    # The following ALAMO options are not (yet) supported
    # Returning model prediction, as we can do that in IDAES
    # Iniital sampling of data, as SurrogateModelTrainer should do that
    # Similarly, adaptive sampling is better handled in Python
    # Single validation set, due to limitations of current API
    # Custom basis functions are not yet implemented

    CONFIG = SurrogateTrainer.CONFIG()

    CONFIG.declare('xfactor', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="List of scaling factors for input variables."))
    CONFIG.declare('xscaling', ConfigValue(
        default=None,
        domain=Bool,
        description="Option to scale input variables.",
        doc="Option to scale input variables. If True and xfactors are not "
        "provided, ALAMO sets XFACTORS equal to the range of each input "
        "variable."))
    CONFIG.declare('scalez', ConfigValue(
        default=None,
        domain=Bool,
        description="Option to scale output variables."))

    # Basis function options
    CONFIG.declare('monomialpower', ConfigValue(
        default=None,
        domain=ListOf(int, In(Reals - {0,1})), # allow any float except for 0 and 1
        description="Vector of monomial powers considered in basis "
        "functions - cannot include 0 or 1."))
    CONFIG.declare('multi2power', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Vector of powers to be considered for pairwise "
        "combinations in basis functions."))
    CONFIG.declare('multi3power', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Vector of three variable combinations of powers to be "
        "considered as basis functions."))
    CONFIG.declare('ratiopower', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Vector of ratio combinations of powers to be considered "
        "in the basis functions."))
    CONFIG.declare('constant', ConfigValue(
        default=True,
        domain=Bool,
        description="Include constant basis function if True. Default = True"))
    CONFIG.declare('linfcns', ConfigValue(
        default=True,
        domain=Bool,
        description="Include linear basis functions if True. Default = True"))
    CONFIG.declare('expfcns', ConfigValue(
        default=None,
        domain=Bool,
        description="Include exponential basis functions if True."))
    CONFIG.declare('logfcns', ConfigValue(
        default=None,
        domain=Bool,
        description="Include logarithmic basis functions if True."))
    CONFIG.declare('sinfcns', ConfigValue(
        default=None,
        domain=Bool,
        description="Include sine basis functions if True."))
    CONFIG.declare('cosfcns', ConfigValue(
        default=None,
        domain=Bool,
        description="Include cosine basis functions if True."))
    CONFIG.declare('grbfcns', ConfigValue(
        default=None,
        domain=Bool,
        description="Include Gaussian radial basis functions if True."))
    CONFIG.declare('rbfparam', ConfigValue(
        default=None,
        domain=float,
        description="Multiplicative constant used in the Gaussian radial basis"
        " functions."))
    CONFIG.declare('custom_basis_functions', ConfigValue(
        default=None,
        domain=ListOf(str),
        description="List of custom basis functions to include in surrogate "
        "fitting.",
        doc="List of custom basis functions to include in surrogate model "
        "fitting. These should be in a form that can be rendered as a string "
        "that meets ALAMO's requirements."))

    # Other fitting options
    CONFIG.declare('modeler', ConfigValue(
        default=None,
        domain=In(Modelers),
        description="Fitness metric to be used for model building. Must be an "
        "instance of Modelers Enum."))
    CONFIG.declare('builder', ConfigValue(
        default=None,
        domain=Bool,
        description="If True, a greedy heuristic builds up a model "
        "by adding one variable at a time.",
        doc="If True, a greedy heuristic builds up a model by adding one "
        "variable at a time. This model is used as a starting point for "
        "solving an integer programming formulation according to the choice "
        "of modeler. If an optimizer is not available, the heuristic model "
        "will be the final model to be returned."))
    CONFIG.declare('backstepper', ConfigValue(
        default=None,
        domain=Bool,
        description="If set to 1, a greedy heuristic builds down a model by "
        "starting from the least squares model and removing one variable at "
        "a time."))
    CONFIG.declare('convpen', ConfigValue(
        default=None,
        domain=float,
        description="Convex penalty term to use if Modeler == SSEP or MADp.",
        doc="When MODELER is set to 6 or 8, a penalty consisting of the sum "
        "of square errors (SSEP) or the maximum absolute error (MADp) and a "
        "term penalizing model size is used for model building. The size of "
        "the model is weighted by convpen. If convpen=0, this metric reduces "
        "to the classical sum of square errors (SSEP) or the maximum absolute "
        "deviation (MADp)."))
    CONFIG.declare('screener', ConfigValue(
        default=None,
        domain=In(Screener),
        description="Regularization method used to reduce the number of "
        "potential basis functions before optimization. Must be instance of "
        "Screener Enum."))
    CONFIG.declare('ncvf', ConfigValue(
        default=None,
        domain=int,
        description="Number of folds to be used for cross validation by the "
        "lasso screener. ALAMO will use a two-fold validation if fewer than "
        "10 data points are available. NCVF must be a nonnegative integer."))
    CONFIG.declare('sismult', ConfigValue(
        default=None,
        domain=int,
        description="This parameter must be non-negative and is used to "
        "determine the number of basis functions retained by the SIS "
        "screener. The number of basis functions retained equals the floor "
        "of SSISmult n ln(n), where n is the number of measurements "
        "available at the current ALAMO iteration."))

    CONFIG.declare('maxiter', ConfigValue(
        default=None,
        domain=int,
        description="Maximum number of ALAMO iterations. 1 = no adaptive "
        "sampling, 0 = no limit.",
        doc="Maximum number of ALAMO iterations. Each iteration begins with "
        "a model-building step. An adaptive sampling step follows if maxiter "
        "does not equal 1. If maxiter is set to a number less than or equal "
        "to 0, ALAMO will enforce no limit on the number of iterations."))
    CONFIG.declare('maxtime', ConfigValue(
        default=1000,
        domain=float,
        description="Maximum total execution time allowed in seconds. "
        "Default = 1000.",
        doc="Maximum total execution time allowed in seconds. This time "
        "includes all steps of the algorithm, including time to read problem, "
        "preprocess data, solve optimization subproblems, and print results."))
    CONFIG.declare('datalimitterms', ConfigValue(
        default=None,
        domain=Bool,
        description="Limit model terms to number of measurements.",
        doc="If True, ALAMO will limit the number of terms in the model to be "
        "no more than the number of data measurements; otherwise, no limit "
        "based on the number of data measurements will be placed. The user "
        "may provide an additional limit on the number of terms in the model "
        "through the maxterms and minterms options."))
    CONFIG.declare('maxterms', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of maximum number of model terms to per output.",
        doc="Row vector of maximum terms allowed in the modeling of output "
        "variables. One per output variable, space separated. A −1 signals "
        "that no limit is imposed."))
    CONFIG.declare('minterms', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of minimum number of model terms to per output.",
        doc="Row vector of minimum terms required in the modeling of output "
        "variables. One per output variable, space separated. A 0 signals "
        "that no limit is imposed."))
    CONFIG.declare('numlimitbasis', ConfigValue(
        default=True,  # default this to true to avoid numerical issues
        domain=Bool,
        description="Eliminate infeasible basis functions. Default = True",
        doc="If True, ALAMO will eliminate basis functions that are not "
        "numerically acceptable (e.g., log(x) will be eliminated if x may be "
        "negative); otherwise, no limit based on the number of data "
        "measurements will be placed. The user may provide additional limits "
        "on the the type and number of selected basis functions through the "
        "options exclude and groupcon."))
    CONFIG.declare('exclude', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of inputs to exclude during building,",
        doc="Row vector of 0/1 flags that specify which input variables, if "
        "any, ALAMO should exclude during the model building process. All "
        "input variables must be present in the data but ALAMO will not "
        "include basis functions that involve input variables for which "
        "exclude equals 1. This feature does not apply to custom basis "
        "functions or RBFs."))
    CONFIG.declare('ignore', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of outputs to ignore during building.",
        doc="Row vector of 0/1 flags that specify which output variables, "
        "if any, ALAMO should ignore. All output variables must be present in "
        "the data but ALAMO does not model output variables for which ignore "
        "equals 1."))
    CONFIG.declare('xisint', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of inputs that should be treated as integers.",
        doc="Row vector of 0/1 flags that specify which input variables, if "
        "any, ALAMO should treat as integers. For integer inputs, ALAMO’s "
        "sampling will be restricted to integer values."))
    CONFIG.declare('zisint', ConfigValue(
        default=None,
        domain=ListOf(int),
        description="List of outputs that should be treated as integers.",
        doc="Row vector of 0/1 flags that specify which output variables, if "
        "any, ALAMO should treat as integers. For integer variables, ALAMO’s "
        "model will include the rounding of a function to the nearest integer "
        "(equivalent to the nint function in Fortran.)"))

    CONFIG.declare('tolrelmetric', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Relative tolerance for outputs.",
        doc="Relative convergence tolerance for the chosen fitness metric for "
        "the modeling of output variables. One per output variable, space "
        "separated. Incremental model building will stop if two consecutive "
        "iterations do not improve the chosen metric by at least this amount."
        ))
    CONFIG.declare('tolabsmetric', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Absolute tolerance for outputs.",
        doc="Absolute convergence tolerance for the chosen fitness metric for "
        "the modeling of output variables. One per output variable, space "
        "separated. Incremental model building will stop if two consecutive "
        "iterations do not improve the chosen metric by at least this amount."
        ))
    CONFIG.declare('tolmeanerror', ConfigValue(
        default=None,
        domain=ListOf(float),
        description="Convergence tolerance for mean errors in outputs.",
        doc="Row vector of convergence tolerances for mean errors in the "
        "modeling of output variables. One per output variable, space "
        "separated. Incremental model building will stop if tolmeanerror, "
        "tolrelmetric, or tolabsmetric is satisfied."))
    CONFIG.declare('tolsse', ConfigValue(
        default=None,
        domain=float,
        description="Absolute tolerance on SSE",
        doc="Absolute tolerance on sum of square errors (SSE). ALAMO will "
        "terminate if it finds a solution whose SSE is within tolsse from "
        "the SSE of the full least squares problem."))

    CONFIG.declare('mipoptca', ConfigValue(
        default=None,
        domain=float,
        description="Absolute tolerance for MIP."))
    CONFIG.declare('mipoptcr', ConfigValue(
        default=None,
        domain=float,
        description="Relative tolerance for MIP."))
    CONFIG.declare('linearerror', ConfigValue(
        default=None,
        domain=Bool,
        description="If True, a linear objective is used when solving "
        "mixed-integer optimization problems; otherwise, a squared error will "
        "be employed."))
    CONFIG.declare('GAMS', ConfigValue(
        default=None,
        domain=str,
        description="Complete path of GAMS executable (or name if GAMS is in "
        "the user path)."))
    CONFIG.declare('GAMSSOLVER', ConfigValue(
        default=None,
        domain=str,
        description="Name of preferred GAMS solver for solving ALAMO’s "
        "mixed-integer quadratic subproblems. Special facilities have been "
        "implemented in ALAMO and BARON that make BARON the preferred "
        "selection for this option. However, any mixed-integer quadratic "
        "programming solver available under GAMS can be used."))
    CONFIG.declare('solvemip', ConfigValue(
        default=None,
        domain=Bool,
        description="Whether to use an optimizer to solve MIP."))
    CONFIG.declare('print_to_screen', ConfigValue(
        default=None,
        domain=Bool,
        description="Send ALAMO output to stdout. Output is returned by the "
        "call_alamo method."))

    # I/O file options
    CONFIG.declare("alamo_path", ConfigValue(
        default=None,
        domain=Path,
        doc="Path to ALAMO executable (if not in path)."))
    CONFIG.declare("filename", ConfigValue(
        default=None,
        domain=str,
        description="File name to use for ALAMO files - must be full path of a"
        " .alm file. Other files will be defined from this pattern. If this "
        "option is not None, then working files will not be deleted."))
    CONFIG.declare("working_directory", ConfigValue(
        default=None,
        domain=str,
        description="Full path to working directory for ALAMO to use. "
        "If this option is not None, then working files will not be deleted."))
    CONFIG.declare("overwrite_files", ConfigValue(
        default=False,
        domain=Bool,
        description="Flag indicating whether existing files can be "
        "overwritten."))

    # TODO: We need to do some processing of the labels since ALAMO is
    # restrictive about the labels
    # TODO: We need to think more carefully about "input_bounds".
    # Alamo uses bounds during training, but we also want to consider
    # "valid" bounds for the surrogate
    def __init__(self, **settings):
        super().__init__(**settings)

        self._temp_context = None
        self._almfile = None
        self._trcfile = None

        if self.config.alamo_path is not None:
            alamo.executable = self.config.alamo_path

        self._results = None

    def train_surrogate(self):
        """
        General workflow method for training an ALAMO surrogate.

        Takes the existing data set and executes the ALAMO workflow to create
        an AlamoSurrogate based on the current configuration arguments.

        Args:
            None

        Returns:
            tuple : (success, AlamoSurrogate, message) where success indicates
            whether ALAMO was usccessfully executed, an instance of an
            AlamoSurrogate representing the trained surrogate, and message is
            the final status line from the ALAMO output log.
        """
        # Get paths for temp files
        self._get_files()

        return_code = None
        alamo_log = None
        alamo_object = None

        try:
            # Write .alm file
            self._write_alm_file()

            # Call ALAMO executable
            return_code, alamo_log = self._call_alamo()

            # Read back results
            trace_dict = self._read_trace_file(
                self._trcfile, self.output_labels())

            # Populate results and SurrogateModel object
            self._populate_results(trace_dict)
            alamo_object = self._build_surrogate_object()

        finally:
            # Clean up temporary files if required
            self._remove_temp_files()

        success = False
        if return_code == 0:
            # Non-zero return code implies an error
            # specifics returned in the msg
            success = True
        alamo_msg = alamo_log.split("\n")[-3]

        return success, alamo_object, alamo_msg

    # TODO: let's generalize this under the metrics?
    def get_alamo_results(self):
        return self._results

    def _get_files(self):
        """
        Method to get/set paths for .alm and .trc files based on filename
        configuration argument.

        If filename is None, temporary files will be created.

        Args:
            None

        Returns:
            None
        """
        if self._temp_context is None:
            self._temp_context = TempfileManager.new_context()

        if self.config.filename is None:
            # Get a temporary file from the manager
            almfile = self._temp_context.create_tempfile(suffix=".alm")
        else:
            almfile = self.config.filename

            if not self.config.overwrite_files:
                # It is OK if the trace file exists, as ALAMO will append to it
                # The trace file reader handles this case
                if os.path.isfile(almfile):
                    raise FileExistsError(
                        f"A file with the name {almfile} already exists. "
                        f"Either choose a new file name or set "
                        f"overwrite_files = True.")

            self._temp_context.add_tempfile(almfile, exists=False)

        trcfile = os.path.splitext(almfile)[0] + ".trc"
        self._temp_context.add_tempfile(trcfile, exists=False)

        if self.config.working_directory is None:
            wrkdir = self._temp_context.create_tempdir()
        else:
            wrkdir = self.config.working_directory

        # Set attributes to track file names
        self._almfile = almfile
        self._trcfile = trcfile
        self._wrkdir = wrkdir

    def _write_alm_to_stream(
            self, stream, trace_fname=None,
            training_data=None, validation_data=None):
        """
        Method to write an ALAMO input file (.alm) to a stream.
        Users may provide specific data sets for training and validation.
        If no data sets are provided, the data sets contained in the
        AlamoModelTrainer are used.

        Args:
            stream: stream that data should be writen to
            trace_fname: name for trace file (.trc) to be included in .alm file
            training_data: Pandas dataframe to use for training surrogate
            validation_data: Pandas dataframe to use for validating surrogate

        Returns:
            None
        """
        if training_data is None:
            training_data = self._training_dataframe
        if validation_data is None:
            validation_data = self._validation_dataframe

        # Check bounds on inputs to avoid potential ALAMO failures
        input_max = list()
        input_min = list()
        for k, b in self._input_bounds.items():
            if b is None or b[0] is None or b[1] is None:
                raise ConfigurationError(
                    f"ALAMO configuration error: invalid bounds on input {k} "
                    f"({b}).")
            elif b[0] == b[1]:
                raise ConfigurationError(
                    f"ALAMO configuration error: upper and lower bounds on "
                    f"input {k} are equal.")
            elif b[1] < b[0]:
                raise ConfigurationError(
                    f"ALAMO configuration error: upper bound is less than "
                    f"lower bound for input {k}.")
            input_min.append(b[0])
            input_max.append(b[1])

        # Get number of data points to build alm file
        n_rdata, n_inputs = training_data.shape

        if validation_data is not None:
            n_vdata, n_inputs = validation_data.shape
        else:
            n_vdata = 0

        stream.write("# IDAES Alamopy input file\n")
        stream.write(f"NINPUTS {len(self._input_labels)}\n")
        stream.write(f"NOUTPUTS {len(self._output_labels)}\n")
        stream.write(f"XLABELS {' '.join(map(str, self._input_labels))}\n")
        stream.write(f"ZLABELS {' '.join(map(str, self._output_labels))}\n")
        stream.write(f"XMIN {' '.join(map(str, input_min))}\n")
        stream.write(f"XMAX {' '.join(map(str, input_max))}\n")
        stream.write(f"NDATA {n_rdata}\n")
        if validation_data is not None:
            stream.write(f"NVALDATA {n_vdata}\n")
        stream.write("\n")

        # Other options for config
        # Can be bool, list of floats, None, Enum, float
        # Special cases
        if self.config.monomialpower is not None:
            stream.write(f"MONO {len(self.config.monomialpower)}\n")
        if self.config.multi2power is not None:
            stream.write(f"MULTI2 {len(self.config.multi2power)}\n")
        if self.config.multi3power is not None:
            stream.write(f"MULTI3 {len(self.config.multi3power)}\n")
        if self.config.ratiopower is not None:
            stream.write(f"RATIOS {len(self.config.ratiopower)}\n")
        if self.config.custom_basis_functions is not None:
            stream.write(
                f"NCUSTOMBAS {len(self.config.custom_basis_functions)}\n")

        for o in supported_options:
            if self.config[o] is None:
                # Write nothing to alm file
                continue
            elif isinstance(self.config[o], Enum):
                # Need to write value of Enum
                stream.write(f"{o} {self.config[o].value}\n")
            elif isinstance(self.config[o], bool):
                # Cast bool to int
                stream.write(f"{o} {int(self.config[o])}\n")
            elif isinstance(self.config[o], (str, float, int)):
                # Write value to file
                stream.write(f"{o} {self.config[o]}\n")
            else:
                # Assume the argument is a list
                stream.write(f"{o} {' '.join(map(str, self.config[o]))}\n")

        stream.write("\nTRACE 1\n")
        if trace_fname is not None:
            stream.write(f"TRACEFNAME {trace_fname}\n")

        def _trim_extra_whitespace(text, sep=' '):
            trimmed_lines = []
            for line in text.splitlines():
                parts = [part.strip() for part in line.split()]
                trimmed_lines.append(str.join(sep, parts))
            return str.join('\n', trimmed_lines)

        def _df_to_data_fragment(df, **kwargs):
            text = df.to_string(
                header=False,
                index=False,
                float_format=lambda x: str(x).format(":g"),
                **kwargs
            )
            # This is only needed to remove the extra spaces (from `justify`?)
            # on python 3.6 since on 3.7 and up pandas.to_string() returns
            # already the proper format without any further processing needed
            return _trim_extra_whitespace(text, sep=' ')

        stream.write("\nBEGIN_DATA\n")
        # Columns will be writen in order in input and output lists
        training_data_str = _df_to_data_fragment(
            training_data,
            columns=self._input_labels + self._output_labels,
        )
        stream.write(training_data_str)
        stream.write("\nEND_DATA\n")

        if validation_data is not None:
            # Add validation data defintion
            stream.write("\nBEGIN_VALDATA\n")
            val_data_str = _df_to_data_fragment(
                validation_data,
                columns=self._input_labels + self._output_labels,
            )
            stream.write(val_data_str)
            stream.write("\nEND_VALDATA\n")

        if self.config.custom_basis_functions is not None:
            stream.write("\nBEGIN_CUSTOMBAS\n")
            for i in self.config.custom_basis_functions:
                stream.write(f"{str(i)}\n")
            stream.write("END_CUSTOMBAS\n")

    def _write_alm_file(self, training_data=None, validation_data=None):
        """
        Method to write an ALAMO input file (.alm) using the current settings.
        Users may provide specific data sets for training and validation.
        If no data sets are provided, the data sets contained in the
        AlamoModelTrainer are used.

        Args:
            training_data: Pandas dataframe to use for training surrogate
            validation_data: Pandas dataframe to use for validating surrogate

        Returns:
            None
        """
        f = open(self._almfile, "w")
        self._write_alm_to_stream(
            f, self._trcfile, training_data, validation_data)
        f.close()

    def _call_alamo(self):
        """
        Method to call ALAMO executable from Python, passing the current .alm
        file as an argument.

        Args:
            None

        Returns:
            ALAMO: return code
            log: string of the text output from ALAMO
        """
        if alamo.executable is None:
            raise FileNotFoundError(
                "Could not find ALAMO executable. Please ensure that ALAMO "
                "is installed and in the system path, or provide a path to "
                "the executable.")

        ostreams = [StringIO(), sys.stdout]

        if self._temp_context is None:
            self._get_files()

        # Set working directory
        cwd = os.getcwd()
        os.chdir(self._wrkdir)

        try:
            # Add lst file to temp file manager
            lstfname = os.path.splitext(
                os.path.basename(self._almfile))[0] + ".lst"
            lstpath = os.path.join(cwd, lstfname)
            self._temp_context.add_tempfile(lstpath, exists=False)

            with TeeStream(*ostreams) as t:
                results = subprocess.run(
                    [alamo.executable, str(self._almfile)],
                    stdout=t.STDOUT,
                    stderr=t.STDERR,
                    universal_newlines=True,
                )

                t.STDOUT.flush()
                t.STDERR.flush()

            return_code = results.returncode
            alamo_log = ostreams[0].getvalue()

        except OSError:
            _log.error(
                f'Could not execute the command: alamo {str(self._almfile)}. ',
                f'Error message: {sys.exc_info()[1]}.')
            raise

        finally:
            # Revert cwd to where it started
            os.chdir(cwd)

        if "ALAMO terminated with termination code " in alamo_log:
            self._remove_temp_files()
            _log.warn("ALAMO executable returned non-zero return code. Check "
                      "the ALAMO output for more information.")

        return return_code, alamo_log

    @staticmethod
    def _read_trace_file(trcfile, output_labels):
        """
        Method to read the results of an ALAMO run from a trace (.trc) file.
        The name location of the trace file is tored on the AlamoModelTrainer
        object and is generally set automatically.

        Args:
            trcfile : str
               Path to the trcfile to read
            output_labels : list of str
               List of strings of the output_labels (in order)

        Returns:
            trace_dict: contents of trace file as a dict
        """
        with open(trcfile, "r") as f:
            lines = f.readlines()
        f.close()

        trace_read = {}
        # Get headers from first line in trace file
        headers = lines[0].split(", ")
        for i in range(len(headers)):
            header = headers[i].strip("#\n")
            if header in common_trace:
                trace_read[header] = None
            else:
                trace_read[header] = {}

        # Get trace output from final line(s) of file
        # ALAMO will append new lines to existing trace files
        # For multiple outputs, each output has its own line in trace file
        for j in range(len(output_labels)):
            trace = lines[-len(output_labels)+j].split(", ")

            for i in range(len(headers)):
                header = headers[i].strip("#\n")
                trace_val = trace[i].strip("\n")

                # Replace Fortran powers (^) with Python powers (**)
                trace_val = trace_val.replace("^", "**")
                # Replace = with ==
                trace_val = trace_val.replace("=", "==")

                if header in common_trace:
                    # These should be common across all output
                    if trace_read[header] is None:
                        # No value yet, so set value
                        trace_read[header] = trace_val
                    else:
                        # Check that current value matches the existng value
                        if trace_read[header] != trace_val:
                            raise RuntimeError(
                                f"Mismatch in values when reading ALAMO trace "
                                f"file. Values for {header}: "
                                f"{trace_read[header]}, {header}")
                else:
                    trace_read[header][output_labels[j]] = trace_val

                # Do some final sanity checks
                if header == "OUTPUT":
                    # OUTPUT should be equal to the current index of outputs
                    if trace_val != str(j+1):
                        raise RuntimeError(
                            f"Mismatch when reading ALAMO trace file. "
                            f"Expected OUTPUT = {j+1}, found {trace_val}.")
                elif header == "Model":
                    # Var label on LHS should match output label
                    vlabel = trace_val.split("==")[0].strip()
                    if vlabel != output_labels[j]:
                        raise RuntimeError(
                            f"Mismatch when reading ALAMO trace file. "
                            f"Label of output variable in expression "
                            f"({vlabel}) does not match expected label "
                            f"({output_labels[j]}).")

        return trace_read

    def _populate_results(self, trace_dict):
        """
        Method to populate the results object with data from a trace file.

        Args:
            trace_dict: trace file data in form of a dict

        Returns:
            None
        """
        self._results = trace_dict

    def _build_surrogate_object(self):
        """
        Method to construct an AlmaoObject from the current results
        object.

        Args:
            None

        Returns:
            AlamoSurrogate
        """
        return AlamoSurrogate(
            surrogate_expressions=self._results["Model"],
            input_labels=self._input_labels,
            output_labels=self._output_labels,
            input_bounds=self._input_bounds)

    def _remove_temp_files(self):
        """
        Method to remove temporary files created during the ALAMO workflow,
        i.e. the .alm and .trc files.

        Args:
            None

        Returns:
            None
        """
        remove = True
        if self.config.filename is not None:
            remove = False

        self._temp_context.release(remove=remove)
        # Release tempfile context
        self._temp_context = None
Beispiel #6
0
class GibbsReactorData(UnitModelBlockData):
    """
    Standard Gibbs Reactor Unit Model Class

    This model assume all possible reactions reach equilibrium such that the
    system partial molar Gibbs free energy is minimized.
    Since some species mole flow rate might be very small,
    the natural log of the species molar flow rate is used.
    Instead of specifying the system Gibbs free energy as an objective
    function, the equations for zero partial derivatives of the grand function
    with Lagrangian multiple terms with repect to product species mole flow
    rates and the multiples are specified as constraints.
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc=
            """Gibbs reactors do not support dynamic models, thus this must be
False."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc="""Gibbs reactors do not have defined volume, thus this must be
False."""))
    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.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy 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,
**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_heat_transfer",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    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(
        "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,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare(
        "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(
        "inert_species",
        ConfigValue(
            default=[],
            domain=ListOf(str),
            description="List of inert species",
            doc="List of species which do not take part in reactions."))

    def build(self):
        """
        Begin building model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(GibbsReactorData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args
            })

        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        # Validate list of inert species
        for i in self.config.inert_species:
            if i not in self.control_volume.properties_in.component_list:
                raise ConfigurationError(
                    "{} invalid component in inert_species argument. {} is "
                    "not in the property package component list.".format(
                        self.name, i))

        self.control_volume.add_total_element_balances()

        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Add performance equations
        # Add Lagrangian multiplier variables
        e_units = self.config.property_package.get_metadata(
        ).get_derived_units("energy_mole")
        self.lagrange_mult = Var(self.flowsheet().time,
                                 self.config.property_package.element_list,
                                 domain=Reals,
                                 initialize=100,
                                 doc="Lagrangian multipliers",
                                 units=e_units)

        # TODO : Remove this once sacling is properly implemented
        self.gibbs_scaling = Param(default=1, mutable=True)

        # Use Lagrangian multiple method to derive equations for Out_Fi
        # Use RT*lagrange as the Lagrangian multiple such that lagrange is in
        # a similar order of magnitude as log(Yi)

        @self.Constraint(self.flowsheet().time,
                         self.control_volume.properties_in.phase_component_set,
                         doc="Gibbs energy minimisation constraint")
        def gibbs_minimization(b, t, p, j):
            # Use natural log of species mole flow to avoid Pyomo solver
            # warnings of reaching infeasible point
            if j in self.config.inert_species:
                return Constraint.Skip
            return 0 == b.gibbs_scaling * (
                b.control_volume.properties_out[t].gibbs_mol_phase_comp[p, j] +
                sum(b.lagrange_mult[t, e] * b.control_volume.properties_out[t].
                    config.parameters.element_comp[j][e]
                    for e in b.config.property_package.element_list))

        if len(self.config.inert_species) > 0:

            @self.Constraint(self.flowsheet().time,
                             self.control_volume.properties_in.phase_list,
                             self.config.inert_species,
                             doc="Inert species balances")
            def inert_species_balance(b, t, p, j):
                # Add species balances for inert components
                cv = b.control_volume
                e_comp = cv.properties_out[t].config.parameters.element_comp

                # Check for linear dependence with element balances
                # If an inert species is the only source of element e,
                # the inert species balance would be linearly dependent on the
                # element balance for e.
                dependent = True

                if len(self.control_volume.properties_in.phase_list) > 1:
                    # Multiple phases avoid linear dependency
                    dependent = False
                else:
                    for e in self.config.property_package.element_list:
                        if e_comp[j][e] == 0:
                            # Element e not in component j, no effect
                            continue
                        else:
                            for i in self.control_volume.properties_in.component_list:
                                if i == j:
                                    continue
                                else:
                                    # If comp j shares element e with comp i
                                    # cannot be linearly dependent
                                    if e_comp[i][e] != 0:
                                        dependent = False

                if (not dependent and (p, j) in
                        self.control_volume.properties_in.phase_component_set):
                    return 0 == (
                        cv.properties_in[t].get_material_flow_terms(p, j) -
                        cv.properties_out[t].get_material_flow_terms(p, j))
                else:
                    return Constraint.Skip

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            self.heat_duty = Reference(self.control_volume.heat[:])
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.control_volume.deltaP[:])

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        if hasattr(self, "heat_duty"):
            var_dict["Heat Duty"] = self.heat_duty[time_point]
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]

        return {"vars": var_dict}
Beispiel #7
0
class HelmMixerData(UnitModelBlockData):
    """
    This is a Helmholtz EOS specific mixed unit model.
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Indicates whether this model will be dynamic or not,
**default** = False. Mixer blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Mixer blocks do not contain holdup, thus this must be
False.""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            doc="""Property parameter object used to define property
calculations, **default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "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(
        "inlet_list",
        ConfigValue(
            domain=ListOf(str),
            description="List of inlet names",
            doc="""A list containing names of inlets,
**default** - None.
**Valid values:** {
**None** - use num_inlets argument,
**list** - a list of names to use for inlets.}""",
        ),
    )
    CONFIG.declare(
        "num_inlets",
        ConfigValue(
            domain=int,
            description="Number of inlets to unit",
            doc="""Argument indicating number (int) of inlets to construct, not
used if inlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use inlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of inlets to create (will be named with sequential integers
from 1 to num_inlets).}""",
        ),
    )
    CONFIG.declare(
        "momentum_mixing_type",
        ConfigValue(
            default=MomentumMixingType.minimize,
            domain=MomentumMixingType,
            description="Method to use when mixing momentum/pressure",
            doc="""Argument indicating what method to use when mixing momentum/
pressure of incoming streams,
**default** - MomentumMixingType.minimize.
**Valid values:** {
**MomentumMixingType.none** - do not include momentum mixing equations,
**MomentumMixingType.minimize** - mixed stream has pressure equal to the
minimimum pressure of the incoming streams (uses smoothMin operator),
**MomentumMixingType.equality** - enforces equality of pressure in mixed and
all incoming streams.,
**MomentumMixingType.minimize_and_equality** - add constraints for pressure
equal to the minimum pressure of the inlets and constraints for equality of
pressure in mixed and all incoming streams. When the model is initially built,
the equality constraints are deactivated.  This option is useful for switching
between flow and pressure driven simulations.}""",
        ),
    )

    def build(self):
        """
        General build method for MixerData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of unit models.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        # Call super.build()
        super().build()

        self._get_property_package()

        # Create list of inlet names
        # PYLINT-TODO: assigning the result of self.create_inlet_list() to unused local variable inlet_list
        # causes pylint error assignment-from-no-return; check if removing assignment is OK
        self.create_inlet_list()

        # Build StateBlocks
        self.add_inlet_state_blocks()
        self.add_mixed_state_block()

        @self.Constraint(self.flowsheet().time)
        def mass_balance(b, t):
            return self.mixed_state[t].flow_mol == sum(
                self.inlet_blocks[i][t].flow_mol for i in self.inlet_list)

        @self.Constraint(self.flowsheet().time)
        def energy_balance(b, t):
            return self.mixed_state[t].enth_mol*self.mixed_state[t].flow_mol == \
                sum(self.inlet_blocks[i][t].enth_mol
                    * self.inlet_blocks[i][t].flow_mol
                    for i in self.inlet_list
                )

        mmx_type = self.config.momentum_mixing_type
        if mmx_type == MomentumMixingType.minimize:
            self.add_pressure_minimization_equations()
        elif mmx_type == MomentumMixingType.equality:
            self.add_pressure_equality_equations()
        elif mmx_type == MomentumMixingType.minimize_and_equality:
            self.add_pressure_minimization_equations()
            self.add_pressure_equality_equations()
            self.use_minimum_inlet_pressure_constraint()

        self.add_port_objects()

    def create_inlet_list(self):
        """
        Create list of inlet stream names based on config arguments.

        Returns:
            list of strings
        """
        if (self.config.inlet_list is not None
                and self.config.num_inlets is not None):
            # If both arguments provided and not consistent, raise Exception
            if len(self.config.inlet_list) != self.config.num_inlets:
                raise ConfigurationError(
                    "{} Mixer provided with both inlet_list and "
                    "num_inlets arguments, which were not consistent ("
                    "length of inlet_list was not equal to num_inlets). "
                    "PLease check your arguments for consistency, and "
                    "note that it is only necessary to provide one of "
                    "these arguments.".format(self.name))
        elif self.config.inlet_list is None and self.config.num_inlets is None:
            # If no arguments provided for inlets, default to num_inlets = 2
            self.config.num_inlets = 2

        # Create a list of names for inlet StateBlocks
        if self.config.inlet_list is not None:
            inlet_list = self.config.inlet_list
        else:
            inlet_list = [
                "inlet_{}".format(n)
                for n in range(1, self.config.num_inlets + 1)
            ]
        self.inlet_list = inlet_list

    def add_inlet_state_blocks(self):
        """
        Construct StateBlocks for all inlet streams.

        Args:
            list of strings to use as StateBlock names

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["defined_state"] = True

        # Create empty list to hold StateBlocks for return
        self.inlet_blocks = {}

        # Create an instance of StateBlock for all inlets
        for i in self.inlet_list:
            i_obj = self.config.property_package.build_state_block(
                self.flowsheet().time,
                doc="Material properties at inlet",
                default=tmp_dict,
            )
            setattr(self, "{}_state".format(i), i_obj)
            self.inlet_blocks[i] = i_obj

    def add_mixed_state_block(self):
        """
        Constructs StateBlock to represent mixed stream.

        Returns:
            New StateBlock object
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["defined_state"] = False

        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().time,
            doc="Material properties of mixed stream",
            default=tmp_dict,
        )
        return self.mixed_state

    def add_pressure_minimization_equations(self):
        """
        Add pressure minimization equations. This is done by sequential
        comparisons of each inlet to the minimum pressure so far, using
        the IDAES smooth minimum function.
        """
        units_meta = self.config.property_package.get_metadata()
        self.eps_pressure = Param(
            mutable=True,
            initialize=1e-3,
            domain=PositiveReals,
            doc="Smoothing term for minimum inlet pressure",
            units=units_meta.get_derived_units("pressure"))

        # Calculate minimum inlet pressure
        @self.Expression(
            self.flowsheet().time,
            self.inlet_list,
            doc="Calculation for minimum inlet pressure",
        )
        def minimum_pressure(b, t, i):
            if i == self.inlet_list[0]:
                return self.inlet_blocks[i][t].pressure
            else:
                pi = self.inlet_list[self.inlet_list.index(i) - 1]
                prev_p = self.minimum_pressure[t, pi]
                this_p = self.inlet_blocks[i][t].pressure
                return smooth_min(this_p, prev_p, self.eps_pressure)

        # Set inlet pressure to minimum pressure
        @self.Constraint(self.flowsheet().time,
                         doc="Link pressure to control volume")
        def minimum_pressure_constraint(b, t):
            return self.mixed_state[t].pressure == (
                self.minimum_pressure[t, self.inlet_list[-1]])

    def add_pressure_equality_equations(self):
        """
        Add pressure equality equations. Note that this writes a number of
        constraints equal to the number of inlets, enforcing equality between
        all inlets and the mixed stream.
        """
        # Create equality constraints
        @self.Constraint(
            self.flowsheet().time,
            self.inlet_list,
            doc="Calculation for minimum inlet pressure",
        )
        def pressure_equality_constraints(b, t, i):
            return self.mixed_state[t].pressure == self.inlet_blocks[i][
                t].pressure

    def add_port_objects(self):
        """
        Adds Port objects if required.

        Args:
            a list of inlet StateBlock objects
            a mixed state StateBlock object

        Returns:
            None
        """
        for p in self.inlet_list:
            self.add_port(name=p, block=self.inlet_blocks[p], doc="Inlet Port")
        self.add_port(name="outlet", block=self.mixed_state, doc="Outlet Port")

    def use_minimum_inlet_pressure_constraint(self):
        """Activate the mixer pressure = mimimum inlet pressure constraint and
        deactivate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_minimum_inlet_pressure_constraint() can only be used
                when momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.activate()
        self.pressure_equality_constraints.deactivate()

    def use_equal_pressure_constraint(self):
        """Deactivate the mixer pressure = mimimum inlet pressure constraint
        and activate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_equal_pressure_constraint() can only be used when
                momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.deactivate()
        self.pressure_equality_constraints.activate()

    def initialize(self, outlvl=idaeslog.NOTSET, optarg=None, solver=None):
        """
        Initialization routine for mixer.

        Keyword Arguments:
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # This shouldn't require too much initializtion, just fixing inlets
        # and solving should always work.

        # sp is what to save to make sure state after init is same as the start
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        for b in self.inlet_blocks.values():
            for bdat in b.values():
                bdat.pressure.fix()
                bdat.enth_mol.fix()
                bdat.flow_mol.fix()

        for t, v in self.outlet.pressure.items():
            if not v.fixed:
                v.value = min([
                    value(self.inlet_blocks[i][t].pressure)
                    for i in self.inlet_blocks
                ])
        self.outlet.unfix()

        if (hasattr(self, "pressure_equality_constraints")
                and self.pressure_equality_constraints.active):
            # If using the equal pressure constraint fix the outlet and free
            # the inlet pressures, this is typical for pressure driven flow
            for i, b in self.inlet_blocks.items():
                for bdat in b.values():
                    bdat.pressure.unfix()
            self.outlet.pressure.fix()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))
        from_json(self, sd=istate, wts=sp)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        for t, c in self.mass_balance.items():
            s = iscale.get_scaling_factor(self.mixed_state[t].flow_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for t, c in self.energy_balance.items():
            s = iscale.get_scaling_factor(self.mixed_state[t].enth_mol)
            s *= iscale.get_scaling_factor(self.mixed_state[t].flow_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        if hasattr(self, "minimum_pressure_constraint"):
            for t, c in self.minimum_pressure_constraint.items():
                s = iscale.get_scaling_factor(self.mixed_state[t].pressure)
                iscale.constraint_scaling_transform(c, s, overwrite=False)
        if hasattr(self, "pressure_equality_constraints"):
            for (t, i), c in self.pressure_equality_constraints.items():
                s = iscale.get_scaling_factor(self.mixed_state[t].pressure)
                iscale.constraint_scaling_transform(c, s, overwrite=False)
Beispiel #8
0
class HelmSplitterData(UnitModelBlockData):
    """
    This is a basic stream splitter which splits flow into outlet streams based
    on split fractions. This does not do phase seperation, and assumes that you
    are using a Helmholtz EOS propery package with P-H state variables. In
    dynamic mode this uses a pseudo-steady-state model.

    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
        ))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            doc="""Property parameter object used to define property
calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "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(
        "outlet_list",
        ConfigValue(
            domain=ListOf(str),
            description="List of outlet names",
            doc="""A list containing names of outlets,
**default** - None.
**Valid values:** {
**None** - use num_outlets argument,
**list** - a list of names to use for outlets.}""",
        ),
    )
    CONFIG.declare(
        "num_outlets",
        ConfigValue(
            domain=int,
            description="Number of outlets to unit",
            doc="""Argument indicating number (int) of outlets to construct,
not used if outlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use outlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of outlets to create (will be named with sequential integers
from 1 to num_outlets).}""",
        ),
    )

    def build(self):
        """
        Build a splitter.

        Args:
            None

        Returns:
            None
        """
        time = self.flowsheet().time
        super().build()

        self._get_property_package()

        self.create_outlet_list()
        self.add_inlet_state_and_port()
        self.add_outlet_state_blocks()
        self.add_outlet_port_objects()

        self.split_fraction = Var(time,
                                  self.outlet_list,
                                  initialize=1.0 / len(self.outlet_list),
                                  doc="Split fractions for outlet streams")

        @self.Constraint(time, doc="Splt constraint")
        def sum_split(b, t):
            return 1 == sum(self.split_fraction[t, o]
                            for o in self.outlet_list)

        @self.Constraint(time, self.outlet_list, doc="Pressure constraint")
        def pressure_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            return self.mixed_state[t].pressure == o_block[t].pressure

        @self.Constraint(time, self.outlet_list, doc="Enthalpy constraint")
        def enthalpy_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            return self.mixed_state[t].enth_mol == o_block[t].enth_mol

        @self.Constraint(time, self.outlet_list, doc="Flow constraint")
        def flow_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            sf = self.split_fraction[t, o]
            return self.mixed_state[t].flow_mol * sf == o_block[t].flow_mol

    def add_inlet_state_and_port(self):
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["defined_state"] = True
        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().time,
            doc="Material properties of mixed (inlet) stream",
            default=tmp_dict,
        )
        self.add_port(name="inlet", block=self.mixed_state, doc="Inlet Port")

    def create_outlet_list(self):
        """
        Create list of outlet stream names based on config arguments.

        Returns:
            list of strings
        """
        config = self.config
        if config.outlet_list is not None and config.num_outlets is not None:
            # If both arguments provided and not consistent, raise Exception
            if len(config.outlet_list) != config.num_outlets:
                raise ConfigurationError(
                    "{} Splitter provided with both outlet_list and "
                    "num_outlets arguments, which were not consistent ("
                    "length of outlet_list was not equal to num_outlets). "
                    "Please check your arguments for consistency, and "
                    "note that it is only necessry to provide one of "
                    "these arguments.".format(self.name))
        elif (config.outlet_list is None and config.num_outlets is None):
            # If no arguments provided for outlets, default to num_outlets = 2
            config.num_outlets = 2

        # Create a list of names for outlet StateBlocks
        if config.outlet_list is not None:
            outlet_list = self.config.outlet_list
        else:
            outlet_list = [
                "outlet_{}".format(n) for n in range(1, config.num_outlets + 1)
            ]
        self.outlet_list = outlet_list

    def add_outlet_state_blocks(self):
        """
        Construct StateBlocks for all outlet streams.

        Args:
            None

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["defined_state"] = False

        # Create empty list to hold StateBlocks for return
        self.outlet_blocks = {}

        # Create an instance of StateBlock for all outlets
        for o in self.outlet_list:
            o_obj = self.config.property_package.build_state_block(
                self.flowsheet().time,
                doc="Material properties at outlet",
                default=tmp_dict,
            )
            setattr(self, o + "_state", o_obj)
            self.outlet_blocks[o] = o_obj

    def add_outlet_port_objects(self):
        """
        Adds outlet Port objects if required.

        Args:
            None

        Returns:
            None
        """
        self.outlet_ports = {}
        for p in self.outlet_list:
            self.add_port(name=p, block=self.outlet_blocks[p], doc="Outlet")
            self.outlet_ports[p] = getattr(self, p)

    def initialize(self, outlvl=idaeslog.NOTSET, optarg=None, solver=None):
        """
        Initialization routine for splitter

        Keyword Arguments:
            outlvl: sets output level of initialization routine
            optarg: solver options dictionary object (default=None, use
                    default solver options)
            solver: str indicating which solver to use during
                     initialization (default = None, use default solver)

        Returns:
            If hold_states is True, returns a dict containing flags for which
            states were fixed during initialization.
        """
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # sp is what to save to make sure state after init is same as the start
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # check for fixed outlet flows and use them to calculate fixed split
        # fractions
        for t in self.flowsheet().time:
            for o in self.outlet_list:
                if self.outlet_blocks[o][t].flow_mol.fixed:
                    self.split_fraction[t, o].fix(
                        value(self.mixed_state[t] /
                              self.outlet_blocks[o][t].flow_mol))

        # fix or unfix split fractions so n - 1 are fixed
        for t in self.flowsheet().time:
            # see how many split fractions are fixed
            n = sum(1 for o in self.outlet_list
                    if self.split_fraction[t, o].fixed)
            # if number of outlets - 1 we're good
            if n == len(self.outlet_list) - 1:
                continue
            # if too mant are fixed un fix the first, generally assume that is
            # the main flow, and is the calculated split fraction
            if n == len(self.outlet_list):
                self.split_fraction[t, self.outlet_list[0]].unfix()
            # if not enough fixed, start fixing from the back until there are
            # are enough
            for o in reversed(self.outlet_list):
                if not self.split_fraction[t, o].fixed:
                    self.split_fraction[t, o].fix()
                    n += 1
                if n == len(self.outlet_list) - 1:
                    break

        # This model is really simple so it should easily solve without much
        # effort to initialize
        self.inlet.fix()
        for o, p in self.outlet_ports.items():
            p.unfix()
        assert degrees_of_freedom(self) == 0
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

        from_json(self, sd=istate, wts=sp)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        for (t, i), c in self.pressure_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].pressure)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for (t, i), c in self.enthalpy_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].enth_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for (t, i), c in self.flow_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].flow_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
Beispiel #9
0
class FlowsheetBlockData(ProcessBlockData):
    """
    The FlowsheetBlockData Class forms the base class for all IDAES process
    flowsheet models. The main purpose of this class is to automate the tasks
    common to all flowsheet models and ensure that the necessary attributes of
    a flowsheet model are present.

    The most signfiicant role of the FlowsheetBlockData class is to
    automatically create the time domain for the flowsheet.
    """

    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare("dynamic", ConfigValue(
        default=useDefault,
        domain=DefaultBool,
        description="Dynamic model flag",
        doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent or False,
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare("time", ConfigValue(
        default=None,
        domain=is_time_domain,
        description="Flowsheet time domain",
        doc="""Pointer to the time domain for the flowsheet. Users may provide
an existing time domain from another flowsheet, otherwise the flowsheet will
search for a parent with a time domain or create a new time domain and
reference it here."""))
    CONFIG.declare("time_set", ConfigValue(
        default=[0],
        domain=ListOf(float),
        description="Set of points for initializing time domain",
        doc="""Set of points for initializing time domain. This should be a
list of floating point numbers,
**default** - [0]."""))
    CONFIG.declare("time_units", ConfigValue(
        description="Units for time domain",
        doc="""Pyomo Units object describing the units of the time domain.
This must be defined for dynamic simulations, default = None."""))
    CONFIG.declare("default_property_package", ConfigValue(
        default=None,
        domain=is_physical_parameter_block,
        description="Default property package to use in flowsheet",
        doc="""Indicates the default property package to be used by models
within this flowsheet if not otherwise specified,
**default** - None.
**Valid values:** {
**None** - no default property package,
**a ParameterBlock object**.}"""))

    def build(self):
        """
        General build method for FlowsheetBlockData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of flowsheets.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        super(FlowsheetBlockData, self).build()

        self._time_units = None

        # Set up dynamic flag and time domain
        self._setup_dynamics()

    @property
    def time(self):
        # _time will be created by the _setup_dynamics method
        return self._time

    @property
    def time_units(self):
        return self._time_units

    def is_flowsheet(self):
        """
        Method which returns True to indicate that this component is a
        flowsheet.

        Args:
            None

        Returns:
            True
        """
        return True

    def model_check(self):
        """
        This method runs model checks on all unit models in a flowsheet.

        This method searches for objects which inherit from UnitModelBlockData
        and executes the model_check method if it exists.

        Args:
            None

        Returns:
            None
        """
        _log.info("Executing model checks.")
        for o in self.component_objects(descend_into=False):
            if isinstance(o, UnitModelBlockData):
                try:
                    o.model_check()
                except AttributeError:
                    # This should never happen, but just in case
                    _log.warning('{} Model/block has no model_check method.'
                                 .format(o.name))

    def stream_table(self, true_state=False, time_point=0, orient='columns'):
        """
        Method to generate a stream table by iterating over all Arcs in the
        flowsheet.

        Args:
            true_state : whether the state variables (True) or display
                         variables (False, default) from the StateBlocks should
                         be used in the stream table.
            time_point : point in the time domain at which to create stream
                         table (default = 0)
            orient : whether stream should be shown by columns ("columns") or
                     rows ("index")

        Returns:
            A pandas dataframe containing stream table information
        """
        dict_arcs = {}

        for a in self.component_objects(ctype=Arc, descend_into=False):
            dict_arcs[a.local_name] = a

        return create_stream_table_dataframe(dict_arcs,
                                             time_point=time_point,
                                             orient=orient,
                                             true_state=true_state)

    def visualize(self, model_name, **kwargs):
        """
        Starts up a flask server that serializes the model and pops up a 
        webpage with the visualization

        Args:
            model_name : The name of the model that flask will use as an argument
                         for the webpage
        Keyword Args:
            **kwargs: Additional keywords for :func:`idaes.ui.fsvis.visualize()`

        Returns:
            None
        """
        visualize(self, model_name, **kwargs)

    def get_costing(self, module=costing, year=None, integer_n_units=False):
        """
        Creates a new block called 'costing' at the flowsheet level. This block
        builds global parameters used in costing methods (power plant costing
        and generic costing).

        Args:
            self - idaes flowsheet
            year : used to build parameter CE_index (Chemical Engineering),
            this parameter is the same for all costing blocks in the flowsheet
            integer_n_units : flag to define variable domain (True: domain is
            within Integer numbers, False: domain is NonNegativeReals).
        Returns:
            None
        """
        self.costing = pe.Block()

        module.global_costing_parameters(self.costing, year=year,
                                         integer_n_units=integer_n_units)

    def _get_stream_table_contents(self, time_point=0):
        """
        Calls stream_table method and returns result
        """
        return self.stream_table(time_point)

    def _setup_dynamics(self):
        # Look for parent flowsheet
        fs = self.flowsheet()

        # Check the dynamic flag, and retrieve if necessary
        if self.config.dynamic == useDefault:
            if fs is None:
                # No parent, so default to steady-state and warn user
                _log.warning('{} is a top level flowsheet, but dynamic flag '
                             'set to useDefault. Dynamic '
                             'flag set to False by default'
                             .format(self.name))
                self.config.dynamic = False

            else:
                # Get dynamic flag from parent flowsheet
                self.config.dynamic = fs.config.dynamic

        # Check for case when dynamic=True, but parent dynamic=False
        elif self.config.dynamic is True:
            if fs is not None and fs.config.dynamic is False:
                raise DynamicError(
                        '{} trying to declare a dynamic model within '
                        'a steady-state flowsheet. This is not '
                        'supported by the IDAES framework. Try '
                        'creating a dynamic flowsheet instead, and '
                        'declaring some models as steady-state.'
                        .format(self.name))

        # Validate units for time domain
        if self.config.time is None and fs is not None:
            # We will get units from parent
            pass
        elif self.config.time_units is None and self.config.dynamic:
            raise ConfigurationError(
                f"{self.name} - no units were specified for the time domain. "
                f"Units must be be specified for dynamic models.")
        elif self.config.time_units is None and not self.config.dynamic:
            _log.debug("No units specified for stady-state time domain.")
        elif not isinstance(self.config.time_units, _PyomoUnit):
            raise ConfigurationError(
                "{} unrecognised value for time_units argument. This must be "
                "a Pyomo Unit object (not a compound unit)."
                .format(self.name))

        if self.config.time is not None:
            # Validate user provided time domain
            if (self.config.dynamic is True and
                    not isinstance(self.config.time, ContinuousSet)):
                raise DynamicError(
                        '{} was set as a dynamic flowsheet, but time domain '
                        'provided was not a ContinuousSet.'.format(self.name))
            add_object_reference(self, "_time", self.config.time)
            self._time_units = self.config.time_units
        else:
            # If no parent flowsheet, set up time domain
            if fs is None:
                # Create time domain
                if self.config.dynamic:
                    # Check if time_set has at least two points
                    if len(self.config.time_set) < 2:
                        # Check if time_set is at default value
                        if self.config.time_set == [0.0]:
                            # If default, set default end point to be 1.0
                            self.config.time_set = [0.0, 1.0]
                        else:
                            # Invalid user input, raise Excpetion
                            raise DynamicError(
                                    "Flowsheet provided with invalid "
                                    "time_set attribute - must have at "
                                    "least two values (start and end).")
                    # For dynamics, need a ContinuousSet
                    self._time = ContinuousSet(initialize=self.config.time_set)
                else:
                    # For steady-state, use an ordered Set
                    self._time = pe.Set(initialize=self.config.time_set,
                                        ordered=True)
                self._time_units = self.config.time_units

                # Set time config argument as reference to time domain
                self.config.time = self._time
            else:
                # Set time config argument to parent time
                self.config.time = fs.time
                add_object_reference(self, "_time", fs.time)
                self._time_units = fs._time_units
Beispiel #10
0
class ComponentData(ProcessBlockData):
    CONFIG = ConfigBlock()

    CONFIG.declare("valid_phase_types", ConfigValue(
            domain=ListOf(PhaseType),
            doc="List of valid PhaseTypes (Enums) for this Component."))

    CONFIG.declare("elemental_composition", ConfigValue(
            domain=dict,
            description="Elemental composition of component",
            doc="Dict containing elemental composition in the form element "
                ": stoichiometry"))

    CONFIG.declare("henry_component", ConfigValue(
            domain=dict,
            description="Phases in which component follows Henry's Law",
            doc="Dict indicating phases in which component follows Henry's "
                "Law (keys) with values indicating form of law."))

    CONFIG.declare("vol_mol_liq_comp", ConfigValue(
        description="Method to use to calculate liquid phase molar volume",
        doc="Method to use to calculate liquid phase molar volume. Users "
        "need only define either vol_mol_liq_comp or dens_mol_liq_comp."))
    CONFIG.declare("vol_mol_sol_comp", ConfigValue(
        description="Method to use to calculate solid phase molar volume",
        doc="Method to use to calculate solid phase molar volume. Users "
        "need only define either vol_mol_sol_comp or dens_mol_sol_comp."))
    CONFIG.declare("dens_mol_liq_comp", ConfigValue(
        description="Method to use to calculate liquid phase molar density",
        doc="Method to use to calculate liquid phase molar density. Users "
        "need only define either vol_mol_liq_comp or dens_mol_liq_comp."))
    CONFIG.declare("dens_mol_sol_comp", ConfigValue(
        description="Method to use to calculate solid phase molar density",
        doc="Method to use to calculate solid phase molar density. Users "
        "need only define either vol_mol_sol_comp or dens_mol_sol_comp."))

    CONFIG.declare("cp_mol_liq_comp", ConfigValue(
        description="Method to calculate liquid component specific heats"))
    CONFIG.declare("cp_mol_sol_comp", ConfigValue(
        description="Method to calculate solid component specific heats"))
    CONFIG.declare("cp_mol_ig_comp", ConfigValue(
        description="Method to calculate ideal gas component specific heats"
        ))
    CONFIG.declare("enth_mol_liq_comp", ConfigValue(
        description="Method to calculate liquid component molar enthalpies"))
    CONFIG.declare("enth_mol_sol_comp", ConfigValue(
        description="Method to calculate solid component molar enthalpies"))
    CONFIG.declare("enth_mol_ig_comp", ConfigValue(
        description="Method to calculate ideal gas component molar enthalpies"
        ))
    CONFIG.declare("entr_mol_liq_comp", ConfigValue(
        description="Method to calculate liquid component molar entropies"))
    CONFIG.declare("entr_mol_sol_comp", ConfigValue(
        description="Method to calculate solid component molar entropies"))
    CONFIG.declare("entr_mol_ig_comp", ConfigValue(
        description="Method to calculate ideal gas component molar entropies"))

    CONFIG.declare("has_vapor_pressure", ConfigValue(
        default=True,
        domain=Bool,
        description="Flag indicating whether component has a vapor pressure"))
    CONFIG.declare("pressure_sat_comp", ConfigValue(
        description="Method to use to calculate saturation pressure"))
    CONFIG.declare("relative_permittivity_liq_comp", ConfigValue(
        description=
        "Method to use to calculate liquid phase relative permittivity"))

    CONFIG.declare("phase_equilibrium_form", ConfigValue(
        domain=dict,
        description="Form of phase equilibrium constraints for component"))

    CONFIG.declare("parameter_data", ConfigValue(
        default={},
        domain=dict,
        description="Dict containing initialization data for parameters"))

    CONFIG.declare("_component_list_exists", ConfigValue(
            default=False,
            doc="Internal config argument indicating whether component_list "
            "needs to be populated."))
    CONFIG.declare("_electrolyte", ConfigValue(
            default=False,
            doc="Internal config argument indicating whether electrolyte "
            "component_lists needs to be populated."))

    def build(self):
        super().build()

        # If the component_list does not exist, add reference to new Component
        # The IF is mostly for backwards compatability, to allow for old-style
        # property packages where the component_list already exists but we
        # need to add new Component objects
        if not self.config._component_list_exists:
            if not self.config._electrolyte:
                self._add_to_component_list()
            else:
                self._add_to_electrolyte_component_list()

        base_units = self.parent_block().get_metadata().default_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"])

        # 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():
            if p in self.config.parameter_data:
                self.add_component(p, Var(units=u))
                set_param_from_config(self, p)

    def is_solute(self):
        raise TypeError(
            "{} Generic Component objects do not support is_solute() method. "
            "Use a Solvent or Solute Component instead."
            .format(self.name))

    def is_solvent(self):
        raise TypeError(
            "{} Generic Component objects do not support is_solvent() method. "
            "Use a Solvent or Solute Component instead."
            .format(self.name))

    def _add_to_component_list(self):
        """
        Method to add reference to new Component in component_list
        """
        parent = self.parent_block()
        try:
            comp_list = getattr(parent, "component_list")
            comp_list.add(self.local_name)
        except AttributeError:
            # Parent does not have a component_list yet, so create one
            parent.component_list = Set(initialize=[self.local_name],
                                        ordered=True)

    def _add_to_electrolyte_component_list(self):
        """
        Special case method for adding references to new Component in
        component_lists for electrolyte systems,

        New Component types should overload this method
        """
        parent = self.parent_block()
        parent._non_aqueous_set.add(self.local_name)

    def _is_phase_valid(self, phase):
        # If no valid phases assigned
        if self.config.valid_phase_types is None:
            try:
                if phase.is_aqueous_phase():
                    # If this is an aqueous phase, check for validaity
                    return self._is_aqueous_phase_valid()
            except AttributeError:
                raise TypeError(
                    "{} Phase {} is not a valid phase object or is undeclared."
                    " Please check your phase declarations."
                    .format(self.name, phase))
            # Otherwise assume all are valid
            return True

        # Check for behaviour of phase, and see if that is a valid behaviour
        # for component.
        elif phase.is_liquid_phase():
            # Check if this is an aqueous phase
            if phase.is_aqueous_phase():
                if (self._is_aqueous_phase_valid() and
                        PhaseType.aqueousPhase in
                        self.config.valid_phase_types):
                    return True
                else:
                    return False
            elif PhaseType.liquidPhase in self.config.valid_phase_types:
                return True
            else:
                return False
        elif (phase.is_vapor_phase() and
                PhaseType.vaporPhase in self.config.valid_phase_types):
            return True
        elif (phase.is_solid_phase() and
                PhaseType.solidPhase in self.config.valid_phase_types):
            return True
        else:
            return False

    def _is_aqueous_phase_valid(self):
        # Method to indicate if a component type is stable in the aqueous phase
        # General components may not appear in aqueous phases
        return False