コード例 #1
0
ファイル: tray_column.py プロジェクト: makaylas/idaes-pse
    def _make_stripping_arcs(self):

        self._stripping_stream_index = RangeSet(
            self.config.feed_tray_location + 1,
            self.config.number_of_trays - 1)

        def rule_liq_stream(self, i):
            return {
                "source": self.stripping_section[i].liq_out,
                "destination": self.stripping_section[i + 1].liq_in
            }

        def rule_vap_stream(self, i):
            return {
                "source": self.stripping_section[i + 1].vap_out,
                "destination": self.stripping_section[i].vap_in
            }

        self.stripping_liq_stream = Arc(self._stripping_stream_index,
                                        rule=rule_liq_stream)
        self.stripping_vap_stream = Arc(self._stripping_stream_index,
                                        rule=rule_vap_stream)
コード例 #2
0
    def build(self):

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

        self.compressor = Compressor(
            default={"property_package": self.config.property_package})

        self.stoic_reactor = StoichiometricReactor(
            default={"property_package": self.config.property_package,
                     "reaction_package": self.config.reaction_package,
                     "has_heat_of_reaction": True,
                     "has_heat_transfer": False,
                     "has_pressure_change": False})

        self.turbine = Turbine(
            default={"property_package": self.config.property_package})

        # Declare var for reactor conversion
        self.stoic_reactor.conversion = Var(initialize=0.75, bounds=(0, 1))

        stoic_reactor_in = self.stoic_reactor.control_volume.properties_in[0.0]
        stoic_reactor_out = self.stoic_reactor.control_volume.properties_out[0.0]

        self.stoic_reactor.conv_constraint = Constraint(
            expr=self.stoic_reactor.conversion *
            stoic_reactor_in.flow_mol_comp["hydrogen"] ==
            (stoic_reactor_in.flow_mol_comp["hydrogen"] -
             stoic_reactor_out.flow_mol_comp["hydrogen"]))

        # Connect arcs
        self.comp_to_reactor = Arc(
            source=self.compressor.outlet,
            destination=self.stoic_reactor.inlet)
        self.reactor_to_turbine = Arc(
            source=self.stoic_reactor.outlet,
            destination=self.turbine.inlet)

        TransformationFactory("network.expand_arcs").apply_to(self)
コード例 #3
0
def test_scale_arcs():
    m = pyo.ConcreteModel()
    m.x = pyo.Var([1, 2, 3, 4])
    m.y = pyo.Var([1, 2, 3, 4])

    m.p1 = Port()
    m.p1.add(m.x[1], name="x")
    m.p1.add(m.y[1], name="y")

    m.p = Port([2, 3, 4])
    m.p[2].add(m.x[2], name="x")
    m.p[2].add(m.y[2], name="y")
    m.p[3].add(m.x[3], name="x")
    m.p[3].add(m.y[3], name="y")
    m.p[4].add(m.x[4], name="x")
    m.p[4].add(m.y[4], name="y")

    def arc_rule(b, i):
        if i == 1:
            return (m.p1, m.p[2])
        elif i == 2:
            return (m.p[3], m.p[4])

    m.arcs = Arc([1, 2], rule=arc_rule)

    sc.set_scaling_factor(m.x, 10)
    sc.set_scaling_factor(m.y, 20)
    sc.set_scaling_factor(m.x[1], 5)

    # make sure there is no error if the scaling is done with unexpanded arcs
    sc.scale_arc_constraints(m)

    # expand and make sure it works
    pyo.TransformationFactory('network.expand_arcs').apply_to(m)
    sc.scale_arc_constraints(m)
    m.x[1] = 1
    m.x[2] = 2
    m.x[3] = 3
    m.x[4] = 4
    m.y[1] = 11
    m.y[2] = 12
    m.y[3] = 13
    m.y[4] = 14

    # for all the arc constraints the differnce is 1 the scale factor is the
    # smallest scale factor for variables in a constraint.  Make sure the
    # constraints are scaled as expected.
    assert abs(m.arcs_expanded[1].x_equality.body()) == 5
    assert abs(m.arcs_expanded[2].x_equality.body()) == 10
    assert abs(m.arcs_expanded[1].y_equality.body()) == 20
    assert abs(m.arcs_expanded[2].y_equality.body()) == 20
コード例 #4
0
def build_turbine_for_run_test():
    m = pyo.ConcreteModel()
    m.fs = FlowsheetBlock(default={"dynamic": False})
    m.fs.properties = iapws95.Iapws95ParameterBlock()
    # roughly based on NETL baseline studies
    m.fs.turb = HelmTurbineMultistage(default={
        "property_package": m.fs.properties,
        "num_hp": 7,
        "num_ip": 14,
        "num_lp": 11,
        "hp_split_locations": [4,7],
        "ip_split_locations": [5, 14],
        "lp_split_locations": [4,7,9,11],
        "hp_disconnect": [7],
        "ip_split_num_outlets": {14:3}})

    # Add reheater
    m.fs.reheat = Heater(default={"property_package": m.fs.properties})
    m.fs.hp_to_reheat = Arc(source=m.fs.turb.hp_split[7].outlet_1,
                            destination=m.fs.reheat.inlet)
    m.fs.reheat_to_ip = Arc(source=m.fs.reheat.outlet,
                            destination=m.fs.turb.ip_stages[1].inlet)
    return m
コード例 #5
0
def test_propagate_state_indexed_fixed():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)

        b.p = Port(b.s)
        b.p[1].add(b.v1, "V1")
        b.p[2].add(b.v2, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    def arc_rule(m, i):
        return {'source': m.b1.p[i], 'destination': m.b2.p[i]}

    m.s1 = Arc([1, 2], rule=arc_rule)

    # Set values on first block
    m.b1.v1.value = 10
    m.b1.v2[1].value = 20
    m.b1.v2[2].value = 30

    # Make sure vars in block 2 haven't been changed accidentally
    assert m.b2.v1.value is None
    assert m.b2.v2[1].value is None
    assert m.b2.v2[2].value is None

    # Fix v1 in block 2
    m.b2.v1.fix(500)

    propagate_state(m.s1[1])

    # Check that values were propagated correctly
    assert m.b2.v1.value == 500
    assert m.b1.v1.fixed is False
    assert m.b2.v1.fixed is True

    propagate_state(m.s1[2])

    # Check that values were propagated correctly
    assert m.b2.v2[1].value == m.b1.v2[1].value
    assert m.b2.v2[2].value == m.b1.v2[2].value
    assert m.b1.v2[1].fixed is False
    assert m.b1.v2[2].fixed is False
    assert m.b2.v2[1].fixed is False
    assert m.b2.v2[2].fixed is False
コード例 #6
0
ファイル: test_gams.py プロジェクト: ZedongPeng/pyomo
 def test_gams_arc_in_active_constraint(self):
     m = ConcreteModel()
     m.b1 = Block()
     m.b2 = Block()
     m.b1.x = Var()
     m.b2.x = Var()
     m.b1.c = Port()
     m.b1.c.add(m.b1.x)
     m.b2.c = Port()
     m.b2.c.add(m.b2.x)
     m.c = Arc(source=m.b1.c, destination=m.b2.c)
     m.o = Objective(expr=m.b1.x)
     outs = StringIO()
     with self.assertRaises(RuntimeError):
         m.write(outs, format="gams")
コード例 #7
0
ファイル: test_gams.py プロジェクト: ZedongPeng/pyomo
 def test_gams_expanded_arcs(self):
     m = ConcreteModel()
     m.x = Var()
     m.y = Var()
     m.CON1 = Port()
     m.CON1.add(m.x, 'v')
     m.CON2 = Port()
     m.CON2.add(m.y, 'v')
     m.c = Arc(source=m.CON1, destination=m.CON2)
     TransformationFactory("network.expand_arcs").apply_to(m)
     m.o = Objective(expr=m.x)
     outs = StringIO()
     io_options = dict(symbolic_solver_labels=True)
     m.write(outs, format="gams", io_options=io_options)
     # no error if we're here, but check for some identifying string
     self.assertIn("x - y", outs.getvalue())
コード例 #8
0
    def create_arcs(self):
        ##################################
        # Arcs                           #
        ##################################
        for tech in self.power_sources.keys():

            def arc_rule(m, t):
                source_port = self.power_sources[tech].dispatch.blocks[t].port
                destination_port = getattr(self.blocks[t], tech + "_port")
                return {'source': source_port, 'destination': destination_port}

            setattr(self.model, tech + "_hybrid_arc",
                    Arc(self.blocks.index_set(), rule=arc_rule))
            self.arcs.append(getattr(self.model, tech + "_hybrid_arc"))

        pyomo.TransformationFactory("network.expand_arcs").apply_to(self.model)
コード例 #9
0
def build(m, **kwargs):
    """
    build an RO
    """
    assert not kwargs['has_desal_feed']
    property_models.build_prop(m, base='ion')
    feed_block.build_feed(m, base='ion')

    property_models.build_prop(m, base=kwargs['RO_base'])
    translator_block.build_tb(m, base_inlet='ion',
                              base_outlet=kwargs['RO_base'],
                              name_str='tb_pretrt_to_desal')
    m.fs.s_pretrt_tb = Arc(source=m.fs.feed.outlet, destination=m.fs.tb_pretrt_to_desal.inlet)

    property_models.build_prop(m, base='eNRTL')
    desal_port = build_components(m, **kwargs)
コード例 #10
0
ファイル: test_tables.py プロジェクト: bhattade/idaes-pse
def m():
    m = ConcreteModel()
    m.fs = FlowsheetBlock(default={"dynamic": False})
    m.fs.thermo_params = thermo_props.SaponificationParameterBlock()
    m.fs.reaction_params = rxn_props.SaponificationReactionParameterBlock(
        default={"property_package": m.fs.thermo_params})

    m.fs.tank1 = CSTR(default={"property_package": m.fs.thermo_params,
                               "reaction_package": m.fs.reaction_params})
    m.fs.tank2 = CSTR(default={"property_package": m.fs.thermo_params,
                               "reaction_package": m.fs.reaction_params})

    m.fs.stream = Arc(source=m.fs.tank1.outlet,
                      destination=m.fs.tank2.inlet)
    TransformationFactory("network.expand_arcs").apply_to(m)

    return m
コード例 #11
0
def build_ideal_naocl_chlorination_block(model, expand_arcs=False):

    # Add properties to model
    build_ideal_naocl_prop(model)

    # Add mixer to the model
    build_ideal_naocl_mixer_unit(model)

    # Add reactor to the model
    build_ideal_naocl_chlorination_unit(model)

    # Connect the mixer to the chlorination unit with arcs
    model.fs.ideal_nacol_arc_mixer_to_chlor = Arc(
        source=model.fs.ideal_naocl_mixer_unit.outlet,
        destination=model.fs.ideal_naocl_chlorination_unit.inlet)
    if expand_arcs == True:
        TransformationFactory("network.expand_arcs").apply_to(model)
コード例 #12
0
ファイル: test_check_units.py プロジェクト: zypher22/pyomo
    def test_assert_units_consistent_all_components(self):
        # test all scalar components consistent
        u = units
        m = self._create_model_and_vars()
        m.obj = Objective(expr=m.dx / m.t - m.vx)
        m.con = Constraint(expr=m.dx / m.t == m.vx)
        # vars already added
        m.exp = Expression(expr=m.dx / m.t - m.vx)
        m.suff = Suffix(direction=Suffix.LOCAL)
        # params already added
        # sets already added
        m.rs = RangeSet(5)
        m.disj1 = Disjunct()
        m.disj1.constraint = Constraint(expr=m.dx / m.t <= m.vx)
        m.disj2 = Disjunct()
        m.disj2.constraint = Constraint(expr=m.dx / m.t <= m.vx)
        m.disjn = Disjunction(expr=[m.disj1, m.disj2])
        # block tested as part of model
        m.extfn = ExternalFunction(python_callback_function,
                                   units=u.m / u.s,
                                   arg_units=[u.m, u.s])
        m.conext = Constraint(expr=m.extfn(m.dx, m.t) - m.vx == 0)
        m.cset = ContinuousSet(bounds=(0, 1))
        m.svar = Var(m.cset, units=u.m)
        m.dvar = DerivativeVar(sVar=m.svar, units=u.m / u.s)

        def prt1_rule(m):
            return {'avar': m.dx}

        def prt2_rule(m):
            return {'avar': m.dy}

        m.prt1 = Port(rule=prt1_rule)
        m.prt2 = Port(rule=prt2_rule)

        def arcrule(m):
            return dict(source=m.prt1, destination=m.prt2)

        m.arc = Arc(rule=arcrule)

        # complementarities do not work yet
        # The expression system removes the u.m since it is multiplied by zero.
        # We need to change the units_container to allow 0 when comparing units
        # m.compl = Complementarity(expr=complements(m.dx/m.t >= m.vx, m.dx == 0*u.m))

        assert_units_consistent(m)
コード例 #13
0
def build_components(m, pretrt_type='NF', **kwargs):
    kwargs_desalination = {k: kwargs[k] for k in ('has_desal_feed', 'is_twostage', 'has_ERD',
                                                  'RO_type', 'RO_base', 'RO_level',)}

    desal_port = desalination.build_desalination(m, **kwargs_desalination)
    m.fs.s_tb_desal = Arc(source=m.fs.tb_pretrt_to_desal.outlet, destination=desal_port['in'])

    if pretrt_type == 'softening':
        property_models.build_prop(m, base='eNRTL')
    gypsum_saturation_index.build(m, section='desalination', pretrt_type=pretrt_type,  **kwargs)

    m.fs.RO.area.fix(80)
    m.fs.pump_RO.control_volume.properties_out[0].pressure.fix(60e5)

    if kwargs['is_twostage']:
        m.fs.RO2.area.fix(20)
        m.fs.pump_RO2.control_volume.properties_out[0].pressure.fix(90e5)

    # touch some properties used in optimization
    if kwargs['is_twostage']:
            product_water_sb = m.fs.mixer_permeate.mixed_state[0]
    else:
        product_water_sb = m.fs.RO.mixed_permeate[0]

    feed_flow_vol = 0.0009769808  # value of feed flowrate using the seawater property package with 1 kg/s mass flowrate
    m.fs.system_recovery = Expression(
        expr=product_water_sb.flow_vol / feed_flow_vol)

    # RO recovery
    m.fs.RO_recovery = Var(initialize=0.5,
                           bounds=(0.01, 0.99),
                           doc='Total volumetric water recovery for RO')
    m.fs.eq_RO_recovery = Constraint(
        expr=m.fs.RO_recovery
             == product_water_sb.flow_vol / m.fs.tb_pretrt_to_desal.properties_out[0].flow_vol)

    # annual water production
    m.fs.treated_flow_vol = Expression(
        expr=product_water_sb.flow_vol)
    costing.build_costing(m, **kwargs)

    return desal_port
コード例 #14
0
def test_propagate_state_invalid_direction():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)

        b.p = Port()
        b.p.add(b.v1, "V1")
        b.p.add(b.v2, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    m.s1 = Arc(source=m.b1.p, destination=m.b2.p)

    with pytest.raises(ValueError):
        propagate_state(m.s1, direction="foo")
コード例 #15
0
def build(m, **kwargs):
    """
    build an RO
    """
    assert not kwargs["has_desal_feed"]
    property_models.build_prop(m, base="ion")
    feed_block.build_feed(m, base="ion")

    property_models.build_prop(m, base=kwargs["RO_base"])
    translator_block.build_tb(
        m,
        base_inlet="ion",
        base_outlet=kwargs["RO_base"],
        name_str="tb_pretrt_to_desal",
    )
    m.fs.s_pretrt_tb = Arc(source=m.fs.feed.outlet,
                           destination=m.fs.tb_pretrt_to_desal.inlet)

    property_models.build_prop(m, base="eNRTL")
    desal_port = build_components(m, **kwargs)
コード例 #16
0
def test_propagate_state_reverse():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)

        b.p = Port()
        b.p.add(b.v1, "V1")
        b.p.add(b.v2, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    m.s1 = Arc(source=m.b1.p, destination=m.b2.p)

    # Test reverse propogation - set values on second block
    m.b2.v1.value = 100
    m.b2.v2[1].value = 200
    m.b2.v2[2].value = 300

    # Make sure vars in block 1 haven't been changed accidentally
    assert m.b1.v1.value is None
    assert m.b1.v2[1].value is None
    assert m.b1.v2[2].value is None

    propagate_state(m.s1, direction="backward")

    # Check that values were propagated correctly
    assert m.b2.v1.value == m.b1.v1.value
    assert m.b2.v2[1].value == m.b1.v2[1].value
    assert m.b2.v2[2].value == m.b1.v2[2].value

    assert m.b1.v1.fixed is False
    assert m.b1.v2[1].fixed is False
    assert m.b1.v2[2].fixed is False
    assert m.b2.v1.fixed is False
    assert m.b2.v2[1].fixed is False
    assert m.b2.v2[2].fixed is False
コード例 #17
0
def model():
    m = pe.ConcreteModel()
    m.fs = idaes.core.FlowsheetBlock()
    m.fs.properties = \
        idaes.generic_models.properties.swco2.SWCO2ParameterBlock()
    m.fs.heater = idaes.generic_models.unit_models.Heater(
        default={
            'dynamic': False,
            'property_package': m.fs.properties,
            'has_pressure_change': True
        })
    m.fs.heater2 = idaes.generic_models.unit_models.Heater(
        default={
            'dynamic': False,
            'property_package': m.fs.properties,
            'has_pressure_change': True
        })
    m.fs.stream = Arc(source=m.fs.heater.outlet,
                      destination=m.fs.heater2.inlet)

    return m
コード例 #18
0
def test_propagate_state_Expression():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)

        b.e = Expression(expr=b.v1)

        b.p = Port()
        b.p.add(b.e, "E")
        b.p.add(b.v2, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    m.s1 = Arc(source=m.b1.p, destination=m.b2.p)

    with pytest.raises(TypeError):
        propagate_state(m.s1)
コード例 #19
0
def build(m):
    """
    Builds softening pretreatment including specified feed and auxiliary equipment.
    """
    pretrt_port = {}

    pssb.build_stoich_softening_block(m)
    build_feed_block(m)

    # connect feed block
    m.fs.s_prtrt_feed_mixer = Arc(
        source=m.fs.feed.outlet, destination=m.fs.stoich_softening_mixer_unit.inlet_stream)

    # set model values
    pssb.set_stoich_softening_mixer_inlets(m, dosing_rate_of_lime_mg_per_s=500)
    pssb.fix_stoich_softening_mixer_lime_stream(m)

    # set ports
    pretrt_port['out'] = m.fs.stoich_softening_separator_unit.outlet_stream
    pretrt_port['waste'] = m.fs.stoich_softening_separator_unit.waste_stream

    return pretrt_port
コード例 #20
0
def test_propagate_state_indexed():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)

        b.p = Port(b.s)
        b.p[1].add(b.v1, "V1")
        b.p[2].add(b.v2, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    def arc_rule(m, i):
        return {'source': m.b1.p[i], 'destination': m.b2.p[i]}

    m.s1 = Arc([1, 2], rule=arc_rule)

    with pytest.raises(AttributeError):
        propagate_state(m.s1)
コード例 #21
0
    def test_get_stream_table_contents(self):
        m = ConcreteModel()
        m.fs = FlowsheetBlock(default={"dynamic": False})

        m.fs.props = PhysicalParameterTestBlock()
        m.fs.config.default_property_package = m.fs.props

        m.fs.unit1 = Heater()
        m.fs.unit2 = Heater()

        m.fs.stream = Arc(source=m.fs.unit1.outlet,
                          destination=m.fs.unit2.inlet)
        TransformationFactory("network.expand_arcs").apply_to(m)

        df = m.fs._get_stream_table_contents()

        assert df.loc["pressure"]["stream"] == 1e5
        assert df.loc["temperature"]["stream"] == 300
        assert df.loc["component_flow_phase ('p1', 'c1')"]["stream"] == 2.0
        assert df.loc["component_flow_phase ('p1', 'c2')"]["stream"] == 2.0
        assert df.loc["component_flow_phase ('p2', 'c1')"]["stream"] == 2.0
        assert df.loc["component_flow_phase ('p2', 'c2')"]["stream"] == 2.0

        m.fs.report()
コード例 #22
0
ファイル: lsrro.py プロジェクト: dangunter/proteuslib
def build(number_of_stages=2):
    # ---building model---
    m = ConcreteModel()

    m.fs = FlowsheetBlock(default={"dynamic": False})
    m.fs.properties = props.NaClParameterBlock()
    m.fs.costing = WaterTAPCosting()

    m.fs.NumberOfStages = Param(initialize=number_of_stages)
    m.fs.StageSet = RangeSet(m.fs.NumberOfStages)
    m.fs.LSRRO_StageSet = RangeSet(2, m.fs.NumberOfStages)
    m.fs.NonFinal_StageSet = RangeSet(m.fs.NumberOfStages-1)

    m.fs.feed = Feed(default={'property_package': m.fs.properties})
    m.fs.product = Product(default={'property_package': m.fs.properties})
    m.fs.disposal = Product(default={'property_package': m.fs.properties})

    # Add the mixers
    m.fs.Mixers = Mixer(m.fs.NonFinal_StageSet, default={
            "property_package": m.fs.properties,
            "momentum_mixing_type": MomentumMixingType.equality,  # booster pump will match pressure
            "inlet_list": ['upstream', 'downstream']})

    total_pump_work = 0
    # Add the pumps
    m.fs.PrimaryPumps = Pump(m.fs.StageSet, default={"property_package": m.fs.properties})
    for pump in m.fs.PrimaryPumps.values():
        pump.costing = UnitModelCostingBlock(default={
                "flowsheet_costing_block":m.fs.costing})
        m.fs.costing.cost_flow(pyunits.convert(pump.work_mechanical[0], to_units=pyunits.kW), "electricity")

    # Add the equalizer pumps
    m.fs.BoosterPumps = Pump(m.fs.LSRRO_StageSet, default={"property_package": m.fs.properties})
    for pump in m.fs.BoosterPumps.values():
        pump.costing = UnitModelCostingBlock(default={
                "flowsheet_costing_block":m.fs.costing})
        m.fs.costing.cost_flow(pyunits.convert(pump.work_mechanical[0], to_units=pyunits.kW), "electricity")

    # Add the stages ROs
    m.fs.ROUnits = ReverseOsmosis0D(m.fs.StageSet, default={
            "property_package": m.fs.properties,
            "has_pressure_change": True,
            "pressure_change_type": PressureChangeType.calculated,
            "mass_transfer_coefficient": MassTransferCoefficient.calculated,
            "concentration_polarization_type": ConcentrationPolarizationType.calculated})
    for ro_unit in m.fs.ROUnits.values():
        ro_unit.costing = UnitModelCostingBlock(default={
                "flowsheet_costing_block":m.fs.costing})

    # Add EnergyRecoveryDevice
    m.fs.EnergyRecoveryDevice = Pump(default={"property_package": m.fs.properties})
    m.fs.EnergyRecoveryDevice.costing = UnitModelCostingBlock(default={
            "flowsheet_costing_block":m.fs.costing,
            "costing_method_arguments":{"pump_type":PumpType.energy_recovery_device}})
    m.fs.costing.cost_flow(pyunits.convert(m.fs.EnergyRecoveryDevice.work_mechanical[0], to_units=pyunits.kW), "electricity")

    # additional variables or expressions
    # system water recovery
    m.fs.water_recovery = Var(
            initialize=0.5,
            bounds=(0, 1),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='System Water Recovery')
    m.fs.eq_water_recovery = Constraint(expr=\
              sum(m.fs.feed.flow_mass_phase_comp[0,'Liq',:]) * m.fs.water_recovery == \
              sum(m.fs.product.flow_mass_phase_comp[0,'Liq',:]) )

    # costing
    m.fs.costing.cost_process()
    product_flow_vol_total = m.fs.product.properties[0].flow_vol
    m.fs.costing.add_LCOW(product_flow_vol_total)
    m.fs.costing.add_specific_energy_consumption(product_flow_vol_total)

    # objective
    m.fs.objective = Objective(expr=m.fs.costing.LCOW)

    # connections

    # Connect the feed to the first pump
    m.fs.feed_to_pump = Arc(source=m.fs.feed.outlet, destination=m.fs.PrimaryPumps[1].inlet)

    # Connect the primary RO permeate to the product
    m.fs.primary_RO_to_product = Arc(source=m.fs.ROUnits[1].permeate, destination=m.fs.product.inlet)

    # Connect the Pump n to the Mixer n
    m.fs.pump_to_mixer = Arc(m.fs.NonFinal_StageSet,
            rule=lambda fs,n : {'source':fs.PrimaryPumps[n].outlet,
                                'destination':fs.Mixers[n].upstream})

    # Connect the Mixer n to the Stage n
    m.fs.mixer_to_stage = Arc(m.fs.NonFinal_StageSet,
            rule=lambda fs,n : {'source':fs.Mixers[n].outlet,
                                'destination':fs.ROUnits[n].inlet})

    # Connect the Stage n to the Pump n+1
    m.fs.stage_to_pump = Arc(m.fs.NonFinal_StageSet,
            rule=lambda fs,n : {'source':fs.ROUnits[n].retentate,
                                'destination':fs.PrimaryPumps[n+1].inlet})

    # Connect the Stage n to the Eq Pump n
    m.fs.stage_to_eq_pump = Arc(m.fs.LSRRO_StageSet,
            rule=lambda fs,n : {'source':fs.ROUnits[n].permeate,
                                'destination':fs.BoosterPumps[n].inlet})

    # Connect the Eq Pump n to the Mixer n-1
    m.fs.eq_pump_to_mixer = Arc(m.fs.LSRRO_StageSet,
            rule=lambda fs,n : {'source':fs.BoosterPumps[n].outlet,
                                'destination':fs.Mixers[n-1].downstream})

    # Connect the Pump N to the Stage N
    last_stage = m.fs.StageSet.last()
    m.fs.pump_to_stage = Arc(source=m.fs.PrimaryPumps[last_stage].outlet,
            destination=m.fs.ROUnits[last_stage].inlet)

    # Connect Final Stage to EnergyRecoveryDevice Pump
    m.fs.stage_to_erd = Arc(source=m.fs.ROUnits[last_stage].retentate,
            destination=m.fs.EnergyRecoveryDevice.inlet)

    # Connect the EnergyRecoveryDevice to the disposal
    m.fs.erd_to_disposal = Arc(source=m.fs.EnergyRecoveryDevice.outlet,
            destination=m.fs.disposal.inlet)

    # additional bounding
    for b in m.component_data_objects(Block, descend_into=True):
        # NaCl solubility limit
        if hasattr(b, 'mass_frac_phase_comp'):
            b.mass_frac_phase_comp['Liq', 'NaCl'].setub(0.26)

    TransformationFactory("network.expand_arcs").apply_to(m)

    return m
コード例 #23
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
コード例 #24
0
    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)
        else:
            self.hp_split = {}
        if config.ip_split_locations:
            self.ip_split = HelmSplitter(config.ip_split_locations,
                                         default=s_sfg_default,
                                         initialize=ip_splt_cfg)
        else:
            self.ip_split = {}
        if config.lp_split_locations:
            self.lp_split = HelmSplitter(config.lp_split_locations,
                                         default=s_sfg_default,
                                         initialize=lp_splt_cfg)
        else:
            self.lp_split = {}
        # 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.power = pyo.Var(self.flowsheet().time,
                             initialize=-1e8,
                             doc="power (W)")

        @self.Constraint(self.flowsheet().time)
        def power_eqn(b, t):
            return (b.power[t] == b.outlet_stage.control_volume.work[t] *
                    b.outlet_stage.efficiency_mech +
                    sum(b.inlet_stage[i].control_volume.work[t] *
                        b.inlet_stage[i].efficiency_mech
                        for i in b.inlet_stage) +
                    sum(b.hp_stages[i].control_volume.work[t] *
                        b.hp_stages[i].efficiency_mech for i in b.hp_stages) +
                    sum(b.ip_stages[i].control_volume.work[t] *
                        b.ip_stages[i].efficiency_mech for i in b.ip_stages) +
                    sum(b.lp_stages[i].control_volume.work[t] *
                        b.lp_stages[i].efficiency_mech 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)
コード例 #25
0
    def build(self):
        super().build()
        config = self.config  # sorter ref to config for less line splitting

        # All feedwater heaters have a condensing section
        _set_prop_pack(config.condense, config)
        self.condense = FWHCondensing0D(default=config.condense)

        # Add a mixer to add the drain stream from another feedwater heater
        if config.has_drain_mixer:
            mix_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,
                "momentum_mixing_type": MomentumMixingType.none,
                "material_balance_type": MaterialBalanceType.componentTotal,
                "inlet_list": ["steam", "drain"],
            }
            self.drain_mix = Mixer(default=mix_cfg)

            @self.drain_mix.Constraint(self.drain_mix.flowsheet().config.time)
            def mixer_pressure_constraint(b, t):
                """
                Constraint to set the drain mixer pressure to the pressure of
                the steam extracted from the turbine. The drain inlet should
                always be a higher pressure than the steam inlet.
                """
                return b.steam_state[t].pressure == b.mixed_state[t].pressure

            # Connect the mixer to the condensing section inlet
            self.mix_out_arc = Arc(source=self.drain_mix.outlet,
                                   destination=self.condense.inlet_1)

        # Add a desuperheat section before the condensing section
        if config.has_desuperheat:
            _set_prop_pack(config.desuperheat, config)
            self.desuperheat = HeatExchanger(default=config.desuperheat)
            # set default area less than condensing section area, this will
            # almost always be overridden by the user fixing an area later
            self.desuperheat.area.value = 10
            if config.has_drain_mixer:
                self.desuperheat_drain_arc = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.drain_mix.steam)
            else:
                self.desuperheat_drain_arc = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.condense.inlet_1)
            self.condense_out2_arc = Arc(source=self.condense.outlet_2,
                                         destination=self.desuperheat.inlet_2)

        # Add a drain cooling section after the condensing section
        if config.has_drain_cooling:
            _set_prop_pack(config.cooling, config)
            self.cooling = HeatExchanger(default=config.cooling)
            # set default area less than condensing section area, this will
            # almost always be overridden by the user fixing an area later
            self.cooling.area.value = 10
            self.cooling_out2_arc = Arc(source=self.cooling.outlet_2,
                                        destination=self.condense.inlet_2)
            self.condense_out1_arc = Arc(source=self.condense.outlet_1,
                                         destination=self.cooling.inlet_1)

        TransformationFactory("network.expand_arcs").apply_to(self)
コード例 #26
0
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
コード例 #27
0
def build_boiler(fs):

    # Add property packages to flowsheet library
    fs.prop_fluegas = FlueGasParameterBlock()

    # Create unit models
    # Boiler Economizer
    fs.ECON = BoilerHeatExchanger(
        default={
            "side_1_property_package": fs.prop_water,
            "side_2_property_package": fs.prop_fluegas,
            "has_pressure_change": True,
            "has_holdup": False,
            "delta_T_method": DeltaTMethod.counterCurrent,
            "tube_arrangement": TubeArrangement.inLine,
            "side_1_water_phase": "Liq",
            "has_radiation": False
        })
    # Primary Superheater
    fs.PrSH = BoilerHeatExchanger(
        default={
            "side_1_property_package": fs.prop_water,
            "side_2_property_package": fs.prop_fluegas,
            "has_pressure_change": True,
            "has_holdup": False,
            "delta_T_method": DeltaTMethod.counterCurrent,
            "tube_arrangement": TubeArrangement.inLine,
            "side_1_water_phase": "Liq",
            "has_radiation": True
        })

    # Finishing Superheater
    fs.FSH = BoilerHeatExchanger(
        default={
            "side_1_property_package": fs.prop_water,
            "side_2_property_package": fs.prop_fluegas,
            "has_pressure_change": True,
            "has_holdup": False,
            "delta_T_method": DeltaTMethod.counterCurrent,
            "tube_arrangement": TubeArrangement.inLine,
            "side_1_water_phase": "Liq",
            "has_radiation": True
        })

    # Reheater
    fs.RH = BoilerHeatExchanger(
        default={
            "side_1_property_package": fs.prop_water,
            "side_2_property_package": fs.prop_fluegas,
            "has_pressure_change": True,
            "has_holdup": False,
            "delta_T_method": DeltaTMethod.counterCurrent,
            "tube_arrangement": TubeArrangement.inLine,
            "side_1_water_phase": "Liq",
            "has_radiation": True
        })
    # Platen Superheater
    fs.PlSH = Heater(default={"property_package": fs.prop_water})

    #Boiler Water Wall
    fs.Water_wall = Heater(default={"property_package": fs.prop_water})

    #Boiler Splitter (splits FSH flue gas outlet to Reheater and PrSH)
    fs.Spl1 = Separator(
        default={
            "property_package": fs.prop_fluegas,
            "split_basis": SplittingType.totalFlow,
            "energy_split_basis": EnergySplittingType.equal_temperature
        })
    # Flue gas mixer (mixing FG from Reheater and Primary SH, inlet to ECON)
    fs.mix1 = Mixer(
        default={
            "property_package": fs.prop_fluegas,
            "inlet_list": ['Reheat_out', 'PrSH_out'],
            "dynamic": False
        })

    # Mixer for Attemperator #1 (between PrSH and PlSH)
    fs.ATMP1 = Mixer(
        default={
            "property_package": fs.prop_water,
            "inlet_list": ['Steam', 'SprayWater'],
            "dynamic": False
        })

    # Build connections (streams)

    # Steam Route (side 1 = tube side = steam/water side)
    # Boiler feed water to Economizer (to be imported in full plant model)
    #    fs.bfw2econ = Arc(source=fs.FWH8.outlet,
    #                           destination=fs.ECON.side_1_inlet)
    fs.econ2ww = Arc(source=fs.ECON.side_1_outlet,
                     destination=fs.Water_wall.inlet)
    fs.ww2prsh = Arc(source=fs.Water_wall.outlet,
                     destination=fs.PrSH.side_1_inlet)
    fs.prsh2plsh = Arc(source=fs.PrSH.side_1_outlet, destination=fs.PlSH.inlet)
    fs.plsh2fsh = Arc(source=fs.PlSH.outlet, destination=fs.FSH.side_1_inlet)
    fs.FSHtoATMP1 = Arc(source=fs.FSH.side_1_outlet,
                        destination=fs.ATMP1.Steam)
    #    fs.fsh2hpturbine=Arc(source=fs.ATMP1.outlet,
    #                           destination=fs.HPTinlet)
    #(to be imported in full plant model)

    # Flue gas route ------------------------------------------------------------
    # water wall connected with boiler block (to fix the heat duty)
    # platen SH connected with boiler block (to fix the heat duty)
    #Finishing superheater connected with a flowsheet level constraint
    fs.fg_fsh2_separator = Arc(source=fs.FSH.side_2_outlet,
                               destination=fs.Spl1.inlet)
    fs.fg_fsh2rh = Arc(source=fs.Spl1.outlet_1, destination=fs.RH.side_2_inlet)
    fs.fg_fsh2PrSH = Arc(source=fs.Spl1.outlet_2,
                         destination=fs.PrSH.side_2_inlet)
    fs.fg_rhtomix = Arc(source=fs.RH.side_2_outlet,
                        destination=fs.mix1.Reheat_out)
    fs.fg_prsh2mix = Arc(source=fs.PrSH.side_2_outlet,
                         destination=fs.mix1.PrSH_out)
    fs.fg_mix2econ = Arc(source=fs.mix1.outlet,
                         destination=fs.ECON.side_2_inlet)
コード例 #28
0
def test_ASM1_reactor():
    m = pyo.ConcreteModel()

    m.fs = FlowsheetBlock(default={"dynamic": False})

    m.fs.props = ASM1ParameterBlock()
    m.fs.rxn_props = ASM1ReactionParameterBlock(
        default={"property_package": m.fs.props})

    m.fs.R1 = CSTR(default={
        "property_package": m.fs.props,
        "reaction_package": m.fs.rxn_props,
    })
    m.fs.R2 = CSTR(default={
        "property_package": m.fs.props,
        "reaction_package": m.fs.rxn_props,
    })
    m.fs.R3 = CSTR(default={
        "property_package": m.fs.props,
        "reaction_package": m.fs.rxn_props,
    })
    m.fs.R4 = CSTR(default={
        "property_package": m.fs.props,
        "reaction_package": m.fs.rxn_props,
    })
    m.fs.R5 = CSTR(default={
        "property_package": m.fs.props,
        "reaction_package": m.fs.rxn_props,
    })

    # Link units
    m.fs.stream1 = Arc(source=m.fs.R1.outlet, destination=m.fs.R2.inlet)
    m.fs.stream2 = Arc(source=m.fs.R2.outlet, destination=m.fs.R3.inlet)
    m.fs.stream3 = Arc(source=m.fs.R3.outlet, destination=m.fs.R4.inlet)
    m.fs.stream4 = Arc(source=m.fs.R4.outlet, destination=m.fs.R5.inlet)
    pyo.TransformationFactory("network.expand_arcs").apply_to(m)

    assert_units_consistent(m)

    # Feed conditions based on manual mass balance of inlet and recycle streams
    m.fs.R1.inlet.flow_vol.fix(92230 * pyo.units.m**3 / pyo.units.day)
    m.fs.R1.inlet.temperature.fix(298.15 * pyo.units.K)
    m.fs.R1.inlet.pressure.fix(1 * pyo.units.atm)
    m.fs.R1.inlet.conc_mass_comp[0,
                                 "S_I"].fix(30 * pyo.units.g / pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "S_S"].fix(14.6112 * pyo.units.g /
                                               pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "X_I"].fix(1149 * pyo.units.g /
                                               pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "X_S"].fix(89.324 * pyo.units.g /
                                               pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "X_BH"].fix(2542.03 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "X_BA"].fix(148.6 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0,
                                 "X_P"].fix(448 * pyo.units.g / pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "S_O"].fix(0.3928 * pyo.units.g /
                                               pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "S_NO"].fix(8.32 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "S_NH"].fix(7.696 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "S_ND"].fix(1.9404 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.conc_mass_comp[0, "X_ND"].fix(5.616 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.R1.inlet.alkalinity.fix(4.704 * pyo.units.mol / pyo.units.m**3)

    m.fs.R1.volume.fix(1000 * pyo.units.m**3)
    m.fs.R2.volume.fix(1000 * pyo.units.m**3)
    m.fs.R3.volume.fix(1333 * pyo.units.m**3)
    m.fs.R4.volume.fix(1333 * pyo.units.m**3)
    m.fs.R5.volume.fix(1333 * pyo.units.m**3)

    assert degrees_of_freedom(m) == 0

    # Initialize flowsheet
    m.fs.R1.initialize()
    propagate_state(m.fs.stream1)
    m.fs.R2.initialize()
    propagate_state(m.fs.stream2)
    m.fs.R3.initialize()
    propagate_state(m.fs.stream3)
    m.fs.R4.initialize()
    propagate_state(m.fs.stream4)
    m.fs.R5.initialize()

    # For aerobic reactors, need to fix the oxygen concentration in outlet
    # To do this, we also need to deactivate the constraint linking O2 from
    # the previous unit
    # Doing this before initialization will cause issues with DoF however
    m.fs.R3.outlet.conc_mass_comp[0, "S_O"].fix(1.72 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.stream2.expanded_block.conc_mass_comp_equality[0, "S_O"].deactivate()

    m.fs.R4.outlet.conc_mass_comp[0, "S_O"].fix(2.43 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.stream3.expanded_block.conc_mass_comp_equality[0, "S_O"].deactivate()
    m.fs.R5.outlet.conc_mass_comp[0, "S_O"].fix(0.491 * pyo.units.g /
                                                pyo.units.m**3)
    m.fs.stream4.expanded_block.conc_mass_comp_equality[0, "S_O"].deactivate()

    solver = get_solver()
    results = solver.solve(m, tee=True)
    assert pyo.check_optimal_termination(results)

    # Verify results against reference
    # First reactor (anoxic)
    assert pyo.value(m.fs.R1.outlet.flow_vol[0]) == pytest.approx(1.0675,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R1.outlet.temperature[0]) == pytest.approx(298.15,
                                                                     rel=1e-4)
    assert pyo.value(m.fs.R1.outlet.pressure[0]) == pytest.approx(101325,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx(
        30e-3, rel=1e-5)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "S_S"]) == pytest.approx(
        2.81e-3, rel=1e-2)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx(
        1149e-3, rel=1e-3)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx(
        82.1e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "X_BH"]) == pytest.approx(2552e-3,
                                                                   rel=1e-3)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "X_BA"]) == pytest.approx(149e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "X_P"]) == pytest.approx(
        449e-3, rel=1e-2)
    assert pyo.value(m.fs.R1.outlet.conc_mass_comp[0, "S_O"]) == pytest.approx(
        4.3e-6, rel=1e-2)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "S_NO"]) == pytest.approx(5.36e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "S_NH"]) == pytest.approx(7.92e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "S_ND"]) == pytest.approx(1.22e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R1.outlet.conc_mass_comp[0, "X_ND"]) == pytest.approx(5.29e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R1.outlet.alkalinity[0]) == pytest.approx(4.93e-3,
                                                                    rel=1e-2)

    # Second reactor (anoixic)
    assert pyo.value(m.fs.R2.outlet.flow_vol[0]) == pytest.approx(1.0675,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R2.outlet.temperature[0]) == pytest.approx(298.15,
                                                                     rel=1e-4)
    assert pyo.value(m.fs.R2.outlet.pressure[0]) == pytest.approx(101325,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx(
        30e-3, rel=1e-5)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "S_S"]) == pytest.approx(
        1.46e-3, rel=1e-2)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx(
        1149e-3, rel=1e-3)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx(
        76.4e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "X_BH"]) == pytest.approx(2553e-3,
                                                                   rel=1e-3)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "X_BA"]) == pytest.approx(148e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "X_P"]) == pytest.approx(
        449e-3, rel=1e-2)
    assert pyo.value(m.fs.R2.outlet.conc_mass_comp[0, "S_O"]) == pytest.approx(
        6.31e-8, rel=1e-2)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "S_NO"]) == pytest.approx(3.65e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "S_NH"]) == pytest.approx(8.34e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "S_ND"]) == pytest.approx(0.882e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R2.outlet.conc_mass_comp[0, "X_ND"]) == pytest.approx(5.03e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R2.outlet.alkalinity[0]) == pytest.approx(5.08e-3,
                                                                    rel=1e-2)

    # Third reactor (aerobic)
    assert pyo.value(m.fs.R3.outlet.flow_vol[0]) == pytest.approx(1.0675,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R3.outlet.temperature[0]) == pytest.approx(298.15,
                                                                     rel=1e-4)
    assert pyo.value(m.fs.R3.outlet.pressure[0]) == pytest.approx(101325,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx(
        30e-3, rel=1e-5)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "S_S"]) == pytest.approx(
        1.15e-3, rel=1e-2)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx(
        1149e-3, rel=1e-3)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx(
        64.9e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "X_BH"]) == pytest.approx(2557e-3,
                                                                   rel=1e-3)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "X_BA"]) == pytest.approx(149e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "X_P"]) == pytest.approx(
        450e-3, rel=1e-2)
    assert pyo.value(m.fs.R3.outlet.conc_mass_comp[0, "S_O"]) == pytest.approx(
        1.72e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "S_NO"]) == pytest.approx(6.54e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "S_NH"]) == pytest.approx(5.55e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "S_ND"]) == pytest.approx(0.829e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R3.outlet.conc_mass_comp[0, "X_ND"]) == pytest.approx(4.39e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R3.outlet.alkalinity[0]) == pytest.approx(4.67e-3,
                                                                    rel=1e-2)

    # Fourth reactor (aerobic)
    assert pyo.value(m.fs.R4.outlet.flow_vol[0]) == pytest.approx(1.0675,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R4.outlet.temperature[0]) == pytest.approx(298.15,
                                                                     rel=1e-4)
    assert pyo.value(m.fs.R4.outlet.pressure[0]) == pytest.approx(101325,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx(
        30e-3, rel=1e-5)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "S_S"]) == pytest.approx(
        0.995e-3, rel=1e-2)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx(
        1149e-3, rel=1e-3)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx(
        55.7e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "X_BH"]) == pytest.approx(2559e-3,
                                                                   rel=1e-3)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "X_BA"]) == pytest.approx(150e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "X_P"]) == pytest.approx(
        451e-3, rel=1e-2)
    assert pyo.value(m.fs.R4.outlet.conc_mass_comp[0, "S_O"]) == pytest.approx(
        2.43e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "S_NO"]) == pytest.approx(9.30e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "S_NH"]) == pytest.approx(2.97e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "S_ND"]) == pytest.approx(0.767e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R4.outlet.conc_mass_comp[0, "X_ND"]) == pytest.approx(3.88e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R4.outlet.alkalinity[0]) == pytest.approx(4.29e-3,
                                                                    rel=1e-2)

    # Fifth reactor (aerobic)
    assert pyo.value(m.fs.R5.outlet.flow_vol[0]) == pytest.approx(1.0675,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R5.outlet.temperature[0]) == pytest.approx(298.15,
                                                                     rel=1e-4)
    assert pyo.value(m.fs.R5.outlet.pressure[0]) == pytest.approx(101325,
                                                                  rel=1e-4)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "S_I"]) == pytest.approx(
        30e-3, rel=1e-5)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "S_S"]) == pytest.approx(
        0.889e-3, rel=1e-2)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "X_I"]) == pytest.approx(
        1149e-3, rel=1e-3)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "X_S"]) == pytest.approx(
        49.3e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "X_BH"]) == pytest.approx(2559e-3,
                                                                   rel=1e-3)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "X_BA"]) == pytest.approx(150e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "X_P"]) == pytest.approx(
        452e-3, rel=1e-2)
    assert pyo.value(m.fs.R5.outlet.conc_mass_comp[0, "S_O"]) == pytest.approx(
        0.491e-3, rel=1e-2)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "S_NO"]) == pytest.approx(10.4e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "S_NH"]) == pytest.approx(1.73e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "S_ND"]) == pytest.approx(0.688e-3,
                                                                   rel=1e-2)
    assert pyo.value(
        m.fs.R5.outlet.conc_mass_comp[0, "X_ND"]) == pytest.approx(3.53e-3,
                                                                   rel=1e-2)
    assert pyo.value(m.fs.R5.outlet.alkalinity[0]) == pytest.approx(4.13e-3,
                                                                    rel=1e-2)
コード例 #29
0
def main():
    """
    Make the flowsheet object, fix some variables, and solve the problem
    """
    # Create a Concrete Model as the top level object
    m = ConcreteModel()

    # Add a flowsheet object to the model
    m.fs = FlowsheetBlock(default={"dynamic": False})

    # Add property packages to flowsheet library
    m.fs.thermo_params = thermo_props.SaponificationParameterBlock()
    m.fs.reaction_params = reaction_props.SaponificationReactionParameterBlock(
        default={"property_package": m.fs.thermo_params})

    # Create unit models
    m.fs.Tank1 = CSTR(
        default={
            "property_package": m.fs.thermo_params,
            "reaction_package": m.fs.reaction_params,
            "has_equilibrium_reactions": False,
            "has_heat_of_reaction": True,
            "has_heat_transfer": True,
            "has_pressure_change": False
        })
    m.fs.Tank2 = CSTR(
        default={
            "property_package": m.fs.thermo_params,
            "reaction_package": m.fs.reaction_params,
            "has_equilibrium_reactions": False,
            "has_heat_of_reaction": True,
            "has_heat_transfer": True,
            "has_pressure_change": False
        })

    # Make Streams to connect units
    m.fs.stream = Arc(source=m.fs.Tank1.outlet, destination=m.fs.Tank2.inlet)

    TransformationFactory("network.expand_arcs").apply_to(m)

    # Set inlet and operating conditions, and some initial conditions.
    m.fs.Tank1.inlet.flow_vol[0].fix(1.0)
    m.fs.Tank1.inlet.conc_mol_comp[0, "H2O"].fix(55388.0)
    m.fs.Tank1.inlet.conc_mol_comp[0, "NaOH"].fix(100.0)
    m.fs.Tank1.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0)
    m.fs.Tank1.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0)
    m.fs.Tank1.inlet.conc_mol_comp[0, "Ethanol"].fix(0.0)

    m.fs.Tank1.inlet.temperature.fix(303.15)
    m.fs.Tank1.inlet.pressure.fix(101325.0)

    m.fs.Tank1.volume.fix(1.0)
    m.fs.Tank1.heat_duty.fix(0.0)

    m.fs.Tank2.volume.fix(1.0)
    m.fs.Tank2.heat_duty.fix(0.0)

    # Initialize Units
    m.fs.Tank1.initialize()
    m.fs.Tank2.initialize(
        state_args={
            "flow_vol": 1.0,
            "conc_mol_comp": {
                "H2O": 55388.0,
                "NaOH": 100.0,
                "EthylAcetate": 100.0,
                "SodiumAcetate": 0.0,
                "Ethanol": 0.0
            },
            "temperature": 303.15,
            "pressure": 101325.0
        })

    # Create a solver
    solver = SolverFactory('ipopt')
    results = solver.solve(m, tee=False)

    # Print results
    print(results)
    print()
    print("Results")
    print()
    print("Tank 1 Outlet")
    m.fs.Tank1.outlet.display()
    print()
    print("Tank 2 Outlet")
    m.fs.Tank2.outlet.display()

    # For testing purposes
    return (m, results)
コード例 #30
0
def test_propagate_state():
    m = ConcreteModel()

    def block_rule(b):
        b.s = Set(initialize=[1, 2])
        b.v1 = Var()
        b.v2 = Var(b.s)
        b.e1 = Expression(expr=b.v1)

        @b.Expression(b.s)
        def e2(blk, i):
            return b.v2[i] * b.v1

        b.p1 = Param(mutable=True, initialize=5)
        b.p2 = Param(b.s, mutable=True, initialize=6)

        b.port1 = Port()
        b.port1.add(b.v1, "V1")
        b.port1.add(b.v2, "V2")

        b.port2 = Port()
        b.port2.add(b.v1, "V1")
        b.port2.add(b.e2, "V2")

        b.port3 = Port()
        b.port3.add(b.e1, "V1")
        b.port3.add(b.v2, "V2")

        b.port4 = Port()
        b.port4.add(b.p1, "V1")
        b.port4.add(b.v2, "V2")

        b.port5 = Port()
        b.port5.add(b.v1, "V1")
        b.port5.add(b.p2, "V2")

        b.port6 = Port()
        b.port6.add(b.v1, "V1")
        b.port6.add(b.v1, "V2")
        return

    m.b1 = Block(rule=block_rule)
    m.b2 = Block(rule=block_rule)

    m.s1 = Arc(source=m.b1.port1, destination=m.b2.port1)
    m.s2 = Arc(source=m.b1.port1, destination=m.b2.port2)
    m.s3 = Arc(source=m.b1.port1, destination=m.b2.port3)
    m.s4 = Arc(source=m.b1.port1, destination=m.b2.port4)
    m.s5 = Arc(source=m.b1.port1, destination=m.b2.port5)
    m.s6 = Arc(source=m.b1.port2, destination=m.b2.port1)
    m.s7 = Arc(source=m.b1.port3, destination=m.b2.port1)
    m.s8 = Arc(source=m.b1.port4, destination=m.b2.port1)
    m.s9 = Arc(source=m.b1.port5, destination=m.b2.port1)
    m.s10 = Arc(source=m.b1.port6, destination=m.b2.port1)
    m.s11 = Arc(source=m.b2.port6, destination=m.b1.port1)

    # Set values on first block
    m.b1.v1.value = 10
    m.b1.v2[1].value = 20
    m.b1.v2[2].value = 30

    # Make sure vars in block 2 haven't been changed accidentally
    assert m.b2.v1.value is None
    assert m.b2.v2[1].value is None
    assert m.b2.v2[2].value is None

    propagate_state(m.s1)

    # Check that values were propagated correctly
    assert m.b2.v1.value == m.b1.v1.value
    assert m.b2.v2[1].value == m.b1.v2[1].value
    assert m.b2.v2[2].value == m.b1.v2[2].value

    assert m.b1.v1.fixed is False
    assert m.b1.v2[1].fixed is False
    assert m.b1.v2[2].fixed is False
    assert m.b2.v1.fixed is False
    assert m.b2.v2[1].fixed is False
    assert m.b2.v2[2].fixed is False

    with pytest.raises(TypeError):
        propagate_state(m.s2)

    with pytest.raises(TypeError):
        propagate_state(m.s3)

    with pytest.raises(TypeError):
        propagate_state(m.s4)

    with pytest.raises(TypeError):
        propagate_state(m.s5)

    propagate_state(m.s6)
    assert value(m.b1.v1) == value(m.b2.v1)
    assert value(m.b1.e2[1]) == value(m.b2.v2[1])
    assert value(m.b1.e2[2]) == value(m.b2.v2[2])

    propagate_state(m.s7)
    assert value(m.b1.e1) == value(m.b2.v1)
    assert value(m.b1.v2[1]) == value(m.b2.v2[1])
    assert value(m.b1.v2[2]) == value(m.b2.v2[2])

    propagate_state(m.s8)
    assert value(m.b1.p1) == value(m.b2.v1)
    assert value(m.b1.v2[1]) == value(m.b2.v2[1])
    assert value(m.b1.v2[2]) == value(m.b2.v2[2])

    propagate_state(m.s9)
    assert value(m.b1.v1) == value(m.b2.v1)
    assert value(m.b1.p2[1]) == value(m.b2.v2[1])
    assert value(m.b1.p2[2]) == value(m.b2.v2[2])

    with pytest.raises(KeyError):
        propagate_state(m.s10)

    with pytest.raises(KeyError):
        propagate_state(m.s11)