def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(DrumData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_geometry() self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=False, has_equilibrium_reactions=False) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=False, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add object references add_object_reference(self, "volume", self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): add_object_reference(self, "deltaP", self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance()
def test_user_set_scaling(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.pp = PhysicalParameterTestBlock() m.fs.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) m.fs.cv.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_phase_equilibrium=False) m.fs.cv.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_transfer=True, has_work_transfer=True) # add momentum balance m.fs.cv.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # The scaling factors used for this test were selected to be easy values to # test, they do not represent typical scaling factors. iscale.set_scaling_factor(m.fs.cv.heat, 11) iscale.set_scaling_factor(m.fs.cv.work, 12) iscale.calculate_scaling_factors(m) # Make sure the heat and work scaling factors are set and not overwritten # by the defaults in calculate_scaling_factors assert iscale.get_scaling_factor(m.fs.cv.heat) == 11 assert iscale.get_scaling_factor(m.fs.cv.work) == 12
def test_full_auto_scaling_mbtype_element(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": True, "time_units": pyo.units.s}) m.fs.pp = PhysicalParameterTestBlock() m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) m.fs.cv = ControlVolume0DBlock(default={ "property_package": m.fs.pp, "reaction_package": m.fs.rp, "dynamic": True}) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) m.fs.cv.add_reaction_blocks(has_equilibrium=False) m.fs.cv.add_total_element_balances(has_mass_transfer=True) m.discretizer = pyo.TransformationFactory('dae.finite_difference') m.discretizer.apply_to(m, nfe=3, wrt=m.fs.time, scheme="BACKWARD") iscale.calculate_scaling_factors(m) # check that all variables have scaling factors unscaled_var_list = list(iscale.unscaled_variables_generator(m)) assert len(unscaled_var_list) == 0 # check that all constraints have been scaled unscaled_constraint_list = list(iscale.unscaled_constraints_generator(m)) assert len(unscaled_constraint_list) == 0
def test_add_outlet_port_CV0D_full_args(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) m.fs.u.cv.add_state_blocks(has_phase_equilibrium=False) p_obj = m.fs.u.add_outlet_port(name="test_port", block=m.fs.u.cv, doc="Test") assert isinstance(p_obj, Port) assert hasattr(m.fs.u, "test_port") assert len(m.fs.u.test_port) == 1 # Set new outlet conditions to differentiate from inlet m.fs.u.cv.properties_out[0].a = 10 m.fs.u.cv.properties_out[0].b = 20 m.fs.u.cv.properties_out[0].c = 30 for p in m.fs.pp.phase_list: for j in m.fs.pp.component_list: assert m.fs.u.test_port.component_flow_phase[0, p, j].value == ( m.fs.u.cv.properties_out[0].flow_mol_phase_comp[p, j].value) assert m.fs.u.test_port.pressure[0].value == \ m.fs.u.cv.properties_out[0].pressure.value assert m.fs.u.test_port.temperature[0].value == \ m.fs.u.cv.properties_out[0].temperature.value
def _make_heater_control_volume(o, name, config, dynamic=None, has_holdup=None): """ This is seperated from the main heater class so it can be reused to create control volumes for different types of heat exchange models. """ if dynamic is None: dynamic = config.dynamic if has_holdup is None: has_holdup = config.has_holdup # we have to attach this control volume to the model for the rest of # the steps to work o.add_component(name, ControlVolume0DBlock(default={ "dynamic": dynamic, "has_holdup": has_holdup, "property_package": config.property_package, "property_package_args": config.property_package_args})) control_volume = getattr(o, name) # Add inlet and outlet state blocks to control volume control_volume.add_state_blocks( has_phase_equilibrium=config.has_phase_equilibrium) # Add material balance control_volume.add_material_balances( balance_type=config.material_balance_type, has_phase_equilibrium=config.has_phase_equilibrium) # add energy balance control_volume.add_energy_balances( balance_type=config.energy_balance_type, has_heat_transfer=True) # add momentum balance control_volume.add_momentum_balances( balance_type=config.momentum_balance_type, has_pressure_change=config.has_pressure_change) return control_volume
def test_get_stream_table_contents_CV0D(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.control_volume = ControlVolume0DBlock( default={"property_package": m.fs.pp}) m.fs.u.control_volume.add_state_blocks(has_phase_equilibrium=False) m.fs.u.add_inlet_port() m.fs.u.add_outlet_port() df = m.fs.u._get_stream_table_contents() assert df.loc["component_flow_phase ('p1', 'c1')"]["Inlet"] == 2 assert df.loc["component_flow_phase ('p1', 'c2')"]["Inlet"] == 2 assert df.loc["component_flow_phase ('p2', 'c1')"]["Inlet"] == 2 assert df.loc["component_flow_phase ('p2', 'c2')"]["Inlet"] == 2 assert df.loc["pressure"]["Inlet"] == 1e5 assert df.loc["temperature"]["Inlet"] == 300 assert df.loc["component_flow_phase ('p1', 'c1')"]["Outlet"] == 2 assert df.loc["component_flow_phase ('p1', 'c2')"]["Outlet"] == 2 assert df.loc["component_flow_phase ('p2', 'c1')"]["Outlet"] == 2 assert df.loc["component_flow_phase ('p2', 'c2')"]["Outlet"] == 2 assert df.loc["pressure"]["Outlet"] == 1e5 assert df.loc["temperature"]["Outlet"] == 300
def test_add_outlet_port_CV0D_full_args(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) m.fs.u.cv.add_state_blocks(has_phase_equilibrium=False) p_obj = m.fs.u.add_outlet_port(name="test_port", block=m.fs.u.cv, doc="Test") assert isinstance(p_obj, Port) assert hasattr(m.fs.u, "test_port") assert len(m.fs.u.test_port) == 1 # Set new outlet conditions to differentiate from inlet m.fs.u.cv.properties_out[0].a = 10 m.fs.u.cv.properties_out[0].b = 20 m.fs.u.cv.properties_out[0].c = 30 assert m.fs.u.test_port.a[0].value == \ m.fs.u.cv.properties_out[0].a.value assert m.fs.u.test_port.b[0].value == \ m.fs.u.cv.properties_out[0].b.value assert m.fs.u.test_port.c[0].value == \ m.fs.u.cv.properties_out[0].c.value
def build(self): """ Begin building model (pre-DAE transformation) Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_geometry() # This model requires the IAPWS95 property package with the mixed phase # option, therefore, phase equilibrium calculations are handled by # the property package. self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add object references self.volume = Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance()
def test_CV_integration(model): model.fs.cv = ControlVolume0DBlock( default={"property_package": model.fs.water_props}) model.fs.cv.add_geometry() model.fs.cv.add_state_blocks(has_phase_equilibrium=True) model.fs.cv.add_material_balances(has_phase_equilibrium=True)
def build(self): """ Begin building model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(FeedFlashData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) # No need for control volume geometry self.control_volume.add_state_blocks(has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) # Add isothermal constraint @self.Constraint(self.flowsheet().config.time, doc="Isothermal constraint") def isothermal(b, t): return (b.control_volume.properties_in[t].temperature == b.control_volume.properties_out[t].temperature) self.control_volume.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal) # Add references to all feed state vars s_vars = self.control_volume.properties_in[ self.flowsheet().config.time.first()].define_state_vars() for s in s_vars: l_name = s_vars[s].local_name if s_vars[s].is_indexed(): slicer = (self.control_volume.properties_in[:].component( l_name)[...]) else: slicer = self.control_volume.properties_in[:].component(l_name) r = Reference(slicer) setattr(self, s, r) # Add Ports self.add_outlet_port()
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(StoichiometricReactorData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_reaction_blocks(has_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer, has_heat_of_reaction=self.config.has_heat_of_reaction) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add performance equations add_object_reference(self, "rate_reaction_idx_ref", self.config.reaction_package.rate_reaction_idx) add_object_reference(self, "rate_reaction_extent", self.control_volume.rate_reaction_extent) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != 'none'): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): add_object_reference(self, "deltaP", self.control_volume.deltaP)
def test_add_outlet_port_CV0D_no_default_block(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) with pytest.raises(ConfigurationError): m.fs.u.add_outlet_port()
def build(self): super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta = self.config.property_package.get_metadata( ).get_derived_units # Add control volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, }) self.control_volume.add_state_blocks(has_phase_equilibrium=False) # complete condensation mass balance @self.control_volume.Constraint( self.flowsheet().time, self.config.property_package.component_list, doc="Mass balance", ) def mass_balance(b, t, j): lb = b.properties_out[t].get_material_flow_terms("Vap", j).lb b.properties_out[t].get_material_flow_terms("Vap", j).fix(lb) return b.properties_in[t].get_material_flow_terms( "Vap", j) + b.properties_in[t].get_material_flow_terms( "Liq", j) == b.properties_out[t].get_material_flow_terms( "Liq", j) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type) # # Add constraints @self.Constraint(self.flowsheet().time, doc="Saturation pressure constraint") def eq_condenser_pressure_sat(b, t): return (b.control_volume.properties_out[t].pressure >= b.control_volume.properties_out[t].pressure_sat) # Add ports self.add_port(name="inlet", block=self.control_volume.properties_in) self.add_port(name="outlet", block=self.control_volume.properties_out)
def test_CV_integration(self, frame): frame.fs.cv = ControlVolume0DBlock( default={"property_package": frame.fs.params}) frame.fs.cv.add_geometry() frame.fs.cv.add_state_blocks(has_phase_equilibrium=True) frame.fs.cv.add_material_balances(has_phase_equilibrium=True) frame.fs.cv.add_energy_balances() frame.fs.cv.add_momentum_balances()
def test_get_stream_table_contents_CV0D_missing_default_port(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.control_volume = ControlVolume0DBlock( default={"property_package": m.fs.pp}) m.fs.u.control_volume.add_state_blocks(has_phase_equilibrium=False) with pytest.raises(ConfigurationError): m.fs.u._get_stream_table_contents()
def test_report_CV0D(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.control_volume = ControlVolume0DBlock( default={"property_package": m.fs.pp}) m.fs.u.control_volume.add_state_blocks(has_phase_equilibrium=False) m.fs.u.add_inlet_port() m.fs.u.add_outlet_port() m.fs.u.report()
def test_add_outlet_port_CV0D_part_args(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) m.fs.u.cv.add_state_blocks(has_phase_equilibrium=False) with pytest.raises(ConfigurationError): m.fs.u.add_outlet_port(block=m.fs.u.cv) with pytest.raises(ConfigurationError): m.fs.u.add_outlet_port(name="foo")
def test_base_build(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.pp = PhysicalParameterTestBlock() m.fs.cv = ControlVolume0DBlock(default={ "property_package": m.fs.pp } ) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) m.fs.cv.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_phase_equilibrium=False) m.fs.cv.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_transfer=True, has_work_transfer=True) # add momentum balance m.fs.cv.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # The scaling factors used for this test were selected to be easy values to # test, they do not represent typical scaling factors. iscale.set_scaling_factor(m.fs.cv.heat, 11) iscale.set_scaling_factor(m.fs.cv.work, 12) iscale.calculate_scaling_factors(m) # Make sure the heat and work scaling factors are set and not overwitten # by the defaults in calculate_scaling_factors assert iscale.get_scaling_factor(m.fs.cv.heat) == 11 assert iscale.get_scaling_factor(m.fs.cv.work) == 12 # Didn't specify a deltaP scaling factor, so by default pressure in scaling # factor * 10 is used. for v in m.fs.cv.deltaP.values(): #deltaP is time indexed assert iscale.get_scaling_factor(v) == 1040 # check scaling on mass, energy, and pressure balances. for c in m.fs.cv.material_balances.values(): # this uses the minmum material flow term scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 112 for c in m.fs.cv.enthalpy_balances.values(): # this uses the minmum enthalpy flow term scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 110 for c in m.fs.cv.pressure_balance.values(): # This uses the inlet pressure scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 104
def test_full_auto_scaling_mbtype_phase(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": True, "time_units": pyo.units.s}) m.fs.pp = PhysicalParameterTestBlock() m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) m.fs.cv = ControlVolume0DBlock(default={ "property_package": m.fs.pp, "reaction_package": m.fs.rp, "dynamic": True}) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=True) m.fs.cv.add_reaction_blocks(has_equilibrium=True) m.fs.cv.add_material_balances( balance_type=MaterialBalanceType.componentPhase, has_rate_reactions=True, has_equilibrium_reactions=True, has_phase_equilibrium=True, has_mass_transfer=True) m.fs.cv.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_of_reaction=True, has_heat_transfer=True, has_work_transfer=True, has_enthalpy_transfer=True) m.fs.cv.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) m.discretizer = pyo.TransformationFactory('dae.finite_difference') m.discretizer.apply_to(m, nfe=3, wrt=m.fs.time, scheme="BACKWARD") iscale.calculate_scaling_factors(m) # check that all variables have scaling factors unscaled_var_list = list(iscale.unscaled_variables_generator(m)) # Unscaled variables are: # rate_reaction_extent (2 reactions, 4 time points) # equilibrium_reaction_extent (2 reactions, 4 time points) # phase_equilibrium_generation (2 reactions, 4 time points) assert len(unscaled_var_list) == 24 # check that all constraints have been scaled unscaled_constraint_list = list(iscale.unscaled_constraints_generator(m)) assert len(unscaled_constraint_list) == 0
def test_full_auto_scaling(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.pp = PhysicalParameterTestBlock() m.fs.rp = ReactionParameterTestBlock(default={"property_package": m.fs.pp}) m.fs.cv = ControlVolume0DBlock(default={ "property_package": m.fs.pp, "reaction_package": m.fs.rp}) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=True) m.fs.cv.add_reaction_blocks(has_equilibrium=True) m.fs.cv.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_rate_reactions=True, has_equilibrium_reactions=True, has_phase_equilibrium=True, has_mass_transfer=True) m.fs.cv.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_of_reaction=True, has_heat_transfer=True, has_work_transfer=True, has_enthalpy_transfer=True) m.fs.cv.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) iscale.calculate_scaling_factors(m) # check that all variables have scaling factors unscaled_var_list = list(iscale.unscaled_variables_generator(m)) # Unscaled variables are: # rate_reaction_extent (2 reactions) # equilibrium_reaction_extent (2 reactions) assert len(unscaled_var_list) == 4 # check that all constraints have been scaled unscaled_constraint_list = list(iscale.unscaled_constraints_generator(m)) assert len(unscaled_constraint_list) == 0
def test_basic_scaling(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.pp = PhysicalParameterTestBlock() # Set flag to include inherent reactions m.fs.pp._has_inherent_reactions = True m.fs.cv = ControlVolume0DBlock(default={"property_package": m.fs.pp}) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) m.fs.cv.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_phase_equilibrium=False) m.fs.cv.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal) m.fs.cv.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) iscale.calculate_scaling_factors(m) # check scaling on select variables assert iscale.get_scaling_factor(m.fs.cv.volume[0]) == 1e-2 assert iscale.get_scaling_factor( m.fs.cv.deltaP[0]) == 1040 # 10x the properties pressure scaling factor assert iscale.get_scaling_factor(m.fs.cv.properties_in[0].flow_vol) == 100 # check scaling on mass, energy, and pressure balances. for c in m.fs.cv.material_balances.values(): # this uses the minimum material flow term scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 112 for c in m.fs.cv.enthalpy_balances.values(): # this uses the minimum enthalpy flow term scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 110 for c in m.fs.cv.pressure_balance.values(): # This uses the inlet pressure scale assert iscale.get_constraint_transform_applied_scaling_factor(c) == 104
def frame_control_volume(self): m = ConcreteModel() self.configure_class(m) m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = self.prop_pack() m.fs.cv = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": m.fs.properties, "property_package_args": self.param_args }) m.fs.cv.add_state_blocks(has_phase_equilibrium=False) m.fs.cv.add_material_balances() m.fs.cv.add_energy_balances() m.fs.cv.add_momentum_balances() for (v_str, ind), sf in self.scaling_args.items(): m.fs.properties.set_default_scaling(v_str, sf, index=ind) return m
def test_get_stream_table_contents_CV0D(): m = ConcreteModel() m.fs = Flowsheet() m.fs.pp = PhysicalParameterTestBlock() m.fs.u = Unit() m.fs.u._setup_dynamics() m.fs.u.control_volume = ControlVolume0DBlock( default={"property_package": m.fs.pp}) m.fs.u.control_volume.add_state_blocks(has_phase_equilibrium=False) m.fs.u.add_inlet_port() m.fs.u.add_outlet_port() df = m.fs.u._get_stream_table_contents() assert df.loc["a"]["Inlet"] == 1 assert df.loc["b"]["Inlet"] == 2 assert df.loc["c"]["Inlet"] == 3 assert df.loc["a"]["Outlet"] == 1 assert df.loc["b"]["Outlet"] == 2 assert df.loc["c"]["Outlet"] == 3
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(GibbsReactorData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_total_element_balances() self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add performance equations # Add Lagrangian multiplier variables self.lagrange_mult = Var(self.flowsheet().config.time, self.config.property_package.element_list, domain=Reals, initialize=100, doc="Lagrangian multipliers") # Use Lagrangian multiple method to derive equations for Out_Fi # Use RT*lagrange as the Lagrangian multiple such that lagrange is in # a similar order of magnitude as log(Yi) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Gibbs energy minimisation constraint") def gibbs_minimization(b, t, p, j): # Use natural log of species mole flow to avoid Pyomo solver # warnings of reaching infeasible point return 0 == ( b.control_volume.properties_out[t].gibbs_mol_phase_comp[p, j] + sum(b.lagrange_mult[t, e] * b.control_volume.properties_out[t]. config.parameters.element_comp[j][e] for e in b.config.property_package.element_list)) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): add_object_reference(self, "deltaP", self.control_volume.deltaP)
def build(self): """ Args: None Returns: None """ # Call UnitModel.build super(PressureChangerData, self).build() # Add a control volume to the unit including setting up dynamics. self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) # Add geomerty variables to control volume if self.config.has_holdup: self.control_volume.add_geometry() # Add inlet and outlet state blocks to control volume self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) # Add mass balance # Set has_equilibrium is False for now # TO DO; set has_equilibrium to True self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=self.config.has_phase_equilibrium) # Add energy balance self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_work_transfer=True) # add momentum balance self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Set Unit Geometry and holdup Volume if self.config.has_holdup is True: add_object_reference(self, "volume", self.control_volume.volume) # Construct performance equations # Set references to balance terms at unit level # Add Work transfer variable 'work' as necessary add_object_reference(self, "work_mechanical", self.control_volume.work) # Add Momentum balance variable 'deltaP' as necessary add_object_reference(self, "deltaP", self.control_volume.deltaP) # Set reference to scaling factor for pressure in control volume add_object_reference(self, "sfp", self.control_volume.scaling_factor_pressure) # Set reference to scaling factor for energy in control volume add_object_reference(self, "sfe", self.control_volume.scaling_factor_energy) # Performance Variables self.ratioP = Var(self.flowsheet().config.time, initialize=1.0, doc="Pressure Ratio") # Pressure Ratio @self.Constraint(self.flowsheet().config.time, doc="Pressure ratio constraint") def ratioP_calculation(b, t): return (self.sfp*b.ratioP[t] * b.control_volume.properties_in[t].pressure == self.sfp*b.control_volume.properties_out[t].pressure) # Construct equations for thermodynamic assumption if self.config.thermodynamic_assumption == \ ThermodynamicAssumption.isothermal: self.add_isothermal() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.isentropic: self.add_isentropic() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.pump: self.add_pump() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.adiabatic: self.add_adiabatic()
def build(self): """ Build the RO model. """ # Call UnitModel.build to setup dynamics super().build() # for quacking like 1D model -> 0. is "in", 1. is "out" self.length_domain = Set(ordered=True, initialize=(0., 1.)) # inlet/outlet set add_object_reference(self, 'difference_elements', self.length_domain) self.first_element = self.length_domain.first() # Build control volume for feed side self.feed_side = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.feed_side.add_state_blocks(has_phase_equilibrium=False) self.feed_side.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) self.feed_side.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=True) self.feed_side.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # for quacking like 1D model add_object_reference( self.feed_side, 'properties', { **{(t, 0.): self.feed_side.properties_in[t] for t in self.flowsheet().config.time}, **{(t, 1.): self.feed_side.properties_out[t] for t in self.flowsheet().config.time} }) # Add additional state blocks tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = False # these blocks are not inlets # Build permeate side self.permeate_side = self.config.property_package.state_block_class( self.flowsheet().config.time, self.length_domain, doc="Material properties of permeate along permeate channel", default=tmp_dict) self.mixed_permeate = self.config.property_package.state_block_class( self.flowsheet().config.time, doc="Material properties of mixed permeate exiting the module", default=tmp_dict) # Interface properties self.feed_side.properties_interface = self.config.property_package.state_block_class( self.flowsheet().config.time, self.length_domain, doc="Material properties of feed-side membrane interface", default=tmp_dict) # Add Ports self.add_inlet_port(name='inlet', block=self.feed_side) self.add_outlet_port(name='retentate', block=self.feed_side) self.add_port(name='permeate', block=self.mixed_permeate) # References for control volume # pressure change if (self.config.has_pressure_change and self.config.momentum_balance_type != 'none'): self.deltaP = Reference(self.feed_side.deltaP) self._make_performance() self._add_expressions()
def build(self): # build always starts by calling super().build() # This triggers a lot of boilerplate in the background for you super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition # Create essential sets. self.membrane_set = Set(initialize=["cem", "aem"]) # Create unit model parameters and vars self.water_density = Param( initialize=1000, mutable=False, units=pyunits.kg * pyunits.m**-3, doc="density of water", ) self.cell_pair_num = Var( initialize=1, domain=NonNegativeIntegers, bounds=(1, 10000), units=pyunits.dimensionless, doc="cell pair number in a stack", ) # electrodialysis cell dimensional properties self.cell_width = Var( initialize=0.1, bounds=(1e-3, 1e2), units=pyunits.meter, doc="The width of the electrodialysis cell, denoted as b in the model description", ) self.cell_length = Var( initialize=0.5, bounds=(1e-3, 1e2), units=pyunits.meter, doc="The length of the electrodialysis cell, denoted as l in the model description", ) self.spacer_thickness = Var( initialize=0.0001, units=pyunits.meter, doc="The distance between the concecutive aem and cem", ) # Material and Operational properties self.membrane_thickness = Var( self.membrane_set, initialize=0.0001, bounds=(1e-6, 1e-1), units=pyunits.meter, doc="Membrane thickness", ) self.solute_diffusivity_membrane = Var( self.membrane_set, self.config.property_package.ion_set | self.config.property_package.solute_set, initialize=1e-10, bounds=(1e-16, 1e-6), units=pyunits.meter**2 * pyunits.second**-1, doc="Solute (ionic and neutral) diffusivity in the membrane phase", ) self.ion_trans_number_membrane = Var( self.membrane_set, self.config.property_package.ion_set, bounds=(0, 1), units=pyunits.dimensionless, doc="Ion transference number in the membrane phase", ) self.water_trans_number_membrane = Var( self.membrane_set, initialize=5, bounds=(0, 50), units=pyunits.dimensionless, doc="Transference number of water in membranes", ) self.water_permeability_membrane = Var( self.membrane_set, initialize=1e-14, units=pyunits.meter * pyunits.second**-1 * pyunits.pascal**-1, doc="Water permeability coefficient", ) self.membrane_surface_resistance = Var( self.membrane_set, initialize=2e-4, bounds=(1e-6, 1), units=pyunits.ohm * pyunits.meter**2, doc="Surface resistance of membrane", ) self.electrodes_resistance = Var( initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=pyunits.ohm * pyunits.meter**2, doc="areal resistance of TWO electrode compartments of a stack", ) self.current = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 1000), units=pyunits.amp, doc="Current across a cell-pair or stack", ) self.voltage = Var( self.flowsheet().config.time, initialize=100, bounds=(0, 1000), units=pyunits.volt, doc="Voltage across a stack, declared under the 'Constant Voltage' mode only", ) self.current_utilization = Var( initialize=1, bounds=(0, 1), units=pyunits.dimensionless, doc="The current utilization including water electro-osmosis and ion diffusion", ) # Performance metrics self.current_efficiency = Var( self.flowsheet().config.time, initialize=0.9, bounds=(0, 1), units=pyunits.dimensionless, doc="The overall current efficiency for deionizaiton", ) self.power_electrical = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 12100), domain=NonNegativeReals, units=pyunits.watt, doc="Electrical power consumption of a stack", ) self.specific_power_electrical = Var( self.flowsheet().config.time, initialize=10, bounds=(0, 1000), domain=NonNegativeReals, units=pyunits.kW * pyunits.hour * pyunits.meter**-3, doc="Diluate-volume-flow-rate-specific electrical power consumption", ) # TODO: consider adding more performance as needed. # Fluxes Vars for constructing mass transfer terms self.elec_migration_flux_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_in of a component across the membrane driven by electrical migration", ) self.elec_migration_flux_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_out of a component across the membrane driven by electrical migration", ) self.nonelec_flux_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_in of a component across the membrane driven by non-electrical forces", ) self.nonelec_flux_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_out of a component across the membrane driven by non-electrical forces", ) # Build control volume for the dilute channel self.diluate_channel = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, } ) self.diluate_channel.add_state_blocks(has_phase_equilibrium=False) self.diluate_channel.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) self.diluate_channel.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False ) # # TODO: Consider adding energy balances # Build control volume for the concentrate channel self.concentrate_channel = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, } ) self.concentrate_channel.add_state_blocks(has_phase_equilibrium=False) self.concentrate_channel.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) self.concentrate_channel.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False ) # # TODO: Consider adding energy balances # Add ports (creates inlets and outlets for each channel) self.add_inlet_port(name="inlet_diluate", block=self.diluate_channel) self.add_outlet_port(name="outlet_diluate", block=self.diluate_channel) self.add_inlet_port(name="inlet_concentrate", block=self.concentrate_channel) self.add_outlet_port(name="outlet_concentrate", block=self.concentrate_channel) # Build Constraints @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, doc="Current-Voltage relationship", ) def eq_current_voltage_relation(self, t, p): surface_resistance_cp = ( self.membrane_surface_resistance["aem"] + self.membrane_surface_resistance["cem"] + self.spacer_thickness / ( 0.5 * ( self.concentrate_channel.properties_in[ t ].electrical_conductivity_phase[p] + self.concentrate_channel.properties_out[ t ].electrical_conductivity_phase[p] + self.diluate_channel.properties_in[ t ].electrical_conductivity_phase[p] + self.diluate_channel.properties_out[ t ].electrical_conductivity_phase[p] ) ) ) return ( self.current[t] * ( surface_resistance_cp * self.cell_pair_num + self.electrodes_resistance ) == self.voltage[t] * self.cell_width * self.cell_length ) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for electrical migration flux_in", ) def eq_elec_migration_flux_in(self, t, p, j): if j == "H2O": return self.elec_migration_flux_in[t, p, j] == ( self.water_trans_number_membrane["cem"] + self.water_trans_number_membrane["aem"] ) * ( self.current[t] / (self.cell_width * self.cell_length) / Constants.faraday_constant ) elif j in self.config.property_package.ion_set: return self.elec_migration_flux_in[t, p, j] == ( self.ion_trans_number_membrane["cem", j] - self.ion_trans_number_membrane["aem", j] ) * ( self.current_utilization * self.current[t] / (self.cell_width * self.cell_length) ) / ( self.config.property_package.charge_comp[j] * Constants.faraday_constant ) else: return self.elec_migration_flux_out[t, p, j] == 0 @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for electrical migration flux_out", ) def eq_elec_migration_flux_out(self, t, p, j): if j == "H2O": return self.elec_migration_flux_out[t, p, j] == ( self.water_trans_number_membrane["cem"] + self.water_trans_number_membrane["aem"] ) * ( self.current[t] / (self.cell_width * self.cell_length) / Constants.faraday_constant ) elif j in self.config.property_package.ion_set: return self.elec_migration_flux_out[t, p, j] == ( self.ion_trans_number_membrane["cem", j] - self.ion_trans_number_membrane["aem", j] ) * ( self.current_utilization * self.current[t] / (self.cell_width * self.cell_length) ) / ( self.config.property_package.charge_comp[j] * Constants.faraday_constant ) else: return self.elec_migration_flux_out[t, p, j] == 0 @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for non-electrical flux_in", ) def eq_nonelec_flux_in(self, t, p, j): if j == "H2O": return self.nonelec_flux_in[ t, p, j ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["cem"] + self.water_permeability_membrane["aem"] ) * ( self.concentrate_channel.properties_in[t].pressure_osm_phase[p] - self.diluate_channel.properties_in[t].pressure_osm_phase[p] ) else: return self.nonelec_flux_in[t, p, j] == -( self.solute_diffusivity_membrane["cem", j] / self.membrane_thickness["cem"] + self.solute_diffusivity_membrane["aem", j] / self.membrane_thickness["aem"] ) * ( self.concentrate_channel.properties_in[t].conc_mol_phase_comp[p, j] - self.diluate_channel.properties_in[t].conc_mol_phase_comp[p, j] ) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for non-electrical flux_out", ) def eq_nonelec_flux_out(self, t, p, j): if j == "H2O": return self.nonelec_flux_out[ t, p, j ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["cem"] + self.water_permeability_membrane["aem"] ) * ( self.concentrate_channel.properties_out[t].pressure_osm_phase[p] - self.diluate_channel.properties_out[t].pressure_osm_phase[p] ) else: return self.nonelec_flux_out[t, p, j] == -( self.solute_diffusivity_membrane["cem", j] / self.membrane_thickness["cem"] + self.solute_diffusivity_membrane["aem", j] / self.membrane_thickness["aem"] ) * ( self.concentrate_channel.properties_out[t].conc_mol_phase_comp[p, j] - self.diluate_channel.properties_out[t].conc_mol_phase_comp[p, j] ) # Add constraints for mass transfer terms (diluate_channel) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term for the diluate channel", ) def eq_mass_transfer_term_diluate(self, t, p, j): return self.diluate_channel.mass_transfer_term[t, p, j] == -0.5 * ( self.elec_migration_flux_in[t, p, j] + self.elec_migration_flux_out[t, p, j] + self.nonelec_flux_in[t, p, j] + self.nonelec_flux_out[t, p, j] ) * (self.cell_width * self.cell_length) # Add constraints for mass transfer terms (concentrate_channel) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term for the concentrate channel", ) def eq_mass_transfer_term_concentrate(self, t, p, j): return self.concentrate_channel.mass_transfer_term[t, p, j] == 0.5 * ( self.elec_migration_flux_in[t, p, j] + self.elec_migration_flux_out[t, p, j] + self.nonelec_flux_in[t, p, j] + self.nonelec_flux_out[t, p, j] ) * (self.cell_width * self.cell_length) # Add isothermal condition @self.Constraint( self.flowsheet().config.time, doc="Isothermal condition for the diluate channel", ) def eq_isothermal_diluate(self, t): return ( self.diluate_channel.properties_in[t].temperature == self.diluate_channel.properties_out[t].temperature ) @self.Constraint( self.flowsheet().config.time, doc="Isothermal condition for the concentrate channel", ) def eq_isothermal_concentrate(self, t): return ( self.concentrate_channel.properties_in[t].temperature == self.concentrate_channel.properties_out[t].temperature ) @self.Constraint( self.flowsheet().config.time, doc="Electrical power consumption of a stack", ) def eq_power_electrical(self, t): return self.power_electrical[t] == self.current[t] * self.voltage[t] @self.Constraint( self.flowsheet().config.time, doc="Diluate_volume_flow_rate_specific electrical power consumption of a stack", ) def eq_specific_power_electrical(self, t): return ( pyunits.convert( self.specific_power_electrical[t], pyunits.watt * pyunits.second * pyunits.meter**-3, ) * self.diluate_channel.properties_out[t].flow_vol_phase["Liq"] == self.current[t] * self.voltage[t] ) @self.Constraint( self.flowsheet().config.time, doc="Overall current efficiency evaluation", ) def eq_current_efficiency(self, t): return ( self.current_efficiency[t] * self.current[t] == sum( self.diluate_channel.properties_in[t].flow_mol_phase_comp["Liq", j] * self.config.property_package.charge_comp[j] - self.diluate_channel.properties_out[t].flow_mol_phase_comp[ "Liq", j ] * self.config.property_package.charge_comp[j] for j in self.config.property_package.cation_set ) * Constants.faraday_constant )
def build(self): # build always starts by calling super().build() # This triggers a lot of boilerplate in the background for you super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta = self.config.property_package.get_metadata().get_derived_units # check the optional config arg 'chemical_additives' common_msg = "The 'chemical_additives' dict MUST contain a dict of 'parameter_data' for " + \ "each chemical name. That 'parameter_data' dict MUST contain 'mw_chem', " + \ "'moles_salt_per_mole_additive', and 'mw_salt' as keys. Users are also " + \ "required to provide the values for the molecular weights and the units " + \ "within a tuple arg. Example format provided below.\n\n" + \ "{'chem_name_1': \n" + \ " {'parameter_data': \n" + \ " {'mw_additive': (value, units), \n" + \ " 'moles_salt_per_mole_additive': value, \n" + \ " 'mw_salt': (value, units)} \n" + \ " }, \n" + \ "}\n\n" mw_adds = {} mw_salts = {} molar_rat = {} for j in self.config.chemical_additives: if type(self.config.chemical_additives[j]) != dict: raise ConfigurationError("\n Did not provide a 'dict' for chemical \n" + common_msg) if 'parameter_data' not in self.config.chemical_additives[j]: raise ConfigurationError("\n Did not provide a 'parameter_data' for chemical \n" + common_msg) if 'mw_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_additive' for chemical \n" + common_msg) if 'moles_salt_per_mole_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'moles_salt_per_mole_additive' for chemical \n" + common_msg) if 'mw_salt' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_salt' for chemical \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_additive']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_additive' \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_salt']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_salt' \n" + common_msg) if not isinstance(self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'], (int,float)): raise ConfigurationError("\n Did not provide a number for 'moles_salt_per_mole_additive' \n" + common_msg) #Populate temp dicts for parameter and variable setting mw_adds[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_additive'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_additive'][1], to_units=pyunits.kg/pyunits.mol) mw_salts[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_salt'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_salt'][1], to_units=pyunits.kg/pyunits.mol) molar_rat[j] = self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'] # Add unit variables # Linear relationship between TSS (mg/L) and Turbidity (NTU) # TSS (mg/L) = Turbidity (NTU) * slope + intercept # Default values come from the following paper: # H. Rugner, M. Schwientek,B. Beckingham, B. Kuch, P. Grathwohl, # Environ. Earth Sci. 69 (2013) 373-380. DOI: 10.1007/s12665-013-2307-1 self.slope = Var( self.flowsheet().config.time, initialize=1.86, bounds=(1e-8, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Slope relation between TSS (mg/L) and Turbidity (NTU)') self.intercept = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Intercept relation between TSS (mg/L) and Turbidity (NTU)') self.initial_turbidity_ntu = Var( self.flowsheet().config.time, initialize=50, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Initial measured Turbidity (NTU) from Jar Test') self.final_turbidity_ntu = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Final measured Turbidity (NTU) from Jar Test') self.chemical_doses = Var( self.flowsheet().config.time, self.config.chemical_additives.keys(), initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Dosages of the set of chemical additives') self.chemical_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_adds, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the set of chemical additives') self.salt_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_salts, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the produced salts from chemical additives') self.salt_from_additive_mole_ratio = Param( self.config.chemical_additives.keys(), mutable=True, initialize=molar_rat, domain=NonNegativeReals, units=pyunits.mol/pyunits.mol, doc='Moles of the produced salts from 1 mole of chemical additives') # Build control volume for feed side self.control_volume = ControlVolume0DBlock(default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_state_blocks( has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) # NOTE: This checks for if an energy_balance_type is defined if hasattr(self.config, "energy_balance_type"): self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=False) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False) # Add ports self.add_inlet_port(name='inlet', block=self.control_volume) self.add_outlet_port(name='outlet', block=self.control_volume) # Check _phase_component_set for required items if ('Liq', 'TDS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TDS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'Sludge') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','Sludge') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'TSS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TSS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) # -------- Add constraints --------- # Adds isothermal constraint if no energy balance present if not hasattr(self.config, "energy_balance_type"): @self.Constraint(self.flowsheet().config.time, doc="Isothermal condition") def eq_isothermal(self, t): return (self.control_volume.properties_out[t].temperature == self.control_volume.properties_in[t].temperature) # Constraint for tss loss rate based on measured final turbidity self.tss_loss_rate = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time loss rate of TSS based on the measured final turbidity') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tss_loss_rate(self, t): tss_out = pyunits.convert(self.slope[t]*self.final_turbidity_ntu[t] + self.intercept[t], to_units=units_meta('mass')*units_meta('length')**-3) input_rate = self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS'] exit_rate = self.control_volume.properties_out[t].flow_vol_phase['Liq']*tss_out return (self.tss_loss_rate[t] == input_rate - exit_rate) # Constraint for tds gain rate based on 'chemical_doses' and 'chemical_additives' if self.config.chemical_additives: self.tds_gain_rate = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time gain rate of TDS based on the chemicals added for coagulation') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tds_gain_rate(self, t): sum = 0 for j in self.config.chemical_additives.keys(): chem_dose = pyunits.convert(self.chemical_doses[t, j], to_units=units_meta('mass')*units_meta('length')**-3) chem_dose = chem_dose/self.chemical_mw[j] * \ self.salt_from_additive_mole_ratio[j] * \ self.salt_mw[j]*self.control_volume.properties_out[t].flow_vol_phase['Liq'] sum = sum+chem_dose return (self.tds_gain_rate[t] == sum) # Add constraints for mass transfer terms @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_transfer_term(self, t, p, j): if (p, j) == ('Liq', 'TSS'): return self.control_volume.mass_transfer_term[t, p, j] == -self.tss_loss_rate[t] elif (p, j) == ('Liq', 'Sludge'): return self.control_volume.mass_transfer_term[t, p, j] == self.tss_loss_rate[t] elif (p, j) == ('Liq', 'TDS'): if self.config.chemical_additives: return self.control_volume.mass_transfer_term[t, p, j] == self.tds_gain_rate[t] else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0 else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0
def build(self): """ Begin building model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(EquilibriumReactorData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args }) # No need for control volume geometry self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_reaction_blocks( has_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=self.config.has_rate_reactions, has_equilibrium_reactions=self.config.has_equilibrium_reactions, has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=self.config.has_heat_of_reaction, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() if self.config.has_rate_reactions: # Add equilibrium reactor performance equation @self.Constraint(self.flowsheet().config.time, self.config.reaction_package.rate_reaction_idx, doc="Rate reaction equilibrium constraint") def rate_reaction_constraint(b, t, r): # Set kinetic reaction rates to zero return b.control_volume.reactions[t].reaction_rate[r] == 0 # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): add_object_reference(self, "deltaP", self.control_volume.deltaP)
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_geometry() self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type,) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) self.flash = HelmPhaseSeparator( default={ "dynamic": False, "property_package": self.config.property_package, } ) self.mixer = Mixer( default={ "dynamic": False, "property_package": self.config.property_package, "inlet_list": ["FeedWater", "SaturatedWater"], "mixed_state_block": self.control_volume.properties_in, } ) # instead of creating a new block use control volume to return solution # Inlet Ports # FeedWater to Drum (from Pipe or Economizer) self.feedwater_inlet = Port(extends=self.mixer.FeedWater) # Sat water from water wall self.water_steam_inlet = Port(extends=self.flash.inlet) # Exit Ports # Liquid to Downcomer # self.liquid_outlet = Port(extends=self.mixer.outlet) self.add_outlet_port('liquid_outlet', self.control_volume) # Steam to superheaters self.steam_outlet = Port(extends=self.flash.vap_outlet) # constraint to make pressures of two inlets of drum mixer the same @self.Constraint(self.flowsheet().config.time, doc="Mixter pressure identical") def mixer_pressure_eqn(b, t): return b.mixer.SaturatedWater.pressure[t]*1e-6 == \ b.mixer.FeedWater.pressure[t]*1e-6 self.stream_flash_out = Arc( source=self.flash.liq_outlet, destination=self.mixer.SaturatedWater ) # Pyomo arc connect flash liq_outlet with mixer SaturatedWater inlet pyo.TransformationFactory("network.expand_arcs").apply_to(self) # Add object references self.volume = pyo.Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = pyo.Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = pyo.Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance()