Exemple #1
0
def generate_discretization_components_along_set(m, set_, active=True):
    # What is a "discretization equation with respect to 's'"?
    # Is it the component itself, or reference-to-slices of the
    # component along set s?
    #
    # If we consider a "variable" to be something that maps time
    # to some data, then a "differential variable" or "discretization
    # equation" should also map time to some data...
    for var in m.component_objects(pyo.Var, active=active):
        if (isinstance(var, dae.DerivativeVar)
                and set_ in ComponentSet(var.get_continuousset_list())):
            block = var.parent_block()
            # NOTE: Making some assumptions about the name and location
            # of discretization equations.
            con = block.find_component(var.local_name + DAE_DISC_SUFFIX)
            var_map = dict(
                (idx, pyo.Reference(slice_))
                for idx, slice_ in slice_component_along_sets(var, (set_, )))
            con_map = dict(
                (idx, pyo.Reference(slice_))
                for idx, slice_ in slice_component_along_sets(con, (set_, )))
            if not active or con.active:
                # We do not check the individual condata objects for activity...
                for idx, var in var_map.items():
                    yield idx, con_map[idx], var
    def make_model(self):
        m = pyo.ConcreteModel()
        m.x = pyo.Var(initialize=1.0)
        m.y = pyo.Var(initialize=1.0)
        m.u = pyo.Var(initialize=1.0)
        m.v = pyo.Var(initialize=1.0)
        m.con_1 = pyo.Constraint(expr=m.x * m.y == m.u)
        m.con_2 = pyo.Constraint(expr=m.x**2 * m.y**3 == m.v)
        m.con_3 = pyo.Constraint(
            expr=self.con_3_body(m.x, m.y, m.u, m.v) == self.con_3_rhs())
        m.con_4 = pyo.Constraint(
            expr=self.con_4_body(m.x, m.y, m.u, m.v) == self.con_4_rhs())

        epm_model = pyo.ConcreteModel()
        epm_model.x = pyo.Reference(m.x)
        epm_model.y = pyo.Reference(m.y)
        epm_model.u = pyo.Reference(m.u)
        epm_model.v = pyo.Reference(m.v)
        epm_model.epm = ExternalPyomoModel(
            [m.u, m.v],
            [m.x, m.y],
            [m.con_3, m.con_4],
            [m.con_1, m.con_2],
        )
        epm_model.obj = pyo.Objective(expr=m.x**2 + m.y**2 + m.u**2 + m.v**2)
        epm_model.egb = ExternalGreyBoxBlock()
        epm_model.egb.set_external_model(epm_model.epm, inputs=[m.u, m.v])
        return epm_model
    def test_extra_category(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        disturbances = [
            pyo.Reference(model.conc_in[:, 'A']),
            pyo.Reference(model.conc_in[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
            VC.DISTURBANCE: disturbances,
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        db.vectors.input[:, t0].set_value(1.1)
        db.vectors.measurement[:, t0].set_value(2.2)
        db.vectors.disturbance[:, t0].set_value(3.3)
        assert model.flow_in[t0].value == 1.1
        assert model.conc[t0, 'A'].value == 2.2
        assert model.conc[t0, 'B'].value == 2.2
        assert model.conc_in[t0, 'A'].value == 3.3
        assert model.conc_in[t0, 'B'].value == 3.3
    def test_empty_category(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
            VC.ALGEBRAIC: [],
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        # Categories with no variables are removed.
        # If they were retained, trying to iterate
        # over, e.g., vectors.algebraic[:, :] would
        # fail due to inconsistent dimension.
        assert VC.ALGEBRAIC not in db.category_dict
Exemple #5
0
def test_tag_ref(model):
    m = model
    m.rw = pyo.Reference(m.w[:, "a"])
    m.ry = pyo.Reference(m.y)

    rw = ModelTag(expr=m.rw, format_string="{:.3f}", doc="Tag for rw")
    ry = ModelTag(expr=m.ry, format_string="{:.2f}", doc="Tag for ry")

    m.w[1, "a"] = 3
    assert str(rw[1]) == "3.000 kg"
    assert rw[1].is_var

    ry[:].set(2)
    assert str(ry[None]) == "2.00 s"
Exemple #6
0
 def test_reference(self):
     m = pyo.ConcreteModel()
     m.v1 = pyo.Var()
     m.ref = pyo.Reference(m.v1)
     m.c1 = pyo.Constraint(expr=m.v1 == 1.0)
     igraph = IncidenceGraphInterface(m)
     self.assertEqual(igraph.incidence_matrix.shape, (1, 1))
Exemple #7
0
def pressure_flow_default_callback(valve):
    """
    Add the default pressure flow relation constraint.  This will be used in the
    valve model, a custom callback is provided.
    """
    umeta = valve.config.property_package.get_metadata().get_derived_units

    valve.Cv = pyo.Var(initialize=0.1,
                       doc="Valve flow coefficent",
                       units=umeta("amount") / umeta("time") /
                       umeta("pressure")**0.5)
    valve.Cv.fix()

    valve.flow_var = pyo.Reference(
        valve.control_volume.properties_in[:].flow_mol)
    valve.pressure_flow_equation_scale = lambda x: x**2

    @valve.Constraint(valve.flowsheet().config.time)
    def pressure_flow_equation(b, t):
        Po = b.control_volume.properties_out[t].pressure
        Pi = b.control_volume.properties_in[t].pressure
        F = b.control_volume.properties_in[t].flow_mol
        Cv = b.Cv
        fun = b.valve_function[t]
        return F**2 == Cv**2 * (Pi - Po) * fun**2
Exemple #8
0
 def __init__(self, expr, format_string="{}", doc="", display_units=None):
     """initialize a model tag instance.
     Args:
         expr: A Pyomo Var, Expression, Param, Reference, or unnamed
             expression to tag. This can be a scalar or indexed.
         format_string: A formating string used to print an elememnt of the
             tagged expression (e.g. '{:.3f}').
         doc: A description of the tagged quantity.
         display_units: Pyomo units to display the quantity in. If a string
             is provided it will be used to display as the unit, but will not
             be used to convert units. If None, use native units of the
             quantity.
     """
     super().__init__()
     self._format = format_string  # format string for printing expression
     if isinstance(expr, IndexedComponent_slice):
         self._expression = pyo.Reference(
             expr)  # tag expression (can be unnamed)
     else:
         self._expression = expr
     self._doc = doc  # documentation for a tag
     self._display_units = display_units  # unit to display value in
     self._cache_validation_value = {}  # value when converted value stored
     self._cache_display_value = {
     }  # value to display after unit conversions
     self._name = None  # tag name (just used to claify error messages)
     self._root = None  # use this to cache scalar tags in indexed parent
     self._index = None  # index to get cached converted value from parent
     self._group = None  # Group object if this is a member of a group
     self._str_units = True  # include units to stringify the tag
     # if _set_in_display_units is True and no units are provided for for
     # set, fix, setub, and setlb, the units will be assumed to be the
     # display units.  If it is false and no units are proided, the units are
     # assumed to be the native units of the quantity
     self._set_in_display_units = False
Exemple #9
0
def plot_outlet_states_over_time(
    m,
    show=True,
    prefix=None,
    extra_states=None,
):
    if prefix is None:
        prefix = ""
    if extra_states is None:
        extra_states = []
    x0 = m.fs.MB.gas_phase.length_domain.first()
    x1 = m.fs.MB.solid_phase.length_domain.last()
    state_slices = [
        m.fs.MB.gas_phase.properties[:, x1].temperature,
        m.fs.MB.gas_phase.properties[:, x1].pressure,
        m.fs.MB.gas_phase.properties[:, x1].flow_mol,
        m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["CH4"],
        m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["H2O"],
        m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["CO2"],
        m.fs.MB.solid_phase.properties[:, x0].temperature,
        m.fs.MB.solid_phase.properties[:, x0].flow_mass,
        m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Fe2O3"],
        m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Fe3O4"],
        m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Al2O3"],
    ]
    state_refs = [pyo.Reference(slice_) for slice_ in state_slices]
    state_refs.extend(extra_states)
    for i, ref in enumerate(state_refs):
        fig, ax = plot_time_indexed_variable(ref)
        fig.savefig(prefix + "state%s.png" % i, transparent=True)
        if show:
            plt.show()
Exemple #10
0
def test_tag_group(model):
    m = model
    g = ModelTagGroup()
    m.rw = pyo.Reference(m.w[:, "a"])
    g.add("w",
          expr=m.rw,
          format_string="{:.3f}",
          doc="make sure this works",
          display_units=pyo.units.g)
    g.add("x", expr=m.x, format_string="{:.3f}")
    g.add("y", expr=m.y, format_string="{:.3f}")
    g.add("z", expr=m.z, format_string="{:.3f}")
    g.add("e", expr=m.e, format_string="{:.3f}")
    g.add("f", ModelTag(expr=m.f, format_string="{:.3f}"))
    g.add("g", expr=m.g, format_string="{:.1f}", display_units="%")

    assert g["w"].doc == "make sure this works"
    g.str_include_units = True

    assert g.str_include_units
    assert g["w"].str_include_units
    assert not g.set_in_display_units
    assert not g["w"].set_in_display_units
    g["w"].fix(2)
    g["x"].fix(1)
    g["y"].fix(3)
    assert str(g["w"][1]) == "2000.000 g"
    assert str(g["x"][1]) == "1.000 kg"
    assert str(g["y"]) == "3.000 s"

    g.str_include_units = False
    assert str(g["w"][1]) == "2000.000"
    assert str(g["x"][1]) == "1.000"
    assert str(g["y"]) == "3.000"

    g.set_in_display_units = True
    g["w"][:].set(1000)
    assert str(g["w"][1]) == "1000.000"
    assert str(g["w"][2]) == "1000.000"
    assert str(g["w"][2]) == "1000.000"

    g.set_in_display_units = True
    g["w"][:].set(2 * pyo.units.kg)
    assert str(g["w"][1]) == "2000.000"
    assert str(g["w"][2]) == "2000.000"
    assert str(g["w"][2]) == "2000.000"

    g["w"][:].fix(3 * pyo.units.kg)
    assert str(g["w"][1]) == "3000.000"
    assert str(g["w"][2]) == "3000.000"
    assert str(g["w"][2]) == "3000.000"

    g["w"][:].fix(4000)
    assert str(g["w"][1]) == "4000.000"
    assert str(g["w"][2]) == "4000.000"
    assert str(g["w"][2]) == "4000.000"
Exemple #11
0
def generate_diff_deriv_disc_components_along_set(m, set_, active=True):
    for var in m.component_objects(pyo.Var, active=active):
        if (isinstance(var, dae.DerivativeVar)
                and set_ in ComponentSet(var.get_continuousset_list())):
            block = var.parent_block()
            con = block.find_component(var.local_name + DAE_DISC_SUFFIX)
            state = var.get_state_var()
            deriv_map = dict(
                (idx, pyo.Reference(slice_))
                for idx, slice_ in slice_component_along_sets(var, (set_, )))
            disc_map = dict(
                (idx, pyo.Reference(slice_))
                for idx, slice_ in slice_component_along_sets(con, (set_, )))
            state_map = dict(
                (idx, pyo.Reference(slice_))
                for idx, slice_ in slice_component_along_sets(state, (set_, )))
            if not active or con.active:
                for idx, deriv in deriv_map.items():
                    yield state_map[idx], deriv, disc_map[idx]
Exemple #12
0
def make_model():
    m = pyo.ConcreteModel()
    m.time = pyo.Set(initialize=range(11))
    m.space = pyo.Set(initialize=range(3))
    m.comp = pyo.Set(initialize=['a', 'b'])

    m.v1 = pyo.Var(m.time, m.space)
    m.v2 = pyo.Var(m.time, m.space, m.comp)

    @m.Block(m.time, m.space)
    def b(b, t, x):
        b.v3 = pyo.Var()
        b.v4 = pyo.Var(m.comp)

    m.v1_refs = [pyo.Reference(m.v1[:, x]) for x in m.space]
    m.v2a_refs = [pyo.Reference(m.v2[:, x, 'a']) for x in m.space]
    m.v2b_refs = [pyo.Reference(m.v2[:, x, 'b']) for x in m.space]
    m.v3_refs = [pyo.Reference(m.b[:, x].v3) for x in m.space]
    m.v4a_refs = [pyo.Reference(m.b[:, x].v4['a']) for x in m.space]
    m.v4b_refs = [pyo.Reference(m.b[:, x].v4['a']) for x in m.space]

    for t in m.time:
        for ref in m.v1_refs:
            ref[t] = 1.0 * t
        for ref in m.v2a_refs + m.v2b_refs:
            ref[t] = 2.0 * t
        for ref in m.v3_refs:
            ref[t] = 3.0 * t
        for ref in m.v4a_refs + m.v4b_refs:
            ref[t] = 4.0 * t

    return m
    def test_initialize_only_measurement_input(self):
        model = make_model(horizon=1, nfe=2)
        time = model.time
        t0 = time.first()
        inputs = [model.flow_in]
        measurements = [
            pyo.Reference(model.conc[:, 'A']),
            pyo.Reference(model.conc[:, 'B']),
        ]
        category_dict = {
            VC.INPUT: inputs,
            VC.MEASUREMENT: measurements,
        }
        db = DynamicBlock(
            model=model,
            time=time,
            category_dict={None: category_dict},
        )
        db.construct()
        db.set_sample_time(0.5)

        db.mod.flow_in[:].set_value(3.0)
        initialize_t0(db.mod)
        copy_values_forward(db.mod)
        db.mod.flow_in[:].set_value(2.0)

        # Don't need to know any of the special categories to initialize
        # by element. This is only because we have an implicit discretization.
        db.initialize_by_solving_elements(solver)

        t0 = time.first()
        tl = time.last()
        vectors = db.vectors
        assert vectors.input[0, tl].value == 2.0
        assert vectors.measurement[0, tl].value == pytest.approx(
            3.185595567867036)
        assert vectors.measurement[1, tl].value == pytest.approx(
            1.1532474073395755)
        assert model.dcdt[tl, 'A'].value == pytest.approx(0.44321329639889284)
        assert model.dcdt[tl, 'B'].value == pytest.approx(0.8791007531878847)
Exemple #14
0
def test_NmpcVector():
    m = pyo.ConcreteModel()
    m.coords = pyo.Set(initialize=[0, 1, 2, 3])
    m.time = pyo.Set(initialize=[0.0, 0.5, 1.0, 1.5, 2.0])

    @m.Block(m.coords)
    def b(b, i):
        b.var = NmpcVar(m.time)

    m.vector = pyo.Reference(m.b[:].var[:], ctype=_NmpcVector)

    assert type(m.vector) is _NmpcVector

    # Test that `vector` is a proper reference
    for i, t in m.coords * m.time:
        assert m.vector[i, t] is m.b[i].var[t]

    # Test that we can generate the underlying NmpcVars
    for v, i in zip(m.vector._generate_referenced_vars(), m.coords):
        assert v is m.b[i].var

    # `set_setpoint`
    m.vector.set_setpoint(3.14)
    for i in m.coords:
        assert m.b[i].var.setpoint == 3.14

    setpoint = tuple(i / 10. for i in m.coords)
    m.vector.set_setpoint(setpoint)
    for i, sp in zip(m.coords, setpoint):
        assert m.b[i].var.setpoint == sp

    # `get_setpoint`
    sp_get = tuple(m.vector.get_setpoint())
    assert setpoint == sp_get

    # `set_values`
    val = -1.
    m.vector.values = -1.
    for i, t in m.coords * m.time:
        assert m.b[i].var[t].value == val

    newvals = tuple(i * 1.1 for i in m.coords)
    m.vector.values = newvals
    for i, val in zip(m.coords, newvals):
        for t in m.time:
            assert m.b[i].var[t].value == val

    # `get_values`
    val_lil = m.vector.values
    for var_values, target_val in zip(val_lil, newvals):
        for var_val in var_values:
            assert var_val == target_val
def reshape(m):
    time = m.time

    # Create block to hold time-decomposition
    m.time_block = pyo.Block(time)

    # Identify time-indexed components
    scalar_vars, dae_vars = flatten_dae_components(m, time, pyo.Var)
    scalar_cons, dae_cons = flatten_dae_components(m, time, pyo.Constraint)

    for t in time:
        b = m.time_block[t]
        var_list = []
        b.vars = pyo.Reference([var[t] for var in dae_vars])
        con_list = []
        for con in dae_cons:
            try:
                condata = con[t]
                con_list.append(condata)
            except KeyError:
                # For discretization equations, which are skipped at t0
                pass
        b.cons = pyo.Reference(con_list)
Exemple #16
0
def TagReference(s, description=""):
    """
    Create a Pyomo reference with an added description string attribute to
    describe the reference. The intended use for these references is to create a
    time-indexed reference to variables in a model corresponding to plant
    measurment tags.

    Args:
        s: Pyomo time slice of a variable or expression
        description (str): A description the measurment

    Returns:
        A Pyomo Reference object with an added doc attribute
    """
    r = pyo.Reference(s)
    r.description = description
    return r
Exemple #17
0
    def build(self):
        """
        Add model equations to the unit model.  This is called by a default block
        construnction rule when the unit model is created.
        """
        super().build()  # Basic unit model build/read config
        config = self.config  # shorter config pointer

        # The thermodynamic expression writer object, te, writes expressions
        # including external function calls to calculate thermodynamic quantities
        # from a set of state variables.
        _assert_properties(config.property_package)
        te = ThermoExpr(blk=self, parameters=config.property_package)

        eff = self.efficiency_pump = pyo.Var(self.flowsheet().config.time,
                                             initialize=0.9,
                                             doc="Pump efficiency")
        self.efficiency_isentropic = pyo.Reference(self.efficiency_pump[:])

        pratio = self.ratioP = pyo.Var(self.flowsheet().config.time,
                                       initialize=0.7,
                                       doc="Ratio of outlet to inlet pressure")

        # Some shorter refernces to property blocks
        properties_in = self.control_volume.properties_in
        properties_out = self.control_volume.properties_out

        @self.Expression(self.flowsheet().config.time,
                         doc="Thermodynamic work")
        def work_fluid(b, t):
            return properties_out[t].flow_vol * (self.deltaP[t])

        @self.Expression(self.flowsheet().config.time,
                         doc="Work required to drive the pump.")
        def shaft_work(b, t):  # Early access to the outlet enthalpy and work
            return self.work_fluid[t] / eff[t]

        @self.Constraint(self.flowsheet().config.time)
        def eq_work(b, t):  # outlet enthalpy coens from energy balance
            return self.control_volume.work[t] == self.shaft_work[t]

        @self.Constraint(self.flowsheet().config.time)
        def eq_pressure_ratio(b, t):
            return (pratio[t] *
                    properties_in[t].pressure == properties_out[t].pressure)
def test_9():
    # test with references the way we handle time indexing a lot in IDAES
    rp = pyo.TransformationFactory("replace_variables")
    block_set = {1,2,3}
    m = pyo.ConcreteModel()
    m.b1 = pyo.Block(block_set)
    for i in block_set:
        m.b1[i].x = pyo.Var(initialize=2)
    m.y = pyo.Var([1,2,3], initialize=3)
    m.xx = pyo.Reference(m.b1[:].x)
    m.display()

    m.e1 = pyo.Expression(expr=sum(m.xx[i] for i in m.xx))
    m.e2 = pyo.Expression(expr=sum(m.b1[i].x for i in m.b1))

    assert(pyo.value(m.e1) == 6)
    assert(pyo.value(m.e2) == 6)
    rp.apply_to(m, substitute=[(m.xx, m.y)])
    assert(pyo.value(m.e1) == 9)
    assert(pyo.value(m.e2) == 9)
Exemple #19
0
    def m(self):
        m = pyo.ConcreteModel()
        m.a = pyo.Var(initialize=0.25, bounds=(-0.5, 0.5))
        m.b = b = pyo.Block()
        b.a = pyo.Var([1, 2], bounds=(-10, 10))

        m.c = pyo.Constraint(expr=(0, 1. / (m.a**2), 100))
        b.c = pyo.Constraint([1, 2], rule=lambda b, i: (i - 1, b.a[i], i + 2))
        b.d = pyo.Constraint(expr=b.a[1]**4 + b.a[2]**4 <= 4)
        set_scaling_factor(b.d, 1e6)

        b.o = pyo.Expression(expr=sum(b.a)**2)

        m.o = pyo.Objective(expr=m.a + b.o)

        # references are tricky, could cause a variable
        # to be iterated over several times in
        # component_data_objects
        m.b_a = pyo.Reference(b.a)

        return m
Exemple #20
0
def _valve_pressure_flow_cb(b):
    """
    For vapor F = Cv*sqrt(Pi**2 - Po**2)*f(x)
    """
    umeta = b.config.property_package.get_metadata().get_derived_units

    b.Cv = pyo.Var(initialize=0.1,
                   doc="Valve flow coefficent",
                   units=umeta("amount") / umeta("time") / umeta("pressure"))
    b.Cv.fix()

    b.flow_var = pyo.Reference(b.control_volume.properties_in[:].flow_mol)
    b.pressure_flow_equation_scale = lambda x: x**2

    @b.Constraint(b.flowsheet().time)
    def pressure_flow_equation(b2, t):
        Po = b2.control_volume.properties_out[t].pressure
        Pi = b2.control_volume.properties_in[t].pressure
        F = b2.control_volume.properties_in[t].flow_mol
        Cv = b2.Cv
        fun = b2.valve_function[t]
        return F**2 == Cv**2 * (Pi**2 - Po**2) * fun**2
Exemple #21
0
def update_metadata_model_references(model, metadata):
    """
    Create model references from refernce strings in the metadata. This updates
    the 'reference' field in the metadata.

    Args:
        model (pyomo.environ.Block): Pyomo model
        metadata (dict): Tag metadata dictionary

    Returns:
        None
    """
    for tag, md in metadata.items():
        if md["reference_string"]:
            try:
                md["reference"] = pyo.Reference(
                    eval(md["reference_string"], {"m": model}))
            except (KeyError, AttributeError, NameError):
                warnings.warn(
                    "Tag reference {} not found".format(
                        md["reference_string"]),
                    UserWarning,
                )
Exemple #22
0
def main(plot_switch=False):

    # This tests the same model constructed in the test_nmpc_constructor_1 file
    m_controller = make_model(horizon=3, ntfe=30, ntcp=2, bounds=True)
    sample_time = 0.5
    m_plant = make_model(horizon=sample_time, ntfe=5, ntcp=2)
    time_plant = m_plant.fs.time

    solve_consistent_initial_conditions(m_plant, time_plant, solver)

    #####
    # Flatten and categorize controller model
    #####
    model = m_controller
    time = model.fs.time
    t0 = time.first()
    t1 = time[2]
    scalar_vars, dae_vars = flatten_dae_components(
        model,
        time,
        pyo.Var,
    )
    scalar_cons, dae_cons = flatten_dae_components(
        model,
        time,
        pyo.Constraint,
    )
    inputs = [
        model.fs.mixer.S_inlet.flow_vol,
        model.fs.mixer.E_inlet.flow_vol,
    ]
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
        model.fs.cstr.outlet.temperature,
    ]
    model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix()
    model.fs.cstr.total_flow_balance.deactivate()
    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    controller = ControllerBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    controller.construct()

    solve_consistent_initial_conditions(m_controller, time, solver)
    controller.initialize_to_initial_conditions()

    m_controller._dummy_obj = pyo.Objective(expr=0)
    nlp = PyomoNLP(m_controller)
    igraph = IncidenceGraphInterface(nlp)
    m_controller.del_component(m_controller._dummy_obj)
    diff_vars = [var[t1] for var in var_partition[VC.DIFFERENTIAL]]
    alg_vars = [var[t1] for var in var_partition[VC.ALGEBRAIC]]
    deriv_vars = [var[t1] for var in var_partition[VC.DERIVATIVE]]
    diff_eqns = [con[t1] for con in con_partition[CC.DIFFERENTIAL]]
    alg_eqns = [con[t1] for con in con_partition[CC.ALGEBRAIC]]

    # Assemble and factorize "derivative Jacobian"
    dfdz = nlp.extract_submatrix_jacobian(diff_vars, diff_eqns)
    dfdy = nlp.extract_submatrix_jacobian(alg_vars, diff_eqns)
    dgdz = nlp.extract_submatrix_jacobian(diff_vars, alg_eqns)
    dgdy = nlp.extract_submatrix_jacobian(alg_vars, alg_eqns)
    dfdzdot = nlp.extract_submatrix_jacobian(deriv_vars, diff_eqns)
    fact = sps.linalg.splu(dgdy.tocsc())
    dydz = fact.solve(dgdz.toarray())
    deriv_jac = dfdz - dfdy.dot(dydz)
    fact = sps.linalg.splu(dfdzdot.tocsc())
    dzdotdz = -fact.solve(deriv_jac)

    # Use some heuristic on the eigenvalues of the derivative Jacobian
    # to identify fast states.
    w, V = np.linalg.eig(dzdotdz)
    w_max = np.max(np.abs(w))
    fast_modes, = np.where(np.abs(w) > w_max / 2)
    fast_states = []
    for idx in fast_modes:
        evec = V[:, idx]
        _fast_states, _ = np.where(np.abs(evec) > 0.5)
        fast_states.extend(_fast_states)
    fast_states = set(fast_states)

    # Store components necessary for model reduction in a model-
    # independent form.
    fast_state_derivs = [
        pyo.ComponentUID(var_partition[VC.DERIVATIVE][idx].referent,
                         context=model) for idx in fast_states
    ]
    fast_state_diffs = [
        pyo.ComponentUID(var_partition[VC.DIFFERENTIAL][idx].referent,
                         context=model) for idx in fast_states
    ]
    fast_state_discs = [
        pyo.ComponentUID(con_partition[CC.DISCRETIZATION][idx].referent,
                         context=model) for idx in fast_states
    ]

    # Perform pseudo-steady state model reduction on the fast states
    # and re-categorize
    for cuid in fast_state_derivs:
        var = cuid.find_component_on(m_controller)
        var.fix(0.0)
    for cuid in fast_state_diffs:
        var = cuid.find_component_on(m_controller)
        var[t0].unfix()
    for cuid in fast_state_discs:
        con = cuid.find_component_on(m_controller)
        con.deactivate()

    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    controller.del_component(model)

    # Re-construct controller block with new categorization
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
    ]
    controller = ControllerBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    controller.construct()

    #####
    # Construct dynamic block for plant
    #####
    model = m_plant
    time = model.fs.time
    t0 = time.first()
    t1 = time[2]
    scalar_vars, dae_vars = flatten_dae_components(
        model,
        time,
        pyo.Var,
    )
    scalar_cons, dae_cons = flatten_dae_components(
        model,
        time,
        pyo.Constraint,
    )
    inputs = [
        model.fs.mixer.S_inlet.flow_vol,
        model.fs.mixer.E_inlet.flow_vol,
    ]
    measurements = [
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']),
        pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']),
    ]
    model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix()
    model.fs.cstr.total_flow_balance.deactivate()

    var_partition, con_partition = categorize_dae_variables_and_constraints(
        model,
        dae_vars,
        dae_cons,
        time,
        input_vars=inputs,
    )
    plant = DynamicBlock(
        model=model,
        time=time,
        measurements=measurements,
        category_dict={None: var_partition},
    )
    plant.construct()

    p_t0 = plant.time.first()
    c_t0 = controller.time.first()
    p_ts = plant.sample_points[1]
    c_ts = controller.sample_points[1]

    controller.set_sample_time(sample_time)
    plant.set_sample_time(sample_time)

    # We now perform the "RTO" calculation: Find the optimal steady state
    # to achieve the following setpoint
    setpoint = [
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 0.4),
        #(controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.01),
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.1),
        (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 300),
        (controller.mod.fs.mixer.E_inlet.flow_vol[0], 0.1),
        (controller.mod.fs.mixer.S_inlet.flow_vol[0], 2.0),
        (controller.mod.fs.cstr.volume[0], 1.0),
    ]
    setpoint_weights = [
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 1.),
        (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 1.),
        (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 1.),
        (controller.mod.fs.mixer.E_inlet.flow_vol[0], 1.),
        (controller.mod.fs.mixer.S_inlet.flow_vol[0], 1.),
        (controller.mod.fs.cstr.volume[0], 1.),
    ]

    # Some of the "differential variables" that have been fixed in the
    # model file are different from the measurements listed above. We
    # unfix them here so the RTO solve is not overconstrained.
    # (The RTO solve will only automatically unfix inputs and measurements.)
    controller.mod.fs.cstr.control_volume.material_holdup[0, ...].unfix()
    controller.mod.fs.cstr.control_volume.energy_holdup[0, ...].unfix()
    #controller.mod.fs.cstr.volume[0].unfix()
    controller.mod.fs.cstr.control_volume.material_holdup[0, 'aq',
                                                          'Solvent'].fix()

    controller.add_setpoint_objective(setpoint, setpoint_weights)
    controller.solve_setpoint(solver)

    # Now we are ready to construct the tracking NMPC problem
    tracking_weights = [
        *((v, 1.) for v in controller.vectors.differential[:, 0]),
        *((v, 1.) for v in controller.vectors.input[:, 0]),
    ]

    controller.add_tracking_objective(tracking_weights)

    controller.constrain_control_inputs_piecewise_constant()

    controller.initialize_to_initial_conditions()

    # Solve the first control problem
    controller.vectors.input[...].unfix()
    controller.vectors.input[:, 0].fix()
    solver.solve(controller, tee=True)

    # For a proper NMPC simulation, we must have noise.
    # We do this by treating inputs and measurements as Gaussian random
    # variables with the following variances (and bounds).
    cstr = controller.mod.fs.cstr
    variance = [
        (cstr.outlet.conc_mol[0.0, 'S'], 0.01),
        (cstr.outlet.conc_mol[0.0, 'E'], 0.005),
        (cstr.outlet.conc_mol[0.0, 'C'], 0.01),
        (cstr.outlet.conc_mol[0.0, 'P'], 0.005),
        (cstr.outlet.temperature[0.0], 1.),
        (cstr.volume[0.0], 0.05),
    ]
    controller.set_variance(variance)
    measurement_variance = [
        v.variance for v in controller.MEASUREMENT_BLOCK[:].var
    ]
    measurement_noise_bounds = [(0.0, var[c_t0].ub)
                                for var in controller.MEASUREMENT_BLOCK[:].var]

    mx = plant.mod.fs.mixer
    variance = [
        (mx.S_inlet_state[0.0].flow_vol, 0.02),
        (mx.E_inlet_state[0.0].flow_vol, 0.001),
    ]
    plant.set_variance(variance)
    input_variance = [v.variance for v in plant.INPUT_BLOCK[:].var]
    input_noise_bounds = [(0.0, var[p_t0].ub)
                          for var in plant.INPUT_BLOCK[:].var]

    random.seed(100)

    # Extract inputs from controller and inject them into plant
    inputs = controller.generate_inputs_at_time(c_ts)
    plant.inject_inputs(inputs)

    # This "initialization" really simulates the plant with the new inputs.
    plant.vectors.input[:, :].fix()
    plant.initialize_by_solving_elements(solver)
    plant.vectors.input[:, :].fix()
    solver.solve(plant, tee=True)

    for i in range(1, 11):
        print('\nENTERING NMPC LOOP ITERATION %s\n' % i)
        measured = plant.generate_measurements_at_time(p_ts)
        plant.advance_one_sample()
        plant.initialize_to_initial_conditions()
        measured = apply_noise_with_bounds(
            measured,
            measurement_variance,
            random.gauss,
            measurement_noise_bounds,
        )

        controller.advance_one_sample()
        controller.load_measurements(measured)

        solver.solve(controller, tee=True)

        inputs = controller.generate_inputs_at_time(c_ts)
        inputs = apply_noise_with_bounds(
            inputs,
            input_variance,
            random.gauss,
            input_noise_bounds,
        )
        plant.inject_inputs(inputs)

        plant.initialize_by_solving_elements(solver)
        solver.solve(plant)

    import pdb
    pdb.set_trace()
Exemple #23
0
    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        ########################################################################
        #  Call UnitModel.build to setup dynamics and configure                #
        ########################################################################
        super().build()
        self._process_config()
        config = self.config
        time = self.flowsheet().config.time

        ########################################################################
        # Add control volumes                                                  #
        ########################################################################
        hot_side = _make_heater_control_volume(
            self,
            config.hot_side_name,
            config.hot_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        cold_side = _make_heater_control_volume(
            self,
            config.cold_side_name,
            config.cold_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        # Add refernces to the hot side and cold side, so that we have solid
        # names to refere to internally.  side_1 and side_2 also maintain
        # compatability with older models.  Using add_object_reference keeps
        # these from showing up when you iterate through pyomo compoents in a
        # model, so only the user specified control volume names are "seen"
        if not hasattr(self, "side_1"):
            add_object_reference(self, "side_1", hot_side)
        if not hasattr(self, "side_2"):
            add_object_reference(self, "side_2", cold_side)
        if not hasattr(self, "hot_side"):
            add_object_reference(self, "hot_side", hot_side)
        if not hasattr(self, "cold_side"):
            add_object_reference(self, "cold_side", cold_side)

        ########################################################################
        # Add variables                                                        #
        ########################################################################
        # Use hot side units as basis
        s1_metadata = config.hot_side_config.property_package.get_metadata()

        f_units = s1_metadata.get_derived_units("flow_mole")
        cp_units = s1_metadata.get_derived_units("heat_capacity_mole")
        q_units = s1_metadata.get_derived_units("power")
        u_units = s1_metadata.get_derived_units("heat_transfer_coefficient")
        a_units = s1_metadata.get_derived_units("area")
        temp_units = s1_metadata.get_derived_units("temperature")

        self.overall_heat_transfer_coefficient = pyo.Var(
            time,
            domain=pyo.PositiveReals,
            initialize=100.0,
            doc="Overall heat transfer coefficient",
            units=u_units,
        )
        self.area = pyo.Var(
            domain=pyo.PositiveReals,
            initialize=1000.0,
            doc="Heat exchange area",
            units=a_units,
        )
        self.heat_duty = pyo.Reference(cold_side.heat)
        ########################################################################
        # Add ports                                                            #
        ########################################################################
        i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet",
                                 block=hot_side,
                                 doc="Hot side inlet")
        i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet",
                                 block=cold_side,
                                 doc="Cold side inlet")
        o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet",
                                  block=hot_side,
                                  doc="Hot side outlet")
        o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet",
                                  block=cold_side,
                                  doc="Cold side outlet")

        # Using Andrew's function for now.  I want these port names for backward
        # compatablity, but I don't want them to appear if you iterate throught
        # components and add_object_reference hides them from Pyomo.
        if not hasattr(self, "inlet_1"):
            add_object_reference(self, "inlet_1", i1)
        if not hasattr(self, "inlet_2"):
            add_object_reference(self, "inlet_2", i2)
        if not hasattr(self, "outlet_1"):
            add_object_reference(self, "outlet_1", o1)
        if not hasattr(self, "outlet_2"):
            add_object_reference(self, "outlet_2", o2)

        if not hasattr(self, "hot_inlet"):
            add_object_reference(self, "hot_inlet", i1)
        if not hasattr(self, "cold_inlet"):
            add_object_reference(self, "cold_inlet", i2)
        if not hasattr(self, "hot_outlet"):
            add_object_reference(self, "hot_outlet", o1)
        if not hasattr(self, "cold_outlet"):
            add_object_reference(self, "cold_outlet", o2)

        ########################################################################
        # Add a unit level energy balance                                      #
        ########################################################################
        @self.Constraint(time, doc="Heat balance equation")
        def unit_heat_balance(b, t):
            return 0 == (hot_side.heat[t] +
                         pyunits.convert(cold_side.heat[t], to_units=q_units))

        ########################################################################
        # Add some useful expressions for condenser performance                #
        ########################################################################

        @self.Expression(time, doc="Inlet temperature difference")
        def delta_temperature_in(b, t):
            return (hot_side.properties_in[t].temperature - pyunits.convert(
                cold_side.properties_in[t].temperature, temp_units))

        @self.Expression(time, doc="Outlet temperature difference")
        def delta_temperature_out(b, t):
            return (hot_side.properties_out[t].temperature - pyunits.convert(
                cold_side.properties_out[t].temperature, temp_units))

        @self.Expression(time, doc="NTU Based temperature difference")
        def delta_temperature_ntu(b, t):
            return (hot_side.properties_in[t].temperature_sat -
                    pyunits.convert(cold_side.properties_in[t].temperature,
                                    temp_units))

        @self.Expression(
            time,
            doc="Minimum product of flow rate and heat "
            "capacity (always on tube side since shell side has phase change)")
        def mcp_min(b, t):
            return pyunits.convert(
                cold_side.properties_in[t].flow_mol *
                cold_side.properties_in[t].cp_mol_phase['Liq'],
                f_units * cp_units)

        @self.Expression(time, doc="Number of transfer units (NTU)")
        def ntu(b, t):
            return b.overall_heat_transfer_coefficient[t] * b.area / b.mcp_min[
                t]

        @self.Expression(time, doc="Condenser effectiveness factor")
        def effectiveness(b, t):
            return 1 - pyo.exp(-self.ntu[t])

        @self.Expression(time, doc="Heat treansfer")
        def heat_transfer(b, t):
            return b.effectiveness[t] * b.mcp_min[t] * b.delta_temperature_ntu[
                t]

        ########################################################################
        # Add Equations to calculate heat duty based on NTU method             #
        ########################################################################
        @self.Constraint(time,
                         doc="Heat transfer rate equation based on NTU method")
        def heat_transfer_equation(b, t):
            return (pyunits.convert(cold_side.heat[t],
                                    q_units) == self.heat_transfer[t])

        @self.Constraint(
            time, doc="Shell side outlet enthalpy is saturated water enthalpy")
        def saturation_eqn(b, t):
            return (hot_side.properties_out[t].enth_mol ==
                    hot_side.properties_in[t].enth_mol_sat_phase["Liq"])
Exemple #24
0
    def build(self):
        """
        Build the PID block
        """
        if isinstance(self.flowsheet().time, ContinuousSet):
            # time may not be a continuous set if you have a steady state model
            # in the steady state model case obviously the controller should
            # not be active, but you can still add it.
            if 'scheme' not in self.flowsheet().time.get_discretization_info():
                # if you have a dynamic model, must do time discretization
                # before adding the PID model
                raise RunTimeError(
                    "PIDBlock must be added after time discretization")

        super().build()  # do the ProcessBlockData voodoo for config
        # Check for required config
        if self.config.pv is None:
            raise ConfigurationError("Controller configuration requires 'pv'")
        if self.config.output is None:
            raise ConfigurationError(
                "Controller configuration requires 'output'")
        # Shorter pointers to time set information
        time_set = self.flowsheet().time
        t0 = time_set.first()
        # Variable for basic controller settings may change with time.
        self.setpoint = pyo.Var(time_set, doc="Setpoint")
        self.gain = pyo.Var(time_set, doc="Controller gain")
        self.time_i = pyo.Var(time_set, doc="Integral time")
        self.time_d = pyo.Var(time_set, doc="Derivative time")
        # Make the initial derivative term a variable so you can set it. This
        # should let you carry on from the end of another time period
        self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0)
        self.err_d0.fix()
        if not self.config.calculate_initial_integral:
            self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0)
            self.err_i0.fix()
        # Make references to the output and measured variables
        self.pv = pyo.Reference(self.config.pv)  # No duplicate
        self.output = pyo.Reference(self.config.output)  # No duplicate
        # Create an expression for error from setpoint
        @self.Expression(time_set, doc="Setpoint error")
        def err(b, t):
            return self.setpoint[t] - self.pv[t]

        # Use expressions to allow the some future configuration
        @self.Expression(time_set)
        def pterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def dterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def iterm(b, t):
            return self.err[t]

        # Output limits parameter
        self.limits = pyo.Param(["l", "h"],
                                mutable=True,
                                doc="controller output limits",
                                initialize={
                                    "l": self.config.lower,
                                    "h": self.config.upper
                                })
        # Smooth min and max are used to limit output, smoothing parameter here
        self.smooth_eps = pyo.Param(
            mutable=True,
            initialize=1e-4,
            doc="Smoothing parameter for controller output limits")
        # This is ugly, but want integral and derivative error as expressions,
        # nice implementation with variables is harder to initialize and solve
        @self.Expression(time_set, doc="Derivative error.")
        def err_d(b, t):
            if t == t0:
                return self.err_d0
            else:
                return (b.dterm[t] - b.dterm[time_set.prev(t)])\
                       /(t - time_set.prev(t))

        if self.config.pid_form == PIDForm.standard:
            self._build_standard(time_set, t0)
        else:
            self._build_velocity(time_set, t0)

        # Add the controller output constraint and limit it with smooth min/max
        e = self.smooth_eps
        h = self.limits["h"]
        l = self.limits["l"]

        @self.Constraint(time_set, doc="Controller output constraint")
        def output_constraint(b, t):
            if t == t0:
                return pyo.Constraint.Skip
            else:
                return self.output[t] ==\
                    smooth_min(
                        smooth_max(self.unconstrained_output[t], l, e), h, e)
Exemple #25
0
    def test_scaling_without_rename(self):
        m = pyo.ConcreteModel()
        m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
        m.v1 = pyo.Var(initialize=10)
        m.v2 = pyo.Var(initialize=20)
        m.v3 = pyo.Var(initialize=30)

        def c1_rule(m):
            return m.v1 == 1e6
        m.c1 = pyo.Constraint(rule=c1_rule)
        def c2_rule(m):
            return m.v2 == 1e-4
        m.c2 = pyo.Constraint(rule=c2_rule)

        m.scaling_factor[m.v1] = 1.0
        m.scaling_factor[m.v2] = 0.5
        m.scaling_factor[m.v3] = 0.25
        m.scaling_factor[m.c1] = 1e-5
        m.scaling_factor[m.c2] = 1e5

        values = {}
        values[id(m.v1)] = (m.v1.value, m.scaling_factor[m.v1])
        values[id(m.v2)] = (m.v2.value, m.scaling_factor[m.v2])
        values[id(m.v3)] = (m.v3.value, m.scaling_factor[m.v3])
        values[id(m.c1)] = (pyo.value(m.c1.body), m.scaling_factor[m.c1])
        values[id(m.c2)] = (pyo.value(m.c2.body), m.scaling_factor[m.c2])

        m.c2_ref = pyo.Reference(m.c2)
        m.v3_ref = pyo.Reference(m.v3)

        scale = pyo.TransformationFactory('core.scale_model')
        scale.apply_to(m, rename=False)

        self.assertTrue(hasattr(m, 'v1'))
        self.assertTrue(hasattr(m, 'v2'))
        self.assertTrue(hasattr(m, 'c1'))
        self.assertTrue(hasattr(m, 'c2'))

        orig_val, factor = values[id(m.v1)]
        self.assertAlmostEqual(
                m.v1.value,
                orig_val*factor,
                )

        orig_val, factor = values[id(m.v2)]
        self.assertAlmostEqual(
                m.v2.value,
                orig_val*factor,
                )

        orig_val, factor = values[id(m.c1)]
        self.assertAlmostEqual(
                pyo.value(m.c1.body),
                orig_val*factor,
                )

        orig_val, factor = values[id(m.c2)]
        self.assertAlmostEqual(
                pyo.value(m.c2.body),
                orig_val*factor,
                )   

        orig_val, factor = values[id(m.v3)]
        self.assertAlmostEqual(
                m.v3_ref[None].value,
                orig_val*factor,
                )
        # Note that because the model was not renamed,
        # v3_ref is still intact.

        lhs = m.c2.body
        monom_factor = lhs.arg(0)
        scale_factor = (m.scaling_factor[m.c2]/
                        m.scaling_factor[m.v2])
        self.assertAlmostEqual(
                monom_factor,
                scale_factor,
                )
Exemple #26
0
    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 = Mixer(
            default={
                "dynamic": False,
                "property_package": self.config.property_package,
                "inlet_list": ["FeedWater", "SaturatedWater"],
                "mixed_state_block": self.control_volume.properties_in,
                }
            )
        # instead of creating a new block use control volume to return solution

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

        # 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()
Exemple #27
0
    def build(self):
        """
        Build the PID block
        """
        super().build()  # do the ProcessBlockData voodoo for config
        # Check for required config
        if self.config.pv is None:
            raise ConfigurationError("Controller configuration requires 'pv'")
        if self.config.output is None:
            raise ConfigurationError(
                "Controller configuration requires 'output'")
        # Shorter pointers to time set information
        time_set = self.flowsheet().time
        t0 = time_set.first()
        # Variable for basic controller settings may change with time.
        self.setpoint = pyo.Var(time_set, doc="Setpoint")
        self.gain = pyo.Var(time_set, doc="Controller gain")
        self.time_i = pyo.Var(time_set, doc="Integral time")
        self.time_d = pyo.Var(time_set, doc="Derivative time")
        # Make the initial derivative term a variable so you can set it. This
        # should let you carry on from the end of another time period
        self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0)
        self.err_d0.fix()
        if not self.config.calculate_initial_integral:
            self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0)
            self.err_i0.fix()
        # Make references to the output and measured variables
        self.pv = pyo.Reference(self.config.pv)  # No duplicate
        self.output = pyo.Reference(self.config.output)  # No duplicate
        # Create an expression for error from setpoint
        @self.Expression(time_set, doc="Setpoint error")
        def err(b, t):
            return self.setpoint[t] - self.pv[t]

        # Use expressions to allow the some future configuration
        @self.Expression(time_set)
        def pterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def dterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def iterm(b, t):
            return self.err[t]

        # Output limits parameter
        self.limits = pyo.Param(["l", "h"],
                                mutable=True,
                                doc="controller output limits",
                                initialize={
                                    "l": self.config.lower,
                                    "h": self.config.upper
                                })
        # Smooth min and max are used to limit output, smoothing parameter here
        self.smooth_eps = pyo.Param(
            mutable=True,
            initialize=1e-4,
            doc="Smoothing parameter for controller output limits")
        # This is ugly, but want integral and derivative error as expressions,
        # nice implementation with variables is harder to initialize and solve
        @self.Expression(time_set, doc="Derivative error.")
        def err_d(b, t):
            if t == t0:
                return self.err_d0
            else:
                return (b.dterm[t] - b.dterm[time_set.prev(t)])\
                       /(t - time_set.prev(t))

        # Want to fix the output varaible at the first time step to make
        # solving easier. This calculates the initial integral error to line up
        # with the initial output value, keeps the controller from initially
        # jumping.
        if self.config.calculate_initial_integral:

            @self.Expression(doc="Initial integral error")
            def err_i0(b):
                return b.time_i[t0]*(b.output[0] - b.gain[t0]*b.pterm[t0]\
                       - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0]

        # integral error
        @self.Expression(time_set, doc="Integral error")
        def err_i(b, t_end):
            return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) *
                                  (t - time_set.prev(t)) / 2.0
                                  for t in time_set if t <= t_end and t > t0)

        # Calculate the unconstrainted controller output
        @self.Expression(time_set, doc="Unconstrained contorler output")
        def unconstrained_output(b, t):
            return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] +
                                b.time_d[t] * b.err_d[t])

        # Add the controller output constraint and limit it with smooth min/max
        e = self.smooth_eps
        h = self.limits["h"]
        l = self.limits["l"]

        @self.Constraint(time_set, doc="Controller output constraint")
        def output_constraint(b, t):
            if t == t0:
                return pyo.Constraint.Skip
            else:
                return self.output[t] ==\
                    smooth_min(
                        smooth_max(self.unconstrained_output[t], l, e), h, e)
def create_model():
    """Create the flowsheet and add unit models. Fixing model inputs is done
    in a separate function to try to keep this fairly clean and easy to follow.

    Args:
        None

    Returns:
        (ConcreteModel) Steam cycle model
    """
    ############################################################################
    #  Flowsheet and Properties                                                #
    ############################################################################
    m = pyo.ConcreteModel(name="Steam Cycle Model")
    m.fs = FlowsheetBlock(default={"dynamic":
                                   False})  # Add steady state flowsheet

    # A physical property parameter block for IAPWS-95 with pressure and enthalpy
    # (PH) state variables.  Usually pressure and enthalpy state variables are
    # more robust especially when the phases are unknown.
    m.fs.prop_water = iapws95.Iapws95ParameterBlock(
        default={"phase_presentation": iapws95.PhaseType.MIX})

    # A physical property parameter block with temperature, pressure and vapor
    # fraction (TPx) state variables. There are a few instances where the vapor
    # fraction is known and the temperature and pressure state variables are
    # preferable.
    m.fs.prop_water_tpx = iapws95.Iapws95ParameterBlock(
        default={
            "phase_presentation": iapws95.PhaseType.LG,
            "state_vars": iapws95.StateVars.TPX,
        })
    ############################################################################
    #  Turbine with fill-in reheat constraints                                 #
    ############################################################################
    # The TurbineMultistage class allows creation of the full turbine model by
    # providing several configuration options, including: throttle valves;
    # high-, intermediate-, and low-pressure sections; steam extractions; and
    # pressure driven flow.  See the IDAES documentation for details.
    m.fs.turb = HelmTurbineMultistage(
        default={
            "property_package": m.fs.prop_water,
            "num_parallel_inlet_stages": 4,  # number of admission arcs
            "num_hp": 7,  # number of high-pressure stages
            "num_ip": 10,  # number of intermediate-pressure stages
            "num_lp": 11,  # number of low-pressure stages
            "hp_split_locations": [4, 7],  # hp steam extraction locations
            "ip_split_locations": [5, 10],  # ip steam extraction locations
            "lp_split_locations": [4, 8, 10, 11
                                   ],  # lp steam extraction locations
            "hp_disconnect": [7],  # disconnect hp from ip to insert reheater
            "ip_split_num_outlets": {
                10: 3
            },  # number of split streams (default is 2)
        })
    # This model is only the steam cycle, and the reheater is part of the boiler.
    # To fill in the reheater gap, a few constraints for the flow, pressure drop,
    # and outlet temperature are added. A detailed boiler model can be coupled later.
    #
    # hp_split[7] is the splitter directly after the last HP stage.  The splitter
    # outlet "outlet_1" is always taken to be the main steam flow through the turbine.
    # When the turbine model was instantiated the stream from the HP section to the IP
    # section was omitted, so the reheater could be inserted.

    # The flow constraint sets flow from outlet_1 of the splitter equal to
    # flow into the IP turbine.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_flow(b, t):
        return b.ip_stages[1].inlet.flow_mol[t] == b.hp_split[
            7].outlet_1.flow_mol[t]

    # Create a variable for pressure change in the reheater (assuming
    # reheat_delta_p should be negative).
    m.fs.turb.reheat_delta_p = pyo.Var(m.fs.time,
                                       initialize=0,
                                       units=pyo.units.Pa)

    # Add a constraint to calculate the IP section inlet pressure based on the
    # pressure drop in the reheater and the outlet pressure of the HP section.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_press(b, t):
        return (b.ip_stages[1].inlet.pressure[t] ==
                b.hp_split[7].outlet_1.pressure[t] + b.reheat_delta_p[t])

    # Create a variable for reheat temperature and fix it to the desired reheater
    # outlet temperature
    m.fs.turb.reheat_out_T = pyo.Var(m.fs.time,
                                     initialize=866,
                                     units=pyo.units.K)

    # Create a constraint for the IP section inlet temperature.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_temp(b, t):
        return (b.ip_stages[1].control_volume.properties_in[t].temperature ==
                b.reheat_out_T[t])

    ############################################################################
    #  Add Condenser/hotwell/condensate pump                                   #
    ############################################################################
    # Add a mixer for all the streams coming into the condenser.  In this case the
    # main steam, and the boiler feed pump turbine outlet go to the condenser
    m.fs.condenser_mix = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["main", "bfpt"],
            "property_package": m.fs.prop_water,
        })
    # The pressure in the mixer comes from the connection to the condenser.  All
    # the streams coming in and going out of the mixer are equal, but we created
    # the mixer with no calculation for the unit pressure. Here a constraint that
    # specifies that the mixer pressure is equal to the main steam pressure is
    # added.  There is also a constraint that specifies the that BFP turbine outlet
    # pressure is the same as the condenser pressure.  Combined with the stream
    # connections between units, these constraints effectively specify that the
    # mixer inlet and outlet streams all have the same pressure.
    @m.fs.condenser_mix.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.main_state[t].pressure == b.mixed_state[t].pressure

    # The condenser model uses the physical property model with TPx state
    # variables, while the rest of the model uses PH state variables. To
    # translate between the two property calculations, an extra port is added to
    # the mixer which contains temperature, pressure, and vapor fraction
    # quantities.
    m.fs.condenser_mix.outlet_tpx = Port(
        initialize={
            "flow_mol":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].flow_mol),
            "temperature":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].temperature),
            "pressure":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].pressure),
            "vapor_frac":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].vapor_frac),
        })

    # Add the heat exchanger model for the condenser.
    m.fs.condenser = HeatExchanger(
        default={
            "delta_temperature_callback": delta_temperature_underwood_callback,
            "shell": {
                "property_package": m.fs.prop_water_tpx
            },
            "tube": {
                "property_package": m.fs.prop_water
            },
        })
    m.fs.condenser.delta_temperature_out.fix(5)

    # Everything condenses so the saturation pressure determines the condenser
    # pressure. Deactivate the constraint that is used in the TPx version vapor
    # fraction constraint and fix vapor fraction to 0.
    m.fs.condenser.shell.properties_out[:].eq_complementarity.deactivate()
    m.fs.condenser.shell.properties_out[:].vapor_frac.fix(0)

    # There is some subcooling in the condenser, so we assume the condenser
    # pressure is actually going to be slightly higher than the saturation
    # pressure.
    m.fs.condenser.pressure_over_sat = pyo.Var(
        m.fs.time,
        initialize=500,
        doc="Pressure added to Psat in the condeser. This is to account for"
        "some subcooling. (Pa)",
        units=pyo.units.Pa)
    # Add a constraint for condenser pressure
    @m.fs.condenser.Constraint(m.fs.time)
    def eq_pressure(b, t):
        return (b.shell.properties_out[t].pressure ==
                b.shell.properties_out[t].pressure_sat +
                b.pressure_over_sat[t])

    # Extra port on condenser to hook back up to pressure-enthalpy properties
    m.fs.condenser.outlet_1_ph = Port(
        initialize={
            "flow_mol":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].flow_mol),
            "pressure":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].pressure),
            "enth_mol":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].enth_mol),
        })

    # Add the condenser hotwell.  In steady state a mixer will work.  This is
    # where makeup water is added if needed.
    m.fs.hotwell = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["condensate", "makeup"],
            "property_package": m.fs.prop_water,
        })

    # The hotwell is assumed to be at the same pressure as the condenser.
    @m.fs.hotwell.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.condensate_state[t].pressure == b.mixed_state[t].pressure

    # Condensate pump (Use compressor model, since it is more robust if vapor form)
    m.fs.cond_pump = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})
    ############################################################################
    #  Add low pressure feedwater heaters                                      #
    ############################################################################
    # All the feedwater heater sections will be set to use the Underwood
    # approximation for LMTD, so create the fwh_config dict to make the config
    # slightly cleaner
    fwh_config = {
        "delta_temperature_callback": delta_temperature_underwood_callback
    }

    # The feedwater heater model allows feedwater heaters with a desuperheat,
    # condensing, and subcooling section to be added an a reasonably simple way.
    # See the IDAES documentation for more information of configuring feedwater
    # heaters
    m.fs.fwh1 = FWH0D(
        default={
            "has_desuperheat": False,
            "has_drain_cooling": False,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "condense": fwh_config,
        })
    # pump for fwh1 condensate, to pump it ahead and mix with feedwater
    m.fs.fwh1_pump = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})
    # Mix the FWH1 drain back into the feedwater
    m.fs.fwh1_return = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["feedwater", "fwh1_drain"],
            "property_package": m.fs.prop_water,
        })

    # Set the mixer pressure to the feedwater pressure
    @m.fs.fwh1_return.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.feedwater_state[t].pressure == b.mixed_state[t].pressure

    # Add the rest of the low pressure feedwater heaters
    m.fs.fwh2 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh3 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh4 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": False,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    ############################################################################
    #  Add deaerator and boiler feed pump (BFP)                                #
    ############################################################################
    # The deaerator is basically an open tank with multiple inlets.  For steady-
    # state, a mixer model is sufficient.
    m.fs.fwh5_da = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["steam", "drain", "feedwater"],
            "property_package": m.fs.prop_water,
        })

    @m.fs.fwh5_da.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        # Not sure about deaerator pressure, so assume same as feedwater inlet
        return b.feedwater_state[t].pressure == b.mixed_state[t].pressure

    # Add the boiler feed pump and boiler feed pump turbine
    m.fs.bfp = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})

    m.fs.bfpt = HelmTurbineStage(default={"property_package": m.fs.prop_water})

    # The boiler feed pump outlet pressure is the same as the condenser
    @m.fs.Constraint(m.fs.time)
    def constraint_out_pressure(b, t):
        return (b.bfpt.control_volume.properties_out[t].pressure ==
                b.condenser.shell.properties_out[t].pressure)

    # Instead of specifying a fixed efficiency, specify that the steam is just
    # starting to condense at the outlet of the boiler feed pump turbine.  This
    # ensures approximately the right behavior in the turbine.  With a fixed
    # efficiency, depending on the conditions you can get odd things like steam
    # fully condensing in the turbine.
    @m.fs.Constraint(m.fs.time)
    def constraint_out_enthalpy(b, t):
        return (
            b.bfpt.control_volume.properties_out[t].enth_mol ==
            b.bfpt.control_volume.properties_out[t].enth_mol_sat_phase["Vap"] -
            200 * pyo.units.J / pyo.units.mol)

    # The boiler feed pump power is the same as the power generated by the
    # boiler feed pump turbine. This constraint determines the steam flow to the
    # BFP turbine. The turbine work is negative for power out, while pump work
    # is positive for power in.
    @m.fs.Constraint(m.fs.time)
    def constraint_bfp_power(b, t):
        return 0 == b.bfp.control_volume.work[t] + b.bfpt.control_volume.work[t]

    ############################################################################
    #  Add high pressure feedwater heaters                                     #
    ############################################################################
    m.fs.fwh6 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh7 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh8 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": False,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    ############################################################################
    #  Additional Constraints/Expressions                                      #
    ############################################################################

    # Add a few constraints to allow a for complete plant results despite the
    # lack of a detailed boiler model.

    # Boiler pressure drop
    m.fs.boiler_pressure_drop_fraction = pyo.Var(
        m.fs.time,
        initialize=0.01,
        doc="Fraction of pressure lost from boiler feed pump and turbine inlet",
    )

    @m.fs.Constraint(m.fs.time)
    def boiler_pressure_drop(b, t):
        return (m.fs.bfp.control_volume.properties_out[t].pressure *
                (1 - b.boiler_pressure_drop_fraction[t]) ==
                m.fs.turb.inlet_split.mixed_state[t].pressure)

    # Again, since the boiler is missing, set the flow of steam into the turbine
    # equal to the flow of feedwater out of the last feedwater heater.
    @m.fs.Constraint(m.fs.time)
    def close_flow(b, t):
        return (m.fs.bfp.control_volume.properties_out[t].flow_mol ==
                m.fs.turb.inlet_split.mixed_state[t].flow_mol)

    # Calculate the amount of heat that is added in the boiler, including the
    # reheater.
    @m.fs.Expression(m.fs.time)
    def boiler_heat(b, t):
        return (b.turb.inlet_split.mixed_state[t].enth_mol *
                b.turb.inlet_split.mixed_state[t].flow_mol -
                b.fwh8.desuperheat.tube.properties_out[t].enth_mol *
                b.fwh8.desuperheat.tube.properties_out[t].flow_mol +
                b.turb.ip_stages[1].control_volume.properties_in[t].enth_mol *
                b.turb.ip_stages[1].control_volume.properties_in[t].flow_mol -
                b.turb.hp_split[7].outlet_1.enth_mol[t] *
                b.turb.hp_split[7].outlet_1.flow_mol[t])

    # Calculate the efficiency of the steam cycle.  This doesn't account for
    # heat loss in the boiler, so actual plant efficiency would be lower.
    @m.fs.Expression(m.fs.time)
    def steam_cycle_eff(b, t):
        return -100 * b.turb.power[t] / b.boiler_heat[t]

    ############################################################################
    ##  Create the stream Arcs                                                ##
    ############################################################################

    ############################################################################
    #  Connect turbine and condenser units                                     #
    ############################################################################
    m.fs.EXHST_MAIN = Arc(source=m.fs.turb.outlet_stage.outlet,
                          destination=m.fs.condenser_mix.main)
    m.fs.condenser_mix_to_condenser = Arc(source=m.fs.condenser_mix.outlet_tpx,
                                          destination=m.fs.condenser.inlet_1)
    m.fs.COND_01 = Arc(source=m.fs.condenser.outlet_1_ph,
                       destination=m.fs.hotwell.condensate)
    m.fs.COND_02 = Arc(source=m.fs.hotwell.outlet,
                       destination=m.fs.cond_pump.inlet)
    ############################################################################
    #  Low pressure FWHs                                                       #
    ############################################################################
    m.fs.EXTR_LP11 = Arc(source=m.fs.turb.lp_split[11].outlet_2,
                         destination=m.fs.fwh1.drain_mix.steam)
    m.fs.COND_03 = Arc(source=m.fs.cond_pump.outlet,
                       destination=m.fs.fwh1.condense.inlet_2)
    m.fs.FWH1_DRN1 = Arc(source=m.fs.fwh1.condense.outlet_1,
                         destination=m.fs.fwh1_pump.inlet)
    m.fs.FWH1_DRN2 = Arc(source=m.fs.fwh1_pump.outlet,
                         destination=m.fs.fwh1_return.fwh1_drain)
    m.fs.FW01A = Arc(source=m.fs.fwh1.condense.outlet_2,
                     destination=m.fs.fwh1_return.feedwater)
    # fwh2
    m.fs.FW01B = Arc(source=m.fs.fwh1_return.outlet,
                     destination=m.fs.fwh2.cooling.inlet_2)
    m.fs.FWH2_DRN = Arc(source=m.fs.fwh2.cooling.outlet_1,
                        destination=m.fs.fwh1.drain_mix.drain)
    m.fs.EXTR_LP10 = Arc(
        source=m.fs.turb.lp_split[10].outlet_2,
        destination=m.fs.fwh2.desuperheat.inlet_1,
    )
    # fwh3
    m.fs.FW02 = Arc(source=m.fs.fwh2.desuperheat.outlet_2,
                    destination=m.fs.fwh3.cooling.inlet_2)
    m.fs.FWH3_DRN = Arc(source=m.fs.fwh3.cooling.outlet_1,
                        destination=m.fs.fwh2.drain_mix.drain)
    m.fs.EXTR_LP8 = Arc(source=m.fs.turb.lp_split[8].outlet_2,
                        destination=m.fs.fwh3.desuperheat.inlet_1)
    # fwh4
    m.fs.FW03 = Arc(source=m.fs.fwh3.desuperheat.outlet_2,
                    destination=m.fs.fwh4.cooling.inlet_2)
    m.fs.FWH4_DRN = Arc(source=m.fs.fwh4.cooling.outlet_1,
                        destination=m.fs.fwh3.drain_mix.drain)
    m.fs.EXTR_LP4 = Arc(source=m.fs.turb.lp_split[4].outlet_2,
                        destination=m.fs.fwh4.desuperheat.inlet_1)
    ############################################################################
    #  FWH5 (Deaerator) and boiler feed pump (BFP)                             #
    ############################################################################
    m.fs.FW04 = Arc(source=m.fs.fwh4.desuperheat.outlet_2,
                    destination=m.fs.fwh5_da.feedwater)
    m.fs.EXTR_IP10 = Arc(source=m.fs.turb.ip_split[10].outlet_2,
                         destination=m.fs.fwh5_da.steam)
    m.fs.FW05A = Arc(source=m.fs.fwh5_da.outlet, destination=m.fs.bfp.inlet)
    m.fs.EXTR_BFPT_A = Arc(source=m.fs.turb.ip_split[10].outlet_3,
                           destination=m.fs.bfpt.inlet)
    m.fs.EXHST_BFPT = Arc(source=m.fs.bfpt.outlet,
                          destination=m.fs.condenser_mix.bfpt)
    ############################################################################
    #  High-pressure feedwater heaters                                         #
    ############################################################################
    # fwh6
    m.fs.FW05B = Arc(source=m.fs.bfp.outlet,
                     destination=m.fs.fwh6.cooling.inlet_2)
    m.fs.FWH6_DRN = Arc(source=m.fs.fwh6.cooling.outlet_1,
                        destination=m.fs.fwh5_da.drain)
    m.fs.EXTR_IP5 = Arc(source=m.fs.turb.ip_split[5].outlet_2,
                        destination=m.fs.fwh6.desuperheat.inlet_1)
    # fwh7
    m.fs.FW06 = Arc(source=m.fs.fwh6.desuperheat.outlet_2,
                    destination=m.fs.fwh7.cooling.inlet_2)
    m.fs.FWH7_DRN = Arc(source=m.fs.fwh7.cooling.outlet_1,
                        destination=m.fs.fwh6.drain_mix.drain)
    m.fs.EXTR_HP7 = Arc(source=m.fs.turb.hp_split[7].outlet_2,
                        destination=m.fs.fwh7.desuperheat.inlet_1)
    # fwh8
    m.fs.FW07 = Arc(source=m.fs.fwh7.desuperheat.outlet_2,
                    destination=m.fs.fwh8.cooling.inlet_2)
    m.fs.FWH8_DRN = Arc(source=m.fs.fwh8.cooling.outlet_1,
                        destination=m.fs.fwh7.drain_mix.drain)
    m.fs.EXTR_HP4 = Arc(source=m.fs.turb.hp_split[4].outlet_2,
                        destination=m.fs.fwh8.desuperheat.inlet_1)

    ############################################################################
    # Turn the Arcs into constraints and return the model                      #
    ############################################################################
    pyo.TransformationFactory("network.expand_arcs").apply_to(m.fs)
    return m
Exemple #29
0
def create_model(
    steady_state=True,
    time_set=[0,3],
    nfe=5,
    calc_integ=True,
    form=PIDForm.standard
):
    """ Create a test model and solver

    Args:
        steady_state (bool): If True, create a steady state model, otherwise
            create a dynamic model
        time_set (list): The begining and end point of the time domain
        nfe (int): Number of finite elements argument for the DAE transformation
        calc_integ (bool): If Ture, calculate in the initial condition for
            the integral term, else use a fixed variable (fs.ctrl.err_i0), flase
            is the better option if you have a value from a previous time period
        form: whether the equations are written in the standard or velocity form

    Returns
        (tuple): (ConcreteModel, Solver)
    """
    if steady_state:
        fs_cfg = {"dynamic":False}
        model_name = "Steam Tank, Steady State"
    else:
        fs_cfg = {"dynamic":True, "time_set":time_set}
        model_name = "Steam Tank, Dynamic"

    m = pyo.ConcreteModel(name=model_name)
    m.fs = FlowsheetBlock(default=fs_cfg)
    # Create a property parameter block
    m.fs.prop_water = iapws95.Iapws95ParameterBlock(
        default={"phase_presentation":iapws95.PhaseType.LG})
    # Create the valve and tank models
    m.fs.valve_1 = SteamValve(default={
        "dynamic":False,
        "has_holdup":False,
        "material_balance_type":MaterialBalanceType.componentTotal,
        "property_package":m.fs.prop_water})
    m.fs.tank = Heater(default={
        "has_holdup":True,
        "material_balance_type":MaterialBalanceType.componentTotal,
        "property_package":m.fs.prop_water})
    m.fs.valve_2 = SteamValve(default={
        "dynamic":False,
        "has_holdup":False,
        "material_balance_type":MaterialBalanceType.componentTotal,
        "property_package":m.fs.prop_water})

    # Connect the models
    m.fs.v1_to_t = Arc(source=m.fs.valve_1.outlet, destination=m.fs.tank.inlet)
    m.fs.t_to_v2 = Arc(source=m.fs.tank.outlet, destination=m.fs.valve_2.inlet)

    # The control volume block doesn't assume the two phases are in equilibrium
    # by default, so I'll make that assumption here, I don't actually expect
    # liquid to form but who knows. The phase_fraction in the control volume is
    # volumetric phase fraction hence the densities.
    @m.fs.tank.Constraint(m.fs.time)
    def vol_frac_vap(b, t):
        return b.control_volume.properties_out[t].phase_frac["Vap"]\
            *b.control_volume.properties_out[t].dens_mol\
            /b.control_volume.properties_out[t].dens_mol_phase["Vap"]\
            == b.control_volume.phase_fraction[t, "Vap"]

    # Add the stream constraints and do the DAE transformation
    pyo.TransformationFactory('network.expand_arcs').apply_to(m.fs)
    if not steady_state:
        pyo.TransformationFactory('dae.finite_difference').apply_to(
            m.fs, nfe=nfe, wrt=m.fs.time, scheme='BACKWARD')

    # Fix the derivative variables to zero at time 0 (steady state assumption)
    m.fs.fix_initial_conditions()

    # A tank pressure reference that's directly time-indexed
    m.fs.tank_pressure = pyo.Reference(
        m.fs.tank.control_volume.properties_out[:].pressure)

    # Add a controller
    m.fs.ctrl = PIDBlock(default={"pv":m.fs.tank_pressure,
                                  "output":m.fs.valve_1.valve_opening,
                                  "upper":1.0,
                                  "lower":0.0,
                                  "calculate_initial_integral":calc_integ,
                                  "pid_form":form})
    m.fs.ctrl.deactivate() # Don't want controller turned on by default

    # Fix the input variables
    m.fs.valve_1.inlet.enth_mol.fix(50000)
    m.fs.valve_1.inlet.pressure.fix(5e5)
    m.fs.valve_2.outlet.pressure.fix(101325)
    m.fs.valve_1.Cv.fix(0.001)
    m.fs.valve_2.Cv.fix(0.001)
    m.fs.valve_1.valve_opening.fix(1)
    m.fs.valve_2.valve_opening.fix(1)
    m.fs.tank.heat_duty.fix(0)
    m.fs.tank.control_volume.volume.fix(2.0)
    m.fs.ctrl.gain.fix(1e-6)
    m.fs.ctrl.time_i.fix(0.1)
    m.fs.ctrl.time_d.fix(0.1)
    m.fs.ctrl.setpoint.fix(3e5)

    # Initialize the model
    solver = pyo.SolverFactory("ipopt")
    solver.options = {'tol': 1e-6,
                      'linear_solver': "ma27",
                      'max_iter': 100}
    for t in m.fs.time:
        m.fs.valve_1.inlet.flow_mol = 100 # initial guess on flow
    # simple initialize
    m.fs.valve_1.initialize(outlvl=1)
    _set_port(m.fs.tank.inlet, m.fs.valve_1.outlet)
    m.fs.tank.initialize(outlvl=1)
    _set_port(m.fs.valve_2.inlet, m.fs.tank.outlet)
    m.fs.valve_2.initialize(outlvl=1)
    solver.solve(m, tee=True)

    # Return the model and solver
    return m, solver
Exemple #30
0
def main():
    """
    """
    nxfe = 10
    # NOTE: Default inputs: (128.2, 591.4)
    ic_inputs = {
        #"fs.MB.gas_phase.properties[*,0.0].flow_mol": 120.0,
        #"fs.MB.solid_phase.properties[*,1.0].flow_mass": 550.0,
    }
    ic_model_params = {"nxfe": nxfe}
    m_ic = get_steady_state_model(
        ic_inputs,
        solve_kwds={"tee": True},
        model_params=ic_model_params,
    )
    time = m_ic.fs.time
    scalar_data, dae_data = get_data_from_steady_model(m_ic, time)
    # TODO: get steady state data (scalar and dae both necessary)
    x0 = 0.0
    x1 = 1.0

    # These (as well as nxfe above) are the parameters for a small model
    # that I'm using to test NMPC (defaults are 900, 15, 60), nxfe=10
    horizon = 1800
    tfe_width = 60
    sample_width = 120
    sample_points = [
        # Calculate sample points first with integer arithmetic
        # to avoid roundoff error
        float(sample_width*i) for i in range(0, horizon//sample_width + 1)
    ]
    horizon = float(horizon)
    tfe_width = float(tfe_width)
    model_params = {
        "horizon": horizon,
        "tfe_width": tfe_width,
        "ntcp": 1,
        "nxfe": nxfe,
    }

    # These are approximately the default values:
    #disturbance_dict = {"CO2": 0.03, "H2O": 0.0, "CH4": 0.97}
    disturbance_dict = {"CO2": 0.5, "H2O": 0.0, "CH4": 0.5}
    disturbance = dict(
        (
            "fs.MB.gas_phase.properties[*,%s].mole_frac_comp[%s]" % (x0, j),
            {(0.0, horizon): val},
        )
        for j, val in disturbance_dict.items()
    )

    # Create solver here as it is needed to solve for the setpoint
    solver = pyo.SolverFactory("ipopt")
    solver.options["linear_solver"] = "ma57"
    solver.options["max_cpu_time"] = 1500

    #
    # Get setpoint data
    #
    sp_inputs = get_inputs_at_time(disturbance, horizon)
    #sp_inputs.update({
    #    "fs.MB.gas_phase.properties[*,0.0].flow_mol": 272.8,
    #    "fs.MB.solid_phase.properties[*,1.0].flow_mass": 591.4,
    #})
    sp_model_params = {"nxfe": nxfe}
    m_sp = get_steady_state_model(
        sp_inputs,
        solve_kwds={"tee": True},
        model_params=sp_model_params,
    )
    time = m_sp.fs.time
    space = m_sp.fs.MB.gas_phase.length_domain
    t0 = time.first()
    # Solve optimization problem for setpoint
    sp_objective_states = [
        "fs.MB.solid_phase.reactions[*,%s].OC_conv" % x0,
    ]
    sp_target = {
        "fs.MB.solid_phase.reactions[*,%s].OC_conv" % x0: 0.95,
    }
    m_sp.fs.MB.gas_inlet.flow_mol[:].unfix()
    m_sp.setpoint_expr = get_tracking_cost_expressions(
        sp_objective_states, time, sp_target
    )
    m_sp.objective = pyo.Objective(expr=m_sp.setpoint_expr[t0])
    solver.solve(m_sp, tee=True)
    scalar_vars, dae_vars = flatten_dae_components(m_sp, time, pyo.Var)
    setpoint = {
        str(pyo.ComponentUID(var.referent)): var[t0].value
        for var in dae_vars
    }
    ###

    max_data = get_max_values_from_steady(m_sp)
    variance_data = get_variance_of_time_slices(m_sp, time, space)
    #weight_data = None
    weight_data = {
        name: 1.0/s if s != 0 else 1.0 for name, s in variance_data.items()
        #name: 1/w if w != 0 else 1.0 for name, w in max_data.items()
        # Note: 1/w**2 does not converge with states in objective...
    }
    objective_states = get_state_variable_names(space)

    flattened_vars = [None, None]
    m = get_model_for_dynamic_optimization(
        sample_points=sample_points,
        parameter_perturbation=disturbance,
        model_params=model_params,
        ic_scalar_data=scalar_data,
        ic_dae_data=dae_data,
        setpoint_data=setpoint,
        objective_weights=weight_data,
        objective_states=objective_states,

        # this argument is a huge hack to get the flattened
        # vars without having to do a bit more work.
        flatten_out=flattened_vars,
    )
    add_constraints_for_missing_variables(m)
    time = m.fs.time
    t0 = time.first()
    scalar_vars, dae_vars = flattened_vars
    initialize_dynamic(m, dae_vars)

    # Should we initialize to setpoint inputs?
    #sp_input_dict = {
    #    "fs.MB.gas_phase.properties[*,0.0].flow_mol": {(t0, horizon): 250.0},
    #    "fs.MB.solid_phase.properties[*,1.0].flow_mass": {(t0, horizon): 591.4},
    #}
    #load_inputs_into_model(m, time, sp_input_dict)

    # TODO: Should I set inlet flow rates to their target values for
    # this simulation?
    input_vardata = (
        [m.fs.MB.gas_inlet.flow_mol[t] for t in time if t != t0]
        + [m.fs.MB.solid_inlet.flow_mass[t] for t in time if t != t0]
    )
    with TemporarySubsystemManager(
            to_fix=input_vardata,
            to_deactivate=[m.piecewise_constant_constraint],
            ):
        print("Initializing by time element...")
        with TIMER.context("elem-init"):
            initialize_by_time_element(m, time, solver)

    m.fs.MB.solid_phase.reactions[:,0.0].OC_conv.setlb(0.89)

    print("Starting dynamic optimization solve...")
    with TIMER.context("solve dynamic"):
        solver.solve(m, tee=True)

    extra_states = [
        pyo.Reference(m.fs.MB.solid_phase.reactions[:,0.0].OC_conv),
    ]
    plot_outlet_states_over_time(m, show=False, extra_states=extra_states)
    inputs = [
        "fs.MB.gas_phase.properties[*,0.0].flow_mol",
        "fs.MB.solid_phase.properties[*,1.0].flow_mass",
    ]
    plot_inputs_over_time(m, inputs, show=False)
    print(m.tracking_cost.name)
    for t in m.fs.time:
        print(t, pyo.value(m.tracking_cost[t]))
    print()