コード例 #1
0
class GDPoptSolver(pyomo.common.plugin.Plugin):
    """A decomposition-based GDP solver."""

    pyomo.common.plugin.implements(IOptSolver)
    pyomo.common.plugin.alias(
        'gdpopt',
        doc='The GDPopt decomposition-based '
        'Generalized Disjunctive Programming (GDP) solver')

    _metasolver = False

    CONFIG = ConfigBlock("GDPopt")
    CONFIG.declare("iterlim", ConfigValue(
        default=30, domain=NonNegativeInt,
        description="Iteration limit."
    ))
    CONFIG.declare("strategy", ConfigValue(
        default="LOA", domain=In(["LOA", "GLOA"]),
        description="Decomposition strategy to use."
    ))
    CONFIG.declare("init_strategy", ConfigValue(
        default="set_covering", domain=In(valid_init_strategies.keys()),
        description="Initialization strategy to use.",
        doc="""Selects the initialization strategy to use when generating
        the initial cuts to construct the master problem."""
    ))
    CONFIG.declare("custom_init_disjuncts", ConfigList(
        # domain=ComponentSets of Disjuncts,
        default=None,
        description="List of disjunct sets to use for initialization."
    ))
    CONFIG.declare("max_slack", ConfigValue(
        default=1000, domain=NonNegativeFloat,
        description="Upper bound on slack variables for OA"
    ))
    CONFIG.declare("OA_penalty_factor", ConfigValue(
        default=1000, domain=NonNegativeFloat,
        description="Penalty multiplication term for slack variables on the "
        "objective value."
    ))
    CONFIG.declare("set_cover_iterlim", ConfigValue(
        default=8, domain=NonNegativeInt,
        description="Limit on the number of set covering iterations."
    ))
    CONFIG.declare("mip_solver", ConfigValue(
        default="gurobi",
        description="Mixed integer linear solver to use."
    ))
    mip_solver_args = CONFIG.declare(
        "mip_solver_args", ConfigBlock(implicit=True))
    CONFIG.declare("nlp_solver", ConfigValue(
        default="ipopt",
        description="Nonlinear solver to use"))
    nlp_solver_args = CONFIG.declare(
        "nlp_solver_args", ConfigBlock(implicit=True))
    CONFIG.declare("call_after_master_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook after a solution of the master problem"
    ))
    CONFIG.declare("call_before_subproblem_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook before calling the subproblem solver"
    ))
    CONFIG.declare("call_after_subproblem_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook after a solution of the "
        "nonlinear subproblem"
    ))
    CONFIG.declare("call_after_subproblem_feasible", ConfigValue(
        default=_DoNothing,
        description="callback hook after feasible solution of "
        "the nonlinear subproblem"
    ))
    CONFIG.declare("algorithm_stall_after", ConfigValue(
        default=2,
        description="number of non-improving master iterations after which "
        "the algorithm will stall and exit."
    ))
    CONFIG.declare("tee", ConfigValue(
        default=False,
        description="Stream output to terminal.",
        domain=bool
    ))
    CONFIG.declare("logger", ConfigValue(
        default='pyomo.contrib.gdpopt',
        description="The logger object or name to use for reporting.",
        domain=a_logger
    ))
    CONFIG.declare("bound_tolerance", ConfigValue(
        default=1E-6, domain=NonNegativeFloat,
        description="Tolerance for bound convergence."
    ))
    CONFIG.declare("small_dual_tolerance", ConfigValue(
        default=1E-8,
        description="When generating cuts, small duals multiplied "
        "by expressions can cause problems. Exclude all duals "
        "smaller in absolue value than the following."
    ))
    CONFIG.declare("integer_tolerance", ConfigValue(
        default=1E-5,
        description="Tolerance on integral values."
    ))
    CONFIG.declare("constraint_tolerance", ConfigValue(
        default=1E-6,
        description="Tolerance on constraint satisfaction."
    ))
    CONFIG.declare("variable_tolerance", ConfigValue(
        default=1E-8,
        description="Tolerance on variable bounds."
    ))
    CONFIG.declare("round_NLP_binaries", ConfigValue(
        default=True,
        description="flag to round binary values to exactly 0 or 1. "
        "Rounding is done before fixing disjuncts."
    ))
    CONFIG.declare("reformulate_integer_vars_using", ConfigValue(
        default=None,
        description="The method to use for reformulating integer variables "
        "into binary for this solver."
    ))

    def available(self, exception_flag=True):
        """Check if solver is available.

        TODO: For now, it is always available. However, sub-solvers may not
        always be available, and so this should reflect that possibility.

        """
        return True

    def version(self):
        """Return a 3-tuple describing the solver version."""
        return __version__

    def solve(self, model, **kwds):
        """Solve the model.

        Warning: this solver is still in beta. Keyword arguments subject to
        change. Undocumented keyword arguments definitely subject to change.

        This function performs all of the GDPopt solver setup and problem
        validation. It then calls upon helper functions to construct the
        initial master approximation and iteration loop.

        Args:
            model (Block): a Pyomo model or block to be solved

        """
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        solve_data = GDPoptSolveData()
        created_GDPopt_block = False

        old_logger_level = config.logger.getEffectiveLevel()
        try:
            if config.tee and old_logger_level > logging.INFO:
                # If the logger does not already include INFO, include it.
                config.logger.setLevel(logging.INFO)
            config.logger.info("---Starting GDPopt---")

            # Create a model block on which to store GDPopt-specific utility
            # modeling objects.
            if hasattr(model, 'GDPopt_utils'):
                raise RuntimeError(
                    "GDPopt needs to create a Block named GDPopt_utils "
                    "on the model object, but an attribute with that name "
                    "already exists.")
            else:
                created_GDPopt_block = True
                model.GDPopt_utils = Block(
                    doc="Container for GDPopt solver utility modeling objects")

            solve_data.original_model = model

            solve_data.working_model = clone_orig_model_with_lists(model)
            GDPopt = solve_data.working_model.GDPopt_utils
            record_original_model_statistics(solve_data, config)

            solve_data.current_strategy = config.strategy

            # Reformulate integer variables to binary
            reformulate_integer_variables(solve_data.working_model, config)

            # Save ordered lists of main modeling components, so that data can
            # be easily transferred between future model clones.
            build_ordered_component_lists(solve_data.working_model)
            record_working_model_statistics(solve_data, config)
            solve_data.results.solver.name = 'GDPopt ' + str(self.version())

            # Save model initial values. These are used later to initialize NLP
            # subproblems.
            solve_data.initial_var_values = list(
                v.value for v in GDPopt.working_var_list)

            # Store the initial model state as the best solution found. If we
            # find no better solution, then we will restore from this copy.
            solve_data.best_solution_found = solve_data.initial_var_values

            # Validate the model to ensure that GDPopt is able to solve it.
            if not model_is_valid(solve_data, config):
                return

            # Maps in order to keep track of certain generated constraints
            GDPopt.oa_cut_map = ComponentMap()

            # Integer cuts exclude particular discrete decisions
            GDPopt.integer_cuts = ConstraintList(doc='integer cuts')

            # Feasible integer cuts exclude discrete realizations that have
            # been explored via an NLP subproblem. Depending on model
            # characteristics, the user may wish to revisit NLP subproblems
            # (with a different initialization, for example). Therefore, these
            # cuts are not enabled by default, unless the initial model has no
            # discrete decisions.

            # Note: these cuts will only exclude integer realizations that are
            # not already in the primary GDPopt_integer_cuts ConstraintList.
            GDPopt.no_backtracking = ConstraintList(
                doc='explored integer cuts')

            # Set up iteration counters
            solve_data.master_iteration = 0
            solve_data.mip_iteration = 0
            solve_data.nlp_iteration = 0

            # set up bounds
            solve_data.LB = float('-inf')
            solve_data.UB = float('inf')
            solve_data.iteration_log = {}

            # Flag indicating whether the solution improved in the past
            # iteration or not
            solve_data.feasible_solution_improved = False

            # Initialize the master problem
            GDPopt_initialize_master(solve_data, config)

            # Algorithm main loop
            GDPopt_iteration_loop(solve_data, config)

            # Update values in working model
            copy_var_list_values(
                from_list=solve_data.best_solution_found,
                to_list=GDPopt.working_var_list,
                config=config)
            GDPopt.objective_value.set_value(
                value(solve_data.working_objective_expr, exception=False))

            # Update values in original model
            copy_var_list_values(
                GDPopt.orig_var_list,
                solve_data.original_model.GDPopt_utils.orig_var_list,
                config)

            solve_data.results.problem.lower_bound = solve_data.LB
            solve_data.results.problem.upper_bound = solve_data.UB

        finally:
            config.logger.setLevel(old_logger_level)
            if created_GDPopt_block:
                model.del_component('GDPopt_utils')
コード例 #2
0
ファイル: bigm.py プロジェクト: vova292/pyomo
class BigM_Transformation(Transformation):
    """Relax disjunctive model using big-M terms.

    Relaxes a disjunctive model into an algebraic model by adding Big-M
    terms to all disjunctive constraints.

    This transformation accepts the following keyword arguments:
        bigM: A user-specified value (or dict) of M values to use (see below)
        targets: the targets to transform [default: the instance]

    M values are determined as follows:
       1) if the constraint appears in the bigM argument dict
       2) if the constraint parent_component appears in the bigM
          argument dict
       3) if any block which is an ancestor to the constraint appears in
          the bigM argument dict
       3) if 'None' is in the bigM argument dict
       4) if the constraint or the constraint parent_component appear in
          a BigM Suffix attached to any parent_block() beginning with the
          constraint's parent_block and moving up to the root model.
       5) if None appears in a BigM Suffix attached to any
          parent_block() between the constraint and the root model.
       6) if the constraint is linear, estimate M using the variable bounds

    M values may be a single value or a 2-tuple specifying the M for the
    lower bound and the upper bound of the constraint body.

    Specifying "bigM=N" is automatically mapped to "bigM={None: N}".

    The transformation will create a new Block with a unique
    name beginning "_pyomo_gdp_bigm_reformulation".  That Block will
    contain an indexed Block named "relaxedDisjuncts", which will hold
    the relaxed disjuncts.  This block is indexed by an integer
    indicating the order in which the disjuncts were relaxed.
    Each block has a dictionary "_constraintMap":

        'srcConstraints': ComponentMap(<transformed constraint>:
                                       <src constraint>)
        'transformedConstraints': ComponentMap(<src constraint>:
                                               <transformed constraint>)

    All transformed Disjuncts will have a pointer to the block their transformed
    constraints are on, and all transformed Disjunctions will have a
    pointer to the corresponding OR or XOR constraint.

    """

    CONFIG = ConfigBlock("gdp.bigm")
    CONFIG.declare(
        'targets',
        ConfigValue(
            default=None,
            domain=target_list,
            description="target or list of targets that will be relaxed",
            doc="""

        This specifies the list of components to relax. If None (default), the
        entire model is transformed. Note that if the transformation is done out
        of place, the list of targets should be attached to the model before it
        is cloned, and the list will specify the targets on the cloned
        instance."""))
    CONFIG.declare(
        'bigM',
        ConfigValue(default=None,
                    domain=_to_dict,
                    description="Big-M value used for constraint relaxation",
                    doc="""

        A user-specified value, dict, or ComponentMap of M values that override
        M-values found through model Suffixes or that would otherwise be
        calculated using variable domains."""))
    CONFIG.declare(
        'assume_fixed_vars_permanent',
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Boolean indicating whether or not to transform so that the "
            "the transformed model will still be valid when fixed Vars are unfixed.",
            doc="""
        This is only relevant when the transformation will be estimating values
        for M. If True, the transformation will calculate M values assuming that
        fixed variables will always be fixed to their current values. This means
        that if a fixed variable is unfixed after transformation, the
        transformed model is potentially no longer valid. By default, the
        transformation will assume fixed variables could be unfixed in the
        future and will use their bounds to calculate the M value rather than
        their value. Note that this could make for a weaker LP relaxation
        while the variables remain fixed.
        """))

    def __init__(self):
        """Initialize transformation object."""
        super(BigM_Transformation, self).__init__()
        self.handlers = {
            Constraint: self._transform_constraint,
            Var: False,  # Note that if a Var appears on a Disjunct, we
            # still treat its bounds as global. If the
            # intent is for its bounds to be on the
            # disjunct, it should be declared with no bounds
            # and the bounds should be set in constraints on
            # the Disjunct.
            BooleanVar: False,
            Connector: False,
            Expression: False,
            Suffix: False,
            Param: False,
            Set: False,
            SetOf: False,
            RangeSet: False,
            Disjunction: self._warn_for_active_disjunction,
            Disjunct: self._warn_for_active_disjunct,
            Block: self._transform_block_on_disjunct,
            LogicalConstraint: self._warn_for_active_logical_statement,
            ExternalFunction: False,
        }
        self._generate_debug_messages = False

    def _get_bigm_suffix_list(self, block, stopping_block=None):
        # Note that you can only specify suffixes on BlockData objects or
        # SimpleBlocks. Though it is possible at this point to stick them
        # on whatever components you want, we won't pick them up.
        suffix_list = []

        # go searching above block in the tree, stop when we hit stopping_block
        # (This is so that we can search on each Disjunct once, but get any
        # information between a constraint and its Disjunct while transforming
        # the constraint).
        while block is not stopping_block:
            bigm = block.component('BigM')
            if type(bigm) is Suffix:
                suffix_list.append(bigm)
            block = block.parent_block()

        return suffix_list

    def _get_bigm_arg_list(self, bigm_args, block):
        # Gather what we know about blocks from args exactly once. We'll still
        # check for constraints in the moment, but if that fails, we've
        # preprocessed the time-consuming part of traversing up the tree.
        arg_list = []
        if bigm_args is None:
            return arg_list
        while block is not None:
            if block in bigm_args:
                arg_list.append({block: bigm_args[block]})
            block = block.parent_block()
        return arg_list

    def _apply_to(self, instance, **kwds):
        assert not NAME_BUFFER
        self._generate_debug_messages = is_debug_set(logger)
        self.used_args = ComponentMap()  # If everything was sure to go well,
        # this could be a dictionary. But if
        # someone messes up and gives us a Var
        # as a key in bigMargs, I need the error
        # not to be when I try to put it into
        # this map!
        try:
            self._apply_to_impl(instance, **kwds)
        finally:
            # Clear the global name buffer now that we are done
            NAME_BUFFER.clear()
            # same for our bookkeeping about what we used from bigM arg dict
            self.used_args.clear()

    def _apply_to_impl(self, instance, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))

        # We will let args override suffixes and estimate as a last
        # resort. More specific args/suffixes override ones anywhere in
        # the tree. Suffixes lower down in the tree override ones higher
        # up.
        if 'default_bigM' in kwds:
            deprecation_warning(
                "the 'default_bigM=' argument has been "
                "replaced by 'bigM='",
                version='5.4')
            config.bigM = kwds.pop('default_bigM')

        config.set_value(kwds)
        bigM = config.bigM
        self.assume_fixed_vars_permanent = config.assume_fixed_vars_permanent

        targets = config.targets
        if targets is None:
            targets = (instance, )
        # We need to check that all the targets are in fact on instance. As we
        # do this, we will use the set below to cache components we know to be
        # in the tree rooted at instance.
        knownBlocks = {}
        for t in targets:
            # check that t is in fact a child of instance
            if not is_child_of(
                    parent=instance, child=t, knownBlocks=knownBlocks):
                raise GDP_Error(
                    "Target '%s' is not a component on instance '%s'!" %
                    (t.name, instance.name))
            elif t.ctype is Disjunction:
                if t.is_indexed():
                    self._transform_disjunction(t, bigM)
                else:
                    self._transform_disjunctionData(t, bigM, t.index())
            elif t.ctype in (Block, Disjunct):
                if t.is_indexed():
                    self._transform_block(t, bigM)
                else:
                    self._transform_blockData(t, bigM)
            else:
                raise GDP_Error(
                    "Target '%s' was not a Block, Disjunct, or Disjunction. "
                    "It was of type %s and can't be transformed." %
                    (t.name, type(t)))

        # issue warnings about anything that was in the bigM args dict that we
        # didn't use
        if bigM is not None:
            unused_args = ComponentSet(bigM.keys()) - \
                          ComponentSet(self.used_args.keys())
            if len(unused_args) > 0:
                warning_msg = ("Unused arguments in the bigM map! "
                               "These arguments were not used by the "
                               "transformation:\n")
                for component in unused_args:
                    if hasattr(component, 'name'):
                        warning_msg += "\t%s\n" % component.name
                    else:
                        warning_msg += "\t%s\n" % component
                logger.warn(warning_msg)

    def _add_transformation_block(self, instance):
        # make a transformation block on instance to put transformed disjuncts
        # on
        transBlockName = unique_component_name(
            instance, '_pyomo_gdp_bigm_reformulation')
        transBlock = Block()
        instance.add_component(transBlockName, transBlock)
        transBlock.relaxedDisjuncts = Block(NonNegativeIntegers)
        transBlock.lbub = Set(initialize=['lb', 'ub'])

        return transBlock

    def _transform_block(self, obj, bigM):
        for i in sorted(iterkeys(obj)):
            self._transform_blockData(obj[i], bigM)

    def _transform_blockData(self, obj, bigM):
        # Transform every (active) disjunction in the block
        for disjunction in obj.component_objects(
                Disjunction,
                active=True,
                sort=SortComponents.deterministic,
                descend_into=(Block, Disjunct),
                descent_order=TraversalStrategy.PostfixDFS):
            self._transform_disjunction(disjunction, bigM)

    def _add_xor_constraint(self, disjunction, transBlock):
        # Put the disjunction constraint on the transformation block and
        # determine whether it is an OR or XOR constraint.

        # We never do this for just a DisjunctionData because we need to know
        # about the index set of its parent component (so that we can make the
        # index of this constraint match). So if we called this on a
        # DisjunctionData, we did something wrong.
        assert isinstance(disjunction, Disjunction)

        # first check if the constraint already exists
        if disjunction._algebraic_constraint is not None:
            return disjunction._algebraic_constraint()

        # add the XOR (or OR) constraints to parent block (with unique name)
        # It's indexed if this is an IndexedDisjunction, not otherwise
        orC = Constraint(disjunction.index_set()) if \
            disjunction.is_indexed() else Constraint()
        # The name used to indicate if there were OR or XOR disjunctions,
        # however now that Disjunctions are allowed to mix the state we
        # can no longer make that distinction in the name.
        #    nm = '_xor' if xor else '_or'
        nm = '_xor'
        orCname = unique_component_name(
            transBlock,
            disjunction.getname(fully_qualified=True, name_buffer=NAME_BUFFER)
            + nm)
        transBlock.add_component(orCname, orC)
        disjunction._algebraic_constraint = weakref_ref(orC)

        return orC

    def _transform_disjunction(self, obj, bigM):
        if not obj.active:
            return

        # if this is an IndexedDisjunction we have seen in a prior call to the
        # transformation, we already have a transformation block for it. We'll
        # use that.
        if obj._algebraic_constraint is not None:
            transBlock = obj._algebraic_constraint().parent_block()
        else:
            transBlock = self._add_transformation_block(obj.parent_block())

        # relax each of the disjunctionDatas
        for i in sorted(iterkeys(obj)):
            self._transform_disjunctionData(obj[i], bigM, i, transBlock)

        # deactivate so the writers don't scream
        obj.deactivate()

    def _transform_disjunctionData(self, obj, bigM, index, transBlock=None):
        if not obj.active:
            return  # Do not process a deactivated disjunction
        # We won't have these arguments if this got called straight from
        # targets. But else, we created them earlier, and have just been passing
        # them through.
        if transBlock is None:
            # It's possible that we have already created a transformation block
            # for another disjunctionData from this same container. If that's
            # the case, let's use the same transformation block. (Else it will
            # be really confusing that the XOR constraint goes to that old block
            # but we create a new one here.)
            if obj.parent_component()._algebraic_constraint is not None:
                transBlock = obj.parent_component()._algebraic_constraint().\
                             parent_block()
            else:
                transBlock = self._add_transformation_block(obj.parent_block())
        # create or fetch the xor constraint
        xorConstraint = self._add_xor_constraint(obj.parent_component(),
                                                 transBlock)

        xor = obj.xor
        or_expr = 0
        # Just because it's unlikely this is what someone meant to do...
        if len(obj.disjuncts) == 0:
            raise GDP_Error(
                "Disjunction '%s' is empty. This is "
                "likely indicative of a modeling error." %
                obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER))
        for disjunct in obj.disjuncts:
            or_expr += disjunct.indicator_var
            # make suffix list. (We don't need it until we are
            # transforming constraints, but it gets created at the
            # disjunct level, so more efficient to make it here and
            # pass it down.)
            suffix_list = self._get_bigm_suffix_list(disjunct)
            arg_list = self._get_bigm_arg_list(bigM, disjunct)
            # relax the disjunct
            self._transform_disjunct(disjunct, transBlock, bigM, arg_list,
                                     suffix_list)

        # add or (or xor) constraint
        if xor:
            xorConstraint[index] = or_expr == 1
        else:
            xorConstraint[index] = or_expr >= 1
        # Mark the DisjunctionData as transformed by mapping it to its XOR
        # constraint.
        obj._algebraic_constraint = weakref_ref(xorConstraint[index])

        # and deactivate for the writers
        obj.deactivate()

    def _transform_disjunct(self, obj, transBlock, bigM, arg_list,
                            suffix_list):
        # deactivated -> either we've already transformed or user deactivated
        if not obj.active:
            if obj.indicator_var.is_fixed():
                if value(obj.indicator_var) == 0:
                    # The user cleanly deactivated the disjunct: there
                    # is nothing for us to do here.
                    return
                else:
                    raise GDP_Error(
                        "The disjunct '%s' is deactivated, but the "
                        "indicator_var is fixed to %s. This makes no sense." %
                        (obj.name, value(obj.indicator_var)))
            if obj._transformation_block is None:
                raise GDP_Error(
                    "The disjunct '%s' is deactivated, but the "
                    "indicator_var is not fixed and the disjunct does not "
                    "appear to have been relaxed. This makes no sense. "
                    "(If the intent is to deactivate the disjunct, fix its "
                    "indicator_var to 0.)" % (obj.name, ))

        if obj._transformation_block is not None:
            # we've transformed it, which means this is the second time it's
            # appearing in a Disjunction
            raise GDP_Error(
                "The disjunct '%s' has been transformed, but a disjunction "
                "it appears in has not. Putting the same disjunct in "
                "multiple disjunctions is not supported." % obj.name)

        # add reference to original disjunct on transformation block
        relaxedDisjuncts = transBlock.relaxedDisjuncts
        relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)]
        # we will keep a map of constraints (hashable, ha!) to a tuple to
        # indicate what their M value is and where it came from, of the form:
        # ((lower_value, lower_source, lower_key), (upper_value, upper_source,
        # upper_key)), where the first tuple is the information for the lower M,
        # the second tuple is the info for the upper M, source is the Suffix or
        # argument dictionary and None if the value was calculated, and key is
        # the key in the Suffix or argument dictionary, and None if it was
        # calculated. (Note that it is possible the lower or upper is
        # user-specified and the other is not, hence the need to store
        # information for both.)
        relaxationBlock.bigm_src = {}
        relaxationBlock.localVarReferences = Block()
        obj._transformation_block = weakref_ref(relaxationBlock)
        relaxationBlock._srcDisjunct = weakref_ref(obj)

        # This is crazy, but if the disjunction has been previously
        # relaxed, the disjunct *could* be deactivated.  This is a big
        # deal for Hull, as it uses the component_objects /
        # component_data_objects generators.  For BigM, that is OK,
        # because we never use those generators with active=True.  I am
        # only noting it here for the future when someone (me?) is
        # comparing the two relaxations.
        #
        # Transform each component within this disjunct
        self._transform_block_components(obj, obj, bigM, arg_list, suffix_list)

        # deactivate disjunct to keep the writers happy
        obj._deactivate_without_fixing_indicator()

    def _transform_block_components(self, block, disjunct, bigM, arg_list,
                                    suffix_list):
        # Find all the variables declared here (including the indicator_var) and
        # add a reference on the transformation block so these will be
        # accessible when the Disjunct is deactivated. We don't descend into
        # Disjuncts because we'll just reference the references which are
        # already on their transformation blocks.
        disjunctBlock = disjunct._transformation_block()
        varRefBlock = disjunctBlock.localVarReferences
        for v in block.component_objects(Var, descend_into=Block, active=None):
            varRefBlock.add_component(
                unique_component_name(
                    varRefBlock,
                    v.getname(fully_qualified=True, name_buffer=NAME_BUFFER)),
                Reference(v))

        # Now need to find any transformed disjunctions that might be here
        # because we need to move their transformation blocks up onto the parent
        # block before we transform anything else on this block
        destinationBlock = disjunctBlock.parent_block()
        for obj in block.component_data_objects(
                Disjunction,
                sort=SortComponents.deterministic,
                descend_into=(Block)):
            if obj.algebraic_constraint is None:
                # This could be bad if it's active since that means its
                # untransformed, but we'll wait to yell until the next loop
                continue
            # get this disjunction's relaxation block.
            transBlock = obj.algebraic_constraint().parent_block()

            # move transBlock up to parent component
            self._transfer_transBlock_data(transBlock, destinationBlock)
            # we leave the transformation block because it still has the XOR
            # constraints, which we want to be on the parent disjunct.

        # Now look through the component map of block and transform everything
        # we have a handler for. Yell if we don't know how to handle it. (Note
        # that because we only iterate through active components, this means
        # non-ActiveComponent types cannot have handlers.)
        for obj in block.component_objects(active=True, descend_into=False):
            handler = self.handlers.get(obj.ctype, None)
            if not handler:
                if handler is None:
                    raise GDP_Error(
                        "No BigM transformation handler registered "
                        "for modeling components of type %s. If your "
                        "disjuncts contain non-GDP Pyomo components that "
                        "require transformation, please transform them first."
                        % obj.ctype)
                continue
            # obj is what we are transforming, we pass disjunct
            # through so that we will have access to the indicator
            # variables down the line.
            handler(obj, disjunct, bigM, arg_list, suffix_list)

    def _transfer_transBlock_data(self, fromBlock, toBlock):
        # We know that we have a list of transformed disjuncts on both. We need
        # to move those over. We know the XOR constraints are on the block, and
        # we need to leave those on the disjunct.
        disjunctList = toBlock.relaxedDisjuncts
        to_delete = []
        for idx, disjunctBlock in iteritems(fromBlock.relaxedDisjuncts):
            newblock = disjunctList[len(disjunctList)]
            newblock.transfer_attributes_from(disjunctBlock)

            # update the mappings
            original = disjunctBlock._srcDisjunct()
            original._transformation_block = weakref_ref(newblock)
            newblock._srcDisjunct = weakref_ref(original)

            # save index of what we just moved so that we can delete it
            to_delete.append(idx)

        # delete everything we moved.
        for idx in to_delete:
            del fromBlock.relaxedDisjuncts[idx]

        # Note that we could handle other components here if we ever needed
        # to, but we control what is on the transformation block and
        # currently everything is on the blocks that we just moved...

    def _warn_for_active_disjunction(self, disjunction, disjunct, bigMargs,
                                     arg_list, suffix_list):
        _warn_for_active_disjunction(disjunction, disjunct, NAME_BUFFER)

    def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs,
                                  arg_list, suffix_list):
        _warn_for_active_disjunct(innerdisjunct, outerdisjunct, NAME_BUFFER)

    def _warn_for_active_logical_statement(self, logical_statment, disjunct,
                                           infodict, bigMargs, suffix_list):
        _warn_for_active_logical_constraint(logical_statment, disjunct,
                                            NAME_BUFFER)

    def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list,
                                     suffix_list):
        # We look through everything on the component map of the block
        # and transform it just as we would if it was on the disjunct
        # directly.  (We are passing the disjunct through so that when
        # we find constraints, _xform_constraint will have access to
        # the correct indicator variable.)
        for i in sorted(iterkeys(block)):
            self._transform_block_components(block[i], disjunct, bigMargs,
                                             arg_list, suffix_list)

    def _get_constraint_map_dict(self, transBlock):
        if not hasattr(transBlock, "_constraintMap"):
            transBlock._constraintMap = {
                'srcConstraints': ComponentMap(),
                'transformedConstraints': ComponentMap()
            }
        return transBlock._constraintMap

    def _convert_M_to_tuple(self, M, constraint_name):
        if not isinstance(M, (tuple, list)):
            if M is None:
                M = (None, None)
            else:
                try:
                    M = (-M, M)
                except:
                    logger.error("Error converting scalar M-value %s "
                                 "to (-M,M).  Is %s not a numeric type?" %
                                 (M, type(M)))
                    raise
        if len(M) != 2:
            raise GDP_Error("Big-M %s for constraint %s is not of "
                            "length two. "
                            "Expected either a single value or "
                            "tuple or list of length two for M." %
                            (str(M), constraint_name))

        return M

    def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
                              disjunct_suffix_list):
        # add constraint to the transformation block, we'll transform it there.
        transBlock = disjunct._transformation_block()
        bigm_src = transBlock.bigm_src
        constraintMap = self._get_constraint_map_dict(transBlock)

        disjunctionRelaxationBlock = transBlock.parent_block()
        # Though rare, it is possible to get naming conflicts here
        # since constraints from all blocks are getting moved onto the
        # same block. So we get a unique name
        cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)
        name = unique_component_name(transBlock, cons_name)

        if obj.is_indexed():
            newConstraint = Constraint(obj.index_set(),
                                       disjunctionRelaxationBlock.lbub)
            # we map the container of the original to the container of the
            # transformed constraint. Don't do this if obj is a SimpleConstraint
            # because we will treat that like a _ConstraintData and map to a
            # list of transformed _ConstraintDatas
            constraintMap['transformedConstraints'][obj] = newConstraint
        else:
            newConstraint = Constraint(disjunctionRelaxationBlock.lbub)
        transBlock.add_component(name, newConstraint)
        # add mapping of transformed constraint to original constraint
        constraintMap['srcConstraints'][newConstraint] = obj

        for i in sorted(iterkeys(obj)):
            c = obj[i]
            if not c.active:
                continue

            lower = (None, None, None)
            upper = (None, None, None)

            # first, we see if an M value was specified in the arguments.
            # (This returns None if not)
            lower, upper = self._get_M_from_args(c, bigMargs, arg_list, lower,
                                                 upper)
            M = (lower[0], upper[0])

            if self._generate_debug_messages:
                _name = obj.getname(fully_qualified=True,
                                    name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "from the BigM argument is %s." %
                             (cons_name, str(M)))

            # if we didn't get something we need from args, try suffixes:
            if (M[0] is None and c.lower is not None) or \
               (M[1] is None and c.upper is not None):
                # first get anything parent to c but below disjunct
                suffix_list = self._get_bigm_suffix_list(
                    c.parent_block(), stopping_block=disjunct)
                # prepend that to what we already collected for the disjunct.
                suffix_list.extend(disjunct_suffix_list)
                lower, upper = self._update_M_from_suffixes(
                    c, suffix_list, lower, upper)
                M = (lower[0], upper[0])

            if self._generate_debug_messages:
                _name = obj.getname(fully_qualified=True,
                                    name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "after checking suffixes is %s." %
                             (cons_name, str(M)))

            if c.lower is not None and M[0] is None:
                M = (self._estimate_M(c.body, name)[0] - c.lower, M[1])
                lower = (M[0], None, None)
            if c.upper is not None and M[1] is None:
                M = (M[0], self._estimate_M(c.body, name)[1] - c.upper)
                upper = (M[1], None, None)

            if self._generate_debug_messages:
                _name = obj.getname(fully_qualified=True,
                                    name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "after estimating (if needed) is %s." %
                             (cons_name, str(M)))

            # save the source information
            bigm_src[c] = (lower, upper)

            # Handle indices for both SimpleConstraint and IndexedConstraint
            if i.__class__ is tuple:
                i_lb = i + ('lb', )
                i_ub = i + ('ub', )
            elif obj.is_indexed():
                i_lb = (
                    i,
                    'lb',
                )
                i_ub = (
                    i,
                    'ub',
                )
            else:
                i_lb = 'lb'
                i_ub = 'ub'

            if c.lower is not None:
                if M[0] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint '%s' "
                                    "because M is not defined." % name)
                M_expr = M[0] * (1 - disjunct.indicator_var)
                newConstraint.add(i_lb, c.lower <= c.body - M_expr)
                constraintMap['transformedConstraints'][c] = [
                    newConstraint[i_lb]
                ]
                constraintMap['srcConstraints'][newConstraint[i_lb]] = c
            if c.upper is not None:
                if M[1] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint '%s' "
                                    "because M is not defined." % name)
                M_expr = M[1] * (1 - disjunct.indicator_var)
                newConstraint.add(i_ub, c.body - M_expr <= c.upper)
                transformed = constraintMap['transformedConstraints'].get(c)
                if transformed is not None:
                    constraintMap['transformedConstraints'][c].append(
                        newConstraint[i_ub])
                else:
                    constraintMap['transformedConstraints'][c] = [
                        newConstraint[i_ub]
                    ]
                constraintMap['srcConstraints'][newConstraint[i_ub]] = c

            # deactivate because we relaxed
            c.deactivate()

    def _process_M_value(self,
                         m,
                         lower,
                         upper,
                         need_lower,
                         need_upper,
                         src,
                         key,
                         constraint_name,
                         from_args=False):
        m = self._convert_M_to_tuple(m, constraint_name)
        if need_lower and m[0] is not None:
            if from_args:
                self.used_args[key] = m
            lower = (m[0], src, key)
            need_lower = False
        if need_upper and m[1] is not None:
            if from_args:
                self.used_args[key] = m
            upper = (m[1], src, key)
            need_upper = False
        return lower, upper, need_lower, need_upper

    def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper):
        # check args: we first look in the keys for constraint and
        # constraintdata. In the absence of those, we traverse up the blocks,
        # and as a last resort check for a value for None
        if bigMargs is None:
            return (lower, upper)

        # since we check for args first, we know lower[0] and upper[0] are both
        # None
        need_lower = constraint.lower is not None
        need_upper = constraint.upper is not None
        constraint_name = constraint.getname(fully_qualified=True,
                                             name_buffer=NAME_BUFFER)

        # check for the constraint itself and its container
        parent = constraint.parent_component()
        if constraint in bigMargs:
            m = bigMargs[constraint]
            (lower, upper, need_lower,
             need_upper) = self._process_M_value(m,
                                                 lower,
                                                 upper,
                                                 need_lower,
                                                 need_upper,
                                                 bigMargs,
                                                 constraint,
                                                 constraint_name,
                                                 from_args=True)
            if not need_lower and not need_upper:
                return lower, upper
        elif parent in bigMargs:
            m = bigMargs[parent]
            (lower, upper, need_lower,
             need_upper) = self._process_M_value(m,
                                                 lower,
                                                 upper,
                                                 need_lower,
                                                 need_upper,
                                                 bigMargs,
                                                 parent,
                                                 constraint_name,
                                                 from_args=True)
            if not need_lower and not need_upper:
                return lower, upper

        # use the precomputed traversal up the blocks
        for arg in arg_list:
            for block, val in iteritems(arg):
                (lower, upper, need_lower,
                 need_upper) = self._process_M_value(val,
                                                     lower,
                                                     upper,
                                                     need_lower,
                                                     need_upper,
                                                     bigMargs,
                                                     block,
                                                     constraint_name,
                                                     from_args=True)
                if not need_lower and not need_upper:
                    return lower, upper

        # last check for value for None!
        if None in bigMargs:
            m = bigMargs[None]
            (lower, upper, need_lower,
             need_upper) = self._process_M_value(m,
                                                 lower,
                                                 upper,
                                                 need_lower,
                                                 need_upper,
                                                 bigMargs,
                                                 None,
                                                 constraint_name,
                                                 from_args=True)
            if not need_lower and not need_upper:
                return lower, upper

        return lower, upper

    def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper):
        # It's possible we found half the answer in args, but we are still
        # looking for half the answer.
        need_lower = constraint.lower is not None and lower[0] is None
        need_upper = constraint.upper is not None and upper[0] is None
        constraint_name = constraint.getname(fully_qualified=True,
                                             name_buffer=NAME_BUFFER)
        M = None
        # first we check if the constraint or its parent is a key in any of the
        # suffix lists
        for bigm in suffix_list:
            if constraint in bigm:
                M = bigm[constraint]
                (lower, upper, need_lower, need_upper) = self._process_M_value(
                    M, lower, upper, need_lower, need_upper, bigm, constraint,
                    constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper

            # if c is indexed, check for the parent component
            if constraint.parent_component() in bigm:
                parent = constraint.parent_component()
                M = bigm[parent]
                (lower, upper, need_lower, need_upper) = self._process_M_value(
                    M, lower, upper, need_lower, need_upper, bigm, parent,
                    constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper

        # if we didn't get an M that way, traverse upwards through the blocks
        # and see if None has a value on any of them.
        if M is None:
            for bigm in suffix_list:
                if None in bigm:
                    M = bigm[None]
                    (lower, upper, need_lower,
                     need_upper) = self._process_M_value(
                         M, lower, upper, need_lower, need_upper, bigm, None,
                         constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper
        return lower, upper

    def _estimate_M(self, expr, name):
        # If there are fixed variables here, unfix them for this calculation,
        # and we'll restore them at the end.
        fixed_vars = ComponentMap()
        if not self.assume_fixed_vars_permanent:
            for v in EXPR.identify_variables(expr, include_fixed=True):
                if v.fixed:
                    fixed_vars[v] = value(v)
                    v.fixed = False

        # Calculate a best guess at M
        repn = generate_standard_repn(expr, quadratic=False)
        M = [0, 0]

        if not repn.is_nonlinear():
            if repn.constant is not None:
                for i in (0, 1):
                    if M[i] is not None:
                        M[i] += repn.constant

            for i, coef in enumerate(repn.linear_coefs or []):
                var = repn.linear_vars[i]
                bounds = (value(var.lb), value(var.ub))
                for i in (0, 1):
                    # reverse the bounds if the coefficient is negative
                    if coef > 0:
                        j = i
                    else:
                        j = 1 - i

                    if bounds[i] is not None:
                        M[j] += value(bounds[i]) * coef
                    else:
                        raise GDP_Error(
                            "Cannot estimate M for "
                            "expressions with unbounded variables."
                            "\n\t(found unbounded var '%s' while processing "
                            "constraint '%s')" % (var.name, name))
        else:
            # expression is nonlinear. Try using `contrib.fbbt` to estimate.
            expr_lb, expr_ub = compute_bounds_on_expr(expr)
            if expr_lb is None or expr_ub is None:
                raise GDP_Error("Cannot estimate M for unbounded nonlinear "
                                "expressions.\n\t(found while processing "
                                "constraint '%s')" % name)
            else:
                M = (expr_lb, expr_ub)

        # clean up if we unfixed things (fixed_vars is empty if we were assuming
        # fixed vars are fixed for life)
        for v, val in iteritems(fixed_vars):
            v.fix(val)

        return tuple(M)

    # These are all functions to retrieve transformed components from
    # original ones and vice versa.

    @wraps(get_src_disjunct)
    def get_src_disjunct(self, transBlock):
        return get_src_disjunct(transBlock)

    @wraps(get_src_disjunction)
    def get_src_disjunction(self, xor_constraint):
        return get_src_disjunction(xor_constraint)

    @wraps(get_src_constraint)
    def get_src_constraint(self, transformedConstraint):
        return get_src_constraint(transformedConstraint)

    @wraps(get_transformed_constraints)
    def get_transformed_constraints(self, srcConstraint):
        return get_transformed_constraints(srcConstraint)

    @deprecated("The get_m_value_src function is deprecated. Use "
                "the get_M_value_src function is you need source "
                "information or the get_M_value function if you "
                "only need values.",
                version='5.7.1')
    def get_m_value_src(self, constraint):
        transBlock = _get_constraint_transBlock(constraint)
        ((lower_val, lower_source, lower_key),
         (upper_val, upper_source,
          upper_key)) = transBlock.bigm_src[constraint]

        if constraint.lower is not None and constraint.upper is not None and \
           (not lower_source is upper_source or not lower_key is upper_key):
            raise GDP_Error(
                "This is why this method is deprecated: The lower "
                "and upper M values for constraint %s came from "
                "different sources, please use the get_M_value_src "
                "method." % constraint.name)
        # if source and key are equal for the two, this is representable in the
        # old format.
        if constraint.lower is not None and lower_source is not None:
            return (lower_source, lower_key)
        if constraint.upper is not None and upper_source is not None:
            return (upper_source, upper_key)
        # else it was calculated:
        return (lower_val, upper_val)

    def get_M_value_src(self, constraint):
        """Return a tuple indicating how the M value used to transform
        constraint was specified. (In particular, this can be used to
        verify which BigM Suffixes were actually necessary to the
        transformation.)

        Return is of the form: ((lower_M_val, lower_M_source, lower_M_key),
                                (upper_M_val, upper_M_source, upper_M_key))

        If the constraint does not have a lower bound (or an upper bound), 
        the first (second) element will be (None, None, None). Note that if
        a constraint is of the form a <= expr <= b or is an equality constraint,
        it is not necessarily true that the source of lower_M and upper_M
        are the same.

        If the M value came from an arg, source is the  dictionary itself and 
        key is the key in that dictionary which gave us the M value.

        If the M value came from a Suffix, source is the BigM suffix used and 
        key is the key in that Suffix.

        If the transformation calculated the value, both source and key are None.

        Parameters
        ----------
        constraint: Constraint, which must be in the subtree of a transformed
                    Disjunct
        """
        transBlock = _get_constraint_transBlock(constraint)
        # This is a KeyError if it fails, but it is also my fault if it
        # fails... (That is, it's a bug in the mapping.)
        return transBlock.bigm_src[constraint]

    def get_M_value(self, constraint):
        """Returns the M values used to transform constraint. Return is a tuple:
        (lower_M_value, upper_M_value). Either can be None if constraint does 
        not have a lower or upper bound, respectively.

        Parameters
        ----------
        constraint: Constraint, which must be in the subtree of a transformed
                    Disjunct
        """
        transBlock = _get_constraint_transBlock(constraint)
        # This is a KeyError if it fails, but it is also my fault if it
        # fails... (That is, it's a bug in the mapping.)
        lower, upper = transBlock.bigm_src[constraint]
        return (lower[0], upper[0])
コード例 #3
0
class FixedVarPropagator(IsomorphicTransformation):
    """Propagate variable fixing for equalities of type :math:`x = y`.

    If :math:`x` is fixed and :math:`y` is not fixed, then this transformation
    will fix :math:`y` to the value of :math:`x`.

    This transformation can also be performed as a temporary transformation,
    whereby the transformed variables are saved and can be later unfixed.

    Keyword arguments below are specified for the ``apply_to`` and
    ``create_using`` functions.

    """

    CONFIG = ConfigBlock()
    CONFIG.declare("tmp", ConfigValue(
        default=False, domain=bool,
        description="True to store the set of transformed variables and "
        "their old states so that they can be later restored."
    ))

    __doc__ = add_docstring_list(__doc__, CONFIG)

    def _apply_to(self, instance, **kwds):
        config = self.CONFIG(kwds)
        if config.tmp and not hasattr(instance, '_tmp_propagate_fixed'):
            instance._tmp_propagate_fixed = ComponentSet()
        eq_var_map, relevant_vars = _build_equality_set(instance)
        #: ComponentSet: The set of all fixed variables
        fixed_vars = ComponentSet((v for v in relevant_vars if v.fixed))
        newly_fixed = _detect_fixed_variables(instance)
        if config.tmp:
            instance._tmp_propagate_fixed.update(newly_fixed)
        fixed_vars.update(newly_fixed)
        processed = ComponentSet()
        # Go through each fixed variable to propagate the 'fixed' status to all
        # equality-linked variabes.
        for v1 in fixed_vars:
            # If we have already processed the variable, skip it.
            if v1 in processed:
                continue

            eq_set = eq_var_map.get(v1, ComponentSet([v1]))
            for v2 in eq_set:
                if (v2.fixed and value(v1) != value(v2)):
                    raise ValueError(
                        'Variables {} and {} have conflicting fixed '
                        'values of {} and {}, but are linked by '
                        'equality constraints.'
                        .format(v1.name,
                                v2.name,
                                value(v1),
                                value(v2)))
                elif not v2.fixed:
                    v2.fix(value(v1))
                    if config.tmp:
                        instance._tmp_propagate_fixed.add(v2)
            # Add all variables in the equality set to the set of processed
            # variables.
            processed.update(eq_set)

    def revert(self, instance):
        """Revert variables fixed by the transformation."""
        for var in instance._tmp_propagate_fixed:
            var.unfix()
        del instance._tmp_propagate_fixed
コード例 #4
0
def default_config_block(solver, init=False):
    config, blocks = ProblemConfigFactory('default').config_block(init)

    #
    # Solver
    #
    solver = ConfigBlock()
    solver.declare('solver name', ConfigValue('glpk', str, 'Solver name',
                                              None))
    solver.declare(
        'solver executable',
        ConfigValue(
            default=None,
            domain=str,
            description="The solver executable used by the solver interface.",
            doc=
            ("The solver executable used by the solver interface. "
             "This option is only valid for those solver interfaces that "
             "interact with a local executable through the shell. If unset, "
             "the solver interface will attempt to find an executable within "
             "the search path of the shell's environment that matches a name "
             "commonly associated with the solver interface.")))
    solver.declare(
        'io format',
        ConfigValue(
            None, str,
            'The type of IO used to execute the solver. Different solvers support different types of IO, but the following are common options: lp - generate LP files, nl - generate NL files, python - direct Python interface, os - generate OSiL XML files.',
            None))
    solver.declare(
        'manager',
        ConfigValue('serial', str,
                    'The technique that is used to manage solver executions.',
                    None))
    solver.declare(
        'pyro host',
        ConfigValue(
            None, str,
            "The hostname to bind on when searching for a Pyro nameserver.",
            None))
    solver.declare(
        'pyro port',
        ConfigValue(
            None, int,
            "The port to bind on when searching for a Pyro nameserver.", None))
    solver.declare(
        'options',
        ConfigBlock(implicit=True,
                    implicit_domain=ConfigValue(None, str, 'Solver option',
                                                None),
                    description="Options passed into the solver"))
    solver.declare(
        'options string',
        ConfigValue(None, str, 'String describing solver options', None))
    solver.declare(
        'suffixes',
        ConfigList([], ConfigValue(
            None, str, 'Suffix', None
        ), 'Solution suffixes that will be extracted by the solver (e.g., rc, dual, or slack). The use of this option is not required when a suffix has been declared on the model using Pyomo\'s Suffix component.',
                   None))
    blocks['solver'] = solver
    #
    solver_list = config.declare(
        'solvers',
        ConfigList(
            [],
            solver,  #ConfigValue(None, str, 'Solver', None),
            'List of solvers.  The first solver in this list is the master solver.',
            None))
    #
    # Make sure that there is one solver in the list.
    #
    # This will be the solver into which we dump command line options.
    # Note that we CANNOT declare the argparse options on the base block
    # definition above, as we use that definition as the DOMAIN TYPE for
    # the list of solvers.  As that information is NOT copied to
    # derivative blocks, the initial solver entry we are creating would
    # be missing all argparse information. Plus, if we were to have more
    # than one solver defined, we wouldn't want command line options
    # going to both.
    solver_list.append()
    solver_list[0].get('solver name').\
        declare_as_argument('--solver', dest='solver')
    solver_list[0].get('solver executable').\
        declare_as_argument('--solver-executable',
                            dest="solver_executable", metavar="FILE")
    solver_list[0].get('io format').\
        declare_as_argument('--solver-io', dest='io_format', metavar="FORMAT")
    solver_list[0].get('manager').\
        declare_as_argument('--solver-manager', dest="smanager_type",
                            metavar="TYPE")
    solver_list[0].get('pyro host').\
        declare_as_argument('--pyro-host', dest="pyro_host")
    solver_list[0].get('pyro port').\
        declare_as_argument('--pyro-port', dest="pyro_port")
    solver_list[0].get('options string').\
        declare_as_argument('--solver-options', dest='options_string',
                            metavar="STRING")
    solver_list[0].get('suffixes').\
        declare_as_argument('--solver-suffix', dest="solver_suffixes")

    #
    # Postprocess
    #
    config.declare(
        'postprocess',
        ConfigList(
            [], ConfigValue(None, str, 'Module', None),
            'Specify a Python module that gets executed after optimization.',
            None)).declare_as_argument(dest='postprocess')

    #
    # Postsolve
    #
    postsolve = config.declare('postsolve', ConfigBlock())
    postsolve.declare(
        'print logfile',
        ConfigValue(False, bool,
                    'Print the solver logfile after performing optimization.',
                    None)).declare_as_argument('-l', '--log', dest="log")
    postsolve.declare(
        'save results',
        ConfigValue(None, str,
                    'Specify the filename to which the results are saved.',
                    None)).declare_as_argument('--save-results',
                                               dest="save_results",
                                               metavar="FILE")
    postsolve.declare(
        'show results',
        ConfigValue(False, bool,
                    'Print the results object after optimization.',
                    None)).declare_as_argument(dest="show_results")
    postsolve.declare(
        'results format',
        ConfigValue(None, str, 'Specify the results format:  json or yaml.',
                    None)).declare_as_argument(
                        '--results-format',
                        dest="results_format",
                        metavar="FORMAT").declare_as_argument(
                            '--json',
                            dest="results_format",
                            action="store_const",
                            const="json",
                            help="Store results in JSON format")
    postsolve.declare(
        'summary',
        ConfigValue(
            False, bool,
            'Summarize the final solution after performing optimization.',
            None)).declare_as_argument(dest="summary")
    blocks['postsolve'] = postsolve

    #
    # Runtime
    #
    runtime = blocks['runtime']
    runtime.declare(
        'only instance',
        ConfigValue(False, bool, "Generate a model instance, and then exit",
                    None)).declare_as_argument('--instance-only',
                                               dest='only_instance')
    runtime.declare(
        'stream output',
        ConfigValue(
            False, bool,
            "Stream the solver output to provide information about the solver's progress.",
            None)).declare_as_argument('--stream-output',
                                       '--stream-solver',
                                       dest="tee")
    #
    return config, blocks
コード例 #5
0
class GDPbbSolver(object):
    """
    A branch and bound-based solver for Generalized Disjunctive Programming (GDP) problems

    The GDPbb solver solves subproblems relaxing certain disjunctions, and
    builds up a tree of potential active disjunctions. By exploring promising
    branches, it eventually results in an optimal configuration of disjunctions.

    Keyword arguments below are specified for the ``solve`` function.

    """
    CONFIG = ConfigBlock("gdpbb")
    CONFIG.declare(
        "solver",
        ConfigValue(default="baron",
                    description="Subproblem solver to use, defaults to baron"))
    CONFIG.declare(
        "solver_args",
        ConfigBlock(
            implicit=True,
            description="Block of keyword arguments to pass to the solver."))
    CONFIG.declare(
        "tee",
        ConfigValue(default=False,
                    domain=bool,
                    description="Flag to stream solver output to console."))
    CONFIG.declare(
        "check_sat",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "When True, GDPBB will check satisfiability via the pyomo.contrib.satsolver interface at each node"
        ))
    CONFIG.declare(
        "logger",
        ConfigValue(
            default='pyomo.contrib.gdpbb',
            description="The logger object or name to use for reporting.",
            domain=a_logger))
    CONFIG.declare(
        "time_limit",
        ConfigValue(
            default=600,
            domain=PositiveInt,
            description="Time limit (seconds, default=600)",
            doc="Seconds allowed until terminated. Note that the time limit can"
            "currently only be enforced between subsolver invocations. You may"
            "need to set subsolver time limits as well."))

    @deprecated("GDPbb has been merged into GDPopt. "
                "You can use the algorithm using GDPopt with strategy='LBB'.",
                logger="pyomo.solvers",
                version='5.6.9')
    def __init__(self, *args, **kwargs):
        super(GDPbbSolver, self).__init__(*args, **kwargs)

    def available(self, exception_flag=True):
        """Check if solver is available.

        TODO: For now, it is always available. However, sub-solvers may not
        always be available, and so this should reflect that possibility.

        """
        return True

    def license_is_valid(self):
        return True

    def version(self):
        return __version__

    def solve(self, model, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        return SolverFactory('gdpopt').solve(
            model,
            strategy='LBB',
            minlp_solver=config.solver,
            minlp_solver_args=config.solver_args,
            tee=config.tee,
            check_sat=config.check_sat,
            logger=config.logger,
            time_limit=config.time_limit)

        # Validate model to be used with gdpbb
        self.validate_model(model)
        # Set solver as an MINLP
        solve_data = GDPbbSolveData()
        solve_data.timing = Bunch()
        solve_data.original_model = model
        solve_data.results = SolverResults()

        old_logger_level = config.logger.getEffectiveLevel()
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                restore_logger_level(config.logger), \
                create_utility_block(model, 'GDPbb_utils', solve_data):
            if config.tee and old_logger_level > logging.INFO:
                # If the logger does not already include INFO, include it.
                config.logger.setLevel(logging.INFO)
            config.logger.info(
                "Starting GDPbb version %s using %s as subsolver" %
                (".".join(map(str, self.version())), config.solver))

            # Setup results
            solve_data.results.solver.name = 'GDPbb - %s' % (str(
                config.solver))
            setup_results_object(solve_data, config)

            # clone original model for root node of branch and bound
            root = solve_data.working_model = solve_data.original_model.clone()

            # get objective sense
            process_objective(solve_data, config)
            objectives = solve_data.original_model.component_data_objects(
                Objective, active=True)
            obj = next(objectives, None)
            solve_data.results.problem.sense = obj.sense

            # set up lists to keep track of which disjunctions have been covered.

            # this list keeps track of the relaxed disjunctions
            root.GDPbb_utils.unenforced_disjunctions = list(
                disjunction
                for disjunction in root.GDPbb_utils.disjunction_list
                if disjunction.active)

            root.GDPbb_utils.deactivated_constraints = ComponentSet([
                constr
                for disjunction in root.GDPbb_utils.unenforced_disjunctions
                for disjunct in disjunction.disjuncts
                for constr in disjunct.component_data_objects(ctype=Constraint,
                                                              active=True)
                if constr.body.polynomial_degree() not in (1, 0)
            ])
            # Deactivate nonlinear constraints in unenforced disjunctions
            for constr in root.GDPbb_utils.deactivated_constraints:
                constr.deactivate()

            # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation.
            if not hasattr(root, 'BigM'):
                root.BigM = Suffix()

            # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed
            # to True/False values.
            # TODO this should also be done within the loop, but we aren't handling it right now.
            # Should affect efficiency, but not correctness.
            root.GDPbb_utils.disjuncts_fixed_True = ComponentSet()
            # Only find top-level (non-nested) disjunctions
            for disjunction in root.component_data_objects(Disjunction,
                                                           active=True):
                fixed_true_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 1
                ]
                fixed_false_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 0
                ]
                for disjunct in fixed_false_disjuncts:
                    disjunct.deactivate()
                if len(fixed_false_disjuncts) == len(
                        disjunction.disjuncts) - 1:
                    # all but one disjunct in the disjunction is fixed to False. Remaining one must be true.
                    if not fixed_true_disjuncts:
                        fixed_true_disjuncts = [
                            disjunct for disjunct in disjunction.disjuncts
                            if disjunct not in fixed_false_disjuncts
                        ]
                # Reactivate the fixed-true disjuncts
                for disjunct in fixed_true_disjuncts:
                    newly_activated = ComponentSet()
                    for constr in disjunct.component_data_objects(Constraint):
                        if constr in root.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            root.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    root.GDPbb_utils.deactivated_constraints -= newly_activated
                    root.GDPbb_utils.disjuncts_fixed_True.add(disjunct)

                if fixed_true_disjuncts:
                    assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                        "%s violates this assumption." % (disjunction.name, )
                    root.GDPbb_utils.unenforced_disjunctions.remove(
                        disjunction)

            # Check satisfiability
            if config.check_sat and satisfiable(root, config.logger) is False:
                # Problem is not satisfiable. Problem is infeasible.
                obj_value = obj_sign * float('inf')
            else:
                # solve the root node
                config.logger.info("Solving the root node.")
                obj_value, result, var_values = self.subproblem_solve(
                    root, config)

            if obj_sign * obj_value == float('inf'):
                config.logger.info(
                    "Model was found to be infeasible at the root node. Elapsed %.2f seconds."
                    % get_main_elapsed_time(solve_data.timing))
                if solve_data.results.problem.sense == minimize:
                    solve_data.results.problem.lower_bound = float('inf')
                    solve_data.results.problem.upper_bound = None
                else:
                    solve_data.results.problem.lower_bound = None
                    solve_data.results.problem.upper_bound = float('-inf')
                solve_data.results.solver.timing = solve_data.timing
                solve_data.results.solver.iterations = 0
                solve_data.results.solver.termination_condition = tc.infeasible
                return solve_data.results

            # initialize minheap for Branch and Bound algorithm
            # Heap structure: (ordering tuple, model)
            # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter)
            #  - select solutions with lower objective value,
            #    then fewer disjunctions left to explore (depth first),
            #    then more recently encountered (tiebreaker)
            heap = []
            total_nodes_counter = 0
            disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions)
            heapq.heappush(heap,
                           ((obj_sign * obj_value, disjunctions_left,
                             -total_nodes_counter), root, result, var_values))

            # loop to branch through the tree
            while len(heap) > 0:
                # pop best model off of heap
                sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop(
                    heap)
                incumbent_obj_value, disjunctions_left, _ = sort_tuple

                config.logger.info(
                    "Exploring node with LB %.10g and %s inactive disjunctions."
                    % (incumbent_obj_value, disjunctions_left))

                # if all the originally active disjunctions are active, solve and
                # return solution
                if disjunctions_left == 0:
                    config.logger.info("Model solved.")
                    # Model is solved. Copy over solution values.
                    original_model = solve_data.original_model
                    for orig_var, val in zip(
                            original_model.GDPbb_utils.variable_list,
                            incumbent_var_values):
                        orig_var.value = val

                    solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound
                    solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound
                    solve_data.results.solver.timing = solve_data.timing
                    solve_data.results.solver.iterations = total_nodes_counter
                    solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition
                    return solve_data.results

                # Pick the next disjunction to branch on
                next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[
                    0]
                config.logger.info("Branching on disjunction %s" %
                                   next_disjunction.name)
                assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                    "%s violates this assumption." % (next_disjunction.name, )

                new_nodes_counter = 0

                for i, disjunct in enumerate(next_disjunction.disjuncts):
                    # Create one branch for each of the disjuncts on the disjunction

                    if any(disj.indicator_var.fixed
                           and disj.indicator_var.value == 1
                           for disj in next_disjunction.disjuncts
                           if disj is not disjunct):
                        # If any other disjunct is fixed to 1 and an xor relationship applies,
                        # then this disjunct cannot be activated.
                        continue

                    # Check time limit
                    if get_main_elapsed_time(
                            solve_data.timing) >= config.time_limit:
                        if solve_data.results.problem.sense == minimize:
                            solve_data.results.problem.lower_bound = incumbent_obj_value
                            solve_data.results.problem.upper_bound = float(
                                'inf')
                        else:
                            solve_data.results.problem.lower_bound = float(
                                '-inf')
                            solve_data.results.problem.upper_bound = incumbent_obj_value
                        config.logger.info('GDPopt unable to converge bounds '
                                           'before time limit of {} seconds. '
                                           'Elapsed: {} seconds'.format(
                                               config.time_limit,
                                               get_main_elapsed_time(
                                                   solve_data.timing)))
                        config.logger.info(
                            'Final bound values: LB: {}  UB: {}'.format(
                                solve_data.results.problem.lower_bound,
                                solve_data.results.problem.upper_bound))
                        solve_data.results.solver.timing = solve_data.timing
                        solve_data.results.solver.iterations = total_nodes_counter
                        solve_data.results.solver.termination_condition = tc.maxTimeLimit
                        return solve_data.results

                    # Branch on the disjunct
                    child = incumbent_model.clone()
                    # TODO I am leaving the old branching system in place, but there should be
                    # something better, ideally that deals with nested disjunctions as well.
                    disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop(
                        0)
                    child_disjunct = disjunction_to_branch.disjuncts[i]
                    child_disjunct.indicator_var.fix(1)
                    # Deactivate (and fix to 0) other disjuncts on the disjunction
                    for disj in disjunction_to_branch.disjuncts:
                        if disj is not child_disjunct:
                            disj.deactivate()
                    # Activate nonlinear constraints on the newly fixed child disjunct
                    newly_activated = ComponentSet()
                    for constr in child_disjunct.component_data_objects(
                            Constraint):
                        if constr in child.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            child.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    child.GDPbb_utils.deactivated_constraints -= newly_activated
                    child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct)

                    if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True:
                        # If the disjunct was already branched to True from a parent disjunct branching, just pass
                        # through the incumbent value without resolving. The solution should be the same as the parent.
                        total_nodes_counter += 1
                        ordering_tuple = (obj_sign * incumbent_obj_value,
                                          disjunctions_left - 1,
                                          -total_nodes_counter)
                        heapq.heappush(heap, (ordering_tuple, child, result,
                                              incumbent_var_values))
                        new_nodes_counter += 1
                        continue

                    if config.check_sat and satisfiable(
                            child, config.logger) is False:
                        # Problem is not satisfiable. Skip this disjunct.
                        continue

                    obj_value, result, var_values = self.subproblem_solve(
                        child, config)
                    total_nodes_counter += 1
                    ordering_tuple = (obj_sign * obj_value,
                                      disjunctions_left - 1,
                                      -total_nodes_counter)
                    heapq.heappush(heap,
                                   (ordering_tuple, child, result, var_values))
                    new_nodes_counter += 1

                config.logger.info(
                    "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s."
                    % (new_nodes_counter, disjunctions_left - 1, len(heap)))

    @staticmethod
    def validate_model(model):
        # Validates that model has only exclusive disjunctions
        for d in model.component_data_objects(ctype=Disjunction, active=True):
            if not d.xor:
                raise ValueError('GDPbb solver unable to handle '
                                 'non-exclusive disjunctions')
        objectives = model.component_data_objects(Objective, active=True)
        obj = next(objectives, None)
        if next(objectives, None) is not None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with multiple active objectives."
            )
        if obj is None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with no active objective."
            )

    @staticmethod
    def subproblem_solve(gdp, config):
        subproblem = gdp.clone()
        TransformationFactory('gdp.bigm').apply_to(subproblem)
        main_obj = next(
            subproblem.component_data_objects(Objective, active=True))
        obj_sign = 1 if main_obj.sense == minimize else -1

        try:
            result = SolverFactory(config.solver).solve(
                subproblem, **config.solver_args)
        except RuntimeError as e:
            config.logger.warning(
                "Solver encountered RuntimeError. Treating as infeasible. "
                "Msg: %s\n%s" % (str(e), traceback.format_exc()))
            var_values = [
                v.value for v in subproblem.GDPbb_utils.variable_list
            ]
            return obj_sign * float('inf'), SolverResults(), var_values

        var_values = [v.value for v in subproblem.GDPbb_utils.variable_list]
        term_cond = result.solver.termination_condition
        if result.solver.status is SolverStatus.ok and any(
                term_cond == valid_cond
                for valid_cond in (tc.optimal, tc.locallyOptimal,
                                   tc.feasible)):
            return value(main_obj.expr), result, var_values
        elif term_cond == tc.unbounded:
            return obj_sign * float('-inf'), result, var_values
        elif term_cond == tc.infeasible:
            return obj_sign * float('inf'), result, var_values
        else:
            config.logger.warning("Unknown termination condition of %s" %
                                  term_cond)
            return obj_sign * float('inf'), result, var_values

    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
コード例 #6
0
ファイル: bounds_to_vars.py プロジェクト: ZedongPeng/pyomo
class ConstraintToVarBoundTransform(IsomorphicTransformation):
    """Change constraints to be a bound on the variable.

    Looks for constraints of form: :math:`k*v + c_1 \\leq c_2`. Changes
    variable lower bound on :math:`v` to match :math:`(c_2 - c_1)/k` if it
    results in a tighter bound. Also does the same thing for lower bounds.

    Keyword arguments below are specified for the ``apply_to`` and
    ``create_using`` functions.

    """

    CONFIG = ConfigBlock("ConstraintToVarBounds")
    CONFIG.declare(
        "tolerance",
        ConfigValue(
            default=1E-13,
            domain=NonNegativeFloat,
            description="tolerance on bound equality (:math:`LB = UB`)"))
    CONFIG.declare(
        "detect_fixed",
        ConfigValue(default=True,
                    domain=bool,
                    description="If True, fix variable when "
                    ":math:`| LB - UB | \\leq tolerance`."))

    __doc__ = add_docstring_list(__doc__, CONFIG)

    def _apply_to(self, model, **kwds):
        config = self.CONFIG(kwds)

        for constr in model.component_data_objects(ctype=Constraint,
                                                   active=True,
                                                   descend_into=True):
            # Check if the constraint is k * x + c1 <= c2 or c2 <= k * x + c1
            repn = generate_standard_repn(constr.body)
            if not repn.is_linear() or len(repn.linear_vars) != 1:
                # Skip nonlinear constraints, trivial constraints, and those
                # that involve more than one variable.
                continue
            else:
                var = repn.linear_vars[0]
                const = repn.constant
                coef = float(repn.linear_coefs[0])

            if coef == 0:
                # Skip trivial constraints
                continue
            elif coef > 0:
                if constr.has_ub():
                    new_ub = (constr.ub - const) / coef
                    var_ub = float('inf') if var.ub is None else var.ub
                    var.setub(min(var_ub, new_ub))
                if constr.has_lb():
                    new_lb = (constr.lb - const) / coef
                    var_lb = float('-inf') if var.lb is None else var.lb
                    var.setlb(max(var_lb, new_lb))
            elif coef < 0:
                if constr.has_ub():
                    new_lb = (constr.ub - const) / coef
                    var_lb = float('-inf') if var.lb is None else var.lb
                    var.setlb(max(var_lb, new_lb))
                if constr.has_lb():
                    new_ub = (constr.lb - const) / coef
                    var_ub = float('inf') if var.ub is None else var.ub
                    var.setub(min(var_ub, new_ub))

            if var.is_integer():
                # Make sure that the lb and ub are integral. Use safe
                # construction if near to integer.
                if var.has_lb():
                    var.setlb(
                        int(
                            min(math.ceil(var.lb - config.tolerance),
                                math.ceil(var.lb))))
                if var.has_ub():
                    var.setub(
                        int(
                            max(math.floor(var.ub + config.tolerance),
                                math.floor(var.ub))))

            if var is not None and var.value is not None:
                _adjust_var_value_if_not_feasible(var)

            if config.detect_fixed and var.has_lb() and var.has_ub():
                lb, ub = var.bounds
                if lb == ub:
                    var.fix(lb)
                elif fabs(lb - ub) <= config.tolerance:
                    # If the bounds are note exactly equal, set the
                    # value to the midpoint
                    var.fix((lb + ub) / 2)

            constr.deactivate()
コード例 #7
0
ファイル: wind_power.py プロジェクト: ksbeattie/dispatches
class WindpowerData(UnitModelBlockData):
    """
    Wind plant using turbine powercurve and resource data.
    Unit model to convert wind resource into electricity.
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc=
            """Wind plant does not support dynamic models, thus this must be False."""
        ))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc=
            """Wind plant does not have defined volume, thus this must be False."""
        ))
    CONFIG.declare(
        "resource_probability_density",
        ConfigValue(
            default=None,
            domain=dict_of_list_of_list_of_floats,
            description=
            "Dictionary of Time: List of (wind meters per sec, wind degrees clockwise from north, probability)",
            doc=
            "For each time in flowsheet's time set, a probability density function of "
            "Wind speed [m/s] and Wind direction [degrees clockwise from N]"))

    def build(self):
        """Building model

        Args:
            None
        Returns:
            None
        """
        super().build()

        self.system_capacity = Var(within=NonNegativeReals,
                                   initialize=0.0,
                                   doc="Rated system capacity of wind farm",
                                   units=pyunits.kW)

        self.capacity_factor = Param(
            self.flowsheet().config.time,
            within=NonNegativeReals,
            mutable=True,
            initialize=0.0,
            doc=
            "Ratio of power output to rated capacity, on annual or time series basis",
            units=pyunits.kW / pyunits.kW)

        self.electricity = Var(self.flowsheet().config.time,
                               within=NonNegativeReals,
                               initialize=0.0,
                               doc="Electricity production",
                               units=pyunits.kW)

        self.electricity_out = Port(noruleinit=True,
                                    doc="A port for electricity flow")
        self.electricity_out.add(self.electricity, "electricity")

        self.wind_simulation = None
        self.setup_atb_turbine()
        self.setup_resource()

    def _get_performance_contents(self, time_point=0):
        return {"vars": {"Electricity": self.electricity[time_point].value}}

    def setup_atb_turbine(self):
        self.wind_simulation = wind.default("WindpowerSingleowner")

        # Use ATB Turbine 2018 Market Average
        self.wind_simulation.Turbine.wind_turbine_hub_ht = 88
        self.wind_simulation.Turbine.wind_turbine_rotor_diameter = 116
        self.wind_simulation.Turbine.wind_turbine_powercurve_windspeeds = [
            0.25 * i for i in range(161)
        ]
        self.wind_simulation.Turbine.wind_turbine_powercurve_powerout = [
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 78, 104, 133, 167, 204,
            246, 293, 345, 402, 464, 532, 606, 686, 772, 865, 965, 1072, 1186,
            1308, 1438, 1576, 1723, 1878, 2042, 2215, 2397, 2430, 2430, 2430,
            2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430,
            2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430,
            2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430,
            2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430,
            2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430, 2430,
            2430, 2430, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        ]

        # Use a single turbine, do not model wake effects
        self.wind_simulation.Farm.wind_farm_xCoordinates = [0]
        self.wind_simulation.Farm.wind_farm_yCoordinates = [0]
        self.wind_simulation.Farm.system_capacity = max(
            self.wind_simulation.Turbine.wind_turbine_powercurve_powerout)
        self.wind_simulation.Resource.wind_resource_model_choice = 2

    def setup_resource(self):
        if len(self.config.resource_probability_density) >= len(
                self.flowsheet().config.time.data()):
            for time in list(self.flowsheet().config.time.data()):
                if time not in self.config.resource_probability_density.keys():
                    raise ValueError(
                        "'resource_probability_density' must contain data for time {}"
                        .format(time))

            for time, resource in self.config.resource_probability_density.items(
            ):
                if abs(sum(r[2] for r in resource) - 1) > 1e-3:
                    raise ValueError(
                        "Error in 'resource_probability_density' for time {}: Probabilities of "
                        "Wind Speed and Direction Probability Density Function must sum to 1"
                    )
                self.wind_simulation.Resource.wind_resource_distribution = resource
                self.wind_simulation.execute(0)
                self.capacity_factor[time].set_value(
                    self.wind_simulation.Outputs.capacity_factor / 100.)

            @self.Constraint(self.flowsheet().config.time)
            def elec_from_capacity_factor(b, t):
                return b.electricity[
                    t] == self.system_capacity * self.capacity_factor[t]
        else:
            raise ValueError(
                "Config with 'resource_probability_density' must be provided using `default` argument"
            )
コード例 #8
0
class GDP_Disjunct_Fixer(Transformation):
    """Fix disjuncts to their current logical values.

    This reclassifies all disjuncts as ctype Block and deactivates the
    constraints and disjunctions within inactive disjuncts.

    """
    def __init__(self, *args, **kwargs):
        # TODO This uses an integer tolerance. At some point, these should be
        # standardized.
        super(GDP_Disjunct_Fixer, self).__init__(*args, **kwargs)
        self._transformedDisjuncts = ComponentSet()

    CONFIG = ConfigBlock("gdp.fix_disjuncts")
    CONFIG.declare(
        'targets',
        ConfigValue(
            default=None,
            domain=target_list,
            description="target or list of targets that will be fixed",
        ))
    CONFIG.declare(
        'integer_tolerance',
        ConfigValue(default=1E-6,
                    domain=NonNegativeFloat,
                    description="tolerance on binary variable 0, 1 values"))

    def _apply_to(self, instance, **kwds):
        """Apply the transformation on the targets in the given instance.

        The instance is expected to be Block-like.

        The target ctype is expected to be Block, Disjunct, or Disjunction.
        For a Block or Disjunct, the transformation will fix all disjuncts
        found in disjunctions within the container. If no target is specified,
        the whole instance is transformed.

        """
        config = self.config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        targets = config.targets if config.targets is not None else (
            instance, )
        for t in targets:
            if not t.active:
                return  # do nothing for inactive containers

            # screen for allowable instance types
            if (type(t) not in (_DisjunctData, _BlockData, _DisjunctionData)
                    and t.type() not in (Disjunct, Block, Disjunction)):
                raise GDP_Error(
                    "Target %s was not a Block, Disjunct, or Disjunction. "
                    "It was of type %s and can't be transformed." %
                    (t.name, type(t)))

            # if the object is indexed, transform all of its _ComponentData
            if t.is_indexed():
                for obj in itervalues(t):
                    self._transformObject(obj)
            else:
                self._transformObject(t)

    def _transformObject(self, obj):
        # If the object is a disjunction, transform it.
        if obj.type() == Disjunction and not obj.is_indexed():
            self._transformDisjunctionData(obj)
        # Otherwise, treat it like a container and transform its contents.
        else:
            self._transformContainer(obj)

    def _transformDisjunctionData(self, disjunction):
        # the sum of all the indicator variable values of disjuncts in the
        # disjunction
        logical_sum = sum(
            value(disj.indicator_var) for disj in disjunction.disjuncts)

        # Check that the disjunctions are not being fixed to an infeasible
        # realization.
        if disjunction.xor and not logical_sum == 1:
            # for XOR disjunctions, the sum of all disjunct values should be 1
            raise GDP_Error(
                "Disjunction %s violated. "
                "Expected 1 disjunct to be active, but %s were active." %
                (disjunction.name, logical_sum))
        elif not logical_sum >= 1:
            # for non-XOR disjunctions, the sum of all disjunct values should
            # be at least 1
            raise GDP_Error("Disjunction %s violated. "
                            "Expected at least 1 disjunct to be active, "
                            "but none were active.")
        else:
            # disjunction is in feasible realization. Deactivate it.
            disjunction.deactivate()

        # Process the disjuncts associated with the disjunction that have not
        # already been transformed.
        for disj in (ComponentSet(disjunction.disjuncts) -
                     self._transformedDisjuncts):
            self._transformDisjunctData(disj)
        # Update the set of transformed disjuncts with those from this
        # disjunction
        self._transformedDisjuncts.update(disjunction.disjuncts)

    def _transformDisjunctData(self, obj):
        """Fix the disjunct either to a Block or a deactivated Block."""
        if fabs(value(obj.indicator_var) - 1) <= self.config.integer_tolerance:
            # Disjunct is active. Convert to Block.
            obj.parent_block().reclassify_component_type(obj, Block)
            obj.indicator_var.fix(1)
            # Process the components attached to this disjunct.
            self._transformContainer(obj)
        elif fabs(value(obj.indicator_var)) <= self.config.integer_tolerance:
            obj.parent_block().reclassify_component_type(obj, Block)
            obj.indicator_var.fix(0)
            # Deactivate all constituent constraints and disjunctions
            # HACK I do not deactivate the whole block because some writers
            # do not look for variables in deactivated blocks.
            for constr in obj.component_objects(ctype=(Constraint,
                                                       Disjunction),
                                                active=True,
                                                descend_into=True):
                constr.deactivate()
        else:
            raise ValueError(
                'Non-binary indicator variable value %s for disjunct %s' %
                (obj.name, value(obj.indicator_var)))

    def _transformContainer(self, obj):
        """Find all disjunctions in the container and transform them."""
        for disjunction in obj.component_data_objects(ctype=Disjunction,
                                                      active=True,
                                                      descend_into=Block):
            self._transformDisjunctionData(disjunction)
コード例 #9
0
class WaterwallSectionData(UnitModelBlockData):
    """
    WaterwallSection Unit Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([useDefault, True, False]),
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**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=In([True, False]),
            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=In([True, False]),
            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,
**PhysicalParameterObject** - a PhysicalParameterBlock 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(
        "rigorous_boiling",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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.}"""))

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


        Args:
            None

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

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            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
            })

        self.control_volume.add_geometry()
        # This model requires the IAPWS95 property package with the mixed phase
        # option, therefore, phase equilibrium calculations are handled by
        # the property package.
        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type)

        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=True)

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

        # Add object references
        self.volume = Reference(self.control_volume.volume)

        # 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 != 'none'):
            self.deltaP = Reference(self.control_volume.deltaP)

        # Set Unit Geometry and Holdup Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary, and link to holdup volume
        """
        units_meta = self.config.property_package.get_metadata()
        # Total projected wall area of waterwall section from fire-side model
        self.projected_area = Var(
            initialize=10,
            doc="Total projected wall area of waterwall section",
            units=units_meta.get_derived_units("area"))
        # Number of waterwall tubes
        self.number_tubes = Var(initialize=4, doc="Number of waterwall tubes")
        # Height of waterwall section, given by boiler model
        self.height = Var(initialize=5.0,
                          doc="Height of waterwall section",
                          units=units_meta.get_derived_units("length"))
        # Length of waterwall tubes calculated based on given total area
        # and perimeter of waterwall
        self.tube_length = Var(initialize=5.0,
                               doc="length waterwall tube",
                               units=units_meta.get_derived_units("length"))
        # Inner diameter of waterwall tubes
        self.tube_diameter = Var(initialize=0.05,
                                 doc="Inside diameter of waterwall tube",
                                 units=units_meta.get_derived_units("length"))
        # Inside radius of waterwall tube
        @self.Expression(doc="Inside radius of waterwall tube")
        def radius_inner(b):
            return 0.5 * b.tube_diameter

        # Total cross section area of fluid flow
        @self.Expression(doc="Cross section area of fluid")
        def area_cross_fluid_total(b):
            return 0.25 * const.pi * b.tube_diameter**2 * b.number_tubes

        # Tube thickness
        self.tube_thickness = Var(initialize=0.005,
                                  doc="Thickness of waterwall tube",
                                  units=units_meta.get_derived_units("length"))
        # Outside radius of waterwall tube
        @self.Expression(doc="Outside radius of waterwall tube")
        def radius_outer(b):
            return b.radius_inner + b.tube_thickness

        # Thickness of waterwall fin
        self.fin_thickness = Var(initialize=0.004,
                                 doc="Thickness of waterwall Fin",
                                 units=units_meta.get_derived_units("length"))
        # Half of the waterwall fin thickness
        @self.Expression(doc="Half of the waterwall fin thickness")
        def fin_thickness_half(b):
            return 0.5 * b.fin_thickness

        # Length of waterwall fin
        self.fin_length = Var(initialize=0.005,
                              doc="Length of waterwall fin",
                              units=units_meta.get_derived_units("length"))
        # Thickness of slag layer
        self.slag_thickness = Var(self.flowsheet().config.time,
                                  bounds=(0.0001, 0.009),
                                  initialize=0.001,
                                  doc="Thickness of slag layer",
                                  units=units_meta.get_derived_units("length"))

        @self.Expression(doc="Pitch of two neighboring tubes")
        def pitch(b):
            return b.fin_length + b.radius_outer * 2.0

        # Equivalent tube length (not neccesarily equal to height)
        @self.Constraint(doc="Equivalent length of tube")
        def tube_length_eqn(b):
            return b.tube_length * b.pitch * b.number_tubes == \
                b.projected_area

        @self.Expression(doc="Angle at joint of tube and fin")
        def alpha_tube(b):
            return asin(b.fin_thickness_half / b.radius_outer)

        @self.Expression(self.flowsheet().config.time,
                         doc="Angle at joint of tube and "
                         "fin at outside slag layer")
        def alpha_slag(b, t):
            return asin((b.fin_thickness_half + b.slag_thickness[t]) /
                        (b.radius_outer + b.slag_thickness[t]))

        @self.Expression(doc="Perimeter of interface between slag and tube")
        def perimeter_interface(b):
            return (const.pi-2 * b.alpha_tube) * b.radius_outer + b.pitch \
                - 2 * b.radius_outer * cos(b.alpha_tube)

        @self.Expression(doc="Perimeter on the inner tube side")
        def perimeter_ts(b):
            return const.pi * b.tube_diameter

        @self.Expression(self.flowsheet().config.time,
                         doc="Perimeter on the outer slag side")
        def perimeter_ss(b, t):
            return (const.pi - 2 * b.alpha_slag[t]) * (b.radius_outer
                                                       + b.slag_thickness[t]) \
                + b.pitch - 2 * (b.radius_outer
                                 + b.slag_thickness[t])*cos(b.alpha_slag[t])

        # Cross section area of tube and fin metal
        @self.Expression(doc="Cross section area of tube and fin metal")
        def area_cross_metal(b):
            return const.pi*(b.radius_outer**2-b.radius_inner**2)\
                + b.fin_thickness*b.fin_length

        # Cross section area of slag layer
        @self.Expression(self.flowsheet().config.time,
                         doc="Cross section area of slag layer per tube")
        def area_cross_slag(b, t):
            return b.perimeter_interface * b.slag_thickness[t]

        # Volume constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="waterwall fluid volume of all tubes")
        def volume_eqn(b, t):
            return b.volume[t] == 0.25 * const.pi * b.tube_diameter**2\
                * b.tube_length * b.number_tubes

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        units_meta = self.config.property_package.get_metadata()
        self.fcorrection_dp = Var(
            initialize=1.2,
            within=PositiveReals,
            doc='correction factor for pressure drop due to acceleration'
            'and unsmooth tube applied to friction term')
        # Thermal conductivity of metal
        self.therm_cond_metal = Param(
            initialize=43.0,
            mutable=True,
            doc='Thermal conductivity of tube metal',
            units=units_meta.get_derived_units("thermal_conductivity"))
        # Thermal conductivity of slag
        self.therm_cond_slag = Param(
            initialize=1.3,
            mutable=True,
            doc='Thermal conductivity of slag',
            units=units_meta.get_derived_units("thermal_conductivity"))
        # Heat capacity of metal
        self.cp_metal = Param(
            initialize=500.0,
            mutable=True,
            doc='Heat capacity of tube metal',
            units=units_meta.get_derived_units("heat_capacity_mass"))
        # Heat Capacity of slag
        self.cp_slag = Param(
            initialize=250,
            mutable=True,
            doc='Heat capacity of slag',
            units=units_meta.get_derived_units("heat_capacity_mass"))
        # Density of metal
        self.dens_metal = Param(
            initialize=7800.0,
            mutable=True,
            doc='Density of tube metal',
            units=units_meta.get_derived_units("density_mass"))
        # Density of slag
        self.dens_slag = Param(
            initialize=2550,
            mutable=True,
            doc='Density of slag',
            units=units_meta.get_derived_units("density_mass"))
        # Shape factor of tube metal conduction resistance
        # based on projected area
        self.fshape_metal = Param(initialize=0.7718,
                                  mutable=True,
                                  doc='Shape factor of tube metal conduction')
        # Shape factor of slag conduction resistance
        # based on projected area
        self.fshape_slag = Param(initialize=0.6858,
                                 mutable=True,
                                 doc='Shape factor of slag conduction')
        # Shape factor of convection on tube side resistance
        # based on projected area
        self.fshape_conv = Param(initialize=0.8496,
                                 mutable=True,
                                 doc='Shape factor of convection')

        # Heat conduction resistance of half of metal wall thickness
        # based on interface perimeter
        @self.Expression(doc="half metal layer conduction resistance")
        def half_resistance_metal(b):
            return b.tube_thickness/2/b.therm_cond_metal * b.fshape_metal\
                * b.perimeter_interface/b.pitch

        # Heat conduction resistance of half of slag thickness
        # based on mid slag layer perimeter
        @self.Expression(self.flowsheet().config.time,
                         doc="half slag layer conduction resistance")
        def half_resistance_slag(b, t):
            return b.slag_thickness[t]/2/b.therm_cond_slag * b.fshape_slag \
                * (b.perimeter_ss[t]+b.perimeter_interface)/2/b.pitch

        # Add performance variables
        # Heat from fire side boiler model
        self.heat_fireside = Var(
            self.flowsheet().config.time,
            initialize=1e7,
            doc='total heat from fire side model for the section',
            units=units_meta.get_derived_units("power"))
        # Tube boundary wall temperature
        self.temp_tube_boundary = Var(
            self.flowsheet().config.time,
            initialize=400.0,
            doc='Temperature of tube boundary wall',
            units=units_meta.get_derived_units("temperature"))
        # Tube center point wall temperature
        self.temp_tube_center = Var(
            self.flowsheet().config.time,
            initialize=450.0,
            doc='Temperature of tube center wall',
            units=units_meta.get_derived_units("temperature"))
        # Slag boundary wall temperature
        self.temp_slag_boundary = Var(
            self.flowsheet().config.time,
            initialize=600.0,
            doc='Temperature of slag boundary wall',
            units=units_meta.get_derived_units("temperature"))
        # Slag center point wall temperature
        self.temp_slag_center = Var(
            self.flowsheet().config.time,
            initialize=500.0,
            doc='Temperature of slag layer center point',
            units=units_meta.get_derived_units("temperature"))

        # Energy holdup for slag layer
        self.energy_holdup_slag = Var(
            self.flowsheet().config.time,
            initialize=1e4,
            doc='Energy holdup of slag layer',
            units=(units_meta.get_derived_units("energy") *
                   units_meta.get_derived_units("length")**-1))

        # Energy holdup for metal (tube + fin)
        self.energy_holdup_metal = Var(
            self.flowsheet().config.time,
            initialize=1e6,
            doc='Energy holdup of metal',
            units=(units_meta.get_derived_units("energy") *
                   units_meta.get_derived_units("length")**-1))

        # Energy accumulation for slag and metal
        if self.config.dynamic is True:
            self.energy_accumulation_slag = DerivativeVar(
                self.energy_holdup_slag,
                wrt=self.flowsheet().config.time,
                doc='Energy accumulation of slag layer')
            self.energy_accumulation_metal = DerivativeVar(
                self.energy_holdup_metal,
                wrt=self.flowsheet().config.time,
                doc='Energy accumulation of tube and fin metal')

        def energy_accumulation_term_slag(b, t):
            return b.energy_accumulation_slag[t] if b.config.dynamic else 0

        def energy_accumulation_term_metal(b, t):
            return b.energy_accumulation_metal[t] if b.config.dynamic else 0

        # Velocity of liquid only
        self.velocity_liquid = Var(
            self.flowsheet().config.time,
            initialize=3.0,
            doc='Velocity of liquid only',
            units=units_meta.get_derived_units("velocity"))

        # Reynolds number based on liquid only flow
        self.N_Re = Var(self.flowsheet().config.time,
                        initialize=1.0e6,
                        doc='Reynolds number')

        # Prandtl number of liquid phase
        self.N_Pr = Var(self.flowsheet().config.time,
                        initialize=2.0,
                        doc='Reynolds number')

        # Darcy friction factor
        self.friction_factor_darcy = Var(self.flowsheet().config.time,
                                         initialize=0.01,
                                         doc='Darcy friction factor')

        # Vapor fraction at inlet
        self.vapor_fraction = Var(self.flowsheet().config.time,
                                  initialize=0.0,
                                  doc='Vapor fractoin of vapor-liquid mixture')

        # Liquid fraction at inlet
        self.liquid_fraction = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Liquid fractoin of vapor-liquid mixture')

        # Density ratio of liquid to vapor
        self.ratio_density = Var(self.flowsheet().config.time,
                                 initialize=1.0,
                                 doc='Liquid to vapor density ratio')

        # Void fraction at inlet
        self.void_fraction = Var(self.flowsheet().config.time,
                                 initialize=0.0,
                                 doc='void fraction at inlet')

        # Exponent n for gamma at inlet, typical range in (0.75,0.8294)
        self.n_exp = Var(self.flowsheet().config.time,
                         initialize=0.82,
                         doc='exponent for gamma at inlet')

        # Gamma for velocity slip at inlet
        self.gamma = Var(self.flowsheet().config.time,
                         initialize=3.0,
                         doc='gamma for velocity slip at inlet')

        # Mass flux
        self.mass_flux = Var(self.flowsheet().config.time,
                             initialize=1000.0,
                             doc='mass flux',
                             units=units_meta.get_derived_units("flux_mass"))

        # Reduced pressure
        self.reduced_pressure = Var(self.flowsheet().config.time,
                                    initialize=0.85,
                                    doc='reduced pressure')

        # Two-phase correction factor
        self.phi_correction = Var(self.flowsheet().config.time,
                                  initialize=1.01,
                                  doc='Two-phase flow correction factor')

        # Convective heat transfer coefficient on tube side,
        # typically in range (1000, 5e5)
        self.hconv = Var(
            self.flowsheet().config.time,
            initialize=30000.0,
            doc='Convective heat transfer coefficient',
            units=units_meta.get_derived_units("heat_transfer_coefficient"))

        # Convective heat transfer coefficient for liquid only,
        # typically in range (1000.0, 1e5)
        self.hconv_liquid = Var(
            self.flowsheet().config.time,
            initialize=20000.0,
            doc='Convective heat transfer coefficient of liquid only',
            units=units_meta.get_derived_units("heat_transfer_coefficient"))

        # Pool boiling heat transfer coefficient, typically in range (1e4, 5e5)
        self.hpool = Var(
            self.flowsheet().config.time,
            initialize=1e5,
            doc='Pool boiling heat transfer coefficient',
            units=units_meta.get_derived_units("heat_transfer_coefficient"))

        # Boiling number, typical range in (1e-7, 5e-4) in original formula.
        # we define here as boiling_number_scaled == 1e6*boiling_number
        self.boiling_number_scaled = Var(self.flowsheet().config.time,
                                         initialize=1,
                                         doc='Scaled boiling number')

        # Enhancement factor, typical range in (1.0, 3.0)
        self.enhancement_factor = Var(self.flowsheet().config.time,
                                      initialize=1.3,
                                      doc='Enhancement factor')

        # Suppression factor, typical range in (0.005, 1.0)
        self.suppression_factor = Var(self.flowsheet().config.time,
                                      initialize=0.03,
                                      doc='Suppression factor')

        # Convective heat flux to fluid
        self.heat_flux_conv = Var(
            self.flowsheet().config.time,
            initialize=7e4,
            doc='Convective heat flux to fluid',
            units=units_meta.get_derived_units("flux_energy"))

        # Slag-tube interface heat flux
        self.heat_flux_interface = Var(
            self.flowsheet().config.time,
            initialize=100000.0,
            doc='Slag-tube interface heat flux',
            units=units_meta.get_derived_units("flux_energy"))

        # Pressure change due to friction
        self.deltaP_friction = Var(
            self.flowsheet().config.time,
            initialize=-1000.0,
            doc='Pressure change due to friction',
            units=units_meta.get_derived_units("pressure"))

        # Pressure change due to gravity
        self.deltaP_gravity = Var(
            self.flowsheet().config.time,
            initialize=-1000.0,
            doc='Pressure change due to gravity',
            units=units_meta.get_derived_units("pressure"))

        # Equation to calculate heat flux to slag boundary
        @self.Expression(self.flowsheet().config.time,
                         doc="heat flux at slag outer layer")
        def heat_flux_fireside(b, t):
            return (b.heat_fireside[t] * b.pitch /
                    (b.projected_area * b.perimeter_ss[t]))

        # Equation to calculate slag layer boundary temperature
        @self.Constraint(self.flowsheet().config.time,
                         doc="slag layer boundary temperature")
        def slag_layer_boundary_temperature_eqn(b, t):
            return b.heat_flux_fireside[t] * b.half_resistance_slag[t] == \
                   (b.temp_slag_boundary[t] - b.temp_slag_center[t])

        # Equation to calculate heat flux at the slag-metal interface
        @self.Constraint(self.flowsheet().config.time,
                         doc="heat flux at slag-tube interface")
        def heat_flux_interface_eqn(b, t):
            return b.heat_flux_interface[t] \
                * (b.half_resistance_metal + b.half_resistance_slag[t]) == \
                (b.temp_slag_center[t] - b.temp_tube_center[t])

        # Equation to calculate heat flux at tube boundary
        @self.Constraint(self.flowsheet().config.time,
                         doc="convective heat flux at tube boundary")
        def heat_flux_conv_eqn(b, t):
            return b.heat_flux_conv[t] * b.fshape_conv * b.perimeter_ts ==\
                b.pitch * b.hconv[t] \
                * (b.temp_tube_boundary[t]
                   - b.control_volume.properties_in[t].temperature)

        # Equation to calculate tube boundary wall temperature
        @self.Constraint(self.flowsheet().config.time,
                         doc="tube bounary wall temperature")
        def temperature_tube_boundary_eqn(b, t):
            return b.heat_flux_conv[t] * b.half_resistance_metal \
                * b.perimeter_ts == \
                b.perimeter_interface * (b.temp_tube_center[t]
                                         - b.temp_tube_boundary[t])

        # Equation to calculate energy holdup for slag layer per tube length
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy holdup for slag layer")
        def energy_holdup_slag_eqn(b, t):
            return b.energy_holdup_slag[t] == \
                b.temp_slag_center[t] * b.cp_slag * b.dens_slag \
                * b.area_cross_slag[t]

        # Equation to calculate energy holdup for metal
        # (tube + fin) per tube length
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy holdup for metal")
        def energy_holdup_metal_eqn(b, t):
            return b.energy_holdup_metal[t] == b.temp_tube_center[t] \
                * b.cp_metal * b.dens_metal * b.area_cross_metal

        # Energy balance for slag layer
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy balance for slag layer")
        def energy_balance_slag_eqn(b, t):
            return energy_accumulation_term_slag(b, t) == \
                b.heat_flux_fireside[t]*b.perimeter_ss[t] - \
                b.heat_flux_interface[t]*b.perimeter_interface

        # Energy balance for metal
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy balance for metal")
        def energy_balance_metal_eqn(b, t):
            return energy_accumulation_term_metal(b, t) == \
                   b.heat_flux_interface[t]*b.perimeter_interface - \
                   b.heat_flux_conv[t]*b.perimeter_ts

        # Expression to calculate slag/tube metal interface wall temperature
        @self.Expression(self.flowsheet().config.time,
                         doc="Slag tube interface wall temperature")
        def temp_interface(b, t):
            return b.temp_tube_center[t] + b.heat_flux_interface[t] \
                * b.half_resistance_metal

        # Equations for calculate pressure drop
        # and convective heat transfer coefficient for 2-phase flow
        # Equation to calculate liquid to vapor density ratio
        @self.Constraint(self.flowsheet().config.time,
                         doc="liquid to vapor density ratio")
        def ratio_density_eqn(b, t):
            return 0.01*b.ratio_density[t] \
                * b.control_volume.properties_in[t].dens_mol_phase["Vap"] == \
                0.01*b.control_volume.properties_in[t].dens_mol_phase["Liq"]

        # Equation for calculating velocity if the flow is liquid only
        @self.Constraint(self.flowsheet().config.time,
                         doc="Vecolity of fluid if liquid only")
        def velocity_lo_eqn(b, t):
            return 1e-4*b.velocity_liquid[t]*b.area_cross_fluid_total * \
                   b.control_volume.properties_in[t].dens_mol_phase["Liq"] \
                   == 1e-4*b.control_volume.properties_in[t].flow_mol

        # Equation for calculating Reynolds number if liquid only
        @self.Constraint(self.flowsheet().config.time,
                         doc="Reynolds number if liquid only")
        def Reynolds_number_eqn(b, t):
            return b.N_Re[t] * \
                   b.control_volume.properties_in[t].visc_d_phase["Liq"] == \
                   b.tube_diameter * b.velocity_liquid[t] * \
                   b.control_volume.properties_in[t].dens_mass_phase["Liq"]

        # Friction factor depending on laminar or turbulent flow,
        # usually always turbulent (>1187.385)
        @self.Constraint(self.flowsheet().config.time,
                         doc="Darcy friction factor")
        def friction_factor_darcy_eqn(b, t):
            return b.friction_factor_darcy[t] * b.N_Re[t]**0.25 / 0.3164 == 1.0

        # Vapor fractoin equation at inlet,
        # add 1e-5 such that vapor fraction is always positive
        @self.Constraint(self.flowsheet().config.time,
                         doc="Vapor fractoin at inlet")
        def vapor_fraction_eqn(b, t):
            return 100*b.vapor_fraction[t] == \
                100*(b.control_volume.properties_in[t].vapor_frac + 1e-5)

        # n-exponent equation for inlet
        @self.Constraint(self.flowsheet().config.time, doc="n-exponent")
        def n_exp_eqn(b, t):
            return (0.001 * (0.8294 - b.n_exp[t]) *
                    b.control_volume.properties_in[t].pressure == 8.0478 *
                    units_meta.get_derived_units("pressure"))

        # Gamma equation for inlet
        @self.Constraint(self.flowsheet().config.time, doc="Gamma at inlet")
        def gamma_eqn(b, t):
            return b.gamma[t] == b.ratio_density[t]**b.n_exp[t]

        # void faction at inlet equation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Void fractoin at inlet")
        def void_fraction_eqn(b, t):
            return b.void_fraction[t] * (1.0 + b.vapor_fraction[t]
                                         * (b.gamma[t] - 1.0)) == \
                b.vapor_fraction[t] * b.gamma[t]

        # Two-phase flow correction factor equation
        @self.Constraint(self.flowsheet().config.time, doc="Correction factor")
        def correction_factor_eqn(b, t):
            return (b.phi_correction[t] - 0.027*b.liquid_fraction[t])**2 == \
                (0.973*b.liquid_fraction[t] + b.vapor_fraction[t]
                 * b.ratio_density[t]) * \
                (0.973*b.liquid_fraction[t] + b.vapor_fraction[t])

        # Pressure change equation due to friction,
        # -1/2*density*velocity^2*fD/diameter*length*phi^2
        @self.Constraint(self.flowsheet().config.time,
                         doc="pressure change due to friction")
        def pressure_change_friction_eqn(b, t):
            return 0.01 * b.deltaP_friction[t] * b.tube_diameter == \
                -0.01 * b.fcorrection_dp * 0.5 \
                * b.control_volume.properties_in[t].dens_mass_phase["Liq"] * \
                b.velocity_liquid[t]**2 * b.friction_factor_darcy[t] \
                * b.tube_length * b.phi_correction[t]**2

        # Pressure change equation due to gravity,
        # density_mixture*gravity*height
        @self.Constraint(self.flowsheet().config.time,
                         doc="pressure change due to gravity")
        def pressure_change_gravity_eqn(b, t):
            return 1e-3 * b.deltaP_gravity[t] == -1e-3 \
                * const.acceleration_gravity * b.height \
                * (b.control_volume.properties_in[t].dens_mass_phase["Vap"]
                   * b.void_fraction[t] +
                   b.control_volume.properties_in[t].dens_mass_phase["Liq"]
                   * (1.0 - b.void_fraction[t]))

        # Mass flux of vapor-liquid mixture
        # (density*velocity or mass_flow/area)
        @self.Constraint(self.flowsheet().config.time, doc="mass flux")
        def mass_flux_eqn(b, t):
            return b.mass_flux[t] * b.area_cross_fluid_total == \
                   b.control_volume.properties_in[t].flow_mol * \
                   b.control_volume.properties_in[0].mw

        # Liquid fraction at inlet
        @self.Constraint(self.flowsheet().config.time, doc="liquid fraction")
        def liquid_fraction_eqn(b, t):
            return b.liquid_fraction[t] + b.vapor_fraction[t] == 1.0

        # Total pressure change equation
        @self.Constraint(self.flowsheet().config.time, doc="pressure drop")
        def pressure_change_total_eqn(b, t):
            return b.deltaP[t] == b.deltaP_friction[t] + b.deltaP_gravity[t]

        # Total heat added to control_volume
        @self.Constraint(self.flowsheet().config.time,
                         doc="total heat added to fluid control_volume")
        def heat_eqn(b, t):
            return b.heat_duty[t] == b.number_tubes * b.heat_flux_conv[t] \
                * b.tube_length * b.perimeter_ts

        # Reduced pressure
        @self.Constraint(self.flowsheet().config.time, doc="reduced pressure")
        def reduced_pressure_eqn(b, t):
            return b.reduced_pressure[t] \
                * self.config.property_package.pressure_crit == \
                b.control_volume.properties_in[t].pressure

        # Prandtl number of liquid
        @self.Constraint(self.flowsheet().config.time,
                         doc="liquid Prandtl number")
        def N_Pr_eqn(b, t):
            return b.N_Pr[t] \
                * b.control_volume.properties_in[t].therm_cond_phase["Liq"] \
                * b.control_volume.properties_in[0].mw == \
                b.control_volume.properties_in[t].cp_mol_phase["Liq"] * \
                b.control_volume.properties_in[t].visc_d_phase["Liq"]

        # Forced convection heat transfer coefficient for liquid only
        @self.Constraint(self.flowsheet().config.time,
                         doc="forced convection heat transfer "
                         "coefficient for liquid only")
        def hconv_lo_eqn(b, t):
            return b.hconv_liquid[t] * b.tube_diameter == \
                0.023 * b.N_Re[t]**0.8 * b.N_Pr[t]**0.4 * \
                b.control_volume.properties_in[t].therm_cond_phase["Liq"]

        # Pool boiling heat transfer coefficient
        @self.Constraint(self.flowsheet().config.time,
                         doc="pool boiling heat transfer coefficient")
        def hpool_eqn(b, t):
            return (1e-4 * b.hpool[t] * sqrt(
                pyunits.convert(b.control_volume.properties_in[0].mw,
                                to_units=pyunits.g / pyunits.mol)) *
                    (-log10(b.reduced_pressure[t]))**(0.55) == 1e-4 * 55.0 *
                    b.reduced_pressure[t]**0.12 * b.heat_flux_conv[t]**0.67)

        # Boiling number scaled by a factor of 1e6
        @self.Constraint(self.flowsheet().config.time, doc="boiling number")
        def boiling_number_eqn(b, t):
            return 1e-10*b.boiling_number_scaled[t] \
                * b.control_volume.properties_in[t].dh_vap_mol \
                * b.mass_flux[t] == \
                b.heat_flux_conv[t]*b.control_volume.properties_in[0].mw*1e-4

        if self.config.rigorous_boiling is True:
            '''
        Due to low contribution to the enhancement factor and highly nonlinear
        constraint, the Martinelli paramter has been removed from the model.
        if required user needs to declare the variable and constraint, and
        update constraint in line 909 to add the Martinelli parameter factor
            '''
            # Reciprocal of Martinelli parameter to the power of 0.86,
            # typical range in (1e-3, 1.0)
            self.martinelli_reciprocal_p86 = Var(
                self.flowsheet().config.time,
                initialize=0.2,
                doc='Reciprocal of Martinelli parameter '
                'to the power of 0.86')

            # Reciprocal of Martinelli parameter to the power of 0.86
            @self.Constraint(self.flowsheet().config.time,
                             doc="Reciprocal of Martineli parameter "
                             "to the power of 0.86")
            def martinelli_reciprocal_p86_eqn(b, t):
                return b.martinelli_reciprocal_p86[t]*b.\
                    liquid_fraction[t]**0.774 * b.control_volume.\
                    properties_in[t].visc_d_phase["Liq"]**0.086 == \
                    Expr_if(b.vapor_fraction[t] > 0,
                            (b.vapor_fraction[t])**0.774
                            * b.ratio_density[t]**0.43 * b.control_volume.
                            properties_in[t].visc_d_phase["Vap"]**0.086, 0.0)

        # Enhancement factor
        # due to low contribution the reciprocal Martinalli parameter,
        # it can be removed from the enhancement factor equation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Forced convection enhancement factor")
        def enhancement_factor_eqn(b, t):
            if self.config.rigorous_boiling is True:
                return b.enhancement_factor[t] == 1.0 + 24000.0 \
                    * (b.boiling_number_scaled[t]/1e6)**1.16 + \
                    Expr_if(b.vapor_fraction[t] > 0,
                            1.37*b.martinelli_reciprocal_p86[t], 0.0)
            else:
                return b.enhancement_factor[t] == 1.0 + 24000.0 \
                    * (b.boiling_number_scaled[t]/1e6)**1.16

        # Suppression factor
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pool boiler suppression factor")
        def suppression_factor_eqn(b, t):
            return b.suppression_factor[t] \
                * (1.0 + 1.15e-6*b.enhancement_factor[t]**2
                   * b.N_Re[t]**1.17) == 1.0

        @self.Constraint(self.flowsheet().config.time,
                         doc="convective heat transfer coefficient")
        def hconv_eqn(b, t):
            return 1e-3*b.hconv[t] == 1e-3 * b.hconv_liquid[t] \
                * b.enhancement_factor[t] + 1e-3 * b.hpool[t] \
                * b.suppression_factor[t]

    def set_initial_condition(self):
        ''' Initialization of dynamic accumulation terms '''

        if self.config.dynamic is True:
            self.control_volume.material_accumulation[:, :, :].value = 0
            self.control_volume.energy_accumulation[:, :].value = 0
            self.control_volume.material_accumulation[0, :, :].fix(0)
            self.control_volume.energy_accumulation[0, :].fix(0)
            self.energy_accumulation_slag[:].value = 0
            self.energy_accumulation_metal[:].value = 0
            self.energy_accumulation_slag[0].fix(0)
            self.energy_accumulation_metal[0].fix(0)

    def initialize(blk,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver='ipopt',
                   optarg={'tol': 1e-6}):
        '''
        Waterwall section initialization routine.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) for the control_volume of the model to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

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

        opt = SolverFactory(solver)
        opt.options = optarg

        flags = blk.control_volume.initialize(outlvl=outlvl + 1,
                                              optarg=optarg,
                                              solver=solver,
                                              state_args=state_args)
        init_log.info_high("Initialization Step 1 Complete.")
        # Fix outlet enthalpy and pressure
        for t in blk.flowsheet().config.time:
            blk.control_volume.properties_out[t].enth_mol.fix(
                value(blk.control_volume.properties_in[t].enth_mol) +
                value(blk.heat_fireside[t]) /
                value(blk.control_volume.properties_in[t].flow_mol))
            blk.control_volume.properties_out[t].pressure.fix(
                value(blk.control_volume.properties_in[t].pressure) - 1.0)
        blk.heat_eqn.deactivate()
        blk.pressure_change_total_eqn.deactivate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))

        # Unfix outlet enthalpy and pressure
        for t in blk.flowsheet().config.time:
            blk.control_volume.properties_out[t].enth_mol.unfix()
            blk.control_volume.properties_out[t].pressure.unfix()
        blk.heat_eqn.activate()
        blk.pressure_change_total_eqn.activate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(
            idaeslog.condition(res)))

        blk.control_volume.release_state(flags, outlvl + 1)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        for t, c in self.energy_holdup_slag_eqn.items():
            s = iscale.get_scaling_factor(self.energy_holdup_slag[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s)
        for t, c in self.friction_factor_darcy_eqn.items():
            s = iscale.get_scaling_factor(self.N_Re[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s)
        for t, c in self.volume_eqn.items():
            s = iscale.get_scaling_factor(self.volume[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s)
        for t, c in self.heat_flux_conv_eqn.items():
            s = iscale.get_scaling_factor(self.heat_flux_conv[t],
                                          default=1,
                                          warning=True)
            s *= iscale.get_scaling_factor(self.tube_diameter,
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, s / 10.0)
        for t, c in self.energy_holdup_slag_eqn.items():
            s = iscale.get_scaling_factor(self.energy_holdup_slag[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s)
        for t, c in self.energy_holdup_metal_eqn.items():
            s = iscale.get_scaling_factor(self.energy_holdup_metal[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s)
コード例 #10
0
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single 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_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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(
        "compressor",
        ConfigValue(default=True,
                    domain=In([True, False]),
                    description="Compressor flag",
                    doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease)."""))
    CONFIG.declare(
        "thermodynamic_assumption",
        ConfigValue(
            default=ThermodynamicAssumption.isentropic,
            domain=In(ThermodynamicAssumption),
            description="Thermodynamic assumption to use",
            doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic"""))
    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.}"""))

    def build(self):
        """

        Args:
            None

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

        # Add a control volume to the unit including setting up dynamics.
        self.control_volume = ControlVolume0DBlock(
            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
            })

        # Add geomerty variables to control volume
        if self.config.has_holdup:
            self.control_volume.add_geometry()

        # Add inlet and outlet state blocks to control volume
        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        # Add mass balance
        # Set has_equilibrium is False for now
        # TO DO; set has_equilibrium to True
        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        # Add energy balance
        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_work_transfer=True)

        # add momentum balance
        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=True)

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

        # Set Unit Geometry and holdup Volume
        self.set_geometry()

        # Construct performance equations
        self.add_performance()

        # Construct equations for thermodynamic assumption
        if self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.isothermal:
            self.add_isothermal()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.isentropic:
            self.add_isentropic()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.pump:
            self.add_pump()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.adiabatic:
            self.add_adiabatic()

    def set_geometry(self):
        """
        Define the geometry of the unit as necessary, and link to control
        volume

        Args:
            None

        Returns:
            None
        """
        # For this case, just create a reference to control volume
        if self.config.has_holdup is True:
            add_object_reference(self, "volume", self.control_volume.volume)

    def add_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.

        Args:
            None

        Returns:
            None
        """

        # Set references to balance terms at unit level
        # Add Work transfer variable 'work' as necessary
        add_object_reference(self, "work_mechanical", self.control_volume.work)

        # Add Momentum balance variable 'deltaP' as necessary
        add_object_reference(self, "deltaP", self.control_volume.deltaP)

        # Set reference to scaling factor for pressure in control volume
        add_object_reference(self, "sfp",
                             self.control_volume.scaling_factor_pressure)

        # Set reference to scaling factor for energy in control volume
        add_object_reference(self, "sfe",
                             self.control_volume.scaling_factor_energy)

        # Performance Variables
        self.ratioP = Var(self.flowsheet().config.time,
                          initialize=1.0,
                          doc="Pressure Ratio")

        # Pressure Ratio
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure ratio constraint")
        def ratioP_calculation(b, t):
            return (self.sfp * b.ratioP[t] *
                    b.control_volume.properties_in[t].pressure == self.sfp *
                    b.control_volume.properties_out[t].pressure)

    def add_pump(self):
        """
        Add constraints for the incompressible fluid assumption

        Args:
            None

        Returns:
            None
        """

        self.work_fluid = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc="Work required to increase the pressure of the liquid")
        self.efficiency_pump = Var(self.flowsheet().config.time,
                                   initialize=1.0,
                                   doc="Pump efficiency")

        @self.Constraint(self.flowsheet().config.time,
                         doc="Pump fluid work constraint")
        def fluid_work_calculation(b, t):
            return b.work_fluid[t] == (
                (b.control_volume.properties_out[t].pressure -
                 b.control_volume.properties_in[t].pressure) *
                b.control_volume.properties_out[t].flow_vol)

        # Actual work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Actual mechanical work calculation")
        def actual_work(b, t):
            if b.config.compressor:
                return b.sfe * b.work_fluid[t] == b.sfe * (
                    b.work_mechanical[t] * b.efficiency_pump[t])
            else:
                return b.sfe * b.work_mechanical[t] == b.sfe * (
                    b.work_fluid[t] * b.efficiency_pump[t])

    def add_isothermal(self):
        """
        Add constraints for isothermal assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="For isothermal condition: Equate inlet and "
                         "outlet temperature")
        def isothermal(b, t):
            return b.control_volume.properties_in[t].temperature == \
                       b.control_volume.properties_out[t].temperature

    def add_adiabatic(self):
        """
        Add constraints for adiabatic assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="For isothermal condition: Equate inlet and "
                         "outlet enthalpy")
        def adiabatic(b, t):
            return b.control_volume.properties_in[t].enth_mol == \
                       b.control_volume.properties_out[t].enth_mol

    def add_isentropic(self):
        """
        Add constraints for isentropic assumption.

        Args:
            None

        Returns:
            None
        """
        # Get indexing sets from control volume
        # Add isentropic variables
        self.efficiency_isentropic = Var(self.flowsheet().config.time,
                                         initialize=0.8,
                                         doc="Efficiency with respect to an "
                                         "isentropic process [-]")
        self.work_isentropic = Var(self.flowsheet().config.time,
                                   initialize=0.0,
                                   doc="Work input to unit if isentropic "
                                   "process [-]")

        # Build isentropic state block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False

        self.properties_isentropic = (
            self.config.property_package.state_block_class(
                self.flowsheet().config.time,
                doc="isentropic properties at outlet",
                default=tmp_dict))

        # Connect isentropic state block properties
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure for isentropic calculations")
        def isentropic_pressure(b, t):
            return b.sfp*b.properties_isentropic[t].pressure == \
                b.sfp*b.ratioP[t]*b.control_volume.properties_out[t].pressure

        # This assumes isentropic composition is the same as outlet
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.component_list,
                         doc="Material flows for isentropic properties")
        def isentropic_material(b, t, j):
            return b.properties_isentropic[t].flow_mol_comp[j] == \
                        b.control_volume.properties_out[t].flow_mol_comp[j]

        # This assumes isentropic entropy is the same as outlet
        @self.Constraint(self.flowsheet().config.time,
                         doc="Isentropic assumption")
        def isentropic(b, t):
            return b.properties_isentropic[t].entr_mol == \
                       b.control_volume.properties_out[t].entr_mol

        # Isentropic work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Calculate work of isentropic process")
        def isentropic_energy_balance(b, t):
            return b.sfe * b.work_isentropic[t] == b.sfe * (
                sum(b.properties_isentropic[t].get_enthalpy_flow_terms(p)
                    for p in b.config.property_package.phase_list) -
                sum(b.control_volume.properties_out[t].get_enthalpy_flow_terms(
                    p) for p in b.config.property_package.phase_list))

        # Actual work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Actual mechanical work calculation")
        def actual_work(b, t):
            if b.config.compressor:
                return b.sfe * b.work_isentropic[t] == b.sfe * (
                    b.work_mechanical[t] * b.efficiency_isentropic[t])
            else:
                return b.sfe * b.work_mechanical[t] == b.sfe * (
                    b.work_isentropic[t] * b.efficiency_isentropic[t])

    def model_check(blk):
        """
        Check that pressure change matches with compressor argument (i.e. if
        compressor = True, pressure should increase or work should be positive)

        Args:
            None

        Returns:
            None
        """
        if blk.config.compressor:
            # Compressor
            # Check that pressure does not decrease
            if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Compressor set with negative deltaP.'.format(blk.name))
            if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0)
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Compressor set with ratioP less than 1.'.format(
                        blk.name))
            if any(blk.control_volume.properties_out[t].pressure.fixed and (
                    value(blk.control_volume.properties_in[t].pressure) >
                    value(blk.control_volume.properties_out[t].pressure))
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Compressor set with pressure decrease.'.format(
                        blk.name))
            # Check that work is not negative
            if any(blk.work_mechanical[t].fixed and (
                    value(blk.work_mechanical[t]) < 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Compressor maybe set with negative work.'.format(
                        blk.name))
        else:
            # Expander
            # Check that pressure does not increase
            if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Expander/turbine set with positive deltaP.'.format(
                        blk.name))
            if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0)
                   for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine set with ratioP greater '
                               'than 1.'.format(blk.name))
            if any(blk.control_volume.properties_out[t].pressure.fixed and (
                    value(blk.control_volume.properties_in[t].pressure) <
                    value(blk.control_volume.properties_out[t].pressure))
                   for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine maybe set with pressure ',
                               'increase.'.format(blk.name))
            # Check that work is not positive
            if any(blk.work_mechanical[t].fixed and (
                    value(blk.work_mechanical[t]) > 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning(
                    '{} Expander/turbine set with positive work.'.format(
                        blk.name))

        # Run holdup block model checks
        blk.control_volume.model_check()

        # Run model checks on isentropic property block
        try:
            for t in blk.flowsheet().config.time:
                blk.properties_in[t].model_check()
        except AttributeError:
            pass

    def initialize(blk,
                   state_args={},
                   routine=None,
                   outlvl=0,
                   solver='ipopt',
                   optarg={'tol': 1e-6}):
        '''
        General wrapper for pressure changer initialisation routines

        Keyword Arguments:
            routine : str stating which initialization routine to execute
                        * None - use routine matching thermodynamic_assumption
                        * 'isentropic' - use isentropic initialization routine
                        * 'isothermal' - use isothermal initialization routine
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''
        if routine is None:
            # Use routine for specific type of unit
            routine = blk.config.thermodynamic_assumption

        # Call initialisation routine
        if routine is ThermodynamicAssumption.isentropic:
            blk.init_isentropic(state_args=state_args,
                                outlvl=outlvl,
                                solver=solver,
                                optarg=optarg)
        else:
            # Call the general initialization routine in UnitModelBlockData
            super(PressureChangerData, blk).initialize(state_args=state_args,
                                                       outlvl=outlvl,
                                                       solver=solver,
                                                       optarg=optarg)

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        '''
        Initialisation routine for unit (default solver ipopt)

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''
        # Set solver options
        if outlvl > 3:
            stee = True
        else:
            stee = False

        opt = SolverFactory(solver)
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize Isentropic block
        blk.control_volume.properties_in.initialize(outlvl=outlvl - 1,
                                                    optarg=optarg,
                                                    solver=solver,
                                                    **state_args)

        if outlvl > 0:
            logger.info('{} Initialisation Step 1 Complete.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.control_volume.initialize(outlvl=outlvl - 1,
                                              optarg=optarg,
                                              solver=solver,
                                              state_args=state_args)

        if outlvl > 0:
            logger.info('{} Initialisation Step 2 Complete.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Solve for isothermal conditions
        if isinstance(
                blk.control_volume.properties_in[
                    blk.flowsheet().config.time[1]].temperature, Var):
            for t in blk.flowsheet().config.time:
                blk.control_volume.properties_in[t].temperature.fix()
            blk.isentropic.deactivate()
            results = opt.solve(blk, tee=stee)
            if outlvl > 0:
                if results.solver.termination_condition == \
                        TerminationCondition.optimal:
                    logger.info('{} Initialisation Step 3 Complete.'.format(
                        blk.name))
                else:
                    logger.warning('{} Initialisation Step 3 Failed.'.format(
                        blk.name))
            for t in blk.flowsheet().config.time:
                blk.control_volume.properties_in[t].temperature.unfix()
                blk.isentropic.activate()
        elif outlvl > 0:
            logger.info('{} Initialisation Step 3 Skipped.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Solve unit
        results = opt.solve(blk, tee=stee)

        if outlvl > 0:
            if results.solver.termination_condition == \
                    TerminationCondition.optimal:
                logger.info('{} Initialisation Step 4 Complete.'.format(
                    blk.name))
            else:
                logger.warning('{} Initialisation Step 4 Failed.'.format(
                    blk.name))

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl - 1)

        if outlvl > 0:
            logger.info('{} Initialisation Complete.'.format(blk.name))
コード例 #11
0
class PyomoCyIpoptSolver(object):

    CONFIG = ConfigBlock("cyipopt")
    CONFIG.declare(
        "tee",
        ConfigValue(
            default=False,
            domain=bool,
            description="Stream solver output to console",
        ))
    CONFIG.declare(
        "load_solutions",
        ConfigValue(
            default=True,
            domain=bool,
            description=
            "Store the final solution into the original Pyomo model",
        ))
    CONFIG.declare(
        "return_nlp",
        ConfigValue(
            default=False,
            domain=bool,
            description="Return the results object and the underlying nlp"
            " NLP object from the solve call.",
        ))
    CONFIG.declare("options", ConfigBlock(implicit=True))
    CONFIG.declare(
        "intermediate_callback",
        ConfigValue(default=None,
                    description="Set the function that will be called each"
                    " iteration."))

    def __init__(self, **kwds):
        """Create an instance of the CyIpoptSolver. You must
        provide a problem_interface that corresponds to
        the abstract class CyIpoptProblemInterface

        options can be provided as a dictionary of key value
        pairs
        """
        self.config = self.CONFIG(kwds)

    def _set_model(self, model):
        self._model = model

    def available(self, exception_flag=False):
        return bool(numpy_available and cyipopt_available)

    def license_is_valid(self):
        return True

    def version(self):
        return tuple(int(_) for _ in cyipopt.__version__.split('.'))

    def solve(self, model, **kwds):
        config = self.config(kwds, preserve_implicit=True)

        if not isinstance(model, Block):
            raise ValueError("PyomoCyIpoptSolver.solve(model): model "
                             "must be a Pyomo Block")

        # If this is a Pyomo model / block, then we need to create
        # the appropriate PyomoNLP, then wrap it in a CyIpoptNLP
        grey_box_blocks = list(
            model.component_data_objects(egb.ExternalGreyBoxBlock,
                                         active=True))
        if grey_box_blocks:
            # nlp = pyomo_nlp.PyomoGreyBoxNLP(model)
            nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model)
        else:
            nlp = pyomo_nlp.PyomoNLP(model)

        problem = CyIpoptNLP(
            nlp, intermediate_callback=config.intermediate_callback)

        xl = problem.x_lb()
        xu = problem.x_ub()
        gl = problem.g_lb()
        gu = problem.g_ub()

        nx = len(xl)
        ng = len(gl)

        cyipopt_solver = cyipopt.Problem(n=nx,
                                         m=ng,
                                         problem_obj=problem,
                                         lb=xl,
                                         ub=xu,
                                         cl=gl,
                                         cu=gu)

        # check if we need scaling
        obj_scaling, x_scaling, g_scaling = problem.scaling_factors()
        if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)):
            # need to set scaling factors
            if obj_scaling is None:
                obj_scaling = 1.0
            if x_scaling is None:
                x_scaling = np.ones(nx)
            if g_scaling is None:
                g_scaling = np.ones(ng)
            try:
                set_scaling = cyipopt_solver.set_problem_scaling
            except AttributeError:
                # Fall back to pre-1.0.0 API
                set_scaling = cyipopt_solver.setProblemScaling
            set_scaling(obj_scaling, x_scaling, g_scaling)

        # add options
        try:
            add_option = cyipopt_solver.add_option
        except AttributeError:
            # Fall back to pre-1.0.0 API
            add_option = cyipopt_solver.addOption
        for k, v in config.options.items():
            add_option(k, v)

        timer = TicTocTimer()
        try:
            # We preemptively set up the TeeStream, even if we aren't
            # going to use it: the implementation is such that the
            # context manager does nothing (i.e., doesn't start up any
            # processing threads) until afer a client accesses
            # STDOUT/STDERR
            with TeeStream(sys.stdout) as _teeStream:
                if config.tee:
                    try:
                        fd = sys.stdout.fileno()
                    except (io.UnsupportedOperation, AttributeError):
                        # If sys,stdout doesn't have a valid fileno,
                        # then create one using the TeeStream
                        fd = _teeStream.STDOUT.fileno()
                else:
                    fd = None
                with redirect_fd(fd=1, output=fd, synchronize=False):
                    x, info = cyipopt_solver.solve(problem.x_init())
            solverStatus = SolverStatus.ok
        except:
            msg = "Exception encountered during cyipopt solve:"
            logger.error(msg, exc_info=sys.exc_info())
            solverStatus = SolverStatus.unknown
            raise

        wall_time = timer.toc(None)

        results = SolverResults()

        if config.load_solutions:
            nlp.set_primals(x)
            nlp.set_duals(info['mult_g'])
            nlp.load_state_into_pyomo(bound_multipliers=(info['mult_x_L'],
                                                         info['mult_x_U']))
        else:
            soln = results.solution.add()
            soln.variable.update((i, {
                'Value': j,
                'ipopt_zL_out': zl,
                'ipopt_zU_out': zu
            }) for i, j, zl, zu in zip(nlp.variable_names(), x,
                                       info['mult_x_L'], info['mult_x_U']))
            soln.constraint.update((i, {
                'Dual': j
            }) for i, j in zip(nlp.constraint_names(), info['mult_g']))

        results.problem.name = model.name
        obj = next(model.component_data_objects(Objective, active=True))
        if obj.sense == minimize:
            results.problem.sense = ProblemSense.minimize
            results.problem.upper_bound = info['obj_val']
        else:
            results.problem.sense = ProblemSense.maximize
            results.problem.lower_bound = info['obj_val']
        results.problem.number_of_objectives = 1
        results.problem.number_of_constraints = ng
        results.problem.number_of_variables = nx
        results.problem.number_of_binary_variables = 0
        results.problem.number_of_integer_variables = 0
        results.problem.number_of_continuous_variables = nx
        # TODO: results.problem.number_of_nonzeros

        results.solver.name = 'cyipopt'
        results.solver.return_code = info['status']
        results.solver.message = info['status_msg']
        results.solver.wallclock_time = wall_time
        status_enum = _cyipopt_status_enum[info['status_msg']]
        results.solver.termination_condition = _ipopt_term_cond[status_enum]
        results.solver.status = TerminationCondition.to_solver_status(
            results.solver.termination_condition)

        if config.return_nlp:
            return results, nlp

        return results

    #
    # Support "with" statements.
    #
    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
コード例 #12
0
def _define_turbine_multistage_config(config):
    config.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    config.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    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(
        "num_parallel_inlet_stages",
        ConfigValue(
            default=4,
            domain=int,
            description=
            "Number of parallel inlet stages to simulate partial arc "
            "admission.  Default=4",
        ),
    )
    config.declare(
        "throttle_valve_function",
        ConfigValue(
            default=ValveFunctionType.linear,
            domain=In(ValveFunctionType),
            description=
            "Valve function type, if custom provide an expression rule",
            doc=
            """The type of valve function, if custom provide an expression rule
with the valve_function_rule argument.
**default** - ValveFunctionType.linear
**Valid values** - {
ValveFunctionType.linear,
ValveFunctionType.quick_opening,
ValveFunctionType.equal_percentage,
ValveFunctionType.custom}""",
        ),
    )
    config.declare(
        "throttle_valve_function_callback",
        ConfigValue(
            default=None,
            description="A callback to add a custom valve function to the "
            "throttle valves or None.  If a callback is provided, it should "
            "take the valve block data as an argument and add a "
            "valve_function expressions to it. Default=None",
        ),
    )
    config.declare(
        "num_hp",
        ConfigValue(
            default=2,
            domain=int,
            description=
            "Number of high pressure stages not including inlet stage",
            doc="Number of high pressure stages not including inlet stage",
        ),
    )
    config.declare(
        "num_ip",
        ConfigValue(
            default=10,
            domain=int,
            description="Number of intermediate pressure stages",
            doc="Number of intermediate pressure stages",
        ),
    )
    config.declare(
        "num_lp",
        ConfigValue(
            default=5,
            domain=int,
            description=
            "Number of low pressure stages not including outlet stage",
            doc="Number of low pressure stages not including outlet stage",
        ),
    )
    config.declare(
        "hp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in HP section",
            doc="A list of index locations of splitters in the HP section. The "
            "indexes indicate after which stage to include splitters.  0 is "
            "between the inlet stage and the first regular HP stage.",
        ),
    )
    config.declare(
        "ip_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in IP section",
            doc="A list of index locations of splitters in the IP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "lp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitter in LP section",
            doc="A list of index locations of splitters in the LP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "hp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="HP Turbine stages to not connect to next with an arc.",
            doc="HP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert addtional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "ip_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="IP Turbine stages to not connect to next with an arc.",
            doc="IP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert addtional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "lp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="LP Turbine stages to not connect to next with an arc.",
            doc="LP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert addtional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "hp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, hp split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "ip_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, ip split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "lp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, lp split index: number of splitter outlets, if not 2",
        ),
    )
コード例 #13
0
class HelmTurbineMultistageData(UnitModelBlockData):
    CONFIG = ConfigBlock()
    _define_turbine_multistage_config(CONFIG)

    def build(self):
        super().build()
        config = self.config
        unit_cfg = {  # general unit model config
            "dynamic": config.dynamic,
            "has_holdup": config.has_holdup,
            "property_package": config.property_package,
            "property_package_args": config.property_package_args,
        }
        ni = self.config.num_parallel_inlet_stages
        inlet_idx = self.inlet_stage_idx = pyo.RangeSet(ni)

        thrtl_cfg = unit_cfg.copy()
        thrtl_cfg["valve_function"] = self.config.throttle_valve_function
        thrtl_cfg["valve_function_callback"] = \
            self.config.throttle_valve_function_callback

        # Adding unit models
        # ------------------------

        # Splitter to inlet that splits main flow into parallel flows for
        # paritial arc admission to the turbine
        self.inlet_split = HelmSplitter(default=self._split_cfg(unit_cfg, ni))
        self.throttle_valve = SteamValve(inlet_idx, default=thrtl_cfg)
        self.inlet_stage = HelmTurbineInletStage(inlet_idx, default=unit_cfg)
        # mixer to combine the parallel flows back together
        self.inlet_mix = HelmMixer(default=self._mix_cfg(unit_cfg, ni))
        # add turbine sections.
        # inlet stage -> hp stages -> ip stages -> lp stages -> outlet stage
        self.hp_stages = HelmTurbineStage(pyo.RangeSet(config.num_hp),
                                          default=unit_cfg)
        self.ip_stages = HelmTurbineStage(pyo.RangeSet(config.num_ip),
                                          default=unit_cfg)
        self.lp_stages = HelmTurbineStage(pyo.RangeSet(config.num_lp),
                                          default=unit_cfg)
        self.outlet_stage = HelmTurbineOutletStage(default=unit_cfg)

        for i in self.hp_stages:
            self.hp_stages[i].ratioP.fix()
            self.hp_stages[i].efficiency_isentropic.fix()
        for i in self.ip_stages:
            self.ip_stages[i].ratioP.fix()
            self.ip_stages[i].efficiency_isentropic.fix()
        for i in self.lp_stages:
            self.lp_stages[i].ratioP.fix()
            self.lp_stages[i].efficiency_isentropic.fix()

        # Then make splitter config.  If number of outlets is specified
        # make a specific config, otherwise use default with 2 outlets
        s_sfg_default = self._split_cfg(unit_cfg, 2)
        hp_splt_cfg = {}
        ip_splt_cfg = {}
        lp_splt_cfg = {}
        # Now to finish up if there are more than two outlets, set that
        for i, v in config.hp_split_num_outlets.items():
            hp_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        for i, v in config.ip_split_num_outlets.items():
            ip_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        for i, v in config.lp_split_num_outlets.items():
            lp_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        # put in splitters for turbine steam extractions
        if config.hp_split_locations:
            self.hp_split = HelmSplitter(config.hp_split_locations,
                                         default=s_sfg_default,
                                         initialize=hp_splt_cfg)
        if config.ip_split_locations:
            self.ip_split = HelmSplitter(config.ip_split_locations,
                                         default=s_sfg_default,
                                         initialize=ip_splt_cfg)
        if config.lp_split_locations:
            self.lp_split = HelmSplitter(config.lp_split_locations,
                                         default=s_sfg_default,
                                         initialize=lp_splt_cfg)

        # Done with unit models.  Adding Arcs (streams).
        # ------------------------------------------------

        # First up add streams in the inlet section
        def _split_to_rule(b, i):
            return {
                "source": getattr(self.inlet_split, "outlet_{}".format(i)),
                "destination": self.throttle_valve[i].inlet,
            }

        def _valve_to_rule(b, i):
            return {
                "source": self.throttle_valve[i].outlet,
                "destination": self.inlet_stage[i].inlet,
            }

        def _inlet_to_rule(b, i):
            return {
                "source": self.inlet_stage[i].outlet,
                "destination": getattr(self.inlet_mix, "inlet_{}".format(i)),
            }

        self.stream_throttle_inlet = Arc(inlet_idx, rule=_split_to_rule)
        self.stream_throttle_outlet = Arc(inlet_idx, rule=_valve_to_rule)
        self.stream_inlet_mix_inlet = Arc(inlet_idx, rule=_inlet_to_rule)

        # There are three sections HP, IP, and LP which all have the same sort
        # of internal connctions, so the functions below provide some generic
        # capcbilities for adding the internal Arcs (streams).
        def _arc_indexes(nstages, index_set, discon, splits):
            """
            This takes the index set of all possible streams in a turbine
            section and throws out arc indexes for stages that are disconnected
            and arc indexes that are not needed because there is no splitter
            after a stage.

            Args:
                nstages (int): Number of stages in section
                index_set (Set): Index set for arcs in the section
                discon (list): Disconnected stages in the section
                splits (list): Spliter locations
            """
            sr = set()  # set of things to remove from the Arc index set
            for i in index_set:
                if (i[0] in discon or i[0] == nstages) and i[0] in splits:
                    # don't connect stage i to next remove stream after split
                    sr.add((i[0], 2))
                elif (i[0] in discon
                      or i[0] == nstages) and i[0] not in splits:
                    # no splitter and disconnect so remove both streams
                    sr.add((i[0], 1))
                    sr.add((i[0], 2))
                elif i[0] not in splits:
                    # no splitter and not disconnected so just second stream
                    sr.add((i[0], 2))
                else:
                    # has splitter so need both streams don't remove anything
                    pass
            for i in sr:  # remove the unneeded Arc indexes
                index_set.remove(i)

        def _arc_rule(turbines, splitters):
            """
            This creates a rule function for arcs in a turbine section. When
            this is used, the indexes for nonexistant stream will have already
            been removed, so any indexes the rule will get should have a stream
            associated.

            Args:
                turbines (TurbineStage): Indexed block with turbine section stages
                splitters (Separator): Indexed block of splitters
            """
            def _rule(b, i, j):
                if i in splitters and j == 1:  # stage to splitter
                    return {
                        "source": turbines[i].outlet,
                        "destination": splitters[i].inlet,
                    }
                elif j == 2:  # splitter to next stage
                    return {
                        "source": splitters[i].outlet_1,
                        "destination": turbines[i + 1].inlet,
                    }
                else:  # no splitter, stage to next stage
                    return {
                        "source": turbines[i].outlet,
                        "destination": turbines[i + 1].inlet,
                    }

            return _rule

        # Create initial arcs index sets with all possible streams
        self.hp_stream_idx = pyo.Set(initialize=self.hp_stages.index_set() *
                                     [1, 2])
        self.ip_stream_idx = pyo.Set(initialize=self.ip_stages.index_set() *
                                     [1, 2])
        self.lp_stream_idx = pyo.Set(initialize=self.lp_stages.index_set() *
                                     [1, 2])

        # Throw out unneeded streams for disconnected stages or no splitter
        _arc_indexes(
            config.num_hp,
            self.hp_stream_idx,
            config.hp_disconnect,
            config.hp_split_locations,
        )
        _arc_indexes(
            config.num_ip,
            self.ip_stream_idx,
            config.ip_disconnect,
            config.ip_split_locations,
        )
        _arc_indexes(
            config.num_lp,
            self.lp_stream_idx,
            config.lp_disconnect,
            config.lp_split_locations,
        )

        # Create connections internal to each turbine section (hp, ip, and lp)
        self.hp_stream = Arc(self.hp_stream_idx,
                             rule=_arc_rule(self.hp_stages, self.hp_split))
        self.ip_stream = Arc(self.ip_stream_idx,
                             rule=_arc_rule(self.ip_stages, self.ip_split))
        self.lp_stream = Arc(self.lp_stream_idx,
                             rule=_arc_rule(self.lp_stages, self.lp_split))

        # Connect hp section to ip section unless its a disconnect location
        last_hp = config.num_hp
        if 0 not in config.ip_disconnect and last_hp not in config.hp_disconnect:
            # Not disconnected stage so add stream, depending on splitter existance
            if last_hp in config.hp_split_locations:  # connect splitter to ip
                self.hp_to_ip_stream = Arc(
                    source=self.hp_split[last_hp].outlet_1,
                    destination=self.ip_stages[1].inlet,
                )
            else:  # connect last hp to ip
                self.hp_to_ip_stream = Arc(
                    source=self.hp_stages[last_hp].outlet,
                    destination=self.ip_stages[1].inlet,
                )
        # Connect ip section to lp section unless its a disconnect location
        last_ip = config.num_ip
        if 0 not in config.lp_disconnect and last_ip not in config.ip_disconnect:
            if last_ip in config.ip_split_locations:  # connect splitter to ip
                self.ip_to_lp_stream = Arc(
                    source=self.ip_split[last_ip].outlet_1,
                    destination=self.lp_stages[1].inlet,
                )
            else:  # connect last hp to ip
                self.ip_to_lp_stream = Arc(
                    source=self.ip_stages[last_ip].outlet,
                    destination=self.lp_stages[1].inlet,
                )
        # Connect inlet stage to hp section
        #   not allowing disconnection of inlet and first regular hp stage
        if 0 in config.hp_split_locations:
            # connect inlet mix to splitter and splitter to hp section
            self.inlet_to_splitter_stream = Arc(
                source=self.inlet_mix.outlet,
                destination=self.hp_split[0].inlet)
            self.splitter_to_hp_stream = Arc(
                source=self.hp_split[0].outlet_1,
                destination=self.hp_stages[1].inlet)
        else:  # connect mixer to first hp turbine stage
            self.inlet_to_hp_stream = Arc(source=self.inlet_mix.outlet,
                                          destination=self.hp_stages[1].inlet)

        @self.Expression(self.flowsheet().config.time)
        def power(b, t):
            return (sum(b.inlet_stage[i].power_shaft[t]
                        for i in b.inlet_stage) +
                    b.outlet_stage.power_shaft[t] +
                    sum(b.hp_stages[i].power_shaft[t] for i in b.hp_stages) +
                    sum(b.ip_stages[i].power_shaft[t] for i in b.ip_stages) +
                    sum(b.lp_stages[i].power_shaft[t] for i in b.lp_stages))

        # Connect lp section to outlet stage, not allowing outlet stage to be
        # disconnected
        last_lp = config.num_lp
        if last_lp in config.lp_split_locations:  # connect splitter to outlet
            self.lp_to_outlet_stream = Arc(
                source=self.lp_split[last_lp].outlet_1,
                destination=self.outlet_stage.inlet,
            )
        else:  # connect last lpstage to outlet
            self.lp_to_outlet_stream = Arc(
                source=self.lp_stages[last_lp].outlet,
                destination=self.outlet_stage.inlet,
            )
        pyo.TransformationFactory("network.expand_arcs").apply_to(self)

    def _split_cfg(self, unit_cfg, no=2):
        """
        This creates a configuration dictionary for a splitter.

        Args:
            unit_cfg: The base unit config dict.
            no: Number of outlets, default=2
        """
        # Create a dict for splitter config args
        cfg = copy.copy(unit_cfg)
        cfg.update(num_outlets=no)
        return cfg

    def _mix_cfg(self, unit_cfg, ni=2):
        """
        This creates a configuration dictionary for a mixer.

        Args:
            unit_cfg: The base unit config dict.
            ni: Number of inlets, default=2
        """
        cfg = copy.copy(unit_cfg)
        cfg.update(
            num_inlets=ni,
            momentum_mixing_type=MomentumMixingType.minimize_and_equality)
        return cfg

    def throttle_cv_fix(self, value):
        """
        Fix the thottle valve coefficients.  These are generally the same for
        each of the parallel stages so this provides a convenient way to set
        them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        for i in self.throttle_valve:
            self.throttle_valve[i].Cv.fix(value)

    def turbine_inlet_cf_fix(self, value):
        """
        Fix the inlet turbine stage flow coefficient.  These are
        generally the same for each of the parallel stages so this provides
        a convenient way to set them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        for i in self.inlet_stage:
            self.inlet_stage[i].flow_coeff.fix(value)

    def _init_section(
        self,
        stages,
        splits,
        disconnects,
        prev_port,
        outlvl,
        solver,
        optarg,
        copy_disconneted_flow,
        copy_disconneted_pressure,
    ):
        """ Reuse the initializtion for HP, IP and, LP sections.
        """
        if 0 in splits:
            copy_port(splits[0].inlet, prev_port)
            splits[0].initialize(outlvl=outlvl, solver=solver, optarg=optarg)
            prev_port = splits[0].outlet_1
        for i in stages:
            if i - 1 not in disconnects:
                copy_port(stages[i].inlet, prev_port)
            else:
                if copy_disconneted_flow:
                    for t in stages[i].inlet.flow_mol:
                        stages[i].inlet.flow_mol[t] = pyo.value(
                            prev_port.flow_mol[t])
                if copy_disconneted_pressure:
                    for t in stages[i].inlet.pressure:
                        stages[i].inlet.pressure[t] = pyo.value(
                            prev_port.pressure[t])
            stages[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg)
            prev_port = stages[i].outlet
            if i in splits:
                copy_port(splits[i].inlet, prev_port)
                splits[i].initialize(outlvl=outlvl,
                                     solver=solver,
                                     optarg=optarg)
                prev_port = splits[i].outlet_1
        return prev_port

    def turbine_outlet_cf_fix(self, value):
        """
        Fix the inlet turbine stage flow coefficient.  These are
        generally the same for each of the parallel stages so this provides
        a convenient way to set them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        self.outlet_stage.flow_coeff.fix(value)

    def initialize(self,
                   outlvl=idaeslog.NOTSET,
                   solver="ipopt",
                   optarg={
                       "tol": 1e-6,
                       "max_iter": 35
                   },
                   copy_disconneted_flow=True,
                   copy_disconneted_pressure=True,
                   calculate_outlet_cf=False,
                   calculate_inlet_cf=False):
        """
        Initialize

        Args:
            outlvl: logging level default is NOTSET, which inherits from the
                parent logger
            solver: the NL solver, default is "ipopt"
            optarg: solver arguments, default is {"tol": 1e-6, "max_iter": 35}
            copy_disconneted_flow: Copy the flow through the disconnected stages
                default is True
            copy_disconneted_pressure: Copy the pressure through the disconnected
                stages default is True
            calculate_outlet_cf: Use the flow initial flow guess to calculate
                the outlet stage flow coefficient, default is False,
            calculate_inlet_cf: Use the inlet stage ratioP to calculate the flow
                coefficent for the inlet stage default is False

        Returns:
            None
        """
        # Setup loggers
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")
        # Store initial model specs, restored at the end of initializtion, so
        # the problem is not altered.  This can restore fixed/free vars,
        # active/inactive constraints, and fixed variable values.
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # Assume the flow into the turbine is a reasonable guess for
        # initializtion
        flow_guess = self.inlet_split.inlet.flow_mol[0].value

        for it_count in range(2):
            self.inlet_split.initialize(outlvl=outlvl,
                                        solver=solver,
                                        optarg=optarg)

            # Initialize valves
            for i in self.inlet_stage_idx:
                u = self.throttle_valve[i]
                copy_port(u.inlet,
                          getattr(self.inlet_split, "outlet_{}".format(i)))
                u.initialize(outlvl=outlvl, solver=solver, optarg=optarg)

            # Initialize turbine
            for i in self.inlet_stage_idx:
                u = self.inlet_stage[i]
                copy_port(u.inlet, self.throttle_valve[i].outlet)
                u.initialize(outlvl=outlvl,
                             solver=solver,
                             optarg=optarg,
                             calculate_cf=calculate_inlet_cf)

            # Initialize Mixer
            self.inlet_mix.use_minimum_inlet_pressure_constraint()
            for i in self.inlet_stage_idx:
                copy_port(
                    getattr(self.inlet_mix, "inlet_{}".format(i)),
                    self.inlet_stage[i].outlet,
                )
                getattr(self.inlet_mix, "inlet_{}".format(i)).fix()
            self.inlet_mix.initialize(outlvl=outlvl,
                                      solver=solver,
                                      optarg=optarg)
            for i in self.inlet_stage_idx:
                getattr(self.inlet_mix, "inlet_{}".format(i)).unfix()
            self.inlet_mix.use_equal_pressure_constraint()

            prev_port = self.inlet_mix.outlet
            prev_port = self._init_section(
                self.hp_stages,
                self.hp_split,
                self.config.hp_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )
            if len(self.hp_stages) in self.config.hp_disconnect:
                self.config.ip_disconnect.append(0)
            prev_port = self._init_section(
                self.ip_stages,
                self.ip_split,
                self.config.ip_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )
            if len(self.ip_stages) in self.config.ip_disconnect:
                self.config.lp_disconnect.append(0)
            prev_port = self._init_section(
                self.lp_stages,
                self.lp_split,
                self.config.lp_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )

            copy_port(self.outlet_stage.inlet, prev_port)
            self.outlet_stage.initialize(outlvl=outlvl,
                                         solver=solver,
                                         optarg=optarg,
                                         calculate_cf=calculate_outlet_cf)
            if calculate_outlet_cf:
                break
            for t in self.inlet_split.inlet.flow_mol:
                self.inlet_split.inlet.flow_mol[t].value = \
                    self.outlet_stage.inlet.flow_mol[t].value

        if calculate_inlet_cf:
            # cf was probably fixed, so will have to set the value agian here
            # if you ask for it to be calculated.
            icf = {}
            for i in self.inlet_stage:
                for t in self.inlet_stage[i].flow_coeff:
                    icf[i, t] = pyo.value(self.inlet_stage[i].flow_coeff[t])
        if calculate_outlet_cf:
            ocf = pyo.value(self.outlet_stage.flow_coeff)

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

        if calculate_inlet_cf:
            # cf was probably fixed, so will have to set the value agian here
            # if you ask for it to be calculated.
            for t in self.inlet_stage[i].flow_coeff:
                for i in self.inlet_stage:
                    self.inlet_stage[i].flow_coeff[t] = icf[i, t]
        if calculate_outlet_cf:
            self.outlet_stage.flow_coeff = ocf
コード例 #14
0
class FlashData(UnitModelBlockData):
    """
    Standard Flash Unit Model Class
    """
    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. Flash units do not support dynamic behavior."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
**default** - False. Flash units do not have defined volume, thus
this must be False."""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**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=True,
            domain=In([True, False]),
            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=True,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - True.
**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.}"""))

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

        Args:
            None

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

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            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
            })

        self.control_volume.add_state_blocks(has_phase_equilibrium=True)

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

        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()

        split_map = {}
        for p in self.config.property_package.phase_list:
            split_map[p] = p

        self.split = Separator(
            default={
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "outlet_list": ["Vap", "Liq"],
                "split_basis": SplittingType.phaseFlow,
                "ideal_separation": True,
                "ideal_split_map": split_map,
                "mixed_state_block": self.control_volume.properties_out
            })

        self.vap_outlet = Port(extends=self.split.Vap)
        self.liq_outlet = Port(extends=self.split.Liq)

        # Add references
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != 'none'):
            add_object_reference(self, "heat_duty", self.control_volume.heat)
        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            add_object_reference(self, "deltaP", self.control_volume.deltaP)
コード例 #15
0
ファイル: plug_flow_reactor.py プロジェクト: sel454/idaes-pse
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=In([True, False]),
            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=In([True, False]),
            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=In([True, False]),
            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=In([True, False]),
            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=In([True, False]),
            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=list_of_floats,
            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().config.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)
コード例 #16
0
ファイル: fixed_bed_0D.py プロジェクト: eslickj/idaes-pse
class FixedBed0DData(UnitModelBlockData):
    """
    0D Fixed Bed Reactor Model Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([True]),
                    default=True,
                    description="Dynamic model flag - must be True",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = True. Fixed beds must be dynamic."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=True,
            domain=In([True]),
            description="Holdup construction flag - must be True",
            doc="""Indicates whether holdup terms should be constructed or not.
**default** - True. Fixed bed reactors must be dynamic, thus this must be
True."""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single 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(
        "gas_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for gas phase",
            doc="""Property parameter object used to define property calculations
for the gas phase, **default** - None.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "gas_property_package_args",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Arguments to use for constructing gas phase "
            "property packages",
            doc="""A ConfigBlock with arguments to be passed to a gas phase
property block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "solid_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for solid phase",
            doc="""Property parameter object used to define property calculations
for the solid phase, **default** - None.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "solid_property_package_args",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Arguments to use for constructing solid phase "
            "property packages",
            doc="""A ConfigBlock with arguments to be passed to a solid phase
property block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "reaction_package",
        ConfigValue(
            default=None,
            # Removed domain argument due to limitations in ReactionBase for
            # heterogeneous systems
            description="Reaction package to use for unit",
            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.}"""))

    def build(self):
        # Call UnitModel.build to setup dynamics
        super().build()

        # Get units meta data from property packages (only solid needed)
        units_meta_solid = \
            self.config.solid_property_package.get_metadata().get_derived_units

        # Build Gas Phase StateBlock
        # This block only needed so gas conc. to rxn block is calculated
        # Defined state is set to True so that the "sum(mole_frac)=1" eqn in
        # the gas state block is deactivated.
        self.gas = self.config.gas_property_package.state_block_class(
            self.flowsheet().time,
            default={
                "parameters": self.config.gas_property_package,
                "defined_state": True
            })

        # Build Solid Phase StateBlock
        # As there is no solid flow, only need a single state block
        # Defined state is set to True so that the "sum(mass_frac)=1" eqn in
        # the solid state block is deactivated. This is done here as there is
        # currently no way to deactivate the constraint at the initial time
        # for batch systems (i.e. no inlet or outlet ports).
        # The "sum(mass_frac)=1 for all t neq 0" eqn is written in the
        # unit model instead
        self.solids = self.config.solid_property_package.state_block_class(
            self.flowsheet().time,
            default={
                "parameters": self.config.solid_property_package,
                "defined_state": True
            })

        tmp_dict = dict(**self.config.reaction_package_args)
        tmp_dict["gas_state_block"] = self.gas
        tmp_dict["solid_state_block"] = self.solids
        tmp_dict["parameters"] = (self.config.reaction_package)
        self.reactions = (self.config.reaction_package.reaction_block_class(
            self.flowsheet().time,
            doc="Reaction properties in control volume",
            default=tmp_dict))

        # Volume of reactor
        self.bed_diameter = Var(initialize=1,
                                doc='Reactor diameter',
                                units=units_meta_solid('length'))
        self.bed_height = Var(initialize=1,
                              doc='Bed length',
                              units=units_meta_solid('length'))
        self.volume_bed = Var(initialize=1.0,
                              doc="Volume of the reactor bed",
                              units=units_meta_solid('volume'))

        @self.Constraint(doc="Calculating volume of the reactor bed")
        def volume_bed_constraint(b):
            return b.volume_bed == (constants.pi * b.bed_height *
                                    (0.5 * b.bed_diameter)**2)

        # Solid phase material balance
        # Volume of solid
        self.volume_solid = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc="Solids phase volume including particle pores",
            units=units_meta_solid('volume'))

        @self.Constraint(self.flowsheet().config.time,
                         doc="Calculating solid phase volume")
        def volume_solid_constraint(b, t):
            return b.volume_solid[t] == (b.volume_bed *
                                         (1 - b.solids[t]._params.voidage))

        # Accumulation equal to mass transfer/reaction
        self.solids_material_holdup = Var(
            self.flowsheet().time,
            self.config.solid_property_package.component_list,
            initialize=1,
            doc="Solid phase component holdups",
            units=units_meta_solid('mass'))

        @self.Constraint(self.flowsheet().config.time,
                         self.config.solid_property_package.component_list,
                         doc="Solid phase material holdup constraints")
        def solids_material_holdup_constraints(b, t, j):
            return b.solids_material_holdup[t, j] == (
                b.volume_solid[t] *
                b.solids[t].get_material_density_terms("Sol", j))

        self.solids_material_accumulation = DerivativeVar(
            self.solids_material_holdup,
            wrt=self.flowsheet().config.time,
            doc="Solids material accumulation",
            units=units_meta_solid('mass'))
        # should be mass/time, to get around DAE limitations var is
        # divided by time units when used

        @self.Constraint(self.flowsheet().config.time,
                         self.config.solid_property_package.component_list,
                         doc="Solid phase material accumulation constraints")
        def solids_material_accumulation_constraints(b, t, j):
            return (
                b.solids_material_accumulation[t, j] /
                units_meta_solid('time') == b.volume_solid[t] *
                b.solids[t]._params.mw_comp[j] *
                sum(b.reactions[t].reaction_rate[r] * b.config.
                    reaction_package.rate_reaction_stoichiometry[r, "Sol", j]
                    for r in b.config.reaction_package.rate_reaction_idx))

        # Add solid mass variable and constraint for TGA tracking
        self.mass_solids = Var(self.flowsheet().config.time,
                               doc="Total mass of solids",
                               units=units_meta_solid('mass'))

        @self.Constraint(self.flowsheet().config.time,
                         doc="Calculating total mass of solids")
        def mass_solids_constraint(b, t):
            return b.mass_solids[t] == b.volume_solid[t] * sum(
                b.solids[t].get_material_density_terms("Sol", j)
                for j in b.config.solid_property_package.component_list)

        # Sum of mass fractions at all time equals 1
        @self.Constraint(self.flowsheet().config.time,
                         doc="Sum of mass fractions at all time")
        def sum_component_constraint(b, t):
            if t == b.flowsheet().config.time.first():
                return Constraint.Skip
            else:
                return 1 == sum(b.solids[t].mass_frac_comp[j]
                                for j in b.solids[t]._params.component_list)

        if self.config.energy_balance_type != EnergyBalanceType.none:
            # Solid phase energy balance
            # Accumulation equal to heat transfer
            self.solids_energy_holdup = Var(self.flowsheet().time,
                                            initialize=1,
                                            doc="Solid phase energy holdup",
                                            units=units_meta_solid('energy'))

            @self.Constraint(self.flowsheet().config.time,
                             doc="Solid phase energy holdup constraints")
            def solids_energy_holdup_constraints(b, t):
                return b.solids_energy_holdup[t] == (
                    self.volume_solid[t] * pyunits.convert(
                        b.solids[t].get_energy_density_terms("Sol"),
                        to_units=units_meta_solid('energy') /
                        units_meta_solid('volume')))

            self.solids_energy_accumulation = DerivativeVar(
                self.solids_energy_holdup,
                wrt=self.flowsheet().config.time,
                doc="Solids energy accumulation",
                units=units_meta_solid('energy'))
            # should be energy/time, to get around DAE limitations var is
            # divided by time units when used

            @self.Constraint(self.flowsheet().config.time,
                             doc="Solid phase energy accumulation constraints")
            def solids_energy_accumulation_constraints(b, t):
                return (
                    b.solids_energy_accumulation[t] /
                    units_meta_solid('time') == -sum(
                        b.reactions[t].reaction_rate[r] * b.volume_solid[t] *
                        pyunits.convert(b.reactions[t].dh_rxn[r],
                                        to_units=units_meta_solid(
                                            'energy_mole'))
                        for r in b.config.reaction_package.rate_reaction_idx))

        if self.config.energy_balance_type == EnergyBalanceType.none:
            # Fix solids temperature to initial value for isothermal conditions
            @self.Constraint(self.flowsheet().config.time,
                             doc="Isothermal solid phase constraint")
            def isothermal_solid_phase(b, t):
                if t == b.flowsheet().config.time.first():
                    return Constraint.Skip
                else:
                    return (b.solids[t].temperature == b.solids[0].temperature)

    def initialize(blk,
                   gas_phase_state_args=None,
                   solid_phase_state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        """
        Initialization routine for FB0D unit.

        Keyword Arguments:
            gas_phase_state_args : a dict of arguments to be passed to the
                        property package(s) to provide an initial state for
                        initialization (see documentation of the specific
                        property package) (default = None).
            solid_phase_state_args : a dict of arguments to be passed to the
                        property package(s) to provide an initial state for
                        initialization (see documentation of the specific
                        property package) (default = None).
            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
        """

        # Set solver options
        init_log = idaeslog.getInitLogger(blk.name, outlvl)
        opt = get_solver(solver, optarg)  # create solver
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize control volume block
        init_log.info('Initialize Thermophysical Properties')

        # Initialize gas_phase block
        gas_phase_flags = blk.gas.initialize(outlvl=outlvl,
                                             optarg=optarg,
                                             solver=solver,
                                             state_args=gas_phase_state_args)

        # Initialize solid_phase properties block
        solid_phase_flags = blk.solids.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=solid_phase_state_args)

        init_log.info('Initialize reaction properties')
        # Initialize reactions
        blk.reactions.initialize(outlvl=outlvl, optarg=optarg, solver=solver)

        init_log.info_high("Initialization Step 1 Complete.")

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.gas.release_state(gas_phase_flags, outlvl)
        blk.solids.release_state(solid_phase_flags, outlvl)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        if hasattr(self, "mass_solids_constraint"):
            for t, c in self.mass_solids_constraint.items():
                iscale.set_scaling_factor(c, 1e2)

        if hasattr(self, "sum_component_constraint"):
            for t, c in self.sum_component_constraint.items():
                iscale.set_scaling_factor(c, 1e2)
コード例 #17
0
class ProductData(UnitModelBlockData):
    """
    Standard Product Block Class
    """

    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. Product blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Product 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 control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "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.}""",
        ),
    )

    def build(self):
        """
        Begin building model.

        Args:
            None

        Returns:
            None

        """
        # Call UnitModel.build to setup dynamics
        super(ProductData, self).build()

        # Add State Block
        self.properties = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            doc="Material properties in product",
            default={
                "defined_state": True,
                "has_phase_equilibrium": False,
                **self.config.property_package_args,
            },
        )

        # Add references to all state vars
        s_vars = self.properties[
            self.flowsheet().config.time.first()].define_state_vars()
        for s in s_vars:
            l_name = s_vars[s].local_name
            if s_vars[s].is_indexed():
                slicer = self.properties[:].component(l_name)[...]
            else:
                slicer = self.properties[:].component(l_name)

            r = Reference(slicer)
            setattr(self, s, r)

        # Add outlet port
        self.add_port(name="inlet", block=self.properties, doc="Inlet Port")

    def initialize(blk,
                   state_args={},
                   outlvl=idaeslog.NOTSET,
                   solver="ipopt",
                   optarg={"tol": 1e-6}):
        """
        This method calls the initialization method of the state block.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) to provide an initial state for
                           initialization (see documentation of the specific
                           property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating which solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        # ---------------------------------------------------------------------
        # Initialize state block
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        blk.properties.initialize(outlvl=outlvl,
                                  optarg=optarg,
                                  solver=solver,
                                  **state_args)
        init_log.info("Initialization Complete.")

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe({"Inlet": self.inlet},
                                             time_point=time_point)
コード例 #18
0
class HeatExchangerWith3StreamsData(UnitModelBlockData):
    """
    Standard Heat Exchanger Unit Model Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([useDefault, True, False]),
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare(
        "side_1_property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "side_1_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "side_2_property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "side_2_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "side_3_property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "side_3_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**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=In([True, False]),
            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=In([True, False]),
            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(
        "flow_type_side_2",
        ConfigValue(
            default='counter-current',
            domain=In(['counter-current', 'con-current']),
            description="Flow configuration in unit",
            doc="""Flag indicating type of flow arrangement to use for heat
             exchanger (default = 'counter-current')
                - 'counter-current' : counter-current flow arrangement"""))
    CONFIG.declare(
        "flow_type_side_3",
        ConfigValue(
            default='counter-current',
            domain=In(['counter-current', 'con-current']),
            description="Flow configuration in unit",
            doc="""Flag indicating type of flow arrangement to use for heat
             exchanger (default = 'counter-current')
                - 'counter-current' : counter-current flow arrangement"""))

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

        Args:
            None

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

        # Build Holdup Block
        self.side_1 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_1_property_package,
                "property_package_args":
                self.config.side_1_property_package_args
            })

        self.side_2 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_2_property_package,
                "property_package_args":
                self.config.side_2_property_package_args
            })

        self.side_3 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_3_property_package,
                "property_package_args":
                self.config.side_3_property_package_args
            })

        # Add Geometry
        self.side_1.add_geometry()
        self.side_2.add_geometry()
        self.side_3.add_geometry()

        # Add state block
        self.side_1.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_1.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_1.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_1.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add state block
        self.side_2.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_2.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_2.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_2.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add state block
        self.side_3.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_3.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_3.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_3.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        self._set_geometry()

        # Construct performance equations
        self._make_performance()

        # Construct performance equations
        if self.config.flow_type_side_2 == "counter-current":
            self._make_counter_current_side_2()
        else:
            self._make_con_current_side_2()

        # Construct performance equations
        if self.config.flow_type_side_3 == "counter-current":
            self._make_counter_current_side_3()
        else:
            self._make_con_current_side_3()

        self.add_inlet_port(name="side_1_inlet", block=self.side_1)
        self.add_inlet_port(name="side_2_inlet", block=self.side_2)
        self.add_inlet_port(name="side_3_inlet", block=self.side_3)
        self.add_outlet_port(name="side_1_outlet", block=self.side_1)
        self.add_outlet_port(name="side_2_outlet", block=self.side_2)
        self.add_outlet_port(name="side_3_outlet", block=self.side_3)

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary, and link to holdup volume

        Args:
            None

        Returns:
            None
        """

        # UA (product of overall heat transfer coefficient and area) between side 1 and side 2
        self.ua_side_2 = Var(self.flowsheet().config.time,
                             initialize=10.0,
                             doc='UA between side 1 and side 2')

        # UA (product of overall heat transfer coefficient and area) between side 1 and side 3
        self.ua_side_3 = Var(self.flowsheet().config.time,
                             initialize=10.0,
                             doc='UA between side 1 and side 3')

        # fraction of heat from hot stream as heat loss to ambient
        self.frac_heatloss = Var(initialize=0.05,
                                 doc='fraction of heat loss to ambient')

        if self.config.has_holdup is True:
            add_object_reference(self, "volume_side_1", self.side_1.volume)
            add_object_reference(self, "volume_side_2", self.side_2.volume)
            add_object_reference(self, "volume_side_3", self.side_3.volume)

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.

        Args:
            None

        Returns:
            None
        """
        # Set references to balance terms at unit level
        add_object_reference(self, "heat_duty_side_1", self.side_1.heat)
        add_object_reference(self, "heat_duty_side_2", self.side_2.heat)
        add_object_reference(self, "heat_duty_side_3", self.side_3.heat)
        if self.config.has_pressure_change is True:
            add_object_reference(self, "deltaP_side_1", self.side_1.deltaP)
            add_object_reference(self, "deltaP_side_2", self.side_2.deltaP)
            add_object_reference(self, "deltaP_side_3", self.side_3.deltaP)

        # Performance parameters and variables

        # Temperature driving force
        self.temperature_driving_force_side_2 = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Mean driving force for heat exchange')

        # Temperature driving force
        self.temperature_driving_force_side_3 = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Mean driving force for heat exchange')

        # Temperature difference at side 2 inlet
        self.side_2_inlet_dT = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Temperature difference at side 2 inlet')

        # Temperature difference at side 2 outlet
        self.side_2_outlet_dT = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Temperature difference at side 2 outlet')

        # Temperature difference at side 3 inlet
        self.side_3_inlet_dT = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Temperature difference at side 3 inlet')

        # Temperature difference at side 3 outlet
        self.side_3_outlet_dT = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Temperature difference at side 3 outlet')

        # Driving force side 2
        @self.Constraint(self.flowsheet().config.time,
                         doc="Log mean temperature difference calculation")
        def LMTD_side_2(b, t):
            return b.temperature_driving_force_side_2[t] == (
                (b.side_2_inlet_dT[t]**0.3241 + b.side_2_outlet_dT[t]**0.3241)
                / 1.99996)**(1 / 0.3241)

        # Driving force side 3
        @self.Constraint(self.flowsheet().config.time,
                         doc="Log mean temperature difference calculation")
        def LMTD_side_3(b, t):
            return b.temperature_driving_force_side_3[t] == (
                (b.side_3_inlet_dT[t]**0.3241 + b.side_3_outlet_dT[t]**0.3241)
                / 1.99996)**(1 / 0.3241)

        # Heat duty side 2
        @self.Constraint(self.flowsheet().config.time,
                         doc="Heat transfer rate")
        def heat_duty_side_2_eqn(b, t):
            return b.heat_duty_side_2[t] == (
                b.ua_side_2[t] * b.temperature_driving_force_side_2[t])

        # Heat duty side 3
        @self.Constraint(self.flowsheet().config.time,
                         doc="Heat transfer rate")
        def heat_duty_side_3_eqn(b, t):
            return b.heat_duty_side_3[t] == (
                b.ua_side_3[t] * b.temperature_driving_force_side_3[t])

        # Energy balance equation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Energy balance between two sides")
        def heat_duty_side_1_eqn(b, t):
            return -b.heat_duty_side_1[t] * (1 - b.frac_heatloss) == (
                b.heat_duty_side_2[t] + b.heat_duty_side_3[t])

    def _make_con_current_side_2(self):
        """
        Add temperature driving force Constraints for counter-current flow.

        Args:
            None

        Returns:
            None
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 inlet temperature difference")
        def side_2_inlet_dT_eqn(b, t):
            return b.side_2_inlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_2.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 outlet temperature difference")
        def side_2_outlet_dT_eqn(b, t):
            return b.side_2_outlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_2.properties_out[t].temperature)

    def _make_counter_current_side_2(self):
        """
        Add temperature driving force Constraints for counter-current flow.

        Args:
            None

        Returns:
            None
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 inlet temperature difference")
        def side_2_inlet_dT_eqn(b, t):
            return b.side_2_inlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_2.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 outlet temperature difference")
        def side_2_outlet_dT_eqn(b, t):
            return b.side_2_outlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_2.properties_out[t].temperature)

    def _make_con_current_side_3(self):
        """
        Add temperature driving force Constraints for counter-current flow.

        Args:
            None

        Returns:
            None
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 inlet temperature difference")
        def side_3_inlet_dT_eqn(b, t):
            return b.side_3_inlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_3.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 outlet temperature difference")
        def side_3_outlet_dT_eqn(b, t):
            return b.side_3_outlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_3.properties_out[t].temperature)

    def _make_counter_current_side_3(self):
        """
        Add temperature driving force Constraints for counter-current flow.

        Args:
            None

        Returns:
            None
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 inlet temperature difference")
        def side_3_inlet_dT_eqn(b, t):
            return b.side_3_inlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_3.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 outlet temperature difference")
        def side_3_outlet_dT_eqn(b, t):
            return b.side_3_outlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_3.properties_out[t].temperature)

    def set_initial_condition(self):
        pass

    def initialize(blk,
                   state_args_1=None,
                   state_args_2=None,
                   state_args_3=None,
                   outlvl=4,
                   solver='ipopt',
                   optarg={'tol': 1e-6}):
        '''
        General Heat Exchanger initialisation routine.

        Keyword Arguments:
            state_args_1 : a dict of arguments to be passed to the property
                           package(s) for side 1 of the heat exchanger to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            state_args_2 : a dict of arguments to be passed to the property
                           package(s) for side 2 of the heat exchanger to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            state_args_3 : a dict of arguments to be passed to the property
                           package(s) for side 3 of the heat exchanger to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        opt = SolverFactory(solver)
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize inlet property blocks
        flags1 = blk.side_1.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_1)

        flags2 = blk.side_2.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_2)

        flags3 = blk.side_3.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_3)

        init_log.info('Initialisation Step 1 Complete.')

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info("Initialization Step 2 Complete: {}".format(
            idaeslog.condition(res)))
        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.side_1.release_state(flags1, outlvl + 1)
        blk.side_2.release_state(flags2, outlvl + 1)
        blk.side_3.release_state(flags3, outlvl + 1)

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

    def calculate_scaling_factors(self):
        for t, c in self.heat_duty_side_1_eqn.items():
            sf = iscale.get_scaling_factor(self.heat_duty_side_1[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.heat_duty_side_2_eqn.items():
            sf = iscale.get_scaling_factor(self.heat_duty_side_2[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.heat_duty_side_3_eqn.items():
            sf = iscale.get_scaling_factor(self.heat_duty_side_3[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)
コード例 #19
0
class EquilibriumReactorData(UnitModelBlockData):
    """
    Standard Equilibrium Reactor Unit Model Class
    """
    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. Equilibrium Reactors do not support dynamic behavior."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
**default** - False. Equilibrium reactors do not have defined volume, thus
this must be False."""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single 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_rate_reactions",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Rate reaction construction flag",
            doc="""Indicates whether terms for rate controlled reactions
should be constructed, along with constraints equating these to zero,
**default** - True.
**Valid values:** {
**True** - include rate reaction terms,
**False** - exclude rate reaction terms.}"""))
    CONFIG.declare(
        "has_equilibrium_reactions",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            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=In([True, False]),
            description="Phase equilibrium term construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** - True.
**Valid values:** {
**True** - include phase equilibrium term,
**False** - exclude phase equlibirum terms.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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_heat_of_reaction",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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,
**PhysicalParameterObject** - a PhysicalParameterBlock 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.}"""))

    def build(self):
        """
        Begin building model.

        Args:
            None

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

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            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
            })

        # No need for control volume geometry

        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=self.config.has_rate_reactions,
            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)

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

        if self.config.has_rate_reactions:
            # Add equilibrium reactor performance equation
            @self.Constraint(self.flowsheet().config.time,
                             self.config.reaction_package.rate_reaction_idx,
                             doc="Rate reaction equilibrium constraint")
            def rate_reaction_constraint(b, t, r):
                # Set kinetic reaction rates to zero
                return b.control_volume.reactions[t].reaction_rate[r] == 0

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            add_object_reference(self, "heat_duty", self.control_volume.heat)

        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            add_object_reference(self, "deltaP", self.control_volume.deltaP)
コード例 #20
0
class SolventCondenserData(UnitModelBlockData):
    """
    Condenser unit for solvent column models using separate property packages
    for liquid and vpor phases.

    Unit model to condense the vapor from the top of a solvent column.
    """
    CONFIG = ConfigBlock()
    # TOOO: Add dynamics in future
    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. Equilibrium Reactors do not support dynamic behavior."""))
    CONFIG.declare("has_holdup", ConfigValue(
        default=False,
        domain=In([False]),
        description="Holdup construction flag - must be False",
        doc="""Indicates whether holdup terms should be constructed or not.
**default** - False. Equilibrium reactors do not have defined volume, thus
this must be False."""))
    # TODO : Add boilup ratio back later if needed
    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.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.useDefault,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**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_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("liquid_property_package", ConfigValue(
        default=useDefault,
        domain=is_physical_parameter_block,
        description="Property package to use for liquid phase",
        doc="""Property parameter object used to define property calculations
for the liquid phase,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare("liquid_property_package_args", ConfigBlock(
        implicit=True,
        description="Arguments to use for constructing liquid phase properties",
        doc="""A ConfigBlock with arguments to be passed to liquid phase
property block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare("vapor_property_package", ConfigValue(
        default=useDefault,
        domain=is_physical_parameter_block,
        description="Property package to use for vapor phase",
        doc="""Property parameter object used to define property calculations
for the vapor phase,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare("vapor_property_package_args", ConfigBlock(
        implicit=True,
        description="Arguments to use for constructing vapor phase properties",
        doc="""A ConfigBlock with arguments to be passed to vapor phase
property block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))

    def build(self):
        """Build the model.

        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().build()

        # Check phase lists match assumptions
        if self.config.vapor_property_package.phase_list != ["Vap"]:
            raise ConfigurationError(
                f"{self.name} SolventCondenser model requires that the vapor "
                f"phase property package have a single phase named 'Vap'")
        if self.config.liquid_property_package.phase_list != ["Liq"]:
            raise ConfigurationError(
                f"{self.name} SolventCondenser model requires that the liquid "
                f"phase property package have a single phase named 'Liq'")

        # Check for at least one common component in component lists
        if not any(j in self.config.vapor_property_package.component_list for
                   j in self.config.liquid_property_package.component_list):
            raise ConfigurationError(
                f"{self.name} SolventCondenser model requires that the liquid "
                f"and vapor phase property packages have at least one "
                f"common component.")

        # ---------------------------------------------------------------------
        # Add Control Volume for the Vapor Phase
        self.vapor_phase = ControlVolume0DBlock(default={
            "dynamic": self.config.dynamic,
            "has_holdup": self.config.has_holdup,
            "property_package": self.config.vapor_property_package,
            "property_package_args": self.config.vapor_property_package_args})

        self.vapor_phase.add_state_blocks(
            has_phase_equilibrium=True)

        # Separate liquid and vapor phases means that phase equilibrium will
        # be handled at the unit model level, thus has_phase_equilibrium is
        # False, but has_mass_transfer is True.
        self.vapor_phase.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True,
            has_phase_equilibrium=False)

        # Need to include enthalpy transfer term for the mass transfer
        self.vapor_phase.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=True,
            has_enthalpy_transfer=True)

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

        # ---------------------------------------------------------------------
        # Add single state block for liquid phase
        tmp_dict = dict(**self.config.liquid_property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["defined_state"] = False
        self.liquid_phase = \
            self.config.liquid_property_package.build_state_block(
                self.flowsheet().time,
                doc="Liquid phase properties",
                default=tmp_dict)

        # ---------------------------------------------------------------------
        # Check flow basis is compatable
        # TODO : Could add code to convert flow bases, but not now
        t_init = self.flowsheet().time.first()
        if (self.liquid_phase[t_init].get_material_flow_basis() !=
                self.vapor_phase.properties_out[
                    t_init].get_material_flow_basis()):
            raise ConfigurationError(
                f"{self.name} vapor and liquid property packages must use the "
                f"same material flow basis.")

        # ---------------------------------------------------------------------
        # Add Ports for the condenser
        self.add_inlet_port(
            name="inlet", block=self.vapor_phase, doc="Vapor feed")
        self.add_outlet_port(
            name="vapor_outlet", block=self.vapor_phase, doc="Vapor product")
        self.add_outlet_port(name="reflux",
                             block=self.liquid_phase,
                             doc="Liquid reflux from condenser")

        # ---------------------------------------------------------------------
        # Add unit level constraints
        # First, need the union and intersection of component lists
        all_comps = (self.liquid_phase.component_list |
                     self.vapor_phase.properties_out.component_list)
        common_comps = (self.liquid_phase.component_list &
                        self.vapor_phase.properties_out.component_list)

        # Get units for unit conversion
        vunits = self.config.vapor_property_package.get_metadata(
            ).get_derived_units
        lunits = self.config.liquid_property_package.get_metadata(
            ).get_derived_units
        flow_basis = self.liquid_phase[t_init].get_material_flow_basis()
        if flow_basis == MaterialFlowBasis.molar:
            fb = "flow_mole"
        elif flow_basis == MaterialFlowBasis.molar:
            fb = "flow_mass"
        else:
            raise ConfigurationError(
                f"{self.name} SolventCondenser only supports mass or molar "
                f"basis for MaterialFlowBasis.")

        if any(j not in common_comps for
               j in self.liquid_phase.component_list):
            # We have non-volatile components present, need zero-flow param
            self.zero_flow_param = Param(
                mutable=True,
                default=1e-8,
                units=lunits("flow_mole"))

        # Material balances
        def rule_material_balance(blk, t, j):
            if j in common_comps:
                # Component is in equilibrium
                # Mass transfer equals liquid flowrate
                return (-blk.vapor_phase.mass_transfer_term[t, "Vap", j] ==
                        pyunits.convert(
                            blk.liquid_phase[t].get_material_flow_terms(
                                "Liq", j),
                            to_units=vunits(fb)))
            elif j in self.liquid_phase.component_list:
                # Non-volatile component
                # No mass transfer term
                # Set liquid flowrate to an arbitary small value
                return (blk.liquid_phase[t].get_material_flow_terms(
                    "Liq", j) == blk.zero_flow_param)
            else:
                # Non-condensable comonent
                # Mass transfer term is zero, no vapor flowrate
                return (blk.vapor_phase.mass_transfer_term[t, "Vap", j] ==
                        0*vunits(fb))
        self.unit_material_balance = Constraint(
            self.flowsheet().time,
            all_comps,
            rule=rule_material_balance,
            doc="Unit level material balances")

        # Phase equilibrium constraints
        # For all common components, equate fugacity in vapor and liquid
        def rule_phase_equilibrium(blk, t, j):
            return (
                blk.vapor_phase.properties_out[t].fug_phase_comp[
                    "Vap", j] ==
                pyunits.convert(blk.liquid_phase[t].fug_phase_comp["Liq", j],
                                to_units=vunits("pressure")))
        self.unit_phase_equilibrium = Constraint(
            self.flowsheet().time,
            common_comps,
            rule=rule_phase_equilibrium,
            doc="Unit level phase equilibrium constraints")

        # Temperature equality constraint
        def rule_temperature_balance(blk, t):
            return (blk.vapor_phase.properties_out[t].temperature ==
                    pyunits.convert(blk.liquid_phase[t].temperature,
                                    to_units=vunits("temperature")))
        self.unit_temperature_equality = Constraint(
            self.flowsheet().time,
            rule=rule_temperature_balance,
            doc="Unit level temperature equality")

        # Unit level energy balance
        # Energy leaving in liquid phase must be equal and opposite to enthalpy
        # transfer from vapor phase
        def rule_energy_balance(blk, t):
            return (-blk.vapor_phase.enthalpy_transfer[t] ==
                    pyunits.convert(
                        blk.liquid_phase[t].get_enthalpy_flow_terms("Liq"),
                        to_units=vunits("energy")/lunits("time")))
        self.unit_enthalpy_balance = Constraint(
            self.flowsheet().time,
            rule=rule_energy_balance,
            doc="Unit level enthalpy_balance")

        # Pressure balance constraint
        def rule_pressure_balance(blk, t):
            return (blk.vapor_phase.properties_out[t].pressure ==
                    pyunits.convert(blk.liquid_phase[t].pressure,
                                    to_units=vunits("pressure")))
        self.unit_pressure_balance = Constraint(
            self.flowsheet().time,
            rule=rule_pressure_balance,
            doc="Unit level pressure balance")

        # Set references to balance terms at unit level
        self.heat_duty = Reference(self.vapor_phase.heat[:])

        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.vapor_phase.deltaP[:])

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        common_comps = (self.liquid_phase.component_list &
                        self.vapor_phase.properties_out.component_list)

        for (t, j), v in self.unit_material_balance.items():
            if j in common_comps:
                iscale.constraint_scaling_transform(
                    v,
                    iscale.get_scaling_factor(
                        self.vapor_phase.mass_transfer_term[t, "Vap", j],
                        default=1,
                        warning=True))
            elif j in self.liquid_phase.component_list:
                iscale.constraint_scaling_transform(
                    v,
                    value(1/self.zero_flow_param))
            else:
                pass  # no need to scale this constraint

        for (t, j), v in self.unit_phase_equilibrium.items():
            iscale.constraint_scaling_transform(
                v,
                iscale.get_scaling_factor(
                    self.vapor_phase.properties_out[t].fug_phase_comp[
                        "Vap", j],
                    default=1,
                    warning=True))

        for t, v in self.unit_temperature_equality.items():
            iscale.constraint_scaling_transform(
                v,
                iscale.get_scaling_factor(
                    self.vapor_phase.properties_out[t].temperature,
                    default=1,
                    warning=True))

        for t, v in self.unit_enthalpy_balance.items():
            iscale.constraint_scaling_transform(
                v,
                iscale.get_scaling_factor(
                    self.vapor_phase.enthalpy_transfer[t],
                    default=1,
                    warning=True))

        for t, v in self.unit_pressure_balance.items():
            iscale.constraint_scaling_transform(
                v,
                iscale.get_scaling_factor(
                    self.vapor_phase.properties_out[t].pressure,
                    default=1,
                    warning=True))

    def initialize(blk, liquid_state_args=None, vapor_state_args=None,
                   outlvl=idaeslog.NOTSET, solver=None, optarg=None):
        '''
        Initialization routine for solvent condenser unit model.

        Keyword Arguments:
            liquid_state_args : a dict of arguments to be passed to the
                liquid property package to provide an initial state for
                initialization (see documentation of the specific property
                package) (default = none).
            vapor_state_args : a dict of arguments to be passed to the
                vapor property package to provide an initial state for
                initialization (see documentation of the specific property
                package) (default = none).
            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 IDAES solver)

        Returns:
            None
        '''
        if optarg is None:
            optarg = {}

        # Check DOF
        if degrees_of_freedom(blk) != 0:
            raise InitializationError(
                f"{blk.name} degrees of freedom were not 0 at the beginning "
                f"of initialization. DoF = {degrees_of_freedom(blk)}")

        # Set solver options
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        solverobj = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Initialize liquid phase control volume block
        flags = blk.vapor_phase.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=vapor_state_args,
            hold_state=True
        )

        init_log.info_high('Initialization Step 1 Complete.')
        # ---------------------------------------------------------------------
        # Initialize liquid phase state block
        if liquid_state_args is None:
            t_init = blk.flowsheet().time.first()
            liquid_state_args = {}
            liq_state_vars = blk.liquid_phase[t_init].define_state_vars()

            vap_state = blk.vapor_phase.properties_out[t_init]

            # Check for unindexed state variables
            for sv in liq_state_vars:
                if "flow" in sv:
                    # Flow varaible, assume 10% condensation
                    if "phase_comp" in sv:
                        # Flow is indexed by phase and component
                        liquid_state_args[sv] = {}
                        for p, j in liq_state_vars[sv]:
                            if j in vap_state.component_list:
                                liquid_state_args[sv][p, j] = 0.1*value(
                                    getattr(vap_state, sv)[p, j])
                            else:
                                liquid_state_args[sv][p, j] = 1e-8
                    elif "comp" in sv:
                        # Flow is indexed by component
                        liquid_state_args[sv] = {}
                        for j in liq_state_vars[sv]:
                            if j in vap_state.component_list:
                                liquid_state_args[sv][j] = 0.1*value(
                                    getattr(vap_state, sv)[j])
                            else:
                                liquid_state_args[sv][j] = 1e-8
                    elif "phase" in sv:
                        # Flow is indexed by phase
                        liquid_state_args[sv] = {}
                        for p in liq_state_vars[sv]:
                            liquid_state_args[sv][p] = 0.1*value(
                                    getattr(vap_state, sv)["Vap"])
                    else:
                        liquid_state_args[sv] = 0.1*value(
                            getattr(vap_state, sv))
                elif "mole_frac" in sv:
                    liquid_state_args[sv] = {}
                    if "phase" in sv:
                        # Variable is indexed by phase and component
                        for p, j in liq_state_vars[sv].keys():
                            if j in vap_state.component_list:
                                liquid_state_args[sv][p, j] = value(
                                    vap_state.fug_phase_comp["Vap", j] /
                                    vap_state.pressure)
                            else:
                                liquid_state_args[sv][p, j] = 1e-8
                    else:
                        for j in liq_state_vars[sv].keys():
                            if j in vap_state.component_list:
                                liquid_state_args[sv][j] = value(
                                    vap_state.fug_phase_comp["Vap", j] /
                                    vap_state.pressure)
                            else:
                                liquid_state_args[sv][j] = 1e-8
                else:
                    liquid_state_args[sv] = value(
                        getattr(vap_state, sv))

        blk.liquid_phase.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=liquid_state_args,
            hold_state=False
        )

        init_log.info_high('Initialization Step 2 Complete.')
        # ---------------------------------------------------------------------
        # Solve unit model
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            results = solverobj.solve(blk, tee=slc.tee)

        init_log.info_high(
            "Initialization Step 3 {}.".format(idaeslog.condition(results))
        )

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.vapor_phase.release_state(flags, outlvl)

        # TODO : This fails in the current model
        # if not check_optimal_termination(results):
        #     raise InitializationError(
        #         f"{blk.name} failed to initialize successfully. Please check "
        #         f"the output logs for more information.")

        init_log.info('Initialization Complete: {}'
                      .format(idaeslog.condition(results)))
コード例 #21
0
class DowncomerData(UnitModelBlockData):
    """
    Downcomer Unit Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare("dynamic", ConfigValue(
        domain=In([useDefault, True, False]),
        default=useDefault,
        description="Dynamic model flag",
        doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare("has_holdup", ConfigValue(
        default=False,
        domain=In([True, False]),
        description="Holdup construction flag",
        doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare("material_balance_type", ConfigValue(
        default=MaterialBalanceType.componentPhase,
        domain=In(MaterialBalanceType),
        description="Material balance construction flag",
        doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.enthalpyTotal,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare("momentum_balance_type", ConfigValue(
        default=MomentumBalanceType.pressureTotal,
        domain=In(MomentumBalanceType),
        description="Momentum balance construction flag",
        doc="""Indicates what type of momentum balance should be constructed,
**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=In([True, False]),
        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("property_package", ConfigValue(
        default=useDefault,
        domain=is_physical_parameter_block,
        description="Property package to use for control volume",
        doc="""Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare("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.}"""))

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


        Args:
            None

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

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(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})

        self.control_volume.add_geometry()
        # no phase transitions in the unit - handeled by Helmholtz EoS
        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type)

        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=True)

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

        # Add object references
        self.volume = Reference(self.control_volume.volume)

        # 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)

        self.deltaP = Reference(self.control_volume.deltaP)

        # Set Unit Geometry and Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary
        """
        units_meta = self.config.property_package.get_metadata()

        # Number of downcomers
        self.number_downcomers = Var(
                initialize=4,
                doc="Number of downcomers for the boiler")
        # Height of downcomer
        self.height = Var(
                initialize=10.0,
                doc="Height of downcomer",
                units=units_meta.get_derived_units("length"))
        # Inside diameter of downcomer
        self.diameter = Var(
                initialize=0.6,
                doc="Inside diameter of downcomer",
                units=units_meta.get_derived_units("length"))
        # Volume constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="Downcomer volume of all pipes")
        def volume_eqn(b, t):
            return b.volume[t] == 0.25*const.pi*b.diameter**2*b.height \
                * b.number_downcomers

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        units_meta = self.config.property_package.get_metadata()

        # Add performance variables
        # Velocity of fluid inside downcomer pipe
        self.velocity = Var(
                self.flowsheet().config.time,
                initialize=10.0,
                doc='Liquid water velocity inside downcomer',
                units=units_meta.get_derived_units("velocity"))

        # Reynolds number
        self.N_Re = Var(
                self.flowsheet().config.time,
                initialize=10000.0,
                doc='Reynolds number')

        # Darcy friction factor (turbulent flow)
        self.friction_factor_darcy = Var(
                self.flowsheet().config.time,
                initialize=0.005,
                doc='Darcy friction factor')

        # Pressure change due to friction
        self.deltaP_friction = Var(
                self.flowsheet().config.time,
                initialize=-1.0,
                doc='Pressure change due to friction',
                units=units_meta.get_derived_units("pressure"))

        # Pressure change due to gravity
        self.deltaP_gravity = Var(
                self.flowsheet().config.time,
                initialize=100.0,
                doc='Pressure change due to gravity',
                units=units_meta.get_derived_units("pressure"))

        # Equation for calculating velocity
        @self.Constraint(self.flowsheet().config.time,
                         doc="Velocity of fluid inside downcomer")
        def velocity_eqn(b, t):
            return b.velocity[t]*0.25*const.pi*b.diameter**2 \
                * b.number_downcomers \
                == b.control_volume.properties_in[t].flow_vol

        # Equation for calculating Reynolds number
        @self.Constraint(self.flowsheet().config.time, doc="Reynolds number")
        def Reynolds_number_eqn(b, t):
            return b.N_Re[t] * \
                   b.control_volume.properties_in[t].visc_d_phase["Liq"] == \
                   b.diameter * b.velocity[t] *\
                   b.control_volume.properties_in[t].dens_mass_phase["Liq"]

        # Friction factor expression depending on laminar or turbulent flow
        @self.Constraint(self.flowsheet().config.time,
                         doc="Darcy friction factor as "
                         "a function of Reynolds number")
        def friction_factor_darcy_eqn(b, t):
            return b.friction_factor_darcy[t] * b.N_Re[t]**(0.25) == 0.3164

        # Pressure change equation for friction,
        # -1/2*density*velocity^2*fD/diameter*height
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure change due to friction")
        def pressure_change_friction_eqn(b, t):
            return b.deltaP_friction[t] * b.diameter == -0.5 \
                * b.control_volume.properties_in[t].dens_mass_phase["Liq"] * \
                b.velocity[t]**2 * b.friction_factor_darcy[t] * b.height

        # Pressure change equation for gravity, density*gravity*height
        g_units = units_meta.get_derived_units("acceleration")
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure change due to gravity")
        def pressure_change_gravity_eqn(b, t):
            return b.deltaP_gravity[t] == \
                b.control_volume.properties_in[t].dens_mass_phase["Liq"] \
                * pyunits.convert(const.acceleration_gravity,
                                  to_units=g_units) * b.height

        # Total pressure change equation
        @self.Constraint(self.flowsheet().config.time, doc="Pressure drop")
        def pressure_change_total_eqn(b, t):
            return b.deltaP[t] == (b.deltaP_friction[t] + b.deltaP_gravity[t])

    def set_initial_condition(self):
        if self.config.dynamic is True:
            self.control_volume.material_accumulation[:, :, :].value = 0
            self.control_volume.energy_accumulation[:, :].value = 0
            self.control_volume.material_accumulation[0, :, :].fix(0)
            self.control_volume.energy_accumulation[0, :].fix(0)

    def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET,
                   solver='ipopt', optarg={'tol': 1e-6}):
        '''
        Downcomer initialization routine.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) for the control_volume of the model to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

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

        opt = SolverFactory(solver)
        opt.options = optarg

        init_log.info_low("Starting initialization...")

        flags = blk.control_volume.initialize(
            outlvl=outlvl+1,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # make sure 0 DoF
        if degrees_of_freedom(blk) != 0:
            raise ConfigurationError(
                "Incorrect degrees of freedom when initializing {}: dof = {}".format(
                    blk.name, degrees_of_freedom(blk)))
        # Fix outlet pressure
        for t in blk.flowsheet().config.time:
            blk.control_volume.properties_out[t].pressure.fix(
                value(blk.control_volume.properties_in[t].pressure)
            )
        blk.pressure_change_total_eqn.deactivate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high(
                "Initialization Step 2 {}.".format(idaeslog.condition(res))
            )

        # Unfix outlet enthalpy and pressure
        for t in blk.flowsheet().config.time:
            blk.control_volume.properties_out[t].pressure.unfix()
        blk.pressure_change_total_eqn.activate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high(
                "Initialization Step 3 {}.".format(idaeslog.condition(res))
            )
        blk.control_volume.release_state(flags, outlvl+1)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        # set a default Reynolds number scaling
        for v in self.N_Re.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-4)

        for v in self.friction_factor_darcy.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 100)

        for v in self.deltaP_gravity.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for v in self.deltaP_friction.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for t, c in self.volume_eqn.items():
            sf = iscale.get_scaling_factor(
                self.volume[t], default=1, warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.Reynolds_number_eqn.items():
            sf = iscale.get_scaling_factor(
                self.N_Re[t], default=1, warning=True)
            sf *= iscale.get_scaling_factor(
                self.control_volume.properties_in[t].visc_d_phase["Liq"],
                default=1,
                warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.pressure_change_friction_eqn.items():
            sf = iscale.get_scaling_factor(
                self.deltaP_friction[t], default=1, warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.pressure_change_gravity_eqn.items():
            sf = iscale.get_scaling_factor(
                self.deltaP_gravity[t], default=1, warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.pressure_change_total_eqn.items():
            sf = iscale.get_scaling_factor(
                self.deltaP[t], default=1, warning=True)
            iscale.constraint_scaling_transform(c, sf)
コード例 #22
0
EoS_param = {
    CubicType.PR: {
        'u': 2,
        'w': -1,
        'omegaA': 0.45724,
        'coeff_b': 0.07780
    },
    CubicType.SRK: {
        'u': 1,
        'w': 0,
        'omegaA': 0.42748,
        'coeff_b': 0.08664
    }
}

CubicConfig = ConfigBlock()
CubicConfig.declare(
    "type",
    ConfigValue(domain=In(CubicType),
                description="Equation of state to use",
                doc="Enum indicating type of cubic equation of state to use."))


class Cubic(EoSBase):
    @staticmethod
    def common(b, pobj):
        ctype = pobj._cubic_type
        cname = pobj.config.equation_of_state_options["type"].name

        if hasattr(b, cname + "_fw"):
            # Common components already constructed by previous phase
コード例 #23
0
ファイル: drum.py プロジェクト: sel454/idaes-pse
class DrumData(UnitModelBlockData):
    """
    Boiler Drum Unit Operation Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([useDefault, True, False]),
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**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=In([True, False]),
            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=In([True, False]),
            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,
**PhysicalParameterObject** - a PhysicalParameterBlock 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.}"""))

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

        # Call UnitModel.build to setup dynamics
        super().build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            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
            })

        self.control_volume.add_geometry()

        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type, )

        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=True)

        self.flash = HelmPhaseSeparator(
            default={
                "dynamic": False,
                "property_package": self.config.property_package,
            })

        self.mixer = HelmMixer(
            default={
                "dynamic": False,
                "property_package": self.config.property_package,
                "inlet_list": ["FeedWater", "SaturatedWater"]
            })

        # Inlet Ports
        # FeedWater to Drum (from Pipe or Economizer)
        self.feedwater_inlet = Port(extends=self.mixer.FeedWater)
        # Sat water from water wall
        self.water_steam_inlet = Port(extends=self.flash.inlet)

        # Exit Ports
        # Liquid to Downcomer
        # self.liquid_outlet = Port(extends=self.mixer.outlet)
        self.add_outlet_port('liquid_outlet', self.control_volume)
        # Steam to superheaters
        self.steam_outlet = Port(extends=self.flash.vap_outlet)

        # constraint to make pressures of two inlets of drum mixer the same
        @self.Constraint(self.flowsheet().config.time,
                         doc="Mixter pressure identical")
        def mixer_pressure_eqn(b, t):
            return b.mixer.SaturatedWater.pressure[t]*1e-6 == \
                b.mixer.FeedWater.pressure[t]*1e-6

        self.stream_flash_out = Arc(source=self.flash.liq_outlet,
                                    destination=self.mixer.SaturatedWater)

        # Pyomo arc connect flash liq_outlet with mixer SaturatedWater inlet
        pyo.TransformationFactory("network.expand_arcs").apply_to(self)

        # connect internal units (Mixer to Water Tank Model)
        # Mixer Outlet (mixed_state) to unit control volume.properties_in
        @self.Constraint(self.flowsheet().config.time)
        def connection_material_balance(b, t):
            return 1e-4*b.mixer.mixed_state[t].flow_mol == \
                b.control_volume.properties_in[t].flow_mol*1e-4

        @self.Constraint(self.flowsheet().config.time)
        def connection_enthalpy_balance(b, t):
            return b.mixer.mixed_state[t].enth_mol*1e-4 == \
                b.control_volume.properties_in[t].enth_mol*1e-4

        @self.Constraint(self.flowsheet().config.time)
        def connection_pressure_balance(b, t):
            return b.mixer.mixed_state[t].pressure*1e-6 == \
                b.control_volume.properties_in[t].pressure*1e-6

        # Add object references
        self.volume = pyo.Reference(self.control_volume.volume)

        # 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 = pyo.Reference(self.control_volume.heat)

        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            self.deltaP = pyo.Reference(self.control_volume.deltaP)

        # Set Unit Geometry and Holdup Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary
        """
        units_meta = self.config.property_package.get_metadata()

        # Inside diameter of drum
        self.drum_diameter = Var(initialize=1.0,
                                 doc="Inside diameter of drum",
                                 units=units_meta.get_derived_units("length"))
        # Length of drum
        self.drum_length = Var(initialize=10,
                               doc="Horizontal length of drum",
                               units=units_meta.get_derived_units("length"))
        # Number of downcomers connected at the bottom of drum,
        # used to calculate contrac
        self.number_downcomers = Var(
            initialize=4, doc="Number of downcomers connected to drum")
        # Inside diameter of downcomer
        self.downcomer_diameter = Var(
            initialize=0.6,
            doc="Inside diameter of downcomer",
            units=units_meta.get_derived_units("length"))

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        units_meta = self.config.property_package.get_metadata()

        # Add performance variables
        self.drum_level = Var(self.flowsheet().config.time,
                              within=pyo.PositiveReals,
                              initialize=1.0,
                              doc='Water level from the bottom of the drum',
                              units=units_meta.get_derived_units("length"))

        # Velocity of fluid inside downcomer pipe
        self.downcomer_velocity = Var(
            self.flowsheet().config.time,
            initialize=10.0,
            doc='Liquid water velocity at the top of downcomer',
            units=units_meta.get_derived_units("velocity"))

        # Pressure change due to contraction
        self.deltaP_contraction = Var(
            self.flowsheet().config.time,
            initialize=-1.0,
            doc='Pressure change due to contraction',
            units=units_meta.get_derived_units("pressure"))

        # Pressure change due to gravity
        self.deltaP_gravity = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Pressure change due to gravity',
            units=units_meta.get_derived_units("pressure"))

        # Radius expression
        @self.Expression(doc="Radius of drum")
        def drum_radius(b):
            return 0.5 * b.drum_diameter

        # Expression for the angle from the drum center
        # to the circumference point at water level
        @self.Expression(self.flowsheet().config.time,
                         doc="angle of water level")
        def alpha_drum(b, t):
            return asin((b.drum_level[t] - b.drum_radius) /
                        b.drum_radius) / pyo.units.rad

        # Constraint for volume liquid in drum
        @self.Constraint(self.flowsheet().config.time,
                         doc="volume of liquid in drum")
        def volume_eqn(b, t):
            return b.volume[t] == \
                   ((b.alpha_drum[t]+0.5*const.pi)*b.drum_radius**2 +
                    b.drum_radius * cos(b.alpha_drum[t])
                    * (b.drum_level[t] - b.drum_radius)) \
                       * b.drum_length

        # Equation for velocity at the entrance of downcomer
        @self.Constraint(self.flowsheet().config.time,
                         doc="Velocity at entrance of downcomer")
        def velocity_eqn(b, t):
            return b.downcomer_velocity[t] * 0.25 * const.pi \
                * b.downcomer_diameter**2 * b.number_downcomers \
                == b.control_volume.properties_out[t].flow_vol

        # Pressure change equation for contraction
        # (acceleration pressure change)
        @self.Constraint(self.flowsheet().config.time,
                         doc="pressure change due to contraction")
        def pressure_change_contraction_eqn(b, t):
            return 1e-3*b.deltaP_contraction[t] == -1e-3*0.75 \
                * b.control_volume.properties_out[t].dens_mass_phase["Liq"] \
                * b.downcomer_velocity[t]**2

        # Pressure change equation for gravity, density*gravity*height
        g_units = units_meta.get_derived_units("acceleration")

        @self.Constraint(self.flowsheet().config.time,
                         doc="pressure change due to gravity")
        def pressure_change_gravity_eqn(b, t):
            return 1e-3 * b.deltaP_gravity[t] == 1e-3 * \
                b.control_volume.properties_out[t].dens_mass_phase["Liq"] \
                * pyo.units.convert(const.acceleration_gravity,
                                    to_units=g_units) * b.drum_level[t]

        # Total pressure change equation
        @self.Constraint(self.flowsheet().config.time, doc="pressure drop")
        def pressure_change_total_eqn(b, t):
            return 1e-3 * b.deltaP[t] == 1e-3 * (b.deltaP_contraction[t] +
                                                 b.deltaP_gravity[t])

    def set_initial_condition(self):
        if self.config.dynamic is True:
            self.control_volume.material_accumulation[:, :, :].value = 0
            self.control_volume.energy_accumulation[:, :].value = 0
            self.control_volume.material_accumulation[0, :, :].fix(0)
            self.control_volume.energy_accumulation[0, :].fix(0)

    def initialize(blk,
                   state_args_feedwater={},
                   state_args_water_steam={},
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg={}):
        '''
        Drum initialization routine.

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                           package(s) for the control_volume of the model to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={})
            solver : str indicating whcih solver to use during
                     initialization (default = None, use default solver)

        Returns:
            None
        '''
        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)

        init_log.info_low("Starting initialization...")
        # fix FeedWater Inlet
        flags_fw = fix_state_vars(blk.mixer.FeedWater_state,
                                  state_args_feedwater)

        # expecting 2 DOF due to pressure driven constraint
        if degrees_of_freedom(blk) != 2:
            raise Exception(degrees_of_freedom(blk))

        blk.flash.initialize(state_args_water_steam=state_args_water_steam,
                             outlvl=outlvl,
                             optarg=optarg,
                             solver=solver)
        init_log.info("Initialization Step 1 Complete.")

        blk.mixer.SaturatedWater.flow_mol[:].fix(
            blk.flash.liq_outlet.flow_mol[0].value)
        blk.mixer.SaturatedWater.pressure[:].fix(
            blk.flash.liq_outlet.pressure[0].value)
        blk.mixer.SaturatedWater.enth_mol[:].fix(
            blk.flash.liq_outlet.enth_mol[0].value)
        blk.mixer.initialize(outlvl=outlvl, optarg=optarg, solver=solver)
        init_log.info("Initialization Step 2 Complete.")

        blk.control_volume.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      hold_state=False)
        init_log.info("Initialization Step 3 Complete.")

        # fix flash Inlet
        flags_steam = fix_state_vars(blk.flash.mixed_state,
                                     state_args_water_steam)
        # unfix inlets (connected with arc)
        blk.mixer.SaturatedWater.flow_mol[:].unfix()
        blk.mixer.SaturatedWater.enth_mol[:].unfix()
        blk.mixer.SaturatedWater.pressure[:].unfix()
        blk.mixer.FeedWater.pressure[0].unfix()

        # solve model
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 4 {}.".format(
            idaeslog.condition(res)))
        revert_state_vars(blk.mixer.FeedWater_state, flags_fw)
        revert_state_vars(blk.flash.mixed_state, flags_steam)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        for v in self.deltaP_gravity.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for v in self.deltaP_contraction.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for t, c in self.pressure_change_contraction_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP_contraction[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.pressure_change_gravity_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP_gravity[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)

        for t, c in self.pressure_change_total_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)
コード例 #24
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.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single 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=In([True, False]),
            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=In([True, False]),
            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.}"""))

    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)

        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
        self.lagrange_mult = Var(self.flowsheet().config.time,
                                 self.config.property_package.element_list,
                                 domain=Reals,
                                 initialize=100,
                                 doc="Lagrangian multipliers")

        # 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().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         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
            return 0 == (
                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))

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            add_object_reference(self, "heat_duty", self.control_volume.heat)
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            add_object_reference(self, "deltaP", 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}
コード例 #25
0
class TranslatorData(UnitModelBlockData):
    """
    Standard Translator Block Class
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Translator blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Translator blocks do not contain holdup.""",
        ),
    )
    CONFIG.declare(
        "outlet_state_defined",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Indicated whether outlet state will be fully defined",
            doc="""Indicates whether unit model will fully define outlet state.
If False, the outlet property package will enforce constraints such as sum
of mole fractions and phase equilibrium.
**default** - True.
**Valid values:** {
**True** - outlet state will be fully defined,
**False** - outlet property package should enforce sumation and equilibrium
constraints.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description=
            "Indicated whether outlet state is in phase equilibrium",
            doc="""Indicates whether outlet property package should enforce
phase equilibrium constraints.
**default** - False.
**Valid values:** {
**True** - outlet property package should calculate phase equilibrium,
**False** - outlet property package should notcalculate phase equilibrium.}
""",
        ),
    )
    CONFIG.declare(
        "inlet_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for incoming stream",
            doc="""Property parameter object used to define property calculations
for the incoming stream,
**default** - None.
**Valid values:** {
**PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "inlet_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property package of "
            "the incoming stream",
            doc=
            """A ConfigBlock with arguments to be passed to the property block
associated with the incoming stream,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )
    CONFIG.declare(
        "outlet_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for outgoing stream",
            doc="""Property parameter object used to define property calculations
for the outgoing stream,
**default** - None.
**Valid values:** {
**PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "outlet_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property package of "
            "the outgoing stream",
            doc=
            """A ConfigBlock with arguments to be passed to the property block
associated with the outgoing stream,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )

    def build(self):
        """
        Begin building model.

        Args:
            None

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

        # Check construction argumnet consistency
        if self.config.outlet_state_defined and self.config.has_phase_equilibrium:
            raise ConfigurationError(
                "{} cannot calcuate phase equilibrium (has_phase_equilibrium "
                "= True) when outlet state is set to be fully defined ("
                "outlet_state_defined = True).".format(self.name))

        # Add State Blocks
        self.properties_in = self.config.inlet_property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties in incoming stream",
            default={
                "defined_state": True,
                "parameters": self.config.inlet_property_package,
                "has_phase_equilibrium": False,
                **self.config.inlet_property_package_args,
            },
        )

        self.properties_out = self.config.outlet_property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties in outgoing stream",
            default={
                "defined_state": self.config.outlet_state_defined,
                "parameters": self.config.outlet_property_package,
                "has_phase_equilibrium": self.config.has_phase_equilibrium,
                **self.config.outlet_property_package_args,
            },
        )

        # Add outlet port
        self.add_port(name="inlet", block=self.properties_in, doc="Inlet Port")
        self.add_port(name="outlet",
                      block=self.properties_out,
                      doc="Outlet Port")

    def initialize(
        blk,
        state_args_in={},
        state_args_out={},
        outlvl=idaeslog.NOTSET,
        solver="ipopt",
        optarg={"tol": 1e-6},
    ):
        """
        This method calls the initialization method of the state blocks.

        Keyword Arguments:
            state_args_in : a dict of arguments to be passed to the inlet
                            property package (to provide an initial state for
                            initialization (see documentation of the specific
                            property package) (default = {}).
            state_args_out : a dict of arguments to be passed to the outlet
                             property package (to provide an initial state for
                             initialization (see documentation of the specific
                             property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating which solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        # ---------------------------------------------------------------------
        # Initialize state block
        flags = blk.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_in,
            hold_state=True,
        )

        blk.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_out,
        )

        blk.properties_in.release_state(flags)

        init_log.info("Initialization Complete.")
コード例 #26
0
ファイル: cstr.py プロジェクト: Casinix/idaes-pse
class CSTRData(UnitModelBlockData):
    """
    Standard CSTR Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single 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=In([True, False]),
            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=In([True, False]),
            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(
        "has_equilibrium_reactions",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            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=In([True, False]),
            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=In([True, False]),
            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(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "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.}"""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).
        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(CSTRData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            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
            })

        self.control_volume.add_geometry()

        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_equilibrium_reactions)

        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)

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

        # Add object references
        add_object_reference(self, "volume", self.control_volume.volume)

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

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            add_object_reference(self, "heat_duty", self.control_volume.heat)

        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            add_object_reference(self, "deltaP", self.control_volume.deltaP)

    def _get_performance_contents(self, time_point=0):
        var_dict = {"Volume": self.volume[time_point]}
        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}
コード例 #27
0
class VarBoundPropagator(IsomorphicTransformation):
    """Propagate variable bounds for equalities of type :math:`x = y`.

    If :math:`x` has a tighter bound then :math:`y`, then this transformation
    will adjust the bounds on :math:`y` to match those of :math:`x`.

    Keyword arguments below are specified for the ``apply_to`` and
    ``create_using`` functions.

    """

    CONFIG = ConfigBlock()
    CONFIG.declare("tmp", ConfigValue(
        default=False, domain=bool,
        description="True to store the set of transformed variables and "
        "their old states so that they can be later restored."
    ))

    __doc__ = add_docstring_list(__doc__, CONFIG)

    def _apply_to(self, instance, **kwds):
        config = self.CONFIG(kwds)
        if config.tmp and not hasattr(instance, '_tmp_propagate_original_bounds'):
            instance._tmp_propagate_original_bounds = Suffix(
                direction=Suffix.LOCAL)
        eq_var_map, relevant_vars = _build_equality_set(instance)
        processed = ComponentSet()
        # Go through each variable in an equality set to propagate the variable
        # bounds to all equality-linked variables.
        for var in relevant_vars:
            # If we have already processed the variable, skip it.
            if var in processed:
                continue

            var_equality_set = eq_var_map.get(var, ComponentSet([var]))

            #: variable lower bounds in the equality set
            lbs = [v.lb for v in var_equality_set if v.has_lb()]
            max_lb = max(lbs) if len(lbs) > 0 else None
            #: variable upper bounds in the equality set
            ubs = [v.ub for v in var_equality_set if v.has_ub()]
            min_ub = min(ubs) if len(ubs) > 0 else None

            # Check  for error due to bound cross-over
            if max_lb is not None and min_ub is not None and max_lb > min_ub:
                # the lower bound is above the upper bound. Raise a ValueError.
                # get variable with the highest lower bound
                v1 = next(v for v in var_equality_set if v.lb == max_lb)
                # get variable with the lowest upper bound
                v2 = next(v for v in var_equality_set if v.ub == min_ub)
                raise ValueError(
                    'Variable {} has a lower bound {} '
                    ' > the upper bound {} of variable {}, '
                    'but they are linked by equality constraints.'
                    .format(v1.name, value(v1.lb), value(v2.ub), v2.name))

            for v in var_equality_set:
                if config.tmp:
                    # TODO warn if overwriting
                    instance._tmp_propagate_original_bounds[v] = (
                        v.lb, v.ub)
                v.setlb(max_lb)
                v.setub(min_ub)

            processed.update(var_equality_set)

    def revert(self, instance):
        """Revert variable bounds."""
        for v in instance._tmp_propagate_original_bounds:
            old_LB, old_UB = instance._tmp_propagate_original_bounds[v]
            v.setlb(old_LB)
            v.setub(old_UB)
        del instance._tmp_propagate_original_bounds
コード例 #28
0
class AddSlackVariables(NonIsomorphicTransformation):
    """
    This plugin adds slack variables to every constraint or to the constraints
    specified in targets.
    """

    CONFIG = ConfigBlock("core.add_slack_variables")
    CONFIG.declare(
        'targets',
        ConfigValue(default=None,
                    domain=target_list,
                    description=
                    "target or list of targets to which slacks will be added",
                    doc="""

        This specifies the list of Constraints to add slack variables to.
        """))

    def __init__(self, **kwds):
        kwds['name'] = "add_slack_vars"
        super(AddSlackVariables, self).__init__(**kwds)

    def _apply_to(self, instance, **kwds):
        try:
            assert not NAME_BUFFER
            self._apply_to_impl(instance, **kwds)
        finally:
            NAME_BUFFER.clear()

    def _apply_to_impl(self, instance, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        targets = config.targets

        if targets is None:
            constraintDatas = instance.component_data_objects(
                Constraint, descend_into=True)
        else:
            constraintDatas = []
            for t in targets:
                if isinstance(t, ComponentUID):
                    cons = t.find_component(instance)
                    if cons.is_indexed():
                        for i in cons:
                            constraintDatas.append(cons[i])
                    else:
                        constraintDatas.append(cons)
                else:
                    # we know it's a constraint because that's all we let
                    # through the config block validation.
                    if t.is_indexed():
                        for i in t:
                            constraintDatas.append(t[i])
                    else:
                        constraintDatas.append(t)

        # deactivate the objective
        for o in instance.component_data_objects(Objective):
            o.deactivate()

        # create block where we can add slack variables safely
        xblockname = unique_component_name(instance,
                                           "_core_add_slack_variables")
        instance.add_component(xblockname, Block())
        xblock = instance.component(xblockname)

        obj_expr = 0
        for cons in constraintDatas:
            if (cons.lower is not None and cons.upper is not None) and \
               value(cons.lower) > value(cons.upper):
                # this is a structural infeasibility so slacks aren't going to
                # help:
                raise RuntimeError("Lower bound exceeds upper bound in "
                                   "constraint %s" % cons.name)
            if not cons.active: continue
            cons_name = cons.getname(fully_qualified=True,
                                     name_buffer=NAME_BUFFER)
            if cons.lower is not None:
                # we add positive slack variable to body:
                # declare positive slack
                varName = "_slack_plus_" + cons_name
                posSlack = Var(within=NonNegativeReals)
                xblock.add_component(varName, posSlack)
                # add positive slack to body expression
                cons._body += posSlack
                # penalize slack in objective
                obj_expr += posSlack
            if cons.upper is not None:
                # we subtract a positive slack variable from the body:
                # declare slack
                varName = "_slack_minus_" + cons_name
                negSlack = Var(within=NonNegativeReals)
                xblock.add_component(varName, negSlack)
                # add negative slack to body expression
                cons._body -= negSlack
                # add slack to objective
                obj_expr += negSlack

        # make a new objective that minimizes sum of slack variables
        xblock._slack_objective = Objective(expr=obj_expr)
コード例 #29
0
ファイル: heat_exchanger.py プロジェクト: sel454/idaes-pse
def _make_heat_exchanger_config(config):
    """
    Declare configuration options for HeatExchangerData block.
    """
    config.declare(
        "hot_side_name",
        ConfigValue(
            default="shell",
            domain=str,
            doc="Hot side name, sets control volume and inlet and outlet names",
        ),
    )
    config.declare(
        "cold_side_name",
        ConfigValue(
            default="tube",
            domain=str,
            doc="Cold side name, sets control volume and inlet and outlet names",
        ),
    )
    config.declare(
        "hot_side_config",
        ConfigBlock(
            implicit=True,
            description="Config block for hot side",
            doc="""A config block used to construct the hot side control volume.
This config can be given by the hot side name instead of hot_side_config.""",
        ),
    )
    config.declare(
        "cold_side_config",
        ConfigBlock(
            implicit=True,
            description="Config block for cold side",
            doc="""A config block used to construct the cold side control volume.
This config can be given by the cold side name instead of cold_side_config.""",
        ),
    )
    _make_heater_config_block(config.hot_side_config)
    _make_heater_config_block(config.cold_side_config)
    config.declare(
        "delta_temperature_callback",
        ConfigValue(
            default=delta_temperature_lmtd_callback,
            description="Callback for for temperature difference calculations",
        ),
    )
    config.declare(
        "flow_pattern",
        ConfigValue(
            default=HeatExchangerFlowPattern.countercurrent,
            domain=In(HeatExchangerFlowPattern),
            description="Heat exchanger flow pattern",
            doc="""Heat exchanger flow pattern,
**default** - HeatExchangerFlowPattern.countercurrent.
**Valid values:** {
**HeatExchangerFlowPattern.countercurrent** - countercurrent flow,
**HeatExchangerFlowPattern.cocurrent** - cocurrent flow,
**HeatExchangerFlowPattern.crossflow** - cross flow, factor times
countercurrent temperature difference.}""",
        ),
    )
コード例 #30
0
class BoilerFiresideData(UnitModelBlockData):
    '''
Boiler fire-side surrogate model with enforced mass and energy balance
    '''

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([useDefault, True, False]),
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "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(
        "calculate_PA_SA_flows",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc=
            """Indicates whether the primary air and secondary air calculations
have to be constructed or not,
**default** - False.
**Valid values:** {
**True** - calculate primary air and secondary air based on stoichiometric
ratio, PA to coal ratio, and lower stoichiometric ratio,
**False** - primary air and secondary air are inputs of the model.}"""))
    CONFIG.declare(
        "number_of_zones",
        ConfigValue(default=16,
                    description='number of boiler zones',
                    doc='number of boiler zones'))
    CONFIG.declare(
        "has_platen_superheater",
        ConfigValue(default=True,
                    domain=In([True, False]),
                    description="True if boiler includes a platen superheater",
                    doc="""Indicates if boiler includes a platen superheater,
**default** - True.
**Valid values:** {
**True** - include heat duty to platen superheater,
**False** - exclude heat duty to platen superheater.}"""))
    CONFIG.declare(
        "has_roof_superheater",
        ConfigValue(default=True,
                    domain=In([True, False]),
                    description="True if roof is treated as a superheater",
                    doc="""Indicates if roof is a section of superheater,
**default** - True.
**Valid values:** {
**True** - include heat duty to roof superheater,
**False** - exclude heat duty to roof superheater.}"""))
    CONFIG.declare(
        "surrogate_dictionary",
        ConfigValue(
            default=None,
            description="surrogate model dictionary",
            doc="""user must provide surrogate models or values for heat duty,
**default** - False.
**Valid values:** """))

    def build(self):
        super(BoilerFiresideData, self).build()
        # Insert user provided custom surrogate model function
        # self.config.surrogate_function(self)
        if self.config.surrogate_dictionary is None:
            raise ConfigurationError(
                '{} - User needs to provide a dictionary of surrogates'.format(
                    self.name))

        self.primary_air = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            default=self.config.property_package_args)
        self.primary_air_moist = self.config.property_package.\
            build_state_block(
                self.flowsheet().config.time,
                default=self.config.property_package_args)
        self.secondary_air = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            default=self.config.property_package_args)
        self.flue_gas = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            default=self.config.property_package_args)

        self.add_port("primary_air_inlet", self.primary_air)
        self.add_port("secondary_air_inlet", self.secondary_air)
        self.add_port("flue_gas_outlet", self.flue_gas)
        # Construct performance equations
        self._make_params()
        self._make_vars()
        self._make_mass_balance()
        self._make_energy_balance()
        self._make_momentum_balance()
        self._import_surrogate_models()

    def _import_surrogate_models(self):

        data_dict = self.config.surrogate_dictionary
        if len(data_dict) != (
                len(self.zones) + self.config.has_platen_superheater +
                self.config.has_roof_superheater +
                2  # flyash surrogate and NOx surrogate
        ):
            raise ConfigurationError('User needs to provide the same number '
                                     'of surrogate models and water wall zones'
                                     'and/or platen sh and/or boiler roof')

        # Surrogate model predictions
        # Constraints for heat duty in boiler water wall zones
        @self.Constraint(self.flowsheet().config.time,
                         self.zones,
                         doc="Surrogate model for heat loss"
                         " to water wall zones")
        def eq_surr_waterwall_heat(b, t, z):
            return b.waterwall_heat[t, z] * b.fcorrection_heat_ww[t] ==\
                eval(data_dict[z])

        if self.config.has_platen_superheater is True:

            @self.Constraint(self.flowsheet().config.time,
                             doc="Surrogate model for heat loss"
                             " to platen superheater")
            def eq_surr_platen_heat(b, t):
                return b.platen_heat[t] * b.fcorrection_heat_platen[t]\
                    == eval(data_dict['pl'])

        if self.config.has_roof_superheater is True:

            @self.Constraint(self.flowsheet().config.time,
                             doc="Surrogate model for heat loss in "
                             " the roof and backpass heater")
            def eq_surr_roof_heat(b, t):
                return b.roof_heat[t] * b.fcorrection_heat_ww[t] == \
                    eval(data_dict['roof'])

        # Constraints for unburned carbon
        @self.Constraint(self.flowsheet().config.time,
                         doc="Surrogate model for"
                         " mass fraction of unburned carbon")
        def eq_surr_ln_ubc(b, t):
            return b.ubc_in_flyash[t] == eval(data_dict['flyash'])

        # Constraints for NOx in mol fraction, surrogate model in PPM,
        # converted to mass fraction
        @self.Constraint(self.flowsheet().config.time,
                         doc="NOx in mol fraction"
                         "surrogate model must be in PPM")
        def eq_surr_nox(b, t):
            return b.frac_mol_NOx_fluegas[t] * 1e6 == eval(data_dict['NOx'])

        #                    # 1e6 conversion factor from PPM to mol fract

    def _make_params(self):
        ''' This section is for parameters within this model.'''
        # atomic mass of elements involved in kg/mol
        self.atomic_mass_C = Param(initialize=0.0120107,
                                   doc='Atomic mass of C')
        self.atomic_mass_H = Param(initialize=0.00100795,
                                   doc='Atomic mass of H')
        self.atomic_mass_O = Param(initialize=0.0159994,
                                   doc='Atomic mass of O')
        self.atomic_mass_N = Param(initialize=0.0140067,
                                   doc='Atomic mass of N')
        self.atomic_mass_S = Param(initialize=0.0320652,
                                   doc='Atomic mass of S')
        # mole fractions of O2, H2O, CO2, SO2, and NO in air
        # at 25 C and relative humidity of 25% with SO2 and NO at 0.1 ppm
        # change air compostion accordingly if relative humidity changes
        # N2 mole fraction not needed since summation of mole fractions of
        # all six species is 1. It is 0.7839958
        if self.config.calculate_PA_SA_flows is True:
            self.mole_frac_air = Param(
                self.config.property_package.component_list,
                mutable=True,
                initialize={
                    'O2': 0.20784,
                    'N2': 0.783994,
                    'NO': 0.0000001,
                    'CO2': 0.0003373,
                    'H2O': 0.0078267,
                    'SO2': 0.0000001
                },
                doc='Mole fraction of air species')

    def _make_vars(self):
        ''' This section is for variables within this model.'''
        # Number of waterwall zones is given by config,
        # Optionally the model could contain a platen and a roof superheater

        self.deltaP = Var(self.flowsheet().config.time,
                          initialize=1000,
                          doc='Pressure drop of secondary air '
                          'through windbox and burner [Pa]')

        self.zones = RangeSet(self.config.number_of_zones)

        self.fcorrection_heat_ww = Var(self.flowsheet().config.time,
                                       initialize=1,
                                       doc='Correction factor '
                                       'for waterwall heat duty')

        if self.config.has_platen_superheater is True:
            self.fcorrection_heat_platen = Var(self.flowsheet().config.time,
                                               initialize=1,
                                               doc='Correction factor for '
                                               'platen SH heat duty')

        # wall temperatures of water wall zones
        self.wall_temperature_waterwall = Var(self.flowsheet().config.time,
                                              self.zones,
                                              initialize=700.0,
                                              doc='Wall temperature [K] in '
                                              'Waterwall zones')

        # heat duties for water wall zones
        self.waterwall_heat = Var(self.flowsheet().config.time,
                                  self.zones,
                                  initialize=2.0e7,
                                  doc='Heat duty [W] or heat loss '
                                  'to waterwall zones')

        if self.config.has_platen_superheater is True:
            # heat duty to platen super heater
            self.platen_heat = Var(self.flowsheet().config.time,
                                   initialize=6.0e7,
                                   doc='Platen superheater heat duty [W]')
            # wall temperature of platen superheater
            self.wall_temperature_platen = Var(self.flowsheet().config.time,
                                               initialize=800.0,
                                               doc='Platen superheater'
                                               ' wall temperature [K]')

        if self.config.has_roof_superheater is True:
            # heat duty to roof superheater
            self.roof_heat = Var(self.flowsheet().config.time,
                                 initialize=5e6,
                                 doc='Roof superheater heat duty [W]')
            # wall temperature of roof superheater
            self.wall_temperature_roof = Var(self.flowsheet().config.time,
                                             initialize=800.0,
                                             doc='Roof superheater '
                                             'wall temperature [K]')

        # PA/coal temperature, usually fixed around 150 F
        self.temperature_coal = Var(self.flowsheet().config.time,
                                    initialize=338.7,
                                    doc='Coal temperature in K')

        # overall Stoichiometric ratio, used to calculate total
        # combustion air flowrate
        # If SR is a function of load or raw coal flow rate,
        # specify constraint in flowsheet model
        self.SR = Var(self.flowsheet().config.time,
                      initialize=1.15,
                      doc='Overall furnace Stoichiometric ratio - SR')

        # lower furnace Stoichiometric ratio
        self.SR_lf = Var(self.flowsheet().config.time,
                         initialize=1.15,
                         doc='Lower furnace Stoichiometric ratio'
                         ' - SR excluding overfire air')

        # PA to coal mass flow ratio,
        # typically 2.0 depending on load or mill curve
        # usually this is an input of surrogate model
        self.ratio_PA2coal = Var(self.flowsheet().config.time,
                                 initialize=2.0,
                                 doc='Primary air to coal ratio')

        # mass flow rate of raw coal fed to mill
        # (raw coal flow without moisture vaporization in mill)
        self.flowrate_coal_raw = Var(self.flowsheet().config.time,
                                     initialize=25.0,
                                     doc='Raw coal mass flowrate [kg/s]')

        # mass flow rate of coal to burners after moisture vaporization in mill
        self.flowrate_coal_burner = Var(self.flowsheet().config.time,
                                        initialize=20.0,
                                        doc='Mass flowrate coal to burners '
                                        'with moisture'
                                        ' partially vaporized in mill [kg/s]')

        # raw coal moisture mass fraction (on as received basis),
        # can change with time to represent HHV change on as received basis
        self.mf_H2O_coal_raw = Var(self.flowsheet().config.time,
                                   initialize=0.15,
                                   doc='Raw coal mass fraction of'
                                   ' moisture on as received basis')

        # moisture mass fraction of coal to burners after mill,
        # calculated based on fraction of moistures vaporized in mill
        self.mf_H2O_coal_burner = Var(self.flowsheet().config.time,
                                      initialize=0.15,
                                      doc='Mass fraction of moisture'
                                      ' on as received basis')

        # Fraction of moisture vaporized in mill,
        # set in flowsheet as a function of coal flow rate, default is 0.6
        self.frac_moisture_vaporized = Var(self.flowsheet().config.time,
                                           initialize=0.6,
                                           doc='Fraction of coal'
                                           ' moisture vaporized in mill')

        # Vaporized moisture mass flow rate
        @self.Expression(self.flowsheet().config.time,
                         doc="Vaporized moisture mass flowrate [kg/s]")
        def flowrate_moist_vaporized(b, t):
            return b.flowrate_coal_raw[t] * b.mf_H2O_coal_raw[t] \
                * b.frac_moisture_vaporized[t]

        # Elemental composition of dry coal, assuming fixed over time
        # They are usually fixed as user inputs
        self.mf_C_coal_dry = Var(initialize=0.5,
                                 doc='Mass fraction of C on dry basis')

        self.mf_H_coal_dry = Var(initialize=0.05,
                                 doc='Mass fraction of H on dry basis')

        self.mf_O_coal_dry = Var(initialize=0.1,
                                 doc='Mass fraction of O on dry basis')

        self.mf_N_coal_dry = Var(initialize=0.1,
                                 doc='Mass fraction of N on dry basis')

        self.mf_S_coal_dry = Var(initialize=0.05,
                                 doc='Mass fraction of S on dry basis')

        self.mf_Ash_coal_dry = Var(initialize=0.2,
                                   doc='Mass fraction of Ash on dry basis')

        # High heating value of dry coal, usally as a fixed user input
        self.hhv_coal_dry = Var(initialize=1e7,
                                doc='High heating value (HHV)'
                                'of coal on dry basis J/kg')

        # Fraction of unburned carbon (actually all organic elements) in
        # flyash, predicted by surrogate model.
        # When generating the surrogate, sum up all elements in
        # flyash from fireside boiler model outputs
        self.ubc_in_flyash = Var(self.flowsheet().config.time,
                                 initialize=0.01,
                                 doc='Unburned carbon and'
                                 ' other organic elements in fly ash')

        # mole fraction of NO in flue gas, predicted by surrogate model
        # usually mole fraction is very close to mass fraction of NO
        self.frac_mol_NOx_fluegas = Var(self.flowsheet().config.time,
                                        initialize=1e-4,
                                        doc='Mole fraction of NOx in flue gas')

        # NOx in lb/MMBTU, NO is converted to NO2 as NOx
        @self.Expression(self.flowsheet().config.time, doc="NOx in lb/MMBTU")
        def nox_lb_mmbtu(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "NO"] * 0.046 * 2.20462 \
                / (b.flowrate_coal_raw[t] * (1-b.mf_H2O_coal_raw[t])
                   * b.hhv_coal_dry/1.054e9)

        # coal flow rate after mill
        @self.Constraint(self.flowsheet().config.time,
                         doc="Coal flow rate to burners after mill")
        def flowrate_coal_burner_eqn(b, t):
            return b.flowrate_coal_burner[t] == b.flowrate_coal_raw[t] \
                - b.flowrate_moist_vaporized[t]

        # moisture mass fraction in coal after mill
        @self.Constraint(self.flowsheet().config.time,
                         doc="Moisture mass fraction for coal after mill")
        def mf_H2O_coal_burner_eqn(b, t):
            return b.flowrate_coal_raw[t] * b.mf_H2O_coal_raw[t] == \
                b.flowrate_coal_burner[t] * b.mf_H2O_coal_burner[t] \
                + b.flowrate_moist_vaporized[t]

        # fraction of daf elements on dry basis
        @self.Expression(doc="Mass fraction of dry ash free (daf)")
        def mf_daf_dry(b):
            return 1 - b.mf_Ash_coal_dry

        @self.Expression(self.flowsheet().config.time,
                         doc="Ash mass flow rate kg/s")
        def flowrate_ash(b, t):
            return b.flowrate_coal_raw[t] * \
                (1-b.mf_H2O_coal_raw[t])*b.mf_Ash_coal_dry

        @self.Expression(self.flowsheet().config.time,
                         doc="Dry ash free - daf_coal flow rate "
                         "in fuel fed to the boiler kg/s")
        def flowrate_daf_fuel(b, t):
            return b.flowrate_coal_raw[t] * \
                (1-b.mf_H2O_coal_raw[t]) * b.mf_daf_dry

        @self.Expression(self.flowsheet().config.time,
                         doc="Dry ash free (daf) coal flow rate in fly ash")
        def flowrate_daf_flyash(b, t):
            return b.flowrate_ash[t] / (1.0 - b.ubc_in_flyash[t]) \
                * b.ubc_in_flyash[t]

        @self.Expression(self.flowsheet().config.time,
                         doc="Burned daf coal flow rate")
        def flowrate_daf_burned(b, t):
            return b.flowrate_daf_fuel[t] - b.flowrate_daf_flyash[t]

        @self.Expression(self.flowsheet().config.time, doc="Coal burnout")
        def coal_burnout(b, t):
            return b.flowrate_daf_burned[t] / b.flowrate_daf_fuel[t]

        @self.Expression(doc="Dry ash free - daf C mass fraction")
        def mf_C_daf(b):
            return b.mf_C_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Dry ash free - daf H mass fraction")
        def mf_H_daf(b):
            return b.mf_H_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Dry ash free - daf O mass fraction")
        def mf_O_daf(b):
            return b.mf_O_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Dry ash free - daf N mass fraction")
        def mf_N_daf(b):
            return b.mf_N_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Dry ash free - daf S mass fraction")
        def mf_S_daf(b):
            return b.mf_S_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Dry ash free high heating value in J/kg")
        def hhv_daf(b):
            return b.hhv_coal_dry / b.mf_daf_dry

        @self.Expression(doc="Heat of combustion at constant "
                         "pressure on daf basis in J/kg")
        # Note that measure HHV is the heat of combustion at constant volume
        def dhcoal(b):
            return -b.hhv_daf + const.gas_constant * 298.15 / 2 \
                * (-b.mf_H_daf/2/b.atomic_mass_H + b.mf_O_daf/b.atomic_mass_O
                   + b.mf_N_daf/b.atomic_mass_N)

        @self.Expression(doc="Carbon heat of reaction - dhc J/kg")
        def dhc(b):
            return -94052 * 4.184 / b.atomic_mass_C * b.mf_C_daf

        @self.Expression(doc="Hydrogen heat of reaction - dhh in J/kg")
        def dhh(b):
            return -68317.4 * 4.184 / b.atomic_mass_H / 2 * b.mf_H_daf

        @self.Expression(doc="Sulfur heat of reaction - dhs in J/kg")
        def dhs(b):
            return -70940 * 4.184 / b.atomic_mass_S * b.mf_S_daf

        @self.Expression(doc="Heat of formation for daf coal in J/kg")
        def hf_daf(b):
            return b.dhc + b.dhh + b.dhs - b.dhcoal

        @self.Expression(self.flowsheet().config.time,
                         doc="Heat of formation of "
                         "moisture-containing coal to burners")
        def hf_coal(b, t):
            return b.hf_daf * (1-b.mf_H2O_coal_burner[t]) * b.mf_daf_dry \
                + b.mf_H2O_coal_burner[t]*(-68317.4)*4.184/(b.atomic_mass_H*2
                                                            + b.atomic_mass_O)

        @self.Expression(doc="Average atomic mass of daf coal")
        def am_daf(b):
            return 1 / (
                b.mf_C_daf / b.atomic_mass_C + b.mf_H_daf / b.atomic_mass_H +
                b.mf_O_daf / b.atomic_mass_O + b.mf_N_daf / b.atomic_mass_N +
                b.mf_S_daf / b.atomic_mass_S)

        @self.Expression(self.flowsheet().config.time,
                         doc="Gt1 or Einstein quantum theory function for daf "
                         "coal sensible heat calculation")
        def gt1(b, t):
            return 1 / (exp(380 / b.temperature_coal[t]) - 1)

        # since flyash and flue gas outlet temperature is not fixed
        self.gt1_flyash = Var(self.flowsheet().config.time,
                              initialize=1,
                              doc='Gt1 or Einstein quantum theory function'
                              ' for daf part of flyash')

        @self.Constraint(self.flowsheet().config.time,
                         doc="Gt1 or Einstein quantum theory "
                         "for daf part of flyash")
        def gt1_flyash_eqn(b, t):
            return b.gt1_flyash[t] \
                * (exp(380/b.flue_gas[t].temperature)-1) == 1

        @self.Expression(self.flowsheet().config.time,
                         doc="Gt2 or Einstein quantum theory "
                         "function for daf coal "
                         "sensible heat calculation for high temperature")
        def gt2(b, t):
            return 1 / (exp(1800 / b.temperature_coal[t]) - 1)

        # declare variable rather than use expression
        # since flyash and flue gas outlet temperature is not fixed
        self.gt2_flyash = Var(self.flowsheet().config.time,
                              initialize=1,
                              doc='Gt2 or Einstein quantum theory function '
                              'for daf part of flyash')

        @self.Constraint(self.flowsheet().config.time,
                         doc="Gt2 or Einstein quantum theory function "
                         "for daf part of flyash")
        def gt2_flyash_eqn(b, t):
            return b.gt2_flyash[t] \
                * (exp(1800/b.flue_gas[t].temperature)-1) == 1

        @self.Expression(self.flowsheet().config.time,
                         doc="Sensible heat of daf coal")
        def hs_daf(b, t):
            return const.gas_constant / b.am_daf * (380 *
                                                    (b.gt1[t] - 0.3880471566) +
                                                    3600 *
                                                    (b.gt2[t] - 0.002393883))

        @self.Expression(self.flowsheet().config.time,
                         doc="Sensible heat for daf part of flyash")
        def hs_daf_flyash(b, t):
            return const.gas_constant / b.am_daf * (
                380 * (b.gt1_flyash[t] - 0.3880471566) + 3600 *
                (b.gt2_flyash[t] - 0.002393883))

        @self.Expression(self.flowsheet().config.time,
                         doc="Sensible heat of coal to burners")
        def hs_coal(b, t):
            return (1-b.mf_H2O_coal_burner[t]) * b.mf_daf_dry * b.hs_daf[t] \
                + b.mf_H2O_coal_burner[t]*4184*(b.temperature_coal[t]-298.15) \
                + (1-b.mf_H2O_coal_burner[t]) * b.mf_Ash_coal_dry \
                * (593 * (b.temperature_coal[t]-298.15)
                   + 0.293*(b.temperature_coal[t]**2 - 298.15**2))

        @self.Expression(self.flowsheet().config.time,
                         doc="Total enthalpy of "
                         "moisture-containing coal to burners")
        def h_coal(b, t):
            return b.hs_coal[t] + b.hf_coal[t]

        # variable for O2 mole fraction in flue gas on dry basis
        # it can be used for data reconciliation and parameter estimation
        self.fluegas_o2_pct_dry = Var(self.flowsheet().config.time,
                                      initialize=3,
                                      doc='Mol percent of O2 on dry basis')

    def _make_mass_balance(self):
        # PA flow rate calculated based on PA/coal ratio
        @self.Constraint(self.flowsheet().config.time,
                         doc="Ratio of PA mass flow to raw coal mass flow")
        def ratio_PA2coal_eqn(b, t):
            return b.primary_air[t].flow_mass == b.flowrate_coal_raw[t] \
                    * b.ratio_PA2coal[t]

        @self.Expression(self.flowsheet().config.time,
                         doc="C molar flow from coal")
        def molflow_C_fuel(b, t):
            return b.flowrate_daf_fuel[t] * b.mf_C_daf / b.atomic_mass_C

        @self.Expression(self.flowsheet().config.time,
                         doc="H molar flow from coal")
        def molflow_H_fuel(b, t):
            return b.flowrate_daf_fuel[t] * b.mf_H_daf / b.atomic_mass_H

        @self.Expression(self.flowsheet().config.time,
                         doc="O molar flow from coal")
        def molflow_O_fuel(b, t):
            return b.flowrate_daf_fuel[t] * b.mf_O_daf / b.atomic_mass_O

        @self.Expression(self.flowsheet().config.time,
                         doc="N molar flow from coal")
        def molflow_N_fuel(b, t):
            return b.flowrate_daf_fuel[t] * b.mf_N_daf / b.atomic_mass_N

        @self.Expression(self.flowsheet().config.time,
                         doc="S molar flow from coal")
        def molflow_S_fuel(b, t):
            return b.flowrate_daf_fuel[t] * b.mf_S_daf / b.atomic_mass_S

        if self.config.calculate_PA_SA_flows is True:
            # Calculate PA component molar flow based on air mol fractions
            # specified as model parameter
            # Overwrite the composition from inlets
            # Let N2 molar flow calculated by balance
            @self.Constraint(self.flowsheet().config.time,
                             self.config.property_package.component_list,
                             doc="PA component molar flow")
            def molar_flow_PA_eqn(b, t, j):
                if j == 'N2':
                    return Constraint.Skip
                else:
                    return b.primary_air[t].flow_mol_comp[j] == \
                       b.primary_air[t].flow_mol * b.mole_frac_air[j]

            # Calculate SA component molar flow based on air mol fractions
            # specified as model parameter
            # Overwrite the composition from inlets
            # Let N2 molar flow calculated by balance
            @self.Constraint(self.flowsheet().config.time,
                             self.config.property_package.component_list,
                             doc="SA component molar flow")
            def molar_flow_SA_eqn(b, t, j):
                if j == 'N2':
                    return Constraint.Skip
                else:
                    return b.secondary_air[t].flow_mol_comp[j] == \
                       b.secondary_air[t].flow_mol * b.mole_frac_air[j]

        # calculate molar flow of primary_air_moist stream
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.component_list)
        def primary_air_moist_comp_flow_eqn(b, t, j):
            if j == "H2O":
                return (b.primary_air[t].flow_mol_comp['H2O']
                        + b.flowrate_moist_vaporized[t]
                        / (b.atomic_mass_H*2+b.atomic_mass_O)) \
                    == b.primary_air_moist[t].flow_mol_comp['H2O']
            else:
                return b.primary_air[t].flow_mol_comp[j] == \
                       b.primary_air_moist[t].flow_mol_comp[j]

        # total combustion air mass flow rate
        @self.Expression(self.flowsheet().config.time,
                         doc="Total combustion air mass flow rate")
        def flow_mass_TCA(b, t):
            return b.primary_air[t].flow_mass + b.secondary_air[t].flow_mass

        # overall Stoichiometric ratio (SR)
        @self.Constraint(self.flowsheet().config.time, doc="SR equation")
        def SR_eqn(b, t):
            return b.SR[t]*(b.molflow_C_fuel[t] + b.molflow_H_fuel[t]/4
                            + b.molflow_S_fuel[t] - b.molflow_O_fuel[t]/2) == \
                b.primary_air[t].flow_mol_comp["O2"] \
                + b.secondary_air[t].flow_mol_comp["O2"]

        @self.Expression(self.flowsheet().config.time, doc="C mole flow")
        def molflow_C_fluegas(b, t):
            return b.flowrate_daf_burned[t] \
                * b.mf_C_daf/b.atomic_mass_C \
                + b.primary_air_moist[t].flow_mol_comp["CO2"] \
                + b.secondary_air[t].flow_mol_comp["CO2"]

        @self.Expression(self.flowsheet().config.time, doc="H mole flow")
        def molflow_H_fluegas(b, t):
            return b.flowrate_daf_burned[t] \
                * b.mf_H_daf/b.atomic_mass_H \
                + b.primary_air_moist[t].flow_mol_comp["H2O"]*2 \
                + b.secondary_air[t].flow_mol_comp["H2O"]*2 \
                + b.flowrate_coal_burner[t]*b.mf_H2O_coal_burner[t] \
                / (b.atomic_mass_H*2 + b.atomic_mass_O)*2

        @self.Expression(self.flowsheet().config.time, doc="O mole flow")
        def molflow_O_fluegas(b, t):
            return b.flowrate_daf_burned[t] \
                * b.mf_O_daf/b.atomic_mass_O \
                + b.primary_air_moist[t].flow_mol_comp["O2"]*2 \
                + b.primary_air_moist[t].flow_mol_comp["CO2"]*2 \
                + b.primary_air_moist[t].flow_mol_comp["H2O"] \
                + b.primary_air_moist[t].flow_mol_comp["SO2"]*2 \
                + b.primary_air_moist[t].flow_mol_comp["NO"] \
                + b.secondary_air[t].flow_mol_comp["O2"]*2 \
                + b.secondary_air[t].flow_mol_comp["CO2"]*2 \
                + b.secondary_air[t].flow_mol_comp["H2O"] \
                + b.secondary_air[t].flow_mol_comp["SO2"]*2 \
                + b.secondary_air[t].flow_mol_comp["NO"] \
                + b.flowrate_coal_burner[t]*b.mf_H2O_coal_burner[t] \
                / (b.atomic_mass_H*2 + b.atomic_mass_O)

        @self.Expression(self.flowsheet().config.time, doc="N mole flow")
        def molflow_N_fluegas(b, t):
            return b.flowrate_daf_burned[t] \
                * b.mf_N_daf/b.atomic_mass_N \
                + b.primary_air_moist[t].flow_mol_comp["N2"]*2 \
                + b.primary_air_moist[t].flow_mol_comp["NO"] \
                + b.secondary_air[t].flow_mol_comp["N2"]*2 \
                + b.secondary_air[t].flow_mol_comp["NO"]

        @self.Expression(self.flowsheet().config.time, doc="S mole flow")
        def molflow_S_fluegas(b, t):
            return b.flowrate_daf_burned[t] \
                * b.mf_S_daf/b.atomic_mass_S \
                + b.primary_air_moist[t].flow_mol_comp["SO2"] \
                + b.secondary_air[t].flow_mol_comp["SO2"]

        # calculate flue gas flow component at flue_gas_outlet
        # NO mole fraction in flue gas is given by NOx surrogate model
        @self.Constraint(self.flowsheet().config.time,
                         doc="NO at flue gas outlet")
        def NO_eqn(b, t):
            return b.flue_gas[t].flow_mol_comp["NO"] == \
                b.frac_mol_NOx_fluegas[t] * b.flue_gas[t].flow_mol

        # N2 at outlet
        @self.Constraint(self.flowsheet().config.time, doc="N2 at outlet")
        def N2_eqn(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "N2"] == \
                + b.molflow_N_fluegas[t]/2 \
                - b.flue_gas_outlet.flow_mol_comp[t, 'NO']/2

        # SO2 at outlet
        @self.Constraint(self.flowsheet().config.time, doc="SO2 at outlet")
        def SO2_eqn(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "SO2"] == \
                b.molflow_S_fluegas[t]

        # H2O at outlet
        @self.Constraint(self.flowsheet().config.time, doc="H2O at outlet")
        def H2O_eqn(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "H2O"] == \
                b.molflow_H_fluegas[t]/2

        # CO2 at outlet
        @self.Constraint(self.flowsheet().config.time, doc="CO2 at outlet")
        def CO2_eqn(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "CO2"] == \
                b.molflow_C_fluegas[t]

        # O2 at outlet
        @self.Constraint(self.flowsheet().config.time, doc="O2 at outlet")
        def O2_eqn(b, t):
            return b.flue_gas_outlet.flow_mol_comp[t, "O2"] == \
                (b.molflow_O_fluegas[t] - b.molflow_C_fluegas[t]*2
                 - b.molflow_H_fluegas[t]/2 - b.molflow_S_fluegas[t]*2
                 - b.flue_gas_outlet.flow_mol_comp[t, "NO"])/2

        # constraint for mol percent of O2 in flue gas on dry basis
        @self.Constraint(self.flowsheet().config.time,
                         doc="Mol percent of O2 in flue gas on dry basis")
        def fluegas_o2_pct_dry_eqn(b, t):
            return b.fluegas_o2_pct_dry[t] \
                * (b.flue_gas_outlet.flow_mol_comp[t, "O2"]
                   + b.flue_gas_outlet.flow_mol_comp[t, "N2"]
                   + b.flue_gas_outlet.flow_mol_comp[t, "CO2"]
                   + b.flue_gas_outlet.flow_mol_comp[t, "SO2"]
                   + b.flue_gas_outlet.flow_mol_comp[t, "NO"]) / 100 \
                == b.flue_gas_outlet.flow_mol_comp[t, "O2"]

        # mass flow rate of flyash containing unburned fuel
        @self.Expression(self.flowsheet().config.time,
                         doc="Flyash mass flow rate  kg/s")
        def flow_mass_flyash(b, t):
            return (b.flowrate_daf_flyash[t] + b.flowrate_ash[t])

    def _make_momentum_balance(self):
        # flue gas pressure is secondary air presure
        # - pressure drop through windbox
        # and burner secondary air register
        @self.Constraint(self.flowsheet().config.time,
                         doc="Flue gas pressure in Pascals")
        def flue_gas_pressure_eqn(b, t):
            return b.flue_gas[t].pressure * 1e-5 == (
                b.secondary_air[t].pressure - b.deltaP[t]) * 1e-5

        # set pressure of primary air with moisture at mill outlet
        @self.Constraint(self.flowsheet().config.time,
                         doc="Mill outlet pressure")
        def primary_air_moist_pressure_eqn(b, t):
            return b.primary_air[t].pressure*1e-5 == \
                   b.primary_air_moist[t].pressure*1e-5

    def _make_energy_balance(self):
        # temperature of primary_air_moist is equal to coal temparature
        # leaving mill, ignore the temperature of primary_air_inlet in energy
        # balance equation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Temperature of primary air leaving mill")
        def primary_air_moist_temperature_eqn(b, t):
            return b.primary_air_moist[t].temperature == b.temperature_coal[t]

        # overall energy balance to calculate FEGT
        @self.Constraint(self.flowsheet().config.time,
                         doc="Temperature at flue gas outlet")
        def flue_gas_temp_eqn(b, t):
            return (b.primary_air_moist[t].flow_mol
                    * b.primary_air_moist[t].enth_mol
                    + b.secondary_air[t].flow_mol
                    * b.secondary_air[t].enth_mol
                    + b.h_coal[t] * b.flowrate_coal_burner[t]) == \
                (sum(b.waterwall_heat[t, j] for j in b.zones)
                 + (b.platen_heat[t] if self.config.has_platen_superheater is
                    True else 0)
                 + (b.roof_heat[t] if self.config.has_roof_superheater is
                    True else 0)
                 + b.flue_gas[t].flow_mol * b.flue_gas[t].enth_mol
                 # sensible heat of ash
                 + b.flowrate_ash[t] * (593*(b.flue_gas[t].
                                             temperature-298.15)+0.293
                                        * (b.flue_gas[t].
                                           temperature**2.0-298.15**2.0))
                 # sensible heat of unburned fuel
                 + b.flowrate_daf_flyash[t] * b.hs_daf_flyash[t])

        # expression to calculate total heat duty of waterwall
        @self.Expression(self.flowsheet().config.time,
                         doc="Total heat duty of all waterwall zones")
        def heat_total_ww(b, t):
            return sum(b.waterwall_heat[t, j] for j in b.zones)

        # expression to calculate total heat loss through
        # waterwall, platen SH, and roof SH
        @self.Expression(self.flowsheet().config.time, doc="Total heat duty")
        def heat_total(b, t):
            return b.heat_total_ww[t] \
                + (b.platen_heat[t] if self.config.has_platen_superheater is
                    True else 0) \
                + (b.roof_heat[t] if self.config.has_roof_superheater is
                    True else 0)

    def initialize(blk,
                   state_args_PA=None,
                   state_args_SA=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg={}):
        '''
        Initialization routine.
        1.- initialize state blocks, using an initial guess for inlet
        primary air and secondary air.
        2.- Use PA and SA values to guess flue gas component molar flowrates,
        Temperature, and Pressure. Initialize flue gas state block.
        3.- Then, solve complete model.

        Keyword Arguments:
            state_args_PA : a dict of arguments to be passed to the property
                           package(s) for the primary air state block to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            state_args_SA : a dict of arguments to be passed to the property
                           package(s) for secondary air state block to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine
            optarg : solver options dictionary object (default={})
            solver : str indicating whcih solver to use during
                     initialization (default = None, use default solver)

        Returns:
            None
        '''
        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 property blocks
        blk.primary_air.initialize(outlvl=outlvl,
                                   optarg=optarg,
                                   solver=solver,
                                   state_args=state_args_PA)
        blk.primary_air_moist.initialize(outlvl=outlvl,
                                         optarg=optarg,
                                         solver=solver,
                                         state_args=state_args_PA)
        blk.secondary_air.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=state_args_SA)
        init_log.info_high("Initialization Step 1 Complete.")

        state_args = {
            "flow_mol_comp": {
                "H2O": (blk.primary_air_inlet.flow_mol_comp[0, "H2O"].value +
                        blk.secondary_air_inlet.flow_mol_comp[0, "H2O"].value),
                "CO2": (blk.primary_air_inlet.flow_mol_comp[0, "CO2"].value +
                        blk.secondary_air_inlet.flow_mol_comp[0, "CO2"].value),
                "N2": (blk.primary_air_inlet.flow_mol_comp[0, "N2"].value +
                       blk.secondary_air_inlet.flow_mol_comp[0, "N2"].value),
                "O2": (blk.primary_air_inlet.flow_mol_comp[0, "O2"].value +
                       blk.secondary_air_inlet.flow_mol_comp[0, "O2"].value),
                "SO2": (blk.primary_air_inlet.flow_mol_comp[0, "SO2"].value +
                        blk.secondary_air_inlet.flow_mol_comp[0, "SO2"].value),
                "NO": (blk.primary_air_inlet.flow_mol_comp[0, "NO"].value +
                       blk.secondary_air_inlet.flow_mol_comp[0, "NO"].value)
            },
            "temperature": 1350.00,
            "pressure": blk.primary_air_inlet.pressure[0].value
        }
        # initialize flue gas outlet
        blk.flue_gas.initialize(state_args=state_args,
                                outlvl=outlvl,
                                solver=solver)
        init_log.info_high("Initialization Step 2 Complete.")

        if blk.config.calculate_PA_SA_flows is False:
            # Option 1: given PA and SA component flow rates - fixed inlets
            # fix inlet component molar flow rates
            # unfix ratio_PA2coal, SR, and fluegas_o2_pct_dry
            blk.primary_air_inlet.flow_mol_comp[...].fix()
            blk.secondary_air_inlet.flow_mol_comp[...].fix()
            blk.ratio_PA2coal.unfix()
            blk.SR.unfix()
            blk.fluegas_o2_pct_dry.unfix()
            dof = degrees_of_freedom(blk)

        else:
            # Option 2: SR, ratioPA2_coal to estimate TCA, PA, SA
            # unfix component molar flow rates, but keep T and P fixed.
            blk.primary_air_inlet.flow_mol_comp[:, :].unfix()
            blk.secondary_air_inlet.flow_mol_comp[:, :].unfix()
            dof = degrees_of_freedom(blk)

        if not dof == 0:
            raise ConfigurationError('User needs to check '
                                     'degrees of freedom')

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(
            idaeslog.condition(res)))
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        # set a default waterwall zone heat scaling factor
        for v in self.waterwall_heat.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-7)

        # set a default platen heat scaling factor
        if self.config.has_platen_superheater is True:
            for v in self.platen_heat.values():
                if iscale.get_scaling_factor(v, warning=True) is None:
                    iscale.set_scaling_factor(v, 1e-7)

        # set a default roof heat scaling factor
        if self.config.has_roof_superheater is True:
            for v in self.roof_heat.values():
                if iscale.get_scaling_factor(v, warning=True) is None:
                    iscale.set_scaling_factor(v, 1e-6)

        # set waterwall heat constraint scaling factor
        for t, c in self.eq_surr_waterwall_heat.items():
            sf = iscale.get_scaling_factor(self.waterwall_heat[t],
                                           default=1e-7,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)

        # set platen heat constraint scaling factor
        if self.config.has_platen_superheater is True:
            for t, c in self.eq_surr_platen_heat.items():
                sf = iscale.get_scaling_factor(self.platen_heat[t],
                                               default=1e-7,
                                               warning=True)
                iscale.constraint_scaling_transform(c, sf)

        # set roof heat constraint scaling factor
        if self.config.has_roof_superheater is True:
            for t, c in self.eq_surr_roof_heat.items():
                sf = iscale.get_scaling_factor(self.roof_heat[t],
                                               default=1e-6,
                                               warning=True)
                iscale.constraint_scaling_transform(c, sf)

        # set flue gas temperature constraint scaling factor
        for t, c in self.flue_gas_temp_eqn.items():
            sf = iscale.get_scaling_factor(self.platen_heat[t],
                                           default=1e-7,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf)