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