class BalanceBlockData(UnitModelBlockData): """ Simple mass and energy balance unit. """ CONFIG = UnitModelBlockData.CONFIG() make_balance_config_block(CONFIG) def build(self): """Building model Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Add Control Volume make_balance_control_volume(self, "control_volume", self.config) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add convienient references to control volume quantities deltaP. if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP) if self.config.has_heat_transfer is True: self.heat_duty = Reference(self.control_volume.heat) if self.config.has_work_transfer is True: self.work = Reference(self.control_volume.work)
class HeaterData(UnitModelBlockData): """ Simple 0D heater unit. Unit model to add or remove heat from a material. """ CONFIG = UnitModelBlockData.CONFIG() _make_heater_config_block(CONFIG) def build(self): """Building model Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(HeaterData, self).build() # Add Control Volume _make_heater_control_volume(self, "control_volume", self.config) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add a convienient reference to heat duty. self.heat_duty = Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP) def _get_performance_contents(self, time_point=0): return {"vars": {"Heat Duty": self.heat_duty[time_point]}}
class _UnitData(UnitModelBlockData): CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare("property_package", ConfigValue(default=None)) CONFIG.declare("property_package_args", ConfigValue(default={})) def build(self): super(_UnitData, self).build()
class HeaterData(UnitModelBlockData): """ Simple 0D heater unit. Unit model to add or remove heat from a material. """ CONFIG = UnitModelBlockData.CONFIG() _make_heater_config_block(CONFIG) def build(self): """ Building model Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(HeaterData, self).build() # Add Control Volume _make_heater_control_volume(self, "control_volume", self.config) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add a convienient reference to heat duty. add_object_reference(self, "heat_duty", self.control_volume.heat)
class StoichiometricReactorData(UnitModelBlockData): """ Standard Stoichiometric Reactor Unit Model Class This model assumes that all given reactions are irreversible, and that each reaction has a fixed rate_reaction extent which has to be specified by the user. """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_of_reaction", ConfigValue( default=False, domain=In([True, False]), description="Heat of reaction term construction flag", doc="""Indicates whether terms for heat of reaction terms should be constructed, **default** - False. **Valid values:** { **True** - include heat of reaction terms, **False** - exclude heat of reaction terms.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc= """Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""")) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc= """A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""")) 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 self.rate_reaction_extent = Reference( 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 != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat[:]) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP[:]) def _get_performance_contents(self, time_point=0): var_dict = {} for r in self.config.reaction_package.rate_reaction_idx: var_dict[f"Reaction Extent [{r}]"] = \ self.rate_reaction_extent[time_point, r] if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] return {"vars": var_dict}
class HeatExchangerWith3StreamsData(UnitModelBlockData): """ Standard Heat Exchanger Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "side_1_property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "side_1_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "side_2_property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "side_2_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "side_3_property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "side_3_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=True, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "flow_type_side_2", ConfigValue( default='counter-current', domain=In(['counter-current', 'co-current']), description="Flow configuration in unit", doc="""Flag indicating type of flow arrangement to use for heat exchanger, **default** 'counter-current' counter-current flow arrangement""")) CONFIG.declare( "flow_type_side_3", ConfigValue( default='counter-current', domain=In(['counter-current', 'co-current']), description="Flow configuration in unit", doc="""Flag indicating type of flow arrangement to use for heat exchanger (default = 'counter-current' - counter-current flow arrangement""")) def build(self): """ Begin building model """ # Call UnitModel.build to setup dynamics super(HeatExchangerWith3StreamsData, self).build() # Build Holdup Block self.side_1 = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.side_1_property_package, "property_package_args": self.config.side_1_property_package_args }) self.side_2 = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.side_2_property_package, "property_package_args": self.config.side_2_property_package_args }) self.side_3 = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.side_3_property_package, "property_package_args": self.config.side_3_property_package_args }) # Add Geometry self.side_1.add_geometry() self.side_2.add_geometry() self.side_3.add_geometry() # Add state block self.side_1.add_state_blocks(has_phase_equilibrium=False) # Add material balance self.side_1.add_material_balances( balance_type=self.config.material_balance_type) # add energy balance self.side_1.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) # add momentum balance self.side_1.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add state block self.side_2.add_state_blocks(has_phase_equilibrium=False) # Add material balance self.side_2.add_material_balances( balance_type=self.config.material_balance_type) # add energy balance self.side_2.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) # add momentum balance self.side_2.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add state block self.side_3.add_state_blocks(has_phase_equilibrium=False) # Add material balance self.side_3.add_material_balances( balance_type=self.config.material_balance_type) # add energy balance self.side_3.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) # add momentum balance self.side_3.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) self._set_geometry() # Construct performance equations self._make_performance() # Construct performance equations if self.config.flow_type_side_2 == "counter-current": self._make_counter_current_side_2() else: self._make_co_current_side_2() # Construct performance equations if self.config.flow_type_side_3 == "counter-current": self._make_counter_current_side_3() else: self._make_co_current_side_3() self.add_inlet_port(name="side_1_inlet", block=self.side_1) self.add_inlet_port(name="side_2_inlet", block=self.side_2) self.add_inlet_port(name="side_3_inlet", block=self.side_3) self.add_outlet_port(name="side_1_outlet", block=self.side_1) self.add_outlet_port(name="side_2_outlet", block=self.side_2) self.add_outlet_port(name="side_3_outlet", block=self.side_3) def _set_geometry(self): """ Define the geometry of the unit as necessary, and link to holdup volume """ # UA (product of overall heat transfer coefficient and area) # between side 1 and side 2 self.ua_side_2 = Var(self.flowsheet().config.time, initialize=10.0, doc='UA between side 1 and side 2') # UA (product of overall heat transfer coefficient and area) # between side 1 and side 3 self.ua_side_3 = Var(self.flowsheet().config.time, initialize=10.0, doc='UA between side 1 and side 3') # fraction of heat from hot stream as heat loss to ambient self.frac_heatloss = Var(initialize=0.05, doc='Fraction of heat loss to ambient') if self.config.has_holdup is True: self.volume_side_1 = Reference(self.side_1.volume) self.volume_side_2 = Reference(self.side_2.volume) self.volume_side_3 = Reference(self.side_3.volume) def _make_performance(self): """ Define constraints which describe the behaviour of the unit model. Args: None Returns: None """ # Set references to balance terms at unit level self.heat_duty_side_1 = Reference(self.side_1.heat) self.heat_duty_side_2 = Reference(self.side_2.heat) self.heat_duty_side_3 = Reference(self.side_3.heat) if self.config.has_pressure_change is True: self.deltaP_side_1 = Reference(self.side_1.deltaP) self.deltaP_side_2 = Reference(self.side_2.deltaP) self.deltaP_side_3 = Reference(self.side_3.deltaP) # Performance parameters and variables # Temperature driving force self.temperature_driving_force_side_2 = Var( self.flowsheet().config.time, initialize=1.0, doc='Mean driving force ' 'for heat exchange') # Temperature driving force self.temperature_driving_force_side_3 = Var( self.flowsheet().config.time, initialize=1.0, doc='Mean driving force ' 'for heat exchange') # Temperature difference at side 2 inlet self.side_2_inlet_dT = Var(self.flowsheet().config.time, initialize=1.0, doc='Temperature difference ' 'at side 2 inlet') # Temperature difference at side 2 outlet self.side_2_outlet_dT = Var(self.flowsheet().config.time, initialize=1.0, doc='Temperature difference ' 'at side 2 outlet') # Temperature difference at side 3 inlet self.side_3_inlet_dT = Var(self.flowsheet().config.time, initialize=1.0, doc='Temperature difference' ' at side 3 inlet') # Temperature difference at side 3 outlet self.side_3_outlet_dT = Var(self.flowsheet().config.time, initialize=1.0, doc='Temperature difference ' 'at side 3 outlet') # Driving force side 2 (Underwood approximation) @self.Constraint(self.flowsheet().config.time, doc="Log mean temperature difference calculation " "using Underwood approximation") def LMTD_side_2(b, t): return b.temperature_driving_force_side_2[t] == \ ((b.side_2_inlet_dT[t]**(1/3) + b.side_2_outlet_dT[t]**(1/3))/2)**(3) # Driving force side 3 (Underwood approximation) @self.Constraint(self.flowsheet().config.time, doc="Log mean temperature difference calculation " "using Underwood approximation") def LMTD_side_3(b, t): return b.temperature_driving_force_side_3[t] == \ ((b.side_3_inlet_dT[t]**(1/3) + b.side_3_outlet_dT[t]**(1/3))/2)**(3) # Heat duty side 2 @self.Constraint(self.flowsheet().config.time, doc="Heat transfer rate") def heat_duty_side_2_eqn(b, t): return b.heat_duty_side_2[t] == \ (b.ua_side_2[t] * b.temperature_driving_force_side_2[t]) # Heat duty side 3 @self.Constraint(self.flowsheet().config.time, doc="Heat transfer rate") def heat_duty_side_3_eqn(b, t): return b.heat_duty_side_3[t] == \ (b.ua_side_3[t]*b.temperature_driving_force_side_3[t]) # Energy balance equation @self.Constraint(self.flowsheet().config.time, doc="Energy balance between two sides") def heat_duty_side_1_eqn(b, t): return -b.heat_duty_side_1[t]*(1-b.frac_heatloss) == \ (b.heat_duty_side_2[t] + b.heat_duty_side_3[t]) def _make_co_current_side_2(self): """ Add temperature driving force Constraints for co-current flow. """ # Temperature Differences @self.Constraint(self.flowsheet().config.time, doc="Side 2 inlet temperature difference") def side_2_inlet_dT_eqn(b, t): return b.side_2_inlet_dT[t] == ( b.side_1.properties_in[t].temperature - b.side_2.properties_in[t].temperature) @self.Constraint(self.flowsheet().config.time, doc="Side 2 outlet temperature difference") def side_2_outlet_dT_eqn(b, t): return b.side_2_outlet_dT[t] == ( b.side_1.properties_out[t].temperature - b.side_2.properties_out[t].temperature) def _make_counter_current_side_2(self): """ Add temperature driving force Constraints for counter-current flow. """ # Temperature Differences @self.Constraint(self.flowsheet().config.time, doc="Side 2 inlet temperature difference") def side_2_inlet_dT_eqn(b, t): return b.side_2_inlet_dT[t] == ( b.side_1.properties_out[t].temperature - b.side_2.properties_in[t].temperature) @self.Constraint(self.flowsheet().config.time, doc="Side 2 outlet temperature difference") def side_2_outlet_dT_eqn(b, t): return b.side_2_outlet_dT[t] == ( b.side_1.properties_in[t].temperature - b.side_2.properties_out[t].temperature) def _make_co_current_side_3(self): """ Add temperature driving force Constraints for co-current flow. """ # Temperature Differences @self.Constraint(self.flowsheet().config.time, doc="Side 3 inlet temperature difference") def side_3_inlet_dT_eqn(b, t): return b.side_3_inlet_dT[t] == ( b.side_1.properties_in[t].temperature - b.side_3.properties_in[t].temperature) @self.Constraint(self.flowsheet().config.time, doc="Side 3 outlet temperature difference") def side_3_outlet_dT_eqn(b, t): return b.side_3_outlet_dT[t] == ( b.side_1.properties_out[t].temperature - b.side_3.properties_out[t].temperature) def _make_counter_current_side_3(self): """ Add temperature driving force Constraints for counter-current flow. """ # Temperature Differences @self.Constraint(self.flowsheet().config.time, doc="Side 3 inlet temperature difference") def side_3_inlet_dT_eqn(b, t): return b.side_3_inlet_dT[t] == ( b.side_1.properties_out[t].temperature - b.side_3.properties_in[t].temperature) @self.Constraint(self.flowsheet().config.time, doc="Side 3 outlet temperature difference") def side_3_outlet_dT_eqn(b, t): return b.side_3_outlet_dT[t] == ( b.side_1.properties_in[t].temperature - b.side_3.properties_out[t].temperature) def initialize(blk, state_args_1=None, state_args_2=None, state_args_3=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' General Heat Exchanger initialisation routine. Keyword Arguments: state_args_1 : a dict of arguments to be passed to the property package(s) for side 1 of the heat exchanger to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_2 : a dict of arguments to be passed to the property package(s) for side 2 of the heat exchanger to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_3 : a dict of arguments to be passed to the property package(s) for side 3 of the heat exchanger to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize inlet property blocks flags1 = blk.side_1.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1) flags2 = blk.side_2.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2) flags3 = blk.side_3.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_3) init_log.info('Initialisation Step 1 Complete.') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info("Initialization Step 2 Complete: {}".format( idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.side_1.release_state(flags1, outlvl) blk.side_2.release_state(flags2, outlvl) blk.side_3.release_state(flags3, outlvl) init_log.info_low("Initialization Complete: {}".format( idaeslog.condition(res))) def calculate_scaling_factors(self): for t, c in self.heat_duty_side_1_eqn.items(): sf = iscale.get_scaling_factor(self.heat_duty_side_1[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.heat_duty_side_2_eqn.items(): sf = iscale.get_scaling_factor(self.heat_duty_side_2[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.heat_duty_side_3_eqn.items(): sf = iscale.get_scaling_factor(self.heat_duty_side_3[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False)
class PHEData(UnitModelBlockData): """Plate Heat Exchanger(PHE) Unit Model.""" CONFIG = UnitModelBlockData.CONFIG() # Configuration template for fluid specific arguments _SideCONFIG = ConfigBlock() CONFIG.declare( "passes", ConfigValue( default=4, domain=int, description="Number of passes", doc="""Number of passes of the fluids through the heat exchanger""" )) CONFIG.declare( "channel_list", ConfigValue( default=[12, 12, 12, 12], domain=list, description="Number of channels for each pass", doc="""Number of channels to be used in each pass where a channel is the space between two plates with a flowing fluid""")) CONFIG.declare( "divider_plate_number", ConfigValue( default=0, domain=int, description="Number of divider plates in heat exchanger", doc= """Divider plates are used to create separate partitions in the unit. Each pass can be separated by a divider plate""")) CONFIG.declare( "port_diameter", ConfigValue( default=0.2045, domain=float, description="Diameter of the ports on the plate [m]", doc="""Diameter of the ports on the plate for fluid entry/exit into a channel""")) CONFIG.declare( "plate_thermal_cond", ConfigValue( default=16.2, domain=float, description="Thermal conductivity [W/m.K]", doc="""Thermal conductivity of the plate material [W/m.K]""")) CONFIG.declare( "total_area", ConfigValue( default=114.3, domain=float, description="Total heat transfer area [m2]", doc="""Total heat transfer area as specifed by the manufacturer""") ) CONFIG.declare( "plate_thickness", ConfigValue(default=0.0006, domain=float, description="Plate thickness [m]", doc="""Plate thickness""")) CONFIG.declare( "plate_vertical_dist", ConfigValue( default=1.897, domain=float, description="Vertical distance between centers of ports [m].", doc= """Vertical distance between centers of ports.(Top and bottom ports) (approximately equals to the plate length)""")) CONFIG.declare( "plate_horizontal_dist", ConfigValue( default=0.409, domain=float, description="Horizontal distance between centers of ports [m].", doc= """Horizontal distance between centers of ports(Left and right ports)""" )) CONFIG.declare( "plate_pact_length", ConfigValue(default=0.381, domain=float, description="Compressed plate pact length [m].", doc="""Compressed plate pact length. Length between the Head and the Follower""")) CONFIG.declare( "surface_enlargement_factor", ConfigValue( default=None, domain=float, description="Surface enlargement factor", doc="""Surface enlargement factor is the ratio of single plate area (obtained from the total area) to the projected plate area""")) CONFIG.declare( "plate_gap", ConfigValue( default=None, domain=float, description="Mean channel spacing or gap bewteen two plates [m]", doc="""The plate gap is the distance between two adjacent plates that forms a flow channel """)) _SideCONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use ", doc="""Property parameter object used to define property calculations **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) _SideCONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package", doc="""A ConfigBlock with arguments to be passed to property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) # Create individual config blocks for hot and cold sides CONFIG.declare("hot_side", _SideCONFIG(doc="Hot fluid config arguments")) CONFIG.declare("cold_side", _SideCONFIG(doc="Cold fluid config arguments")) def build(self): # Call UnitModel.build to setup model super(PHEData, self).build() # Consistency check for number of passes and channels in each pass for i in self.config.channel_list: if not isinstance(i, int): raise ConfigurationError("number of channels ({}) must be" " an integer".format(i)) if (self.config.passes != len(self.config.channel_list)): raise ConfigurationError( "The number of elements in the channel list: {} " " does not match the number of passes ({}) given. " "Please provide as integers, the number of channels of each pass" .format(self.config.channel_list, self.config.passes)) # ====================================================================== # Build hot-side Control Volume (Lean Solvent) self.hot_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.hot_side.property_package, "property_package_args": self.config.hot_side.property_package_args }) self.hot_side.add_state_blocks(has_phase_equilibrium=False) self.hot_side.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_mass_transfer=False, has_phase_equilibrium=False, has_rate_reactions=False) self.hot_side.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # Energy balance is based on the effectiveness Number of Transfer units # (E-NTU method) and inluded as performance equations. Hence the control # volume energy balances are not added. # ====================================================================== # Build cold-side Control Volume(Rich solvent) self.cold_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.cold_side.property_package, "property_package_args": self.config.cold_side.property_package_args }) self.cold_side.add_state_blocks(has_phase_equilibrium=False) self.cold_side.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_mass_transfer=False, has_phase_equilibrium=False, has_rate_reactions=False) self.cold_side.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # ====================================================================== # Add Ports to control volumes # hot-side self.add_inlet_port(name="hot_inlet", block=self.hot_side, doc='inlet Port') self.add_outlet_port(name="hot_outlet", block=self.hot_side, doc='outlet Port') # cold-side self.add_inlet_port(name="cold_inlet", block=self.cold_side, doc='inlet Port') self.add_outlet_port(name="cold_outlet", block=self.cold_side, doc='outlet Port') # ====================================================================== # Add performace equation method self._make_params() self._make_performance_method() def _make_params(self): self.P = Param(initialize=self.config.passes, units=None, doc="Total number of passes for hot or cold fluid") self.PH = RangeSet(self.P, doc="Set of hot fluid passes") self.PC = RangeSet(self.P, doc="Set of cold fluid passes(equal to PH)") self.plate_thermal_cond = Param( mutable=True, initialize=self.config.plate_thermal_cond, units=pyunits.W / pyunits.m / pyunits.K, doc="Plate thermal conductivity") self.plate_thick = Param(mutable=True, initialize=self.config.plate_thickness, units=pyunits.m, doc="Plate thickness") self.port_dia = Param(mutable=True, initialize=self.config.port_diameter, units=pyunits.m, doc=" Port diameter of plate ") self.Np = Param(self.PH, units=None, doc="Number of channels in each pass", mutable=True) # Number of channels in each pass for i in self.PH: self.Np[i].value = self.config.channel_list[i - 1] # --------------------------------------------------------------------- # Assign plate specifications # effective plate length & width _effective_plate_length = self.config.plate_vertical_dist - \ self.config.port_diameter _effective_plate_width = self.config.plate_horizontal_dist + \ self.config.port_diameter self.plate_length = Expression(expr=_effective_plate_length) self.plate_width = Expression(expr=_effective_plate_width) # Area of single plate _total_active_plate_number = 2 * sum(self.config.channel_list) - 1 -\ self.config.divider_plate_number self.plate_area = Expression(expr=self.config.total_area / _total_active_plate_number, doc="Heat transfer area of single plate") # Plate gap if self.config.plate_gap is None: _total_plate_number = 2 * sum(self.config.channel_list) + 1 +\ self.config.divider_plate_number _plate_pitch = self.config.plate_pact_length / _total_plate_number _plate_gap = _plate_pitch - self.config.plate_thickness else: _plate_gap = self.config.plate_gap self.plate_gap = Expression(expr=_plate_gap) # Surface enlargement factor if self.config.surface_enlargement_factor is None: _projected_plate_area = _effective_plate_length * _effective_plate_width _surface_enlargement_factor = self.plate_area / _projected_plate_area else: _surface_enlargement_factor = self.config.surface_enlargement_factor self.surface_enlargement_factor = Expression( expr=_surface_enlargement_factor) # Channel equivalent diameter self.channel_dia = Expression(expr=2 * self.plate_gap / _surface_enlargement_factor, doc=" Channel equivalent diameter") # heat transfer parameters self.param_a = Var(initialize=0.3, bounds=(0.2, 0.4), units=None, doc='Nusselt parameter') self.param_b = Var(initialize=0.663, bounds=(0.3, 0.7), units=None, doc='Nusselt parameter') self.param_c = Var(initialize=1 / 3.0, bounds=(1e-5, 2), units=None, doc='Nusselt parameter') self.param_a.fix(0.4) self.param_b.fix(0.663) self.param_c.fix(0.333) def _make_performance_method(self): solvent_list = self.config.hot_side.property_package.component_list_solvent def rule_trh(blk, t): return (blk.hot_side.properties_out[t].temperature / blk.hot_side.properties_in[t].temperature) self.trh = Expression(self.flowsheet().config.time, rule=rule_trh, doc='Ratio of hot outlet temperature to hot' 'inlet temperature') def rule_trc(blk, t): return (blk.cold_side.properties_out[t].temperature / blk.cold_side.properties_in[t].temperature) self.trc = Expression(self.flowsheet().config.time, rule=rule_trc, doc='Ratio of cold outlet temperature to cold' ' inlet temperature') def rule_cp_comp_hot(blk, t, j): return 1e3 * ( blk.hot_side.properties_in[t]._params.cp_param[j, 1] + blk.hot_side.properties_in[t]._params.cp_param[j, 2] / 2 * blk.hot_side.properties_in[t].temperature * (blk.trh[t] + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 3] / 3 * (blk.hot_side.properties_in[t].temperature**2) * (blk.trh[t]**2 + blk.trh[t] + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 4] / 4 * (blk.hot_side.properties_in[t].temperature**3) * (blk.trh[t] + 1) * (blk.trh[t]**2 + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 5] / 5 * (blk.hot_side.properties_in[t].temperature**4) * (blk.trh[t]**4 + blk.trh[t]**3 + blk.trh[t]**2 + blk.trh[t] + 1)) self.cp_comp_hot = Expression( self.flowsheet().config.time, solvent_list, rule=rule_cp_comp_hot, doc='Component mean specific heat capacity' ' btw inlet and outlet' ' of hot-side temperature') def rule_cp_hot(blk, t): return sum(blk.cp_comp_hot[t, j] * blk.hot_side.properties_in[t].mass_frac_co2_free[j] for j in solvent_list) self.cp_hot = Expression(self.flowsheet().config.time, rule=rule_cp_hot, doc='Hot-side mean specific heat capacity on' 'free CO2 basis') def rule_cp_comp_cold(blk, t, j): return 1e3 * ( blk.cold_side.properties_in[t]._params.cp_param[j, 1] + blk.cold_side.properties_in[t]._params.cp_param[j, 2] / 2 * blk.cold_side.properties_in[t].temperature * (blk.trc[t] + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 3] / 3 * (blk.cold_side.properties_in[t].temperature**2) * (blk.trc[t]**2 + blk.trc[t] + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 4] / 4 * (blk.cold_side.properties_in[t].temperature**3) * (blk.trc[t] + 1) * (blk.trc[t]**2 + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 5] / 5 * (blk.cold_side.properties_in[t].temperature**4) * (blk.trc[t]**4 + blk.trc[t]**3 + blk.trc[t]**2 + blk.trc[t] + 1)) self.cp_comp_cold = Expression( self.flowsheet().config.time, solvent_list, rule=rule_cp_comp_cold, doc='Component mean specific heat capacity' 'btw inlet and outlet' ' of cold-side temperature') def rule_cp_cold(blk, t): return sum(blk.cp_comp_cold[t, j] * blk.cold_side.properties_in[t].mass_frac_co2_free[j] for j in solvent_list) self.cp_cold = Expression(self.flowsheet().config.time, rule=rule_cp_cold, doc='Cold-side mean specific heat capacity' 'on free CO2 basis') # Model Variables self.Th_in = Var(self.flowsheet().config.time, self.PH, initialize=393, units=pyunits.K, doc="Hot Temperature IN of pass") self.Th_out = Var(self.flowsheet().config.time, self.PH, initialize=325, units=pyunits.K, doc="Hot Temperature OUT of pass") self.Tc_in = Var(self.flowsheet().config.time, self.PH, initialize=320, units=pyunits.K, doc="Cold Temperature IN of pass") self.Tc_out = Var(self.flowsheet().config.time, self.PH, initialize=390, units=pyunits.K, doc="Cold Temperature OUT of pass") # ====================================================================== # PERFORMANCE EQUATIONS # mass flow rate in kg/s def rule_mh_in(blk, t): return blk.hot_side.properties_in[t].flow_mol *\ blk.hot_side.properties_in[t].mw self.mh_in = Expression(self.flowsheet().config.time, rule=rule_mh_in, doc='Hotside mass flow rate [kg/s]') def rule_mc_in(blk, t): return blk.cold_side.properties_in[t].flow_mol *\ blk.cold_side.properties_in[t].mw self.mc_in = Expression(self.flowsheet().config.time, rule=rule_mc_in, doc='Coldside mass flow rate [kg/s]') # ---------------------------------------------------------------------- # port mass velocity[kg/m2.s] def rule_Gph(blk, t): return (4 * blk.mh_in[t] * 7) / (22 * blk.port_dia**2) self.Gph = Expression(self.flowsheet().config.time, rule=rule_Gph, doc='Hotside port mass velocity[kg/m2.s]') def rule_Gpc(blk, t): return (4 * blk.mc_in[t] * 7) / (22 * blk.port_dia**2) self.Gpc = Expression(self.flowsheet().config.time, rule=rule_Gpc, doc='Coldside port mass velocity[kg/m2.s]') # ---------------------------------------------------------------------- # Reynold & Prandtl numbers def rule_Re_h(blk, t, p): return blk.mh_in[t] * blk.channel_dia /\ (blk.Np[p] * blk.plate_width * blk.plate_gap * blk.hot_side.properties_in[t].visc_d) self.Re_h = Expression(self.flowsheet().config.time, self.PH, rule=rule_Re_h, doc='Hotside Reynolds number') def rule_Re_c(blk, t, p): return blk.mc_in[t] * blk.channel_dia /\ (blk.Np[p] * blk.plate_width * blk.plate_gap * blk.cold_side.properties_in[t].visc_d) self.Re_c = Expression(self.flowsheet().config.time, self.PH, rule=rule_Re_c, doc='Coldside Reynolds number') def rule_Pr_h(blk, t): return blk.cp_hot[t] * blk.hot_side.properties_in[t].visc_d /\ blk.hot_side.properties_in[t].thermal_cond self.Pr_h = Expression(self.flowsheet().config.time, rule=rule_Pr_h, doc='Hotside Prandtl number') def rule_Pr_c(blk, t): return blk.cp_cold[t] * blk.cold_side.properties_in[t].visc_d /\ blk.cold_side.properties_in[t].thermal_cond self.Pr_c = Expression(self.flowsheet().config.time, rule=rule_Pr_c, doc='Coldside Prandtl number') # ---------------------------------------------------------------------- # Film heat transfer coefficients def rule_hotside_transfer_coef(blk, t, p): return (blk.hot_side.properties_in[t].thermal_cond / blk.channel_dia * blk.param_a * blk.Re_h[t, p]**blk.param_b * blk.Pr_h[t]**blk.param_c) self.h_hot = Expression(self.flowsheet().config.time, self.PH, rule=rule_hotside_transfer_coef, doc='Hotside heat transfer coefficient') def rule_coldside_transfer_coef(blk, t, p): return (blk.cold_side.properties_in[t].thermal_cond / blk.channel_dia * blk.param_a * blk.Re_c[t, p]**blk.param_b * blk.Pr_c[t]**blk.param_c) self.h_cold = Expression(self.flowsheet().config.time, self.PH, rule=rule_coldside_transfer_coef, doc='Coldside heat transfer coefficient') # ---------------------------------------------------------------------- # Friction factor calculation def rule_fric_h(blk, t): return 18.29 * blk.Re_h[t, 1]**(-0.652) self.fric_h = Expression(self.flowsheet().config.time, rule=rule_fric_h, doc='Hotside friction factor') def rule_fric_c(blk, t): return 1.441 * self.Re_c[t, 1]**(-0.206) self.fric_c = Expression(self.flowsheet().config.time, rule=rule_fric_c, doc='Coldside friction factor') # ---------------------------------------------------------------------- # pressure drop calculation def rule_hotside_dP(blk, t): return (2 * blk.fric_h[t] * (blk.plate_length + blk.port_dia) * blk.P * blk.Gph[t]**2) /\ (blk.hot_side.properties_in[t].dens_mass * blk.channel_dia) + 1.4 * blk.P * blk.Gph[t]**2 * 0.5 /\ blk.hot_side.properties_in[t].dens_mass + \ blk.hot_side.properties_in[t].dens_mass * \ 9.81 * (blk.plate_length + blk.port_dia) self.dP_h = Expression(self.flowsheet().config.time, rule=rule_hotside_dP, doc='Hotside pressure drop [Pa]') def rule_coldside_dP(blk, t): return (2 * blk.fric_c[t] * (blk.plate_length + blk.port_dia) * blk.P * blk.Gpc[t]**2) /\ (blk.cold_side.properties_in[t].dens_mass * blk.channel_dia) +\ 1.4 * (blk.P * blk.Gpc[t]**2 * 0.5 / blk.cold_side.properties_in[t].dens_mass) + \ blk.cold_side.properties_in[t].dens_mass * \ 9.81 * (blk.plate_length + blk.port_dia) self.dP_c = Expression(self.flowsheet().config.time, rule=rule_coldside_dP, doc='Coldside pressure drop [Pa]') def rule_eq_deltaP_hot(blk, t): return blk.hot_side.deltaP[t] == -blk.dP_h[t] self.eq_deltaP_hot = Constraint(self.flowsheet().config.time, rule=rule_eq_deltaP_hot) def rule_eq_deltaP_cold(blk, t): return blk.cold_side.deltaP[t] == -blk.dP_c[t] self.eq_deltaP_cold = Constraint(self.flowsheet().config.time, rule=rule_eq_deltaP_cold) # ---------------------------------------------------------------------- # Overall heat transfer coefficients def rule_U(blk, t, p): return 1.0 /\ (1.0 / blk.h_hot[t, p] + blk.plate_gap / blk.plate_thermal_cond + 1.0 / blk.h_cold[t, p]) self.U = Expression(self.flowsheet().config.time, self.PH, rule=rule_U, doc='Overall heat transfer coefficient') # ---------------------------------------------------------------------- # capacitance of hot and cold fluid def rule_Caph(blk, t, p): return blk.mh_in[t] * blk.cp_hot[t] / blk.Np[p] self.Caph = Expression(self.flowsheet().config.time, self.PH, rule=rule_Caph, doc='Hotfluid capacitance rate') def rule_Capc(blk, t, p): return blk.mc_in[t] * blk.cp_cold[t] / blk.Np[p] self.Capc = Expression(self.flowsheet().config.time, self.PH, rule=rule_Capc, doc='Coldfluid capacitance rate') # ---------------------------------------------------------------------- # min n max capacitance and capacitance ratio def rule_Cmin(blk, t, p): return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] - ( (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5) self.Cmin = Expression(self.flowsheet().config.time, self.PH, rule=rule_Cmin, doc='Minimum capacitance rate') def rule_Cmax(blk, t, p): return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] + ( (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5) self.Cmax = Expression(self.flowsheet().config.time, self.PH, rule=rule_Cmax, doc='Maximum capacitance rate') def rule_CR(blk, t, p): return blk.Cmin[t, p] / blk.Cmax[t, p] self.CR = Expression(self.flowsheet().config.time, self.PH, rule=rule_CR, doc='Capacitance ratio') # ---------------------------------------------------------------------- # Number of Transfer units for sub heat exchanger def rule_NTU(blk, t, p): return blk.U[t, p] * blk.plate_area / blk.Cmin[t, p] self.NTU = Expression(self.flowsheet().config.time, self.PH, rule=rule_NTU, doc='Number of Transfer Units') # ---------------------------------------------------------------------- # effectiveness of sub-heat exchangers def rule_Ecf(blk, t, p): if blk.P.value % 2 == 0: return (1 - exp(-blk.NTU[t, p] * (1 - blk.CR[t, p]))) / \ (1 - blk.CR[t, p] * exp(-blk.NTU[t, p] * (1 - blk.CR[t, p]))) elif blk.P.value % 2 == 1: return (1 - exp(-blk.NTU[t, p] * (1 + blk.CR[t, p]))) / (1 + blk.CR[t, p]) self.Ecf = Expression(self.flowsheet().config.time, self.PH, rule=rule_Ecf, doc='Effectiveness for sub-HX') # ---------------------------------------------------------------------- # Energy balance equations for hot fluid in sub-heat exhanger def rule_Ebh_eq(blk, t, p): return blk.Th_out[t, p] == blk.Th_in[t, p] -\ blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Caph[t, p] * \ (blk.Th_in[t, p] - blk.Tc_in[t, p]) self.Ebh_eq = Constraint( self.flowsheet().config.time, self.PH, rule=rule_Ebh_eq, doc='Hot fluid sub-heat exchanger energy balance') # Hot fluid exit temperature def rule_Tout_hot(blk, t): return blk.Th_out[t, blk.P.value] ==\ blk.hot_side.properties_out[t].temperature self.Tout_hot_eq = Constraint(self.flowsheet().config.time, rule=rule_Tout_hot, doc='Hot fluid exit temperature') # Energy balance equations for cold fluid in sub-heat exhanger def rule_Ebc_eq(blk, t, p): return blk.Tc_out[t, p] == blk.Tc_in[t, p] + \ blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Capc[t, p] * \ (blk.Th_in[t, p] - blk.Tc_in[t, p]) self.Ebc_eq = Constraint( self.flowsheet().config.time, self.PH, rule=rule_Ebc_eq, doc='Cold fluid sub-heat exchanger energy balance') # Cold fluid exit temperature def rule_Tout_cold(blk, t): return blk.Tc_out[t, 1] ==\ blk.cold_side.properties_out[t].temperature self.Tout_cold_eq = Constraint(self.flowsheet().config.time, rule=rule_Tout_cold, doc='Cold fluid exit temperature') # ---------------------------------------------------------------------- # Energy balance boundary conditions def rule_hot_BCIN(blk, t): return blk.Th_in[t, 1] == \ blk.hot_side.properties_in[t].temperature self.hot_BCIN = Constraint(self.flowsheet().config.time, rule=rule_hot_BCIN, doc='Hot fluid inlet boundary conditions') def rule_cold_BCIN(blk, t): return blk.Tc_in[t, blk.P.value] ==\ blk.cold_side.properties_in[t].temperature self.cold_BCIN = Constraint(self.flowsheet().config.time, rule=rule_cold_BCIN, doc='Cold fluid inlet boundary conditions') Pset = [i for i in range(1, self.P.value)] def rule_hot_BC(blk, t, p): return blk.Th_out[t, p] == blk.Th_in[t, p + 1] self.hot_BC = Constraint( self.flowsheet().config.time, Pset, rule=rule_hot_BC, doc='Hot fluid boundary conditions: change of pass') def rule_cold_BC(blk, t, p): return blk.Tc_out[t, p + 1] == blk.Tc_in[t, p] self.cold_BC = Constraint( self.flowsheet().config.time, Pset, rule=rule_cold_BC, doc='Cold fluid boundary conditions: change of pass') # ---------------------------------------------------------------------- # Energy transferred def rule_QH(blk, t): return blk.mh_in[t] * blk.cp_hot[t] *\ (blk.hot_side.properties_in[t].temperature - blk.hot_side.properties_out[t].temperature) self.QH = Expression(self.flowsheet().config.time, rule=rule_QH, doc='Heat lost by hot fluid') def rule_QC(blk, t): return blk.mc_in[t] * blk.cp_cold[t] *\ (blk.cold_side.properties_out[t].temperature - blk.cold_side.properties_in[t].temperature) self.QC = Expression(self.flowsheet().config.time, rule=rule_QH, doc='Heat gain by cold fluid') def initialize(blk, hotside_state_args=None, coldside_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Initialisation routine for PHE unit (default solver ipopt) Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None) Returns: None ''' # Set solver options init_log = idaeslog.getInitLogger(blk.name, outlvl, tag='unit') solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) hotside_state_args = { 'flow_mol': value(blk.hot_inlet.flow_mol[0]), 'temperature': value(blk.hot_inlet.temperature[0]), 'pressure': value(blk.hot_inlet.pressure[0]), 'mole_frac_comp': { 'H2O': value(blk.hot_inlet.mole_frac_comp[0, 'H2O']), 'CO2': value(blk.hot_inlet.mole_frac_comp[0, 'CO2']), 'MEA': value(blk.hot_inlet.mole_frac_comp[0, 'MEA']) } } coldside_state_args = { 'flow_mol': value(blk.cold_inlet.flow_mol[0]), 'temperature': value(blk.cold_inlet.temperature[0]), 'pressure': value(blk.cold_inlet.pressure[0]), 'mole_frac_comp': { 'H2O': value(blk.cold_inlet.mole_frac_comp[0, 'H2O']), 'CO2': value(blk.cold_inlet.mole_frac_comp[0, 'CO2']), 'MEA': value(blk.cold_inlet.mole_frac_comp[0, 'MEA']) } } # --------------------------------------------------------------------- # Initialize the INLET properties init_log.info('STEP 1: PROPERTY INITIALIZATION') init_log.info_high("INLET Properties initialization") blk.hot_side.properties_in.initialize(state_args=hotside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) blk.cold_side.properties_in.initialize(state_args=coldside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) # Initialize the OUTLET properties init_log.info_high("OUTLET Properties initialization") blk.hot_side.properties_out.initialize(state_args=hotside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False) blk.cold_side.properties_out.initialize(state_args=coldside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False) # ---------------------------------------------------------------------- init_log.info('STEP 2: PHE INITIALIZATION') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("STEP 2 Complete: {}.".format( idaeslog.condition(res))) init_log.info('INITIALIZATION COMPLETED')
class PFRData(UnitModelBlockData): """ Standard Plug Flow Reactor Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_equilibrium_reactions", ConfigValue( default=False, domain=Bool, description="Equilibrium reaction construction flag", doc="""Indicates whether terms for equilibrium controlled reactions should be constructed, **default** - True. **Valid values:** { **True** - include equilibrium reaction terms, **False** - exclude equilibrium reaction terms.}""")) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) CONFIG.declare( "has_heat_of_reaction", ConfigValue( default=False, domain=Bool, description="Heat of reaction term construction flag", doc="""Indicates whether terms for heat of reaction terms should be constructed, **default** - False. **Valid values:** { **True** - include heat of reaction terms, **False** - exclude heat of reaction terms.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=Bool, description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc= """Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""")) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc= """A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""")) CONFIG.declare( "length_domain_set", ConfigValue( default=[0.0, 1.0], domain=ListOf(float), description="List of points to use to initialize length domain", doc= """A list of values to be used when constructing the length domain of the reactor. Point must lie between 0.0 and 1.0, **default** - [0.0, 1.0]. **Valid values:** { a list of floats}""")) CONFIG.declare( "transformation_method", ConfigValue( default="dae.finite_difference", description="Method to use for DAE transformation", doc="""Method to use to transform domain. Must be a method recognised by the Pyomo TransformationFactory, **default** - "dae.finite_difference".""")) CONFIG.declare( "transformation_scheme", ConfigValue(default="BACKWARD", description="Scheme to use for DAE transformation", doc="""Scheme to use when transformating domain. See Pyomo documentation for supported schemes, **default** - "BACKWARD".""")) CONFIG.declare( "finite_elements", ConfigValue( default=20, description= "Number of finite elements to use for DAE transformation", doc="""Number of finite elements to use when transforming length domain, **default** - 20.""")) CONFIG.declare( "collocation_points", ConfigValue( default=3, description="No. collocation points to use for DAE transformation", doc="""Number of collocation points to use when transforming length domain, **default** - 3.""")) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(PFRData, self).build() # Build Control Volume self.control_volume = ControlVolume1DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args, "transformation_method": self.config.transformation_method, "transformation_scheme": self.config.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points }) self.control_volume.add_geometry( length_domain_set=self.config.length_domain_set) self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_reaction_blocks( has_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=True, has_equilibrium_reactions=self.config.has_equilibrium_reactions, has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=self.config.has_heat_of_reaction, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) self.control_volume.apply_transformation() # Add Ports self.add_inlet_port() self.add_outlet_port() # Add PFR performance equation @self.Constraint(self.flowsheet().time, self.control_volume.length_domain, self.config.reaction_package.rate_reaction_idx, doc="PFR performance equation") def performance_eqn(b, t, x, r): return b.control_volume.rate_reaction_extent[t, x, r] == ( b.control_volume.reactions[t, x].reaction_rate[r] * b.control_volume.area) # Set references to balance terms at unit level add_object_reference(self, "length", self.control_volume.length) add_object_reference(self, "area", self.control_volume.area) # Add volume variable for full reactor units = self.config.property_package.get_metadata() self.volume = Var(initialize=1, doc="Reactor Volume", units=units.get_derived_units("volume")) self.geometry = Constraint(expr=self.volume == self.area * self.length) if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat[...]) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP[...]) def _get_performance_contents(self, time_point=0): var_dict = {"Volume": self.volume} var_dict = {"Length": self.length} var_dict = {"Area": self.area} return {"vars": var_dict} def get_costing(self, year=None, module=costing, **kwargs): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year, module=module) self.costing = Block() units_meta = ( self.config.property_package.get_metadata().get_derived_units) self.diameter = Var(initialize=1, units=units_meta('length'), doc='vessel diameter') self.diameter_eq = Constraint( expr=self.volume == (self.length * const.pi * self.diameter**2) / 4) module.pfr_costing(self.costing, **kwargs)
class HeatExchangerNTUData(UnitModelBlockData): """Heat Exchanger Unit Model using NTU method.""" CONFIG = UnitModelBlockData.CONFIG() # Configuration template for fluid specific arguments _SideCONFIG = ConfigBlock() _SideCONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) _SideCONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) _SideCONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) _SideCONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) _SideCONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use ", doc="""Property parameter object used to define property calculations **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) _SideCONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package", doc="""A ConfigBlock with arguments to be passed to property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) # Create individual config blocks for hot and cold sides CONFIG.declare("hot_side", _SideCONFIG(doc="Hot fluid config arguments")) CONFIG.declare("cold_side", _SideCONFIG(doc="Cold fluid config arguments")) def build(self): # Call UnitModel.build to setup model super().build() # --------------------------------------------------------------------- # Build hot-side control volume self.hot_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.hot_side.property_package, "property_package_args": self.config.hot_side.property_package_args }) # TODO : Add support for phase equilibrium? self.hot_side.add_state_blocks(has_phase_equilibrium=False) self.hot_side.add_material_balances( balance_type=self.config.hot_side.material_balance_type, has_phase_equilibrium=False) self.hot_side.add_energy_balances( balance_type=self.config.hot_side.energy_balance_type, has_heat_transfer=True) self.hot_side.add_momentum_balances( balance_type=self.config.hot_side.momentum_balance_type, has_pressure_change=self.config.hot_side.has_pressure_change) # --------------------------------------------------------------------- # Build cold-side control volume self.cold_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.cold_side.property_package, "property_package_args": self.config.cold_side.property_package_args }) self.cold_side.add_state_blocks(has_phase_equilibrium=False) self.cold_side.add_material_balances( balance_type=self.config.cold_side.material_balance_type, has_phase_equilibrium=False) self.cold_side.add_energy_balances( balance_type=self.config.cold_side.energy_balance_type, has_heat_transfer=True) self.cold_side.add_momentum_balances( balance_type=self.config.cold_side.momentum_balance_type, has_pressure_change=self.config.cold_side.has_pressure_change) # --------------------------------------------------------------------- # Add Ports to control volumes self.add_inlet_port(name="hot_inlet", block=self.hot_side, doc='Hot side inlet port') self.add_outlet_port(name="hot_outlet", block=self.hot_side, doc='Hot side outlet port') self.add_inlet_port(name="cold_inlet", block=self.cold_side, doc='Cold side inlet port') self.add_outlet_port(name="cold_outlet", block=self.cold_side, doc='Cold side outlet port') # --------------------------------------------------------------------- # Add unit level References # Set references to balance terms at unit level self.heat_duty = Reference(self.cold_side.heat[:]) # --------------------------------------------------------------------- # Add performance equations # All units of measurement will be based on hot side hunits = self.config.hot_side.property_package.get_metadata( ).get_derived_units # Common heat exchanger variables self.area = Var(initialize=1, units=hunits("area"), domain=PositiveReals, doc="Heat transfer area") self.heat_transfer_coefficient = Var( self.flowsheet().time, initialize=1, units=hunits("heat_transfer_coefficient"), domain=PositiveReals, doc="Overall heat transfer coefficient") # Overall energy balance def rule_energy_balance(blk, t): return blk.hot_side.heat[t] == -pyunits.convert( blk.cold_side.heat[t], to_units=hunits("power")) self.energy_balance_constraint = Constraint(self.flowsheet().time, rule=rule_energy_balance) # Add e-NTU variables self.effectiveness = Var(self.flowsheet().time, initialize=1, units=pyunits.dimensionless, domain=PositiveReals, doc="Effectiveness factor for NTU method") # Minimum heat capacitance ratio for e-NTU method self.eps_cmin = Param(initialize=1e-3, mutable=True, units=hunits("power") / hunits("temperature"), doc="Epsilon parameter for smooth Cmin and Cmax") # TODO : Support both mass and mole based flows def rule_Cmin(blk, t): caph = (blk.hot_side.properties_in[t].flow_mol * blk.hot_side.properties_in[t].cp_mol) capc = pyunits.convert(blk.cold_side.properties_in[t].flow_mol * blk.cold_side.properties_in[t].cp_mol, to_units=hunits("power") / hunits("temperature")) return smooth_min(caph, capc, eps=blk.eps_cmin) self.Cmin = Expression(self.flowsheet().time, rule=rule_Cmin, doc='Minimum heat capacitance rate') def rule_Cmax(blk, t): caph = (blk.hot_side.properties_in[t].flow_mol * blk.hot_side.properties_in[t].cp_mol) capc = pyunits.convert(blk.cold_side.properties_in[t].flow_mol * blk.cold_side.properties_in[t].cp_mol, to_units=hunits("power") / hunits("temperature")) return smooth_max(caph, capc, eps=blk.eps_cmin) self.Cmax = Expression(self.flowsheet().time, rule=rule_Cmax, doc='Maximum heat capacitance rate') # Heat capacitance ratio def rule_Cratio(blk, t): return blk.Cmin[t] / blk.Cmax[t] self.Cratio = Expression(self.flowsheet().time, rule=rule_Cratio, doc='Heat capacitance ratio') def rule_NTU(blk, t): return blk.heat_transfer_coefficient[t] * blk.area / blk.Cmin[t] self.NTU = Expression(self.flowsheet().time, rule=rule_NTU, doc='Number of heat transfer units') # Heat transfer by e-NTU method def rule_entu(blk, t): return blk.hot_side.heat[t] == -( blk.effectiveness[t] * blk.Cmin[t] * (blk.hot_side.properties_in[t].temperature - pyunits.convert(blk.cold_side.properties_in[t].temperature, to_units=hunits("temperature")))) self.heat_duty_constraint = Constraint(self.flowsheet().time, rule=rule_entu) # TODO : Add scaling methods def initialize( self, hot_side_state_args=None, cold_side_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, duty=None, ): """ Heat exchanger initialization method. Args: hot_side_state_args : a dict of arguments to be passed to the property initialization for the hot side (see documentation of the specific property package) (default = None). cold_side_state_args : a dict of arguments to be passed to the property initialization for the cold side (see documentation of the specific property package) (default = None). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) duty : an initial guess for the amount of heat transfered. This should be a tuple in the form (value, units), (default = (1000 J/s)) Returns: None """ # Set solver options init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") hot_side = self.hot_side cold_side = self.cold_side # Create solver opt = get_solver(solver, optarg) flags1 = hot_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=hot_side_state_args) init_log.info_high("Initialization Step 1a (hot side) Complete.") flags2 = cold_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=cold_side_state_args) init_log.info_high("Initialization Step 1b (cold side) Complete.") # --------------------------------------------------------------------- # Solve unit without heat transfer equation # if costing block exists, deactivate if hasattr(self, "costing"): self.costing.deactivate() self.energy_balance_constraint.deactivate() # Get side 1 and side 2 heat units, and convert duty as needed s1_units = hot_side.heat.get_units() s2_units = cold_side.heat.get_units() if duty is None: # Assume 1000 J/s and check for unitless properties if s1_units is None and s2_units is None: # Backwards compatability for unitless properties s1_duty = -1000 s2_duty = 1000 else: s1_duty = pyunits.convert_value(-1000, from_units=pyunits.W, to_units=s1_units) s2_duty = pyunits.convert_value(1000, from_units=pyunits.W, to_units=s2_units) else: # Duty provided with explicit units s1_duty = -pyunits.convert_value( duty[0], from_units=duty[1], to_units=s1_units) s2_duty = pyunits.convert_value(duty[0], from_units=duty[1], to_units=s2_units) cold_side.heat.fix(s2_duty) for i in hot_side.heat: hot_side.heat[i].value = s1_duty with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) cold_side.heat.unfix() self.energy_balance_constraint.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state hot_side.release_state(flags1, outlvl=outlvl) cold_side.release_state(flags2, outlvl=outlvl) init_log.info("Initialization Completed, {}".format( idaeslog.condition(res))) # if costing block exists, activate and initialize if hasattr(self, "costing"): self.costing.activate() costing.initialize(self.costing) def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Hot Inlet": self.hot_inlet, "Hot Outlet": self.hot_outlet, "Cold Inlet": self.cold_inlet, "Cold Outlet": self.cold_outlet, }, time_point=time_point, ) def get_costing(self, module=costing, year=None, **kwargs): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year) self.costing = Block() module.hx_costing(self.costing, **kwargs)
class CondenserData(UnitModelBlockData): """ Condenser unit for distillation model. Unit model to condense (total/partial) the vapor from the top tray of the distillation column. """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "condenser_type", ConfigValue( default=CondenserType.totalCondenser, domain=In(CondenserType), description="Type of condenser flag", doc="""Indicates what type of condenser should be constructed, **default** - CondenserType.totalCondenser. **Valid values:** { **CondenserType.totalCondenser** - Incoming vapor from top tray is condensed to all liquid, **CondenserType.partialCondenser** - Incoming vapor from top tray is partially condensed to a vapor and liquid stream.}""")) CONFIG.declare( "temperature_spec", ConfigValue(default=None, domain=In(TemperatureSpec), description="Temperature spec for the condenser", doc="""Temperature specification for the condenser, **default** - TemperatureSpec.none **Valid values:** { **TemperatureSpec.none** - No spec is selected, **TemperatureSpec.atBubblePoint** - Condenser temperature set at bubble point i.e. total condenser, **TemperatureSpec.customTemperature** - Condenser temperature at user specified temperature.}""")) CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """Build the model. Args: None Returns: None """ # Setup model build logger model_log = idaeslog.getModelLogger(self.name, tag="unit") # Call UnitModel.build to setup dynamics super(CondenserData, self).build() # Check config arguments if self.config.temperature_spec is None: raise ConfigurationError("temperature_spec config argument " "has not been specified. Please select " "a valid option.") if (self.config.condenser_type == CondenserType.partialCondenser) and \ (self.config.temperature_spec == TemperatureSpec.atBubblePoint): raise ConfigurationError("condenser_type set to partial but " "temperature_spec set to atBubblePoint. " "Select customTemperature and specify " "outlet temperature.") # Add Control Volume for the condenser self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Get liquid and vapor phase objects from the property package # to be used below. Avoids repition. _liquid_list = [] _vapor_list = [] for p in self.config.property_package.phase_list: pobj = self.config.property_package.get_phase(p) if pobj.is_vapor_phase(): _vapor_list.append(p) elif pobj.is_liquid_phase(): _liquid_list.append(p) else: _liquid_list.append(p) model_log.warning( "A non-liquid/non-vapor phase was detected but will " "be treated as a liquid.") # Create a pyomo set for indexing purposes. This set is appended to # model otherwise results in an abstract set. self._liquid_set = Set(initialize=_liquid_list) self._vapor_set = Set(initialize=_vapor_list) self._make_ports() if self.config.condenser_type == CondenserType.totalCondenser: self._make_splits_total_condenser() if (self.config.temperature_spec == TemperatureSpec.atBubblePoint): # Option 1: if true, condition for total condenser # (T_cond = T_bubble) # Option 2: if this is false, then user has selected # custom temperature spec and needs to fix an outlet # temperature. def rule_total_cond(self, t): return self.control_volume.properties_out[t].\ temperature == self.control_volume.properties_out[t].\ temperature_bubble self.eq_total_cond_spec = Constraint(self.flowsheet().time, rule=rule_total_cond) else: self._make_splits_partial_condenser() # Add object reference to variables of the control volume # Reference to the heat duty self.heat_duty = Reference(self.control_volume.heat[:]) # Reference to the pressure drop (if set to True) if self.config.has_pressure_change: self.deltaP = Reference(self.control_volume.deltaP[:]) def _make_ports(self): # Add Ports for the condenser # Inlet port (the vapor from the top tray) self.add_inlet_port() # Outlet ports that always exist irrespective of condenser type self.reflux = Port(noruleinit=True, doc="Reflux stream that is" " returned to the top tray.") self.distillate = Port(noruleinit=True, doc="Distillate stream that is" " the top product.") if self.config.condenser_type == CondenserType.partialCondenser: self.vapor_outlet = Port(noruleinit=True, doc="Vapor outlet port from a " "partial condenser") # Add codnenser specific variables self.reflux_ratio = Var(initialize=1, doc="Reflux ratio for the condenser") def _make_splits_total_condenser(self): # Get dict of Port members and names member_list = self.control_volume.\ properties_out[0].define_port_members() # Create references and populate the reflux, distillate ports for k in member_list: # Create references and populate the intensive variables if "flow" not in k: if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(member_list[k].local_name) else: var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the reflux port self.reflux.add(Reference(var), k) # add the reference and variable name to the distillate port self.distillate.add(Reference(var), k) elif "flow" in k: # Create references and populate the extensive variables # This is for vars that are not indexed if not member_list[k].is_indexed(): # Expression for reflux flow and relation to the # reflux_ratio variable def rule_reflux_flow(self, t): return self.control_volume.properties_out[t].\ component(member_list[k].local_name) * \ (self.reflux_ratio / (1 + self.reflux_ratio)) self.e_reflux_flow = Expression(self.flowsheet().time, rule=rule_reflux_flow) self.reflux.add(self.e_reflux_flow, k) # Expression for distillate flow and relation to the # reflux_ratio variable def rule_distillate_flow(self, t): return self.control_volume.properties_out[t].\ component(member_list[k].local_name) / \ (1 + self.reflux_ratio) self.e_distillate_flow = Expression( self.flowsheet().time, rule=rule_distillate_flow) self.distillate.add(self.e_distillate_flow, k) else: # Create references and populate the extensive variables # This is for vars that are indexed by phase, comp or both. index_set = member_list[k].index_set() def rule_reflux_flow(self, t, *args): return self.control_volume.properties_out[t].\ component(member_list[k].local_name)[args] * \ (self.reflux_ratio / (1 + self.reflux_ratio)) self.e_reflux_flow = Expression(self.flowsheet().time, index_set, rule=rule_reflux_flow) self.reflux.add(self.e_reflux_flow, k) def rule_distillate_flow(self, t, *args): return self.control_volume.properties_out[t].\ component(member_list[k].local_name)[args] / \ (1 + self.reflux_ratio) self.e_distillate_flow = Expression( self.flowsheet().time, index_set, rule=rule_distillate_flow) self.distillate.add(self.e_distillate_flow, k) else: raise PropertyNotSupportedError( "Unrecognized names for flow variables encountered while " "building the condenser ports.") def _make_splits_partial_condenser(self): # Get dict of Port members and names member_list = self.control_volume.\ properties_out[0].define_port_members() # Create references and populate the reflux, distillate ports for k in member_list: # Create references and populate the intensive variables if "flow" not in k and "frac" not in k and "enth" not in k: if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(member_list[k].local_name) else: var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the reflux port self.reflux.add(Reference(var), k) # add the reference and variable name to the distillate port self.distillate.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_outlet.add(Reference(var), k) elif "frac" in k: # Mole/mass frac is typically indexed index_set = member_list[k].index_set() # if state var is not mole/mass frac by phase if "phase" not in k: if "mole" in k: # check mole basis/mass basis # The following conditionals are required when a # mole frac or mass frac is a state var i.e. will be # a port member. This gets a bit tricky when handling # non-conventional systems when you have more than one # liquid or vapor phase. Hence, the logic here is that # the mole frac that should be present in the liquid or # vapor port should be computed by accounting for # multiple liquid or vapor phases if present. For the # classical VLE system, this holds too. if hasattr(self.control_volume.properties_out[0], "mole_frac_phase_comp") and \ hasattr(self.control_volume.properties_out[0], "flow_mol_phase"): flow_phase_comp = False local_name_frac = "mole_frac_phase_comp" local_name_flow = "flow_mol_phase" elif hasattr(self.control_volume.properties_out[0], "flow_mol_phase_comp"): flow_phase_comp = True local_name_flow = "flow_mol_phase_comp" else: raise PropertyNotSupportedError( "No mole_frac_phase_comp or flow_mol_phase or" " flow_mol_phase_comp variables encountered " "while building ports for the condenser. ") elif "mass" in k: if hasattr(self.control_volume.properties_out[0], "mass_frac_phase_comp") and \ hasattr(self.control_volume.properties_out[0], "flow_mass_phase"): flow_phase_comp = False local_name_frac = "mass_frac_phase_comp" local_name_flow = "flow_mass_phase" elif hasattr(self.control_volume.properties_out[0], "flow_mass_phase_comp"): flow_phase_comp = True local_name_flow = "flow_mass_phase_comp" else: raise PropertyNotSupportedError( "No mass_frac_phase_comp or flow_mass_phase or" " flow_mass_phase_comp variables encountered " "while building ports for the condenser.") else: raise PropertyNotSupportedError( "No mass frac or mole frac variables encountered " " while building ports for the condenser. " "phase_frac as a state variable is not " "supported with distillation unit models.") # Rule for liquid phase mole fraction def rule_liq_frac(self, t, i): if not flow_phase_comp: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_frac)[p, i] * self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._liquid_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._liquid_set) else: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._liquid_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._liquid_set for i in self.config.property_package.component_list) self.e_liq_frac = Expression(self.flowsheet().time, index_set, rule=rule_liq_frac) # Rule for vapor phase mass/mole fraction def rule_vap_frac(self, t, i): if not flow_phase_comp: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_frac)[p, i] * self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._vapor_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._vapor_set) else: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._vapor_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._vapor_set for i in self.config.property_package.component_list) self.e_vap_frac = Expression(self.flowsheet().time, index_set, rule=rule_vap_frac) # add the reference and variable name to the reflux port self.reflux.add(self.e_liq_frac, k) # add the reference and variable name to the # distillate port self.distillate.add(self.e_liq_frac, k) # add the reference and variable name to the # vapor port self.vapor_outlet.add(self.e_vap_frac, k) else: # Assumes mole_frac_phase or mass_frac_phase exist as # state vars in the port and therefore access directly # from the state block. var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the reflux port self.reflux.add(Reference(var), k) # add the reference and variable name to the distillate port self.distillate.add(Reference(var), k) elif "flow" in k: if "phase" not in k: # Assumes that here the var is total flow or component # flow. However, need to extract the flow by phase from # the state block. Expects to find the var # flow_mol_phase or flow_mass_phase in the state block. # Check if it is not indexed by component list and this # is total flow if not member_list[k].is_indexed(): # if state var is not flow_mol/flow_mass by phase local_name = str(member_list[k].local_name) + \ "_phase" # Rule for vap phase flow def rule_vap_flow(self, t): return sum(self.control_volume.properties_out[t]. component(local_name)[p] for p in self._vapor_set) self.e_vap_flow = Expression(self.flowsheet().time, rule=rule_vap_flow) # Rule to link the liq phase flow to the reflux def rule_reflux_flow(self, t): return sum(self.control_volume.properties_out[t]. component(local_name)[p] for p in self._liquid_set) * \ (self.reflux_ratio / (1 + self.reflux_ratio)) self.e_reflux_flow = Expression(self.flowsheet().time, rule=rule_reflux_flow) # Rule to link the liq flow to the distillate def rule_distillate_flow(self, t): return sum(self.control_volume.properties_out[t]. component(local_name)[p] for p in self._liquid_set) / \ (1 + self.reflux_ratio) self.e_distillate_flow = Expression( self.flowsheet().time, rule=rule_distillate_flow) else: # when it is flow comp indexed by component list str_split = \ str(member_list[k].local_name).split("_") if len(str_split) == 3 and str_split[-1] == "comp": local_name = str_split[0] + "_" + \ str_split[1] + "_phase_" + "comp" # Get the indexing set i.e. component list index_set = member_list[k].index_set() # Rule for vap phase flow to the vapor outlet def rule_vap_flow(self, t, i): return sum(self.control_volume.properties_out[t]. component(local_name)[p, i] for p in self._vapor_set) self.e_vap_flow = Expression(self.flowsheet().time, index_set, rule=rule_vap_flow) # Rule to link the liq flow to the reflux def rule_reflux_flow(self, t, i): return sum(self.control_volume.properties_out[t]. component(local_name)[p, i] for p in self._liquid_set) * \ (self.reflux_ratio / (1 + self.reflux_ratio)) self.e_reflux_flow = Expression(self.flowsheet().time, index_set, rule=rule_reflux_flow) # Rule to link the liq flow to the distillate def rule_distillate_flow(self, t, i): return sum(self.control_volume.properties_out[t]. component(local_name)[p, i] for p in self._liquid_set) / \ (1 + self.reflux_ratio) self.e_distillate_flow = Expression( self.flowsheet().time, index_set, rule=rule_distillate_flow) # add the reference and variable name to the reflux port self.reflux.add(self.e_reflux_flow, k) # add the reference and variable name to the # distillate port self.distillate.add(self.e_distillate_flow, k) # add the reference and variable name to the # distillate port self.vapor_outlet.add(self.e_vap_flow, k) elif "enth" in k: if "phase" not in k: # assumes total mixture enthalpy (enth_mol or enth_mass) # and hence should not be indexed by phase if not member_list[k].is_indexed(): # if state var is not enth_mol/enth_mass # by phase, add _phase string to extract the right # value from the state block local_name = str(member_list[k].local_name) + \ "_phase" else: raise PropertyPackageError( "Enthalpy is indexed but the variable " "name does not reflect the presence of an index. " "Please follow the naming convention outlined " "in the documentation for state variables.") # NOTE:pass phase index when generating expression only # when multiple liquid or vapor phases detected # else ensure consistency with state vars and do not # add phase index to the port members. Hence, the check # for length of local liq and vap phase sets. # Rule for vap enthalpy. Setting the enthalpy to the # enth_mol_phase['Vap'] value from the state block def rule_vap_enth(self, t): return sum( self.control_volume.properties_out[t].component( local_name)[p] for p in self._vapor_set) self.e_vap_enth = Expression(self.flowsheet().time, rule=rule_vap_enth) # Rule to link the liq enthalpy to the reflux. # Setting the enthalpy to the # enth_mol_phase['Liq'] value from the state block def rule_reflux_enth(self, t): return sum( self.control_volume.properties_out[t].component( local_name)[p] for p in self._liquid_set) self.e_reflux_enth = Expression(self.flowsheet().time, rule=rule_reflux_enth) # Rule to link the liq flow to the distillate. # Setting the enthalpy to the # enth_mol_phase['Liq'] value from the state block def rule_distillate_enth(self, t): return sum( self.control_volume.properties_out[t].component( local_name)[p] for p in self._liquid_set) self.e_distillate_enth = Expression( self.flowsheet().time, rule=rule_distillate_enth) # add the reference and variable name to the reflux port self.reflux.add(self.e_reflux_enth, k) # add the reference and variable name to the # distillate port self.distillate.add(self.e_distillate_enth, k) # add the reference and variable name to the # distillate port self.vapor_outlet.add(self.e_vap_enth, k) elif "phase" in k: # assumes enth_mol_phase or enth_mass_phase. # This is an intensive property, you create a direct # reference irrespective of the reflux, distillate and # vap_outlet # Rule for vap flow if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(member_list[k].local_name) else: var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the reflux port self.reflux.add(Reference(var), k) # add the reference and variable name to the distillate port self.distillate.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_outlet.add(Reference(var), k) else: raise PropertyNotSupportedError( "Unrecognized enthalpy state variable encountered " "while building ports for the condenser. Only total " "mixture enthalpy or enthalpy by phase are supported.") def initialize(self, solver=None, outlvl=idaeslog.NOTSET): # TODO: Fix the inlets to the condenser to the vapor flow from # the top tray or take it as an argument to this method. init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") if self.config.temperature_spec == TemperatureSpec.customTemperature: if degrees_of_freedom(self) != 0: raise ConfigurationError( "Degrees of freedom is not 0 during initialization. " "Check if outlet temperature has been fixed in addition " "to the other inputs required as customTemperature was " "selected for temperature_spec config argument.") if self.config.condenser_type == CondenserType.totalCondenser: self.eq_total_cond_spec.deactivate() # Initialize the inlet and outlet state blocks self.control_volume.initialize(outlvl=outlvl) # Activate the total condenser spec if self.config.condenser_type == CondenserType.totalCondenser: self.eq_total_cond_spec.activate() if solver is not None: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info("Initialization Complete, {}.".format( idaeslog.condition(res))) else: init_log.warning( "Solver not provided during initialization, proceeding" " with deafult solver in idaes.") solver = get_default_solver() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info("Initialization Complete, {}.".format( idaeslog.condition(res))) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): stream_attributes = {} if self.config.condenser_type == CondenserType.totalCondenser: stream_dict = { "Inlet": "inlet", "Reflux": "reflux", "Distillate": "distillate" } else: stream_dict = { "Inlet": "inlet", "Vapor Outlet": "vapor_outlet", "Reflux": "reflux", "Distillate": "distillate" } for n, v in stream_dict.items(): port_obj = getattr(self, v) stream_attributes[n] = {} for k in port_obj.vars: for i in port_obj.vars[k].keys(): if isinstance(i, float): stream_attributes[n][k] = value( port_obj.vars[k][time_point]) else: if len(i) == 2: kname = str(i[1]) else: kname = str(i[1:]) stream_attributes[n][k + " " + kname] = \ value(port_obj.vars[k][time_point, i[1:]]) return DataFrame.from_dict(stream_attributes, orient="columns")
class PressureChangerData(UnitModelBlockData): """ Standard Compressor/Expander Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""", ), ) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""", ), ) CONFIG.declare( "compressor", ConfigValue( default=True, domain=In([True, False]), description="Compressor flag", doc="""Indicates whether this unit should be considered a compressor (True (default), pressure increase) or an expander (False, pressure decrease).""", ), ) CONFIG.declare( "thermodynamic_assumption", ConfigValue( default=ThermodynamicAssumption.isothermal, domain=In(ThermodynamicAssumption), description="Thermodynamic assumption to use", doc="""Flag to set the thermodynamic assumption to use for the unit. - ThermodynamicAssumption.isothermal (default) - ThermodynamicAssumption.isentropic - ThermodynamicAssumption.pump - ThermodynamicAssumption.adiabatic""", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""", ), ) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "support_isentropic_performance_curves", ConfigValue( default=False, domain=In([True, False]), doc="Include a block for performance curves, configure via" " isentropic_performance_curves.", ), ) CONFIG.declare( "isentropic_performance_curves", IsentropicPerformanceCurveData.CONFIG(), # doc included in IsentropicPerformanceCurveData ) def build(self): """ Args: None Returns: None """ # Call UnitModel.build super().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: self.volume = Reference(self.control_volume.volume[:]) # Construct performance equations # Set references to balance terms at unit level # Add Work transfer variable 'work' self.work_mechanical = Reference(self.control_volume.work[:]) # Add Momentum balance variable 'deltaP' self.deltaP = Reference(self.control_volume.deltaP[:]) # 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 ( b.ratioP[t] * b.control_volume.properties_in[t].pressure == 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 add_pump(self): """ Add constraints for the incompressible fluid assumption Args: None Returns: None """ units_meta = self.config.property_package.get_metadata() self.work_fluid = Var( self.flowsheet().config.time, initialize=1.0, doc="Work required to increase the pressure of the liquid", units=units_meta.get_derived_units("power") ) self.efficiency_pump = Var( self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency" ) @self.Constraint(self.flowsheet().config.time, doc="Pump fluid work constraint") def fluid_work_calculation(b, t): return b.work_fluid[t] == ( ( b.control_volume.properties_out[t].pressure - b.control_volume.properties_in[t].pressure ) * b.control_volume.properties_out[t].flow_vol ) # Actual work @self.Constraint( self.flowsheet().config.time, doc="Actual mechanical work calculation" ) def actual_work(b, t): if b.config.compressor: return b.work_fluid[t] == ( b.work_mechanical[t] * b.efficiency_pump[t] ) else: return b.work_mechanical[t] == ( b.work_fluid[t] * b.efficiency_pump[t] ) def add_isothermal(self): """ Add constraints for isothermal assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint( self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet temperature", ) def isothermal(b, t): return ( b.control_volume.properties_in[t].temperature == b.control_volume.properties_out[t].temperature ) def add_adiabatic(self): """ Add constraints for adiabatic assumption. Args: None Returns: None """ @self.Constraint(self.flowsheet().config.time) def zero_work_equation(b, t): return self.control_volume.work[t] == 0 def add_isentropic(self): """ Add constraints for isentropic assumption. Args: None Returns: None """ units_meta = self.config.property_package.get_metadata() # Get indexing sets from control volume # Add isentropic variables self.efficiency_isentropic = Var( self.flowsheet().config.time, initialize=0.8, doc="Efficiency with respect to an isentropic process [-]", ) self.work_isentropic = Var( self.flowsheet().config.time, initialize=0.0, doc="Work input to unit if isentropic process", units=units_meta.get_derived_units("power") ) # Build isentropic state block tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium tmp_dict["defined_state"] = False self.properties_isentropic = ( self.config.property_package.build_state_block( self.flowsheet().config.time, doc="isentropic properties at outlet", default=tmp_dict) ) # Connect isentropic state block properties @self.Constraint( self.flowsheet().config.time, doc="Pressure for isentropic calculations" ) def isentropic_pressure(b, t): return ( b.properties_isentropic[t].pressure == b.control_volume.properties_out[t].pressure ) # This assumes isentropic composition is the same as outlet self.add_state_material_balances(self.config.material_balance_type, self.properties_isentropic, self.control_volume.properties_out) # This assumes isentropic entropy is the same as inlet @self.Constraint(self.flowsheet().config.time, doc="Isentropic assumption") def isentropic(b, t): return ( b.properties_isentropic[t].entr_mol == b.control_volume.properties_in[t].entr_mol ) # Isentropic work @self.Constraint( self.flowsheet().config.time, doc="Calculate work of isentropic process" ) def isentropic_energy_balance(b, t): return b.work_isentropic[t] == ( sum( b.properties_isentropic[t].get_enthalpy_flow_terms(p) for p in b.properties_isentropic.phase_list ) - sum( b.control_volume.properties_in[ t].get_enthalpy_flow_terms(p) for p in b.control_volume.properties_in.phase_list ) ) # Actual work @self.Constraint( self.flowsheet().config.time, doc="Actual mechanical work calculation" ) def actual_work(b, t): if b.config.compressor: return b.work_isentropic[t] == ( b.work_mechanical[t] * b.efficiency_isentropic[t] ) else: return b.work_mechanical[t] == ( b.work_isentropic[t] * b.efficiency_isentropic[t] ) if self.config.support_isentropic_performance_curves: self.performance_curve = IsentropicPerformanceCurve( default=self.config.isentropic_performance_curves) def model_check(blk): """ Check that pressure change matches with compressor argument (i.e. if compressor = True, pressure should increase or work should be positive) Args: None Returns: None """ if blk.config.compressor: # Compressor # Check that pressure does not decrease if any( blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0) for t in blk.flowsheet().config.time ): _log.warning("{} Compressor set with negative deltaP." .format(blk.name)) if any( blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor set with ratioP less than 1." .format(blk.name) ) if any( blk.control_volume.properties_out[t].pressure.fixed and ( value(blk.control_volume.properties_in[t].pressure) > value(blk.control_volume.properties_out[t].pressure) ) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor set with pressure decrease." .format(blk.name) ) # Check that work is not negative if any( blk.work_mechanical[t].fixed and ( value(blk.work_mechanical[t]) < 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor maybe set with negative work." .format(blk.name) ) else: # Expander # Check that pressure does not increase if any( blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with positive deltaP." .format(blk.name) ) if any( blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with ratioP greater " "than 1.".format(blk.name) ) if any( blk.control_volume.properties_out[t].pressure.fixed and ( value(blk.control_volume.properties_in[t].pressure) < value(blk.control_volume.properties_out[t].pressure) ) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine maybe set with pressure ", "increase.".format(blk.name), ) # Check that work is not positive if any( blk.work_mechanical[t].fixed and ( value(blk.work_mechanical[t]) > 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with positive work." .format(blk.name) ) # Run holdup block model checks blk.control_volume.model_check() # Run model checks on isentropic property block try: for t in blk.flowsheet().config.time: blk.properties_in[t].model_check() except AttributeError: pass def initialize( blk, state_args=None, routine=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ General wrapper for pressure changer initialization routines Keyword Arguments: routine : str stating which initialization routine to execute * None - use routine matching thermodynamic_assumption * 'isentropic' - use isentropic initialization routine * 'isothermal' - use isothermal initialization routine state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None """ # if costing block exists, deactivate try: blk.costing.deactivate() except AttributeError: pass if routine is None: # Use routine for specific type of unit routine = blk.config.thermodynamic_assumption # Call initialization routine if routine is ThermodynamicAssumption.isentropic: blk.init_isentropic( state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg ) elif routine is ThermodynamicAssumption.adiabatic: blk.init_adiabatic( state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg ) else: # Call the general initialization routine in UnitModelBlockData super().initialize( state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg ) # if costing block exists, activate try: blk.costing.activate() costing.initialize(blk.costing) except AttributeError: pass def init_adiabatic(blk, state_args, outlvl, solver, optarg): """ Initialization routine for adiabatic pressure changers. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={}) solver : str indicating which solver to use during initialization (default = None) Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) cv = blk.control_volume t0 = blk.flowsheet().config.time.first() state_args_out = {} if state_args is None: state_args = {} state_dict = ( cv.properties_in[t0].define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args[k] = {} for m in state_dict[k].keys(): state_args[k][m] = state_dict[k][m].value else: state_args[k] = state_dict[k].value # Get initialisation guesses for outlet and isentropic states for k in state_args: if k == "pressure" and k not in state_args_out: # Work out how to estimate outlet pressure if cv.properties_out[t0].pressure.fixed: # Fixed outlet pressure, use this value state_args_out[k] = value( cv.properties_out[t0].pressure) elif blk.deltaP[t0].fixed: state_args_out[k] = value( state_args[k] + blk.deltaP[t0]) elif blk.ratioP[t0].fixed: state_args_out[k] = value( state_args[k] * blk.ratioP[t0]) else: # Not obvious what to do, use inlet state state_args_out[k] = state_args[k] elif k not in state_args_out: state_args_out[k] = state_args[k] # Initialize state blocks flags = cv.properties_in.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True, state_args=state_args, ) cv.properties_out.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False, state_args=state_args_out, ) init_log.info_high("Initialization Step 1 Complete.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}." .format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}." .format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl) init_log.info(f"Initialization Complete: {idaeslog.condition(res)}") def init_isentropic(blk, state_args, outlvl, solver, optarg): """ Initialization routine for isentropic pressure changers. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={}) solver : str indicating which solver to use during initialization (default = None) Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) cv = blk.control_volume t0 = blk.flowsheet().config.time.first() state_args_out = {} # performance curves exist and are active so initialize with them activate_performance_curves = ( hasattr(blk, "performance_curve") and blk.performance_curve.has_constraints() and blk.performance_curve.active) if activate_performance_curves: blk.performance_curve.deactivate() # The performance curves will provide (maybe indirectly) efficency # and/or pressure ratio. To get through the standard isentropic # pressure changer init, we'll see if the user provided a guess for # pressure ratio or isentropic efficency and fix them if need. If # not fixed and no guess provided, fill in something reasonable # until the performance curves are turned on. unfix_eff = {} unfix_ratioP = {} for t in blk.flowsheet().config.time: if not (blk.ratioP[t].fixed or blk.deltaP[t].fixed or cv.properties_out[t].pressure.fixed): if blk.config.compressor: if not (value(blk.ratioP[t]) >= 1.01 and value(blk.ratioP[t]) <= 50): blk.ratioP[t] = 1.8 else: if not (value(blk.ratioP[t]) >= 0.01 and value(blk.ratioP[t]) <= 0.999): blk.ratioP[t] = 0.7 blk.ratioP[t].fix() unfix_ratioP[t] = True if not blk.efficiency_isentropic[t].fixed: if not (value(blk.efficiency_isentropic[t]) >= 0.05 and value(blk.efficiency_isentropic[t]) <= 1.0): blk.efficiency_isentropic[t] = 0.8 blk.efficiency_isentropic[t].fix() unfix_eff[t] = True if state_args is None: state_args = {} state_dict = ( cv.properties_in[t0].define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args[k] = {} for m in state_dict[k].keys(): state_args[k][m] = state_dict[k][m].value else: state_args[k] = state_dict[k].value # Get initialisation guesses for outlet and isentropic states for k in state_args: if k == "pressure" and k not in state_args_out: # Work out how to estimate outlet pressure if cv.properties_out[t0].pressure.fixed: # Fixed outlet pressure, use this value state_args_out[k] = value( cv.properties_out[t0].pressure) elif blk.deltaP[t0].fixed: state_args_out[k] = value( state_args[k] + blk.deltaP[t0]) elif blk.ratioP[t0].fixed: state_args_out[k] = value( state_args[k] * blk.ratioP[t0]) else: # Not obvious what to do, use inlet state state_args_out[k] = state_args[k] elif k not in state_args_out: state_args_out[k] = state_args[k] # Initialize state blocks flags = cv.properties_in.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True, state_args=state_args, ) cv.properties_out.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False, state_args=state_args_out, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Initialize Isentropic block blk.properties_isentropic.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_out, ) init_log.info_high("Initialization Step 2 Complete.") # --------------------------------------------------------------------- # Solve for isothermal conditions if isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].temperature, Var, ): blk.properties_isentropic[:].temperature.fix() elif isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].enth_mol, Var, ): blk.properties_isentropic[:].enth_mol.fix() elif isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].temperature, Expression, ): def tmp_rule(b, t): return blk.properties_isentropic[t].temperature == \ blk.control_volume.properties_in[t].temperature blk.tmp_init_constraint = Constraint( blk.flowsheet().config.time, rule=tmp_rule) blk.isentropic.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}." .format(idaeslog.condition(res))) if isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].temperature, Var, ): blk.properties_isentropic[:].temperature.unfix() elif isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].enth_mol, Var, ): blk.properties_isentropic[:].enth_mol.unfix() elif isinstance( blk.properties_isentropic[ blk.flowsheet().config.time.first()].temperature, Expression, ): blk.del_component(blk.tmp_init_constraint) blk.isentropic.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 4 {}." .format(idaeslog.condition(res))) if activate_performance_curves: blk.performance_curve.activate() for t, v in unfix_eff.items(): if v: blk.efficiency_isentropic[t].unfix() for t, v in unfix_ratioP.items(): if v: blk.ratioP[t].unfix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.") # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl) init_log.info(f"Initialization Complete: {idaeslog.condition(res)}") def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "deltaP"): var_dict["Mechanical Work"] = self.work_mechanical[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] if hasattr(self, "ratioP"): var_dict["Pressure Ratio"] = self.ratioP[time_point] if hasattr(self, "efficiency_pump"): var_dict["Efficiency"] = self.efficiency_pump[time_point] if hasattr(self, "efficiency_isentropic"): var_dict["Isentropic Efficiency"] = \ self.efficiency_isentropic[time_point] return {"vars": var_dict} def get_costing(self, module=costing, year=None, **kwargs): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year) self.costing = Block() module.pressure_changer_costing( self.costing, **kwargs) def calculate_scaling_factors(self): super().calculate_scaling_factors() if hasattr(self, "work_fluid"): for t, v in self.work_fluid.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "work_mechanical"): for t, v in self.work_mechanical.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "work_isentropic"): for t, v in self.work_isentropic.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "ratioP_calculation"): for t, c in self.ratioP_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].pressure, default=1, warning=True), overwrite=False) if hasattr(self, "fluid_work_calculation"): for t, c in self.fluid_work_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.deltaP[t], default=1, warning=True), overwrite=False) if hasattr(self, "actual_work"): for t, c in self.actual_work.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True), overwrite=False) if hasattr(self, "isentropic_pressure"): for t, c in self.isentropic_pressure.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].pressure, default=1, warning=True), overwrite=False) if hasattr(self, "isentropic"): for t, c in self.isentropic.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].entr_mol, default=1, warning=True), overwrite=False) if hasattr(self, "isentropic_energy_balance"): for t, c in self.isentropic_energy_balance.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True), overwrite=False) if hasattr(self, "zero_work_equation"): for t, c in self.zero_work_equation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "state_material_balances"): cvol = self.control_volume phase_list = cvol.properties_in.phase_list phase_component_set = cvol.properties_in.phase_component_set mb_type = cvol._constructed_material_balance_type if mb_type == MaterialBalanceType.componentPhase: for (t, p, j), c in self.state_material_balances.items(): sf = iscale.get_scaling_factor( cvol.properties_in[t].get_material_flow_terms(p, j), default=1, warning=True) iscale.constraint_scaling_transform(c, sf) elif mb_type == MaterialBalanceType.componentTotal: for (t, j), c in self.state_material_balances.items(): sf = iscale.min_scaling_factor( [cvol.properties_in[t].get_material_flow_terms(p, j) for p in phase_list if (p, j) in phase_component_set]) iscale.constraint_scaling_transform(c, sf) else: # There are some other material balance types but they create # constraints with different names. _log.warning(f"Unknown material balance type {mb_type}") if hasattr(self, "costing"): # import costing scaling factors costing.calculate_scaling_factors(self.costing)
class ReboilerData(UnitModelBlockData): """ Reboiler unit for distillation model. Unit model to reboil the liquid from the bottom tray of the distillation column. """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "has_boilup_ratio", ConfigValue(default=False, domain=In([True, False]), description="Boilup ratio term construction flag", doc="""Indicates whether terms for boilup ratio should be constructed, **default** - False. **Valid values:** { **True** - include construction of boilup ratio constraint, **False** - exclude construction of boilup ratio constraint}""")) CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """Build the model. Args: None Returns: None """ # Setup model build logger model_log = idaeslog.getModelLogger(self.name, tag="unit") # Call UnitModel.build to setup dynamics super(ReboilerData, self).build() # Add Control Volume for the Reboiler self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Get liquid and vapor phase objects from the property package # to be used below. Avoids repition. _liquid_list = [] _vapor_list = [] for p in self.config.property_package.phase_list: pobj = self.config.property_package.get_phase(p) if pobj.is_vapor_phase(): _vapor_list.append(p) elif pobj.is_liquid_phase(): _liquid_list.append(p) else: _liquid_list.append(p) model_log.warning( "A non-liquid/non-vapor phase was detected but will " "be treated as a liquid.") # Create a pyomo set for indexing purposes. This set is appended to # model otherwise results in an abstract set. self._liquid_set = Set(initialize=_liquid_list) self._vapor_set = Set(initialize=_vapor_list) if self.config.has_boilup_ratio is True: self.boilup_ratio = Var(initialize=0.5, doc="Boilup ratio for reboiler") def rule_boilup_ratio(self, t): if hasattr(self.control_volume.properties_out[t], "flow_mol_phase"): return self.boilup_ratio * \ sum(self.control_volume.properties_out[t]. flow_mol_phase[p] for p in self._liquid_set) == \ sum(self.control_volume. properties_out[t].flow_mol_phase["Vap"] for p in self._vapor_set) elif hasattr(self.control_volume.properties_out[t], "flow_mol_phase_comp"): return self.boilup_ratio * \ sum(self.control_volume.properties_out[t]. flow_mol_phase_comp[p, i] for p in self._liquid_set for i in self.control_volume.properties_out[t]. params.component_list) == \ sum(self.control_volume.properties_out[t]. flow_mol_phase_comp[p, i] for p in self._vapor_set for i in self.control_volume.properties_out[t]. params.component_list) else: raise PropertyNotSupportedError( "Unrecognized names for flow variables encountered " "while building the constraint for reboiler.") self.eq_boilup_ratio = Constraint(self.flowsheet().time, rule=rule_boilup_ratio) self._make_ports() self._make_splits_reboiler() # Add object reference to variables of the control volume # Reference to the heat duty self.heat_duty = Reference(self.control_volume.heat[:]) # Reference to the pressure drop (if set to True) if self.config.has_pressure_change: self.deltaP = Reference(self.control_volume.deltaP[:]) def _make_ports(self): # Add Ports for the reboiler # Inlet port (the vapor from the top tray) self.add_inlet_port() # Outlet ports that always exist irrespective of reboiler type self.bottoms = Port(noruleinit=True, doc="Bottoms stream.") self.vapor_reboil = Port(noruleinit=True, doc="Vapor outlet stream that is returned to " "to the bottom tray.") def _make_splits_reboiler(self): # Get dict of Port members and names member_list = self.control_volume.\ properties_out[0].define_port_members() # Create references and populate the reflux, distillate ports for k in member_list: local_name = member_list[k].local_name # Create references and populate the intensive variables if "flow" not in local_name and "frac" not in local_name \ and "enth" not in local_name: if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(local_name) else: var = self.control_volume.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the reflux port self.bottoms.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_reboil.add(Reference(var), k) elif "frac" in local_name: # Mole/mass frac is typically indexed index_set = member_list[k].index_set() # if state var is not mole/mass frac by phase if "phase" not in local_name: if "mole" in local_name: # check mole basis/mass basis # The following conditionals are required when a # mole frac or mass frac is a state var i.e. will be # a port member. This gets a bit tricky when handling # non-conventional systems when you have more than one # liquid or vapor phase. Hence, the logic here is that # the mole frac that should be present in the liquid or # vapor port should be computed by accounting for # multiple liquid or vapor phases if present. For the # classical VLE system, this holds too. if hasattr(self.control_volume.properties_out[0], "mole_frac_phase_comp") and \ hasattr(self.control_volume.properties_out[0], "flow_mol_phase"): flow_phase_comp = False local_name_frac = "mole_frac_phase_comp" local_name_flow = "flow_mol_phase" elif hasattr(self.control_volum.properties_out[0], "flow_mol_phase_comp"): flow_phase_comp = True local_name_flow = "flow_mol_phase_comp" else: raise PropertyNotSupportedError( "No mole_frac_phase_comp or flow_mol_phase or" " flow_mol_phase_comp variables encountered " "while building ports for the reboiler. ") elif "mass" in local_name: if hasattr(self.control_volume.properties_out[0], "mass_frac_phase_comp") and \ hasattr(self.control_volume.properties_out[0], "flow_mass_phase"): flow_phase_comp = False local_name_frac = "mass_frac_phase_comp" local_name_flow = "flow_mass_phase" elif hasattr(self.control_volum.properties_out[0], "flow_mass_phase_comp"): flow_phase_comp = True local_name_flow = "flow_mass_phase_comp" else: raise PropertyNotSupportedError( "No mass_frac_phase_comp or flow_mass_phase or" " flow_mass_phase_comp variables encountered " "while building ports for the reboiler.") else: raise PropertyNotSupportedError( "No mass frac or mole frac variables encountered " " while building ports for the reboiler. " "phase_frac as a state variable is not " "supported with distillation unit models.") # Rule for liquid phase mole fraction def rule_liq_frac(self, t, i): if not flow_phase_comp: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_frac)[p, i] * self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._liquid_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._liquid_set) else: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._liquid_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._liquid_set for i in self.config.property_package.component_list) self.e_liq_frac = Expression(self.flowsheet().time, index_set, rule=rule_liq_frac) # Rule for vapor phase mass/mole fraction def rule_vap_frac(self, t, i): if not flow_phase_comp: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_frac)[p, i] * self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._vapor_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._vapor_set) else: sum_flow_comp = sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._vapor_set) return sum_flow_comp / sum( self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._vapor_set for i in self.config.property_package.component_list) self.e_vap_frac = Expression(self.flowsheet().time, index_set, rule=rule_vap_frac) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_liq_frac, k) # add the reference and variable name to the # vapor port self.vapor_reboil.add(self.e_vap_frac, k) else: # Assumes mole_frac_phase or mass_frac_phase exist as # state vars in the port and therefore access directly # from the state block. var = self.control_volume.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the distillate port self.bottoms.add(Reference(var), k) # add the reference and variable name to the boil up port self.vapor_reboil.add(Reference(var), k) elif "flow" in local_name: if "phase" not in local_name: # Assumes that here the var is total flow or component # flow. However, need to extract the flow by phase from # the state block. Expects to find the var # flow_mol_phase or flow_mass_phase in the state block. # Check if it is not indexed by component list and this # is total flow if not member_list[k].is_indexed(): # if state var is not flow_mol/flow_mass # by phase local_name_flow = local_name + "_phase" # Rule for vap flow def rule_vap_flow(self, t): return sum(self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._vapor_set) self.e_vap_flow = Expression(self.flowsheet().time, rule=rule_vap_flow) # Rule to link the liq flow to the distillate def rule_bottoms_flow(self, t): return sum(self.control_volume.properties_out[t]. component(local_name_flow)[p] for p in self._liquid_set) self.e_bottoms_flow = Expression( self.flowsheet().time, rule=rule_bottoms_flow) else: # when it is flow comp indexed by component list str_split = local_name.split("_") if len(str_split) == 3 and str_split[-1] == "comp": local_name_flow = str_split[0] + "_" + \ str_split[1] + "_phase_" + "comp" # Get the indexing set i.e. component list index_set = member_list[k].index_set() # Rule for vap phase flow to the vapor outlet def rule_vap_flow(self, t, i): return sum(self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._vapor_set) self.e_vap_flow = Expression(self.flowsheet().time, index_set, rule=rule_vap_flow) # Rule for liq phase flow to the liquid outlet def rule_bottoms_flow(self, t, i): return sum(self.control_volume.properties_out[t]. component(local_name_flow)[p, i] for p in self._liquid_set) self.e_bottoms_flow = Expression( self.flowsheet().time, index_set, rule=rule_bottoms_flow) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_bottoms_flow, k) # add the reference and variable name to the # distillate port self.vapor_reboil.add(self.e_vap_flow, k) else: # when it is flow indexed by phase or indexed by # both phase and component. var = self.control_volume.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the bottoms port self.bottoms.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_reboil.add(Reference(var), k) elif "enth" in local_name: if "phase" not in local_name: # assumes total mixture enthalpy (enth_mol or enth_mass) if not member_list[k].is_indexed(): # if state var is not enth_mol/enth_mass # by phase, add _phase string to extract the right # value from the state block local_name_enth = local_name + "_phase" else: raise PropertyPackageError( "Enthalpy is indexed but the variable " "name does not reflect the presence of an index. " "Please follow the naming convention outlined " "in the documentation for state variables.") # Rule for vap enthalpy. Setting the enthalpy to the # enth_mol_phase['Vap'] value from the state block def rule_vap_enth(self, t): return sum( self.control_volume.properties_out[t].component( local_name_enth)[p] for p in self._vapor_set) self.e_vap_enth = Expression(self.flowsheet().time, rule=rule_vap_enth) # Rule to link the liq flow to the distillate. # Setting the enthalpy to the # enth_mol_phase['Liq'] value from the state block def rule_bottoms_enth(self, t): return sum( self.control_volume.properties_out[t].component( local_name_enth)[p] for p in self._liquid_set) self.e_bottoms_enth = Expression(self.flowsheet().time, rule=rule_bottoms_enth) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_bottoms_enth, k) # add the reference and variable name to the # distillate port self.vapor_reboil.add(self.e_vap_enth, k) elif "phase" in local_name: # assumes enth_mol_phase or enth_mass_phase. # This is an intensive property, you create a direct # reference irrespective of the reflux, distillate and # vap_outlet # Rule for vap flow if not k.is_indexed(): var = self.control_volume.properties_out[:].\ component(local_name) else: var = self.control_volume.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the distillate port self.bottoms.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_reboil.add(Reference(var), k) else: raise PropertyNotSupportedError( "Unrecognized enthalpy state variable encountered " "while building ports for the reboiler. Only total " "mixture enthalpy or enthalpy by phase are supported.") def initialize(self, state_args=None, solver=None, optarg=None, outlvl=idaeslog.NOTSET): init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") if solver is None: init_log.warning("Solver not provided. Default solver(ipopt) " " being used for initialization.") solver = get_default_solver() # Initialize the inlet and outlet state blocks. Calling the state # blocks initialize methods directly so that custom set of state args # can be passed to the inlet and outlet state blocks as control_volume # initialize method initializes the state blocks with the same # state conditions. flags = self.control_volume.properties_in. \ initialize(state_args=state_args, solver=solver, optarg=optarg, outlvl=outlvl, hold_state=True) # Initialize outlet state block at same conditions of inlet except # the temperature. Set the temperature to a temperature guess based # on the desired boilup_ratio. # Get index for bubble point temperature and and assume it # will have only a single phase equilibrium pair. This is to # support the generic property framework where the T_bubble # is indexed by the phases_in_equilibrium. In distillation, # the assumption is that there will only be a single pair # i.e. vap-liq. idx = next( iter(self.control_volume.properties_in[0].temperature_bubble)) temp_guess = 0.5 * ( value(self.control_volume.properties_in[0].temperature_dew[idx]) - value(self.control_volume.properties_in[0]. temperature_bubble[idx])) + \ value(self.control_volume.properties_in[0].temperature_bubble[idx]) state_args_outlet = {} state_dict_outlet = (self.control_volume.properties_in[ self.flowsheet().config.time.first()].define_port_members()) for k in state_dict_outlet.keys(): if state_dict_outlet[k].is_indexed(): state_args_outlet[k] = {} for m in state_dict_outlet[k].keys(): state_args_outlet[k][m] = value(state_dict_outlet[k][m]) else: if k != "temperature": state_args_outlet[k] = value(state_dict_outlet[k]) else: state_args_outlet[k] = temp_guess self.control_volume.properties_out.initialize( state_args=state_args_outlet, solver=solver, optarg=optarg, outlvl=outlvl, hold_state=False) if degrees_of_freedom(self) == 0: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info("Initialization Complete, {}.".format( idaeslog.condition(res))) else: raise ConfigurationError( "State vars fixed but degrees of freedom " "for reboiler is not zero during " "initialization. Please ensure that the boilup_ratio " "or the outlet temperature is fixed.") self.control_volume.properties_in.\ release_state(flags=flags, outlvl=outlvl) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): stream_attributes = {} stream_dict = { "Inlet": "inlet", "Vapor Reboil": "vapor_reboil", "Bottoms": "bottoms" } for n, v in stream_dict.items(): port_obj = getattr(self, v) stream_attributes[n] = {} for k in port_obj.vars: for i in port_obj.vars[k].keys(): if isinstance(i, float): stream_attributes[n][k] = value( port_obj.vars[k][time_point]) else: if len(i) == 2: kname = str(i[1]) else: kname = str(i[1:]) stream_attributes[n][k + " " + kname] = \ value(port_obj.vars[k][time_point, i[1:]]) return DataFrame.from_dict(stream_attributes, orient="columns")
class PIDControllerData(UnitModelBlockData): CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "pv", ConfigValue( default=None, description="Process variable to be controlled", doc="A Pyomo Var, Expression, or Reference for the measured" " process variable. Should be indexed by time.")) CONFIG.declare( "mv", ConfigValue( default=None, description="Manipulated process variable", doc="A Pyomo Var, Expression, or Reference for the controlled" " process variable. Should be indexed by time.")) CONFIG.declare( "bounded_output", ConfigValue( default=False, description="Flag to bound manipulated variable", doc= """Indicating if the output for the manipulated variable is bounded. Default: False. If True, user need to set the lower and upper bound parameters""" )) CONFIG.declare( "type", ConfigValue(default="PI", domain=In(['P', 'PI', 'PD', 'PID']), description="Control type", doc="""Controller type options including - P: Proportional only - PI: Proportional and integral only - PD: Proportional and derivative only - PID: Proportional, integral and derivative Default is PI""")) def build(self): """ Build the PID block """ super().build() # do the ProcessBlockData voodoo for config # Do nothing if steady-state if self.config.dynamic == True: # Check for required config if self.config.pv is None: raise ConfigurationError( "Controller configuration requires 'pv'") if self.config.mv is None: raise ConfigurationError( "Controller configuration requires 'mv'") # Shorter pointers to time set information time_set = self.flowsheet().config.time self.pv = Reference(self.config.pv) # No duplicate self.mv = Reference(self.config.mv) # No duplicate # Parameters self.smooth_eps = Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") self.mv_lb = Param( mutable=True, initialize=0.05, doc="controller output lower bound" ) #not use 0 for valve since it could cause negative flow self.mv_ub = Param(mutable=True, initialize=1, doc="controller output upper bound") # Variable for basic controller settings may change with time. self.setpoint = Var(time_set, initialize=0.5, doc="Setpoint") self.gain_p = Var(time_set, initialize=0.1, doc="Gain for proportional part") if self.config.type == 'PI' or self.config.type == 'PID': self.gain_i = Var(time_set, initialize=0.1, doc="Gain for integral part") if self.config.type == 'PD' or self.config.type == 'PID': self.gain_d = Var(time_set, initialize=0.01, doc="Gain for derivative part") self.mv_ref = Var(initialize=0.5, doc="bias value of manipulated variable") if self.config.type == 'P' or self.config.type == 'PI': @self.Expression(time_set, doc="Error expression") def error(b, t): return b.setpoint[t] - b.pv[t] else: self.error = Var(time_set, initialize=0, doc="Error variable") @self.Constraint(time_set, doc="Error variable") def error_eqn(b, t): return b.error[t] == b.setpoint[t] - b.pv[t] if self.config.type == 'PI' or self.config.type == 'PID': self.integral_of_error = Var(time_set, initialize=0, doc="Integral term") self.error_from_integral = DerivativeVar( self.integral_of_error, wrt=self.flowsheet().config.time, initialize=0) @self.Constraint( time_set, doc="Error calculated by derivative of integral") def error_from_integral_eqn(b, t): return b.error[t] == b.error_from_integral[t] if self.config.type == 'PID' or self.config.type == 'PD': self.derivative_of_error = DerivativeVar( self.error, wrt=self.flowsheet().config.time, initialize=0) @self.Expression(time_set, doc="Proportional output") def mv_p_only(b, t): return b.gain_p[t] * b.error[t] @self.Expression(time_set, doc="Proportional output and reference") def mv_p_only_with_ref(b, t): return b.gain_p[t] * b.error[t] + b.mv_ref if self.config.type == 'PI' or self.config.type == 'PID': @self.Expression(time_set, doc="Integral output") def mv_i_only(b, t): return b.gain_i[t] * b.integral_of_error[t] if self.config.type == 'PD' or self.config.type == 'PID': @self.Expression(time_set, doc="Derivative output") def mv_d_only(b, t): return b.gain_d[t] * b.derivative_of_error[t] @self.Expression(time_set, doc="Unbounded output for manimulated variable") def mv_unbounded(b, t): if self.config.type == 'PID': return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[ t] * b.integral_of_error[t] + b.gain_d[ t] * b.derivative_of_error[t] elif self.config.type == 'PI': return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[ t] * b.integral_of_error[t] elif self.config.type == 'PD': return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_d[ t] * b.derivative_of_error[t] else: return b.mv_ref + b.gain_p[t] * b.error[t] @self.Constraint(time_set, doc="Bounded output of manipulated variable") def mv_eqn(b, t): if t == b.flowsheet().config.time.first(): return Constraint.Skip else: if self.config.bounded_output == True: #return b.mv[t] == smooth_min(smooth_max(b.mv_unbounded[t], b.mv_lb, b.smooth_eps), b.mv_ub, b.smooth_eps) return (b.mv[t] - b.mv_lb) * (1 + exp( -4 / (b.mv_ub - b.mv_lb) * (b.mv_unbounded[t] - (b.mv_lb + b.mv_ub) / 2))) == b.mv_ub - b.mv_lb else: return b.mv[t] == b.mv_unbounded[t] @self.Expression(time_set, doc="integral error at error 0 and mv_ref") def integral_of_error_ref(b, t): return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref - log((b.mv_ub - b.mv_lb) / (b.mv_ref - b.mv_lb) - 1) / 4 * (b.mv_ub - b.mv_lb)) / b.gain_i[t]
class PressureChangerData(UnitModelBlockData): """ Standard Compressor/Expander Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""", ), ) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""", ), ) CONFIG.declare( "compressor", ConfigValue( default=True, domain=In([True, False]), description="Compressor flag", doc="""Indicates whether this unit should be considered a compressor (True (default), pressure increase) or an expander (False, pressure decrease).""", ), ) CONFIG.declare( "thermodynamic_assumption", ConfigValue( default=ThermodynamicAssumption.isothermal, domain=In(ThermodynamicAssumption), description="Thermodynamic assumption to use", doc="""Flag to set the thermodynamic assumption to use for the unit. - ThermodynamicAssumption.isothermal (default) - ThermodynamicAssumption.isentropic - ThermodynamicAssumption.pump - ThermodynamicAssumption.adiabatic""", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""", ), ) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) def build(self): """ Args: None Returns: None """ # Call UnitModel.build super(PressureChangerData, self).build() # Add a control volume to the unit including setting up dynamics. self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, } ) # Add geomerty variables to control volume if self.config.has_holdup: self.control_volume.add_geometry() # Add inlet and outlet state blocks to control volume self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium ) # Add mass balance # Set has_equilibrium is False for now # TO DO; set has_equilibrium to True self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=self.config.has_phase_equilibrium, ) # Add energy balance self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_work_transfer=True ) # add momentum balance self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True ) # Add Ports self.add_inlet_port() self.add_outlet_port() # Set Unit Geometry and holdup Volume 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) # 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 ( b.ratioP[t] * b.control_volume.properties_in[t].pressure == 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 add_pump(self): """ Add constraints for the incompressible fluid assumption Args: None Returns: None """ self.work_fluid = Var( self.flowsheet().config.time, initialize=1.0, doc="Work required to increase the pressure of the liquid", ) self.efficiency_pump = Var( self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency" ) @self.Constraint(self.flowsheet().config.time, doc="Pump fluid work constraint") def fluid_work_calculation(b, t): return b.work_fluid[t] == ( ( b.control_volume.properties_out[t].pressure - b.control_volume.properties_in[t].pressure ) * b.control_volume.properties_out[t].flow_vol ) # Actual work @self.Constraint( self.flowsheet().config.time, doc="Actual mechanical work calculation" ) def actual_work(b, t): if b.config.compressor: return b.work_fluid[t] == ( b.work_mechanical[t] * b.efficiency_pump[t] ) else: return b.work_mechanical[t] == ( b.work_fluid[t] * b.efficiency_pump[t] ) def add_isothermal(self): """ Add constraints for isothermal assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint( self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet temperature", ) def isothermal(b, t): return ( b.control_volume.properties_in[t].temperature == b.control_volume.properties_out[t].temperature ) def add_adiabatic(self): """ Add constraints for adiabatic assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint( self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet enthalpy", ) def adiabatic(b, t): return ( b.control_volume.properties_in[t].enth_mol == b.control_volume.properties_out[t].enth_mol ) def add_isentropic(self): """ Add constraints for isentropic assumption. Args: None Returns: None """ # Get indexing sets from control volume # Add isentropic variables self.efficiency_isentropic = Var( self.flowsheet().config.time, initialize=0.8, doc="Efficiency with respect to an " "isentropic process [-]", ) self.work_isentropic = Var( self.flowsheet().config.time, initialize=0.0, doc="Work input to unit if isentropic " "process [-]", ) # Build isentropic state block tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium tmp_dict["defined_state"] = False self.properties_isentropic = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="isentropic properties at outlet", default=tmp_dict, ) # Connect isentropic state block properties @self.Constraint( self.flowsheet().config.time, doc="Pressure for isentropic calculations" ) def isentropic_pressure(b, t): return ( b.properties_isentropic[t].pressure == b.control_volume.properties_out[t].pressure ) # This assumes isentropic composition is the same as outlet self.add_state_material_balances(self.config.material_balance_type, self.properties_isentropic, self.control_volume.properties_out) # This assumes isentropic entropy is the same as inlet @self.Constraint(self.flowsheet().config.time, doc="Isentropic assumption") def isentropic(b, t): return ( b.properties_isentropic[t].entr_mol == b.control_volume.properties_in[t].entr_mol ) # Isentropic work @self.Constraint( self.flowsheet().config.time, doc="Calculate work of isentropic process" ) def isentropic_energy_balance(b, t): return b.work_isentropic[t] == ( sum( b.properties_isentropic[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list ) - sum( b.control_volume.properties_in[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list ) ) # Actual work @self.Constraint( self.flowsheet().config.time, doc="Actual mechanical work calculation" ) def actual_work(b, t): if b.config.compressor: return b.work_isentropic[t] == ( b.work_mechanical[t] * b.efficiency_isentropic[t] ) else: return b.work_mechanical[t] == ( b.work_isentropic[t] * b.efficiency_isentropic[t] ) def model_check(blk): """ Check that pressure change matches with compressor argument (i.e. if compressor = True, pressure should increase or work should be positive) Args: None Returns: None """ if blk.config.compressor: # Compressor # Check that pressure does not decrease if any( blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0) for t in blk.flowsheet().config.time ): _log.warning("{} Compressor set with negative deltaP.".format(blk.name)) if any( blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor set with ratioP less than 1.".format(blk.name) ) if any( blk.control_volume.properties_out[t].pressure.fixed and ( value(blk.control_volume.properties_in[t].pressure) > value(blk.control_volume.properties_out[t].pressure) ) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor set with pressure decrease.".format(blk.name) ) # Check that work is not negative if any( blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) < 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Compressor maybe set with negative work.".format(blk.name) ) else: # Expander # Check that pressure does not increase if any( blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with positive deltaP.".format(blk.name) ) if any( blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with ratioP greater " "than 1.".format(blk.name) ) if any( blk.control_volume.properties_out[t].pressure.fixed and ( value(blk.control_volume.properties_in[t].pressure) < value(blk.control_volume.properties_out[t].pressure) ) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine maybe set with pressure ", "increase.".format(blk.name), ) # Check that work is not positive if any( blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) > 0.0) for t in blk.flowsheet().config.time ): _log.warning( "{} Expander/turbine set with positive work.".format(blk.name) ) # Run holdup block model checks blk.control_volume.model_check() # Run model checks on isentropic property block try: for t in blk.flowsheet().config.time: blk.properties_in[t].model_check() except AttributeError: pass def initialize( blk, state_args=None, routine=None, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6}, ): """ General wrapper for pressure changer initialization routines Keyword Arguments: routine : str stating which initialization routine to execute * None - use routine matching thermodynamic_assumption * 'isentropic' - use isentropic initialization routine * 'isothermal' - use isothermal initialization routine state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None """ # if costing block exists, deactivate try: blk.costing.deactivate() except AttributeError: pass if routine is None: # Use routine for specific type of unit routine = blk.config.thermodynamic_assumption # Call initialization routine if routine is ThermodynamicAssumption.isentropic: blk.init_isentropic( state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg ) else: # Call the general initialization routine in UnitModelBlockData super(PressureChangerData, blk).initialize( state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg ) # if costing block exists, activate try: blk.costing.activate() except AttributeError: pass def init_isentropic(blk, state_args, outlvl, solver, optarg): """ Initialization routine for unit (default solver ipopt) Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Set solver options opt = SolverFactory(solver) opt.options = optarg # --------------------------------------------------------------------- # Initialize holdup block flags = blk.control_volume.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Initialize Isentropic block # Set state_args from inlet state if state_args is None: state_args = {} state_dict = blk.control_volume.properties_in[ blk.flowsheet().config.time.first() ].define_port_members() for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args[k] = {} for m in state_dict[k].keys(): state_args[k][m] = state_dict[k][m].value else: state_args[k] = state_dict[k].value blk.properties_isentropic.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 2 Complete.") # --------------------------------------------------------------------- # Solve for isothermal conditions if isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature, Var, ): blk.properties_isentropic[:].temperature.fix() elif isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].enth_mol, Var, ): blk.properties_isentropic[:].enth_mol.fix() elif isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature, Expression, ): def tmp_rule(b, t): return blk.properties_isentropic[t].temperature == \ blk.control_volume.properties_in[t].temperature blk.tmp_init_constraint = Constraint( blk.flowsheet().config.time, rule=tmp_rule) blk.isentropic.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) if isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature, Var, ): blk.properties_isentropic[:].temperature.unfix() elif isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].enth_mol, Var, ): blk.properties_isentropic[:].enth_mol.unfix() elif isinstance( blk.properties_isentropic[blk.flowsheet().config.time.first()].temperature, Expression, ): blk.del_component(blk.tmp_init_constraint) blk.isentropic.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl + 1) init_log.info( "Initialization Complete: {}".format(idaeslog.condition(res)) ) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "deltaP"): var_dict["Mechanical Work"] = self.work_mechanical[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] if hasattr(self, "ratioP"): var_dict["Pressure Ratio"] = self.ratioP[time_point] if hasattr(self, "efficiency_pump"): var_dict["Efficiency"] = self.efficiency_pump[time_point] if hasattr(self, "efficiency_isentropic"): var_dict["Isentropic Efficiency"] = \ self.efficiency_isentropic[time_point] return {"vars": var_dict} def get_costing(self, module=costing, Mat_factor="stain_steel", mover_type="compressor", compressor_type="centrifugal", driver_mover_type="electrical_motor", pump_type="centrifugal", pump_type_factor='1.4', pump_motor_type_factor='open', year=None): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year) self.costing = Block() module.pressure_changer_costing(self.costing, Mat_factor=Mat_factor, mover_type=mover_type, compressor_type=compressor_type, driver_mover_type=driver_mover_type, pump_type=pump_type, pump_type_factor=pump_type_factor, pump_motor_type_factor=pump_motor_type_factor) def calculate_scaling_factors(self): super().calculate_scaling_factors() if hasattr(self, "work_fluid"): for t, v in self.work_fluid.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "work_mechanical"): for t, v in self.work_mechanical.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "work_isentropic"): for t, v in self.work_isentropic.items(): iscale.set_scaling_factor( v, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "ratioP_calculation"): for t, c in self.ratioP_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].pressure, default=1, warning=True)) if hasattr(self, "fluid_work_calculation"): for t, c in self.fluid_work_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.deltaP[t], default=1, warning=True)) if hasattr(self, "actual_work"): for t, c in self.actual_work.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True)) if hasattr(self, "adiabatic"): for t, c in self.adiabatic.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].enth_mol, default=1, warning=True)) if hasattr(self, "isentropic_pressure"): for t, c in self.isentropic_pressure.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].pressure, default=1, warning=True)) if hasattr(self, "isentropic"): for t, c in self.isentropic.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.properties_in[t].entr_mol, default=1, warning=True)) if hasattr(self, "isentropic_energy_balance"): for t, c in self.isentropic_energy_balance.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.control_volume.work[t], default=1, warning=True))
class SteamHeaterData(UnitModelBlockData): """ WaterwallSection Unit Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "single_side_only", ConfigValue( default=True, domain=In([True, False]), description= "Flag indicating the heat is from one side of tubes only", doc="""Indicates whether tubes are heated from one side only, **default** - True. **Valid values:** { **True** - single side is heated such as roof, **False** - both sides are heated such as platen superheater.}""")) def build(self): """ Build control volume and ports """ # Call UnitModel.build to setup dynamics super(SteamHeaterData, 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) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add object references self.volume = Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance() def _set_geometry(self): """ Define the geometry of the unit as necessary, and link to holdup volume """ # Number of tubes that steam flows through self.number_tubes = Var(initialize=4, doc="Number of tubes") # Average length of tubes that steam flows through from inlet to outlet self.tube_length = Var(initialize=5.0, doc="length tube from inlet to outlet") # Inside diameter of tubes self.diameter_in = Var(initialize=0.05, doc="Inside diameter of tubes") # Inside radius of tube @self.Expression(doc="Inside radius of tube") def radius_in(b): return 0.5 * b.diameter_in # Total cross section area of fluid flow @self.Expression(doc="Cross section area of fluid") def area_cross_fluid_total(b): return 0.25 * const.pi * b.diameter_in**2 * b.number_tubes # Tube thickness self.tube_thickness = Var(initialize=0.005, doc="Thickness of tube") # Outside radius of tube @self.Expression(doc="Outside radius of tube") def radius_out(b): return b.radius_in + b.tube_thickness # Thickness of fin self.fin_thickness = Var(initialize=0.004, doc="Thickness of fin") # Length of fin self.fin_length = Var(initialize=0.005, doc="Length of fin") # Thickness of slag layer self.slag_thickness = Var(self.flowsheet().config.time, initialize=0.001, doc="thickness of slag layer") @self.Expression(doc="Pitch of two neighboring tubes") def pitch(b): return b.fin_length + b.radius_out * 2.0 # total projected area @self.Expression(doc="total projected area for heat transfer") def area_proj_total(b): if self.config.single_side_only: return b.tube_length * b.pitch * b.number_tubes else: return 2 * b.tube_length * b.pitch * b.number_tubes @self.Expression(doc="Angle at joint of tube and fin") def alpha_tube(b): return asin(0.5 * b.fin_thickness / b.radius_out) @self.Expression(self.flowsheet().config.time, doc="Angle at joint of tube " "and fin at outside slag layer") def alpha_slag(b, t): return asin((0.5 * b.fin_thickness + b.slag_thickness[t]) / (b.radius_out + b.slag_thickness[t])) @self.Expression(doc="Perimeter of interface between slag and tube") def perimeter_if(b): if self.config.single_side_only: return (const.pi - 2 * b.alpha_tube) * b.radius_out + b.pitch \ - 2 * b.radius_out * cos(b.alpha_tube) else: return 2 * ((const.pi - 2 * b.alpha_tube) * b.radius_out + b.pitch - 2 * b.radius_out * cos(b.alpha_tube)) @self.Expression(doc="Perimeter on the inner tube side") def perimeter_ts(b): return const.pi * b.diameter_in @self.Expression(self.flowsheet().config.time, doc="Perimeter on the outer slag side") def perimeter_ss(b, t): if self.config.single_side_only: return (const.pi - 2 * b.alpha_slag[t]) \ * (b.radius_out + b.slag_thickness[t]) + \ b.pitch - 2 * (b.radius_out + b.slag_thickness[t]) \ * cos(b.alpha_slag[t]) else: return 2 * ( (const.pi - 2 * b.alpha_slag[t]) * (b.radius_out + b.slag_thickness[t]) + b.pitch - 2 * (b.radius_out + b.slag_thickness[t]) * cos(b.alpha_slag[t])) # Cross section area of tube and fin metal @self.Expression(doc="Cross section area of tube and fin metal") def area_cross_metal(b): return const.pi * (b.radius_out**2 - b.radius_in**2) \ + b.fin_thickness * b.fin_length # Cross section area of slag layer @self.Expression(self.flowsheet().config.time, doc="Cross section area of slag layer per tube") def area_cross_slag(b, t): return b.perimeter_if * b.slag_thickness[t] # Volume constraint @self.Constraint(self.flowsheet().config.time, doc="waterwall fluid volume of all tubes") def volume_eqn(b, t): return b.volume[t] == 0.25 * const.pi * b.diameter_in**2 \ * b.tube_length * b.number_tubes def _make_performance(self): """ Define constraints which describe the behaviour of the unit model. """ # Thermal conductivity of metal self.therm_cond_metal = Param(initialize=43.0, mutable=True, doc='Thermal conductivity of tube metal') # Thermal conductivity of slag self.therm_cond_slag = Var(initialize=1.3, doc='Thermal conductivity of slag') # Heat capacity of metal self.cp_metal = Param(initialize=500.0, mutable=True, doc='Heat capacity of tube metal') # Heat Capacity of slag self.cp_slag = Param(initialize=250, mutable=True, doc='Heat capacity of slag') # Density of metal self.dens_metal = Param(initialize=7800.0, mutable=True, doc='Density of tube metal') # Density of slag self.dens_slag = Param(initialize=2550, mutable=True, doc='Density of slag') # Shape factor of tube metal conduction self.fshape_metal = Param(initialize=1.0, mutable=True, doc='Shape factor of tube metal conduction') # Shape factor of slag conduction self.fshape_slag = Param(initialize=1.0, mutable=True, doc='Shape factor of slag conduction') # Add performance variables # Heat from fire side boiler model self.heat_fireside = Var( self.flowsheet().config.time, initialize=1e7, doc='total heat from fire side model for the section') # Tube boundary wall temperature self.temp_tube_boundary = Var(self.flowsheet().config.time, initialize=400.0, doc='Temperature of tube boundary wall') # Tube center point wall temperature self.temp_tube_center = Var(self.flowsheet().config.time, initialize=450.0, doc='Temperature of tube center wall') # Slag boundary wall temperature self.temp_slag_boundary = Var(self.flowsheet().config.time, initialize=600.0, doc='Temperature of slag boundary wall') # Slag center point slag wall temperature self.temp_slag_center = Var( self.flowsheet().config.time, initialize=500.0, doc='Temperature of slag layer center point') # Energy holdup for slag layer self.energy_holdup_slag = Var(self.flowsheet().config.time, initialize=1.0, doc='Energy holdup of slag layer') # Energy holdup for metal (tube + fin) self.energy_holdup_metal = Var(self.flowsheet().config.time, initialize=1.0, doc='Energy holdup of metal') # Energy accumulation for slag and metal if self.config.dynamic is True: self.energy_accumulation_slag = DerivativeVar( self.energy_holdup_slag, wrt=self.flowsheet().config.time, doc='Energy accumulation of slag layer') self.energy_accumulation_metal = DerivativeVar( self.energy_holdup_metal, wrt=self.flowsheet().config.time, doc='Energy accumulation of tube and fin metal') def energy_accumulation_term_slag(b, t): return b.energy_accumulation_slag[t] if b.config.dynamic else 0 def energy_accumulation_term_metal(b, t): return b.energy_accumulation_metal[t] if b.config.dynamic else 0 # Velocity of steam self.velocity = Var(self.flowsheet().config.time, initialize=3.0, doc='Velocity of steam') # Reynolds number based on liquid only flow self.N_Re = Var(self.flowsheet().config.time, initialize=1.0e6, doc='Reynolds number') # Prandtl number of liquid phase self.N_Pr = Var(self.flowsheet().config.time, initialize=2.0, doc='Reynolds number') # Darcy friction factor self.friction_factor_darcy = Var(self.flowsheet().config.time, initialize=0.01, doc='Darcy friction factor') # Convective heat transfer coefficient on tube side, # typically in range (1000, 5e5) self.hconv = Var(self.flowsheet().config.time, initialize=30000.0, doc='Convective heat transfer coefficient') # Convective heat flux to fluid self.heat_flux_conv = Var(self.flowsheet().config.time, initialize=7e4, doc='Convective heat flux to fluid') # Fire-side heat flux self.heat_flux_fireside = Var( self.flowsheet().config.time, initialize=100000.0, doc='Fireside heat flux to slag boundary') # Slag-tube interface heat flux self.heat_flux_interface = Var(self.flowsheet().config.time, initialize=100000.0, doc='Slag-tube interface heat flux') # Equation to calculate heat flux to slag boundary @self.Constraint(self.flowsheet().config.time, doc="heat flux at slag outer layer") def heat_flux_fireside_from_boiler_eqn(b, t): if self.config.single_side_only: return b.heat_flux_fireside[t] * b.area_proj_total \ * b.perimeter_ss[t] == b.heat_fireside[t] * b.pitch else: return b.heat_flux_fireside[t] * b.area_proj_total \ * b.perimeter_ss[t] == b.heat_fireside[t] * b.pitch * 2.0 # Equation to calculate slag layer boundary temperature @self.Constraint(self.flowsheet().config.time, doc="slag layer boundary temperature") def slag_layer_boundary_temperature_eqn(b, t): return b.heat_flux_fireside[t] * 0.5 * b.slag_thickness[t] == \ b.fshape_slag * b.therm_cond_slag * (b.temp_slag_boundary[t] - b.temp_slag_center[t]) # Equation to calculate heat flux at the slag-metal interface @self.Constraint(self.flowsheet().config.time, doc="heat flux at slag-tube interface") def heat_flux_interface_eqn(b, t): return b.heat_flux_interface[t] * 0.5 * \ (b.slag_thickness[t]/b.therm_cond_slag/b.fshape_slag + b.tube_thickness/b.therm_cond_metal/b.fshape_metal) == \ b.temp_slag_center[t] - b.temp_tube_center[t] # Equation to calculate heat flux at tube boundary @self.Constraint(self.flowsheet().config.time, doc="convective heat flux at tube boundary") def heat_flux_conv_eqn(b, t): return b.heat_flux_conv[t] == \ b.hconv[t] * (b.temp_tube_boundary[t] - (b.control_volume.properties_in[t]. temperature + b.control_volume.properties_out[t]. temperature)/2.0) # Equation to calculate tube boundary wall temperature @self.Constraint(self.flowsheet().config.time, doc="tube bounary wall temperature") def temperature_tube_boundary_eqn(b, t): return b.heat_flux_conv[t] * 0.5 * b.tube_thickness == \ b.fshape_metal * b.therm_cond_metal \ * (b.temp_tube_center[t] - b.temp_tube_boundary[t]) # Equation to calculate energy holdup for slag layer per tube length @self.Constraint(self.flowsheet().config.time, doc="energy holdup for slag layer") def energy_holdup_slag_eqn(b, t): return b.energy_holdup_slag[t] == \ b.temp_slag_center[t] * b.cp_slag * b.dens_slag \ * b.area_cross_slag[t] # Equation to calculate energy holdup for metal # (tube + fin) per tube length @self.Constraint(self.flowsheet().config.time, doc="energy holdup for metal") def energy_holdup_metal_eqn(b, t): return b.energy_holdup_metal[t] == b.temp_tube_center[t] \ * b.cp_metal * b.dens_metal * b.area_cross_metal # Energy balance for slag layer @self.Constraint(self.flowsheet().config.time, doc="energy balance for slag layer") def energy_balance_slag_eqn(b, t): return energy_accumulation_term_slag(b, t) == \ (b.heat_flux_fireside[t] * b.perimeter_ss[t] - b.heat_flux_interface[t] * b.perimeter_if) # Energy balance for metal @self.Constraint(self.flowsheet().config.time, doc="energy balance for metal") def energy_balance_metal_eqn(b, t): return energy_accumulation_term_metal( b, t) == (b.heat_flux_interface[t] * b.perimeter_if - b.heat_flux_conv[t] * b.perimeter_ts) # Expression to calculate slag/tube metal interface wall temperature @self.Expression(self.flowsheet().config.time, doc="Slag tube interface wall temperature") def temp_interface(b, t): return b.temp_tube_center[t] + b.heat_flux_interface[t] * 0.5 \ * b.tube_thickness/b.therm_cond_metal/b.fshape_metal # Equations for calculate pressure drop # and convective heat transfer coefficient # Equation for calculating velocity @self.Constraint(self.flowsheet().config.time, doc="Vecolity of fluid") def velocity_eqn(b, t): return 1e-3*b.velocity[t]*b.area_cross_fluid_total * \ b.control_volume.properties_in[t].dens_mol_phase["Vap"] \ == 1e-3*b.control_volume.properties_in[t].flow_mol # Equation for calculating Reynolds number if liquid only @self.Constraint(self.flowsheet().config.time, doc="Reynolds number") def Reynolds_number_eqn(b, t): return b.N_Re[t] * \ b.control_volume.properties_in[t].visc_d_phase["Vap"] == \ b.diameter_in * b.velocity[t] * \ b.control_volume.properties_in[t].dens_mass # Friction factor depending on laminar or turbulent flow @self.Constraint(self.flowsheet().config.time, doc="Darcy friction factor") def friction_factor_darcy_eqn(b, t): return Expr_if( b.N_Re[t] < 1187.384, b.friction_factor_darcy[t] * b.N_Re[t] / 64.0, b.friction_factor_darcy[t] * b.N_Re[t]**0.25 / 0.3164) == 1.0 # Pressure change equation due to friction, # -1/2*density*velocity^2*fD/diameter*length @self.Constraint(self.flowsheet().config.time, doc="pressure change due to friction") def pressure_change_eqn(b, t): return b.deltaP[t] * b.diameter_in == \ -0.5 * b.control_volume.properties_in[t].dens_mass * \ b.velocity[t]**2 * b.friction_factor_darcy[t] \ * b.tube_length # Total heat added to control_volume @self.Constraint(self.flowsheet().config.time, doc="total heat added to fluid control_volume") def heat_eqn(b, t): return b.heat_duty[t] == b.number_tubes * b.heat_flux_conv[t] \ * b.tube_length * b.perimeter_ts # Prandtl number of steam @self.Constraint(self.flowsheet().config.time, doc="Prandtl number") def N_Pr_eqn(b, t): return b.N_Pr[t] \ * b.control_volume.properties_in[t].therm_cond_phase["Vap"] \ * b.control_volume.properties_in[t].mw == \ b.control_volume.properties_in[t].cp_mol_phase["Vap"] * \ b.control_volume.properties_in[t].visc_d_phase["Vap"] # Forced convection heat transfer coefficient for liquid only @self.Constraint(self.flowsheet().config.time, doc="forced convection heat transfer" " coefficient for liquid only") def hconv_eqn(b, t): return b.hconv[t] * b.diameter_in == 0.023 * b.N_Re[t]**0.8 \ * b.N_Pr[t]**0.4 * \ b.control_volume.properties_in[t].therm_cond_phase["Vap"] def set_initial_condition(self): if self.config.dynamic is True: self.control_volume.material_accumulation[:, :, :].value = 0 self.control_volume.energy_accumulation[:, :].value = 0 self.energy_accumulation_slag[:].value = 0 self.energy_accumulation_metal[:].value = 0 self.control_volume.material_accumulation[0, :, :].fix(0) self.control_volume.energy_accumulation[0, :].fix(0) self.energy_accumulation_slag[0].fix(0) self.energy_accumulation_metal[0].fix(0) def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Waterwall section initialization routine. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) for the control_volume of the model to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) flags = blk.control_volume.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args) init_log.info_high("Initialization Step 1 Complete.") # Fix outlet enthalpy and pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].enth_mol.fix( value(blk.control_volume.properties_in[t].enth_mol) + value(blk.heat_fireside[t]) / value(blk.control_volume.properties_in[t].flow_mol)) blk.control_volume.properties_out[t].pressure.fix( value(blk.control_volume.properties_in[t].pressure) - 1.0) blk.heat_eqn.deactivate() blk.pressure_change_eqn.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) # Unfix outlet enthalpy and pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].enth_mol.unfix() blk.control_volume.properties_out[t].pressure.unfix() blk.heat_eqn.activate() blk.pressure_change_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) blk.control_volume.release_state(flags, outlvl=outlvl) init_log.info("Initialization Complete.") def calculate_scaling_factors(self): super().calculate_scaling_factors() for v in self.N_Re.values(): if iscale.get_scaling_factor(v, warning=True) is None: iscale.set_scaling_factor(v, 1e-6) for t, c in self.Reynolds_number_eqn.items(): s = iscale.get_scaling_factor(self.N_Re[t], default=1, warning=True) iscale.constraint_scaling_transform(c, s * 1e5, overwrite=False) for t, c in self.heat_flux_conv_eqn.items(): s = iscale.get_scaling_factor(self.heat_flux_conv[t], default=1, warning=True) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.hconv_eqn.items(): s = iscale.get_scaling_factor(self.hconv[t], default=1, warning=True) s *= iscale.get_scaling_factor(self.diameter_in, default=1, warning=True) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.pressure_change_eqn.items(): s = iscale.get_scaling_factor(self.deltaP[t], default=1, warning=True) s *= iscale.get_scaling_factor(self.diameter_in, default=1, warning=True) iscale.constraint_scaling_transform(c, s, overwrite=False)
class WaterTankData(UnitModelBlockData): """ Water Tank Unit Operation Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare("tank_type", ConfigValue( default="simple_tank", domain=In(["simple_tank", "rectangular_tank", "vertical_cylindrical_tank", "horizontal_cylindrical_tank"]), description="Flag indicating the tank type", doc="""Flag indicating the type of tank to be modeled, and then calculate the volume of the filled level consequently, **default** - simple_tank. **Valid values:** { **simple_tank** - use a general tank and provide the area, **rectangular_tank** - use a rectangular tank and provide the width and length, **vertical_cylindrical_tank** - use a vertical cylindrical tank and provide the diameter, **horizontal_cylindrical_tank** - use a horizontal cylindrical tank and provide the length and diameter.}""")) CONFIG.declare("material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare("momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare("has_heat_transfer", ConfigValue( default=False, domain=Bool, description="Heat transfer term construction flag", doc="""Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare("has_pressure_change", ConfigValue( default=True, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_geometry() self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Inlet and Outlet Ports self.add_inlet_port() self.add_outlet_port() # Add object references self.volume = Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance() def _set_geometry(self): """ Define the geometry of the unit as necessary """ if self.config.tank_type == "simple_tank": # Declare a variable for cross sectional area self.tank_cross_sect_area = Var(initialize=1.0, doc="Cross-sectional" " area of the tank") elif self.config.tank_type == "rectangular_tank": # Declare variables for width and length self.tank_width = Var(initialize=1.0, doc="Width of the tank") self.tank_length = Var(initialize=1.0, doc="Length of the tank") elif self.config.tank_type == "horizontal_cylindrical_tank" or \ "vertical_cylindrical_tank": # Declare a variable for diameter of the tank self.tank_diameter = Var(initialize=0.5, doc="Inside diameter of the tank") if self.config.tank_type == "horizontal_cylindrical_tank": # Declare a variable for length of the tank self.tank_length = Var(initialize=1, doc="Length of the tank") def _make_performance(self): """ Define constraints which describe the behaviour of the unit model """ # Add performance variables self.tank_level = Var(self.flowsheet().time, initialize=1.0, doc="Water level from in the tank") # Auxiliar expressions for volume # Rectangular tank if self.config.tank_type == "rectangular_tank": # Calculation of cross-sectional area of the rectangle @self.Expression(doc="Cross-sectional area of the tank") def tank_cross_sect_area(b): return b.tank_width * b.tank_length # Vertical cylindrical tank elif self.config.tank_type == "vertical_cylindrical_tank": @self.Expression(doc="Radius of the tank") def tank_radius(b): return b.tank_diameter/2 # Calculation of cross-sectional area of the vertical cylinder @self.Expression(doc="Cross-sectional area of the tank") def tank_cross_sect_area(b): return const.pi * b.tank_radius**2 # Horizontal cylindrical tank elif self.config.tank_type == "horizontal_cylindrical_tank": # Calculation of area covered by the liquid level # at one end of the tank @self.Expression(doc="Radius of the tank") def tank_radius(b): return b.tank_diameter/2 # Angle of the circular sector used to calculate the area @self.Expression(self.flowsheet().time, doc="Angle of the circular" " sector of liquid level") def alpha_tank(b, t): return 2*acos((b.tank_radius-b.tank_level[t])/b.tank_radius) @self.Expression(self.flowsheet().time, doc="Area covered by the liquid level" " at one end of the tank") def tank_area(b, t): return 0.5*b.alpha_tank[t]*b.tank_radius**2 \ - (b.tank_radius - b.tank_level[t]) \ * (2*b.tank_radius * b.tank_level[t] - b.tank_level[t]**2)**0.5 # Constraint for volume of the liquid in tank @self.Constraint(self.flowsheet().time, doc="volume of liquid in the tank") def volume_eqn(b, t): if self.config.tank_type == "horizontal_cylindrical_tank": return b.volume[t] == b.tank_length * b.tank_area[t] else: return b.volume[t] == b.tank_level[t]*b.tank_cross_sect_area # Pressure change equation due gravity @self.Constraint(self.flowsheet().time, doc="pressure drop") def pressure_change_eqn(b, t): return b.deltaP[t] == \ b.control_volume.properties_in[t].dens_mass_phase["Liq"] * \ const.acceleration_gravity * b.tank_level[t] def set_initial_condition(self): if self.config.dynamic is True: self.control_volume.material_accumulation[:, :, :].value = 0 self.control_volume.energy_accumulation[:, :].value = 0 self.control_volume.material_accumulation[0, :, :].fix(0) self.control_volume.energy_accumulation[0, :].fix(0) def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Water tank initialization routine. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) for the control_volume of the model to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) init_log.info_low("Starting initialization...") flags = blk.control_volume.initialize(state_args=state_args, outlvl=outlvl, optarg=optarg, solver=solver) init_log.info_high("Initialization Step 1 Complete.") # Fix outlet pressure for t in blk.flowsheet().time: blk.control_volume.properties_out[t].pressure.\ fix(value(blk.control_volume.properties_in[t].pressure)) blk.pressure_change_eqn.deactivate() # solve model with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Initialization Step 2 {}.".format(idaeslog.condition(res)) ) # Unfix outlet pressure for t in blk.flowsheet().time: blk.control_volume.properties_out[t].pressure.unfix() blk.pressure_change_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Initialization Step 3 {}.".format(idaeslog.condition(res)) ) blk.control_volume.release_state(flags, outlvl) init_log.info("Initialization Complete.") def calculate_scaling_factors(self): pass
class HeatExchanger1DData(UnitModelBlockData): """Standard Heat Exchanger 1D Unit Model Class.""" CONFIG = UnitModelBlockData.CONFIG() # Template for config arguments for shell and tube side _SideTemplate = ConfigBlock() _SideTemplate.declare( "dynamic", ConfigValue( default=useDefault, domain=DefaultBool, description="Dynamic model flag", doc="""Indicates whether this model will be dynamic or not, **default** = useDefault. **Valid values:** { **useDefault** - get flag from parent (default = False), **True** - set as a dynamic model, **False** - set as a steady-state model.}""", ), ) _SideTemplate.declare( "has_holdup", ConfigValue( default=useDefault, domain=DefaultBool, description="Holdup construction flag", doc="""Indicates whether holdup terms should be constructed or not. Must be True if dynamic = True, **default** - False. **Valid values:** { **useDefault** - get flag from parent (default = False), **True** - construct holdup terms, **False** - do not construct holdup terms}""", ), ) _SideTemplate.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""", ), ) _SideTemplate.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) _SideTemplate.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) _SideTemplate.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""", ), ) _SideTemplate.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Phase equilibrium term construction flag", doc="""Argument to enable phase equilibrium on the shell side. - True - include phase equilibrium term - False - do not include phase equilibrium term""", ), ) _SideTemplate.declare( "property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a ParameterBlock object""", ), ) _SideTemplate.declare( "property_package_args", ConfigValue( default={}, description="Arguments for constructing shell property package", doc="""A dict of arguments to be passed to the PropertyBlockData and used when constructing these (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a dict (see property package for documentation)""", ), ) # TODO : We should probably think about adding a consistency check for the # TODO : discretisation methods as well. _SideTemplate.declare( "transformation_method", ConfigValue( default=useDefault, description="Discretization method to use for DAE transformation", doc="""Discretization method to use for DAE transformation. See Pyomo documentation for supported transformations.""", ), ) _SideTemplate.declare( "transformation_scheme", ConfigValue( default=useDefault, description="Discretization scheme to use for DAE transformation", doc="""Discretization scheme to use when transformating domain. See Pyomo documentation for supported schemes.""", ), ) # Create individual config blocks for shell and tube side CONFIG.declare( "shell_side", _SideTemplate(doc="shell side config arguments")) CONFIG.declare( "tube_side", _SideTemplate(doc="tube side config arguments")) # Common config args for both sides CONFIG.declare( "finite_elements", ConfigValue( default=20, domain=int, description="Number of finite elements length domain", doc="""Number of finite elements to use when discretizing length domain (default=20)""", ), ) CONFIG.declare( "collocation_points", ConfigValue( default=5, domain=int, description="Number of collocation points per finite element", doc="""Number of collocation points to use per finite element when discretizing length domain (default=3)""", ), ) CONFIG.declare( "flow_type", ConfigValue( default=HeatExchangerFlowPattern.cocurrent, domain=In(HeatExchangerFlowPattern), description="Flow configuration of heat exchanger", doc="""Flow configuration of heat exchanger - HeatExchangerFlowPattern.cocurrent: shell and tube flows from 0 to 1 (default) - HeatExchangerFlowPattern.countercurrent: shell side flows from 0 to 1 tube side flows from 1 to 0""", ), ) CONFIG.declare( "has_wall_conduction", ConfigValue( default=WallConductionType.zero_dimensional, domain=In(WallConductionType), description="Conduction model for tube wall", doc="""Argument to enable type of wall heat conduction model. - WallConductionType.zero_dimensional - 0D wall model (default), - WallConductionType.one_dimensional - 1D wall model along the thickness of the tube, - WallConductionType.two_dimensional - 2D wall model along the lenghth and thickness of the tube""", ), ) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(HeatExchanger1DData, self).build() # Set flow directions for the control volume blocks and specify # dicretisation if not specified. if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: set_direction_shell = FlowDirection.forward set_direction_tube = FlowDirection.forward if ( self.config.shell_side.transformation_method != self.config.tube_side.transformation_method ) or ( self.config.shell_side.transformation_scheme != self.config.tube_side.transformation_scheme ): raise ConfigurationError( "HeatExchanger1D only supports similar transformation " "schemes on the shell side and tube side domains for " "both cocurrent and countercurrent flow patterns." ) if self.config.shell_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the shell side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the shell side." ) self.config.shell_side.transformation_method = \ "dae.finite_difference" if self.config.tube_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the tube side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the tube side." ) self.config.tube_side.transformation_method = \ "dae.finite_difference" if self.config.shell_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the shell side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the shell side." ) self.config.shell_side.transformation_scheme = "BACKWARD" if self.config.tube_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the tube side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the tube side." ) self.config.tube_side.transformation_scheme = "BACKWARD" elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent: set_direction_shell = FlowDirection.forward set_direction_tube = FlowDirection.backward if self.config.shell_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the shell side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the shell side." ) self.config.shell_side.transformation_method = \ "dae.finite_difference" if self.config.tube_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the tube side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the tube side." ) self.config.tube_side.transformation_method = \ "dae.finite_difference" if self.config.shell_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the shell side of the " "counter-current heat exchanger. " "Defaulting to backward finite " "difference on the shell side." ) self.config.shell_side.transformation_scheme = "BACKWARD" if self.config.tube_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the tube side of the " "counter-current heat exchanger. " "Defaulting to forward finite " "difference on the tube side." ) self.config.tube_side.transformation_scheme = "BACKWARD" else: raise ConfigurationError( "{} HeatExchanger1D only supports cocurrent and " "countercurrent flow patterns, but flow_type configuration" " argument was set to {}." .format(self.name, self.config.flow_type) ) # Control volume 1D for shell self.shell = ControlVolume1DBlock( default={ "dynamic": self.config.shell_side.dynamic, "has_holdup": self.config.shell_side.has_holdup, "property_package": self.config.shell_side.property_package, "property_package_args": self.config.shell_side.property_package_args, "transformation_method": self.config.shell_side.transformation_method, "transformation_scheme": self.config.shell_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, } ) self.tube = ControlVolume1DBlock( default={ "dynamic": self.config.tube_side.dynamic, "has_holdup": self.config.tube_side.has_holdup, "property_package": self.config.tube_side.property_package, "property_package_args": self.config.tube_side.property_package_args, "transformation_method": self.config.tube_side.transformation_method, "transformation_scheme": self.config.tube_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, } ) self.shell.add_geometry(flow_direction=set_direction_shell) self.tube.add_geometry(flow_direction=set_direction_tube) self.shell.add_state_blocks( information_flow=set_direction_shell, has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium, ) self.tube.add_state_blocks( information_flow=set_direction_tube, has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium, ) # Populate shell self.shell.add_material_balances( balance_type=self.config.shell_side.material_balance_type, has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium, ) self.shell.add_energy_balances( balance_type=self.config.shell_side.energy_balance_type, has_heat_transfer=True, ) self.shell.add_momentum_balances( balance_type=self.config.shell_side.momentum_balance_type, has_pressure_change=self.config.shell_side.has_pressure_change, ) self.shell.apply_transformation() # Populate tube self.tube.add_material_balances( balance_type=self.config.tube_side.material_balance_type, has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium, ) self.tube.add_energy_balances( balance_type=self.config.tube_side.energy_balance_type, has_heat_transfer=True, ) self.tube.add_momentum_balances( balance_type=self.config.tube_side.momentum_balance_type, has_pressure_change=self.config.tube_side.has_pressure_change, ) self.tube.apply_transformation() # Add Ports for shell side self.add_inlet_port(name="shell_inlet", block=self.shell) self.add_outlet_port(name="shell_outlet", block=self.shell) # Add Ports for tube side self.add_inlet_port(name="tube_inlet", block=self.tube) self.add_outlet_port(name="tube_outlet", block=self.tube) # Add reference to control volume geometry add_object_reference(self, "shell_area", self.shell.area) add_object_reference(self, "shell_length", self.shell.length) add_object_reference(self, "tube_area", self.tube.area) add_object_reference(self, "tube_length", self.tube.length) self._make_performance() def _make_performance(self): """ Constraints for unit model. Args: None Returns: None """ shell_units = self.config.shell_side.property_package.\ get_metadata().get_derived_units tube_units = self.config.tube_side.property_package.\ get_metadata().get_derived_units # Unit model variables # HX dimensions self.d_shell = Var(initialize=1, doc="Diameter of shell", units=shell_units("length")) self.d_tube_outer = Var(initialize=0.011, doc="Outer diameter of tube", units=shell_units("length")) self.d_tube_inner = Var(initialize=0.010, doc="Inner diameter of tube", units=shell_units("length")) self.N_tubes = Var(initialize=1, doc="Number of tubes", units=pyunits.dimensionless) # Note: In addition to the above variables, "shell_length" and # "tube_length" need to be fixed at the flowsheet level # Performance variables self.shell_heat_transfer_coefficient = Var( self.flowsheet().time, self.shell.length_domain, initialize=50, doc="Heat transfer coefficient", units=shell_units("heat_transfer_coefficient") ) self.tube_heat_transfer_coefficient = Var( self.flowsheet().time, self.tube.length_domain, initialize=50, doc="Heat transfer coefficient", units=tube_units("heat_transfer_coefficient") ) # Wall 0D model (Q_shell = Q_tube*N_tubes) if self.config.has_wall_conduction == \ WallConductionType.zero_dimensional: self.temperature_wall = Var( self.flowsheet().time, self.tube.length_domain, initialize=298.15, units=shell_units("temperature") ) # Performance equations # Energy transfer between shell and tube wall @self.Constraint( self.flowsheet().time, self.shell.length_domain, doc="Heat transfer between shell and tube", ) def shell_heat_transfer_eq(self, t, x): return self.shell.heat[t, x] == -self.N_tubes * ( self.shell_heat_transfer_coefficient[t, x] * c.pi * self.d_tube_outer * ( self.shell.properties[t, x].temperature - self.temperature_wall[t, x] ) ) # Energy transfer between tube wall and tube @self.Constraint( self.flowsheet().time, self.tube.length_domain, doc="Convective heat transfer", ) def tube_heat_transfer_eq(self, t, x): return self.tube.heat[t, x] == \ self.tube_heat_transfer_coefficient[ t, x ] * c.pi * pyunits.convert(self.d_tube_inner, to_units=tube_units("length")) * ( pyunits.convert(self.temperature_wall[t, x], to_units=tube_units('temperature')) - self.tube.properties[t, x].temperature ) if shell_units("length") is None: # Backwards compatability check q_units = None else: q_units = shell_units("power")/shell_units("length") # Wall 0D model @self.Constraint( self.flowsheet().time, self.shell.length_domain, doc="wall 0D model", ) def wall_0D_model(self, t, x): return pyunits.convert(self.tube.heat[t, x], to_units=q_units) == -( self.shell.heat[t, x] / self.N_tubes) else: raise NotImplementedError( "{} HeatExchanger1D has not yet implemented support for " "wall conduction models." ) # Define tube area in terms of tube diameter self.area_calc_tube = Constraint( expr=4 * self.tube_area == c.pi * pyunits.convert( self.d_tube_inner, to_units=tube_units("length"))**2 ) # Define shell area in terms of shell and tube diameter self.area_calc_shell = Constraint( expr=4 * self.shell_area == c.pi * (self.d_shell**2 - self.N_tubes*self.d_tube_outer**2) ) def initialize( self, shell_state_args=None, tube_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ Initialization routine for the unit. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize shell block flags_shell = self.shell.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=shell_state_args, ) flags_tube = self.tube.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=tube_state_args, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Solve unit # Wall 0D if self.config.has_wall_conduction == \ WallConductionType.zero_dimensional: shell_units = self.config.shell_side.property_package.\ get_metadata().get_derived_units for t in self.flowsheet().time: for z in self.shell.length_domain: self.temperature_wall[t, z].fix( value( 0.5 * ( self.shell.properties[t, 0].temperature + pyunits.convert( self.tube.properties[t, 0].temperature, to_units=shell_units('temperature')) ) ) ) self.tube.deactivate() self.tube_heat_transfer_eq.deactivate() self.wall_0D_model.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high( "Initialization Step 2 {}.".format(idaeslog.condition(res)) ) self.tube.activate() self.tube_heat_transfer_eq.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high( "Initialization Step 3 {}.".format(idaeslog.condition(res)) ) self.wall_0D_model.activate() self.temperature_wall.unfix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high( "Initialization Step 4 {}.".format(idaeslog.condition(res)) ) self.shell.release_state(flags_shell) self.tube.release_state(flags_tube) init_log.info("Initialization Complete.") def _get_performance_contents(self, time_point=0): var_dict = {} var_dict["Shell Area"] = self.shell.area var_dict["Shell Diameter"] = self.d_shell var_dict["Shell Length"] = self.shell.length var_dict["Tube Area"] = self.tube.area var_dict["Tube Outer Diameter"] = self.d_tube_outer var_dict["Tube Inner Diameter"] = self.d_tube_inner var_dict["Tube Length"] = self.tube.length var_dict["Number of Tubes"] = self.N_tubes return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Shell Inlet": self.shell_inlet, "Shell Outlet": self.shell_outlet, "Tube Inlet": self.tube_inlet, "Tube Outlet": self.tube_outlet, }, time_point=time_point, ) def calculate_scaling_factors(self): super().calculate_scaling_factors() for i, c in self.shell_heat_transfer_eq.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.shell.heat[i], default=1, warning=True), overwrite=False) for i, c in self.tube_heat_transfer_eq.items(): iscale.constraint_scaling_transform(c, iscale.get_scaling_factor( self.tube.heat[i], default=1, warning=True), overwrite=False)
class PackedColumnData(UnitModelBlockData): """ Standard Continous Differential Contactor (CDC) Model Class. """ # Configuration template for unit level arguments applicable to both phases CONFIG = UnitModelBlockData.CONFIG() # Configuration template for phase specific arguments _PhaseCONFIG = ConfigBlock() CONFIG.declare("finite_elements", ConfigValue( default=20, domain=int, description="Number of finite elements length domain", doc="""Number of finite elements to use when discretizing length domain (default=20)""")) CONFIG.declare("length_domain_set", ConfigValue( default=[0.0, 1.0], domain=list, description="List of points in length domain", doc="""length_domain_set - (optional) list of point to use to initialize a new ContinuousSet if length_domain is not provided (default = [0.0, 1.0])""")) CONFIG.declare("transformation_method", ConfigValue( default="dae.finite_difference", description="Method to use for DAE transformation", doc="""Method to use to transform domain. Must be a method recognised by the Pyomo TransformationFactory, **default** - "dae.finite_difference". **Valid values:** { **"dae.finite_difference"** - Use a finite difference transformation method, **"dae.collocation"** - use a collocation transformation method}""")) CONFIG.declare("collocation_points", ConfigValue( default=3, domain=int, description="Number of collocation points per finite element", doc="""Number of collocation points to use per finite element when discretizing length domain (default=3)""")) CONFIG.declare("column_pressure_drop", ConfigValue( default=0, description="Column pressure drop per unit length in Pa/m", doc="Column pressure drop per unit length in Pa/m provided as a value or expression")) # Populate the phase side template to default values _PhaseCONFIG.declare("has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) _PhaseCONFIG.declare("property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a ParameterBlock object""")) _PhaseCONFIG.declare("property_package_args", ConfigValue( default={}, description="Arguments for constructing vapor property package", doc="""A dict of arguments to be passed to the PropertyBlockData and used when constructing these (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a dict (see property package for documentation) """)) _PhaseCONFIG.declare("transformation_scheme", ConfigValue( default="BACKWARD", description="Scheme to use for DAE transformation", doc="""Scheme to use when transformating domain. See Pyomo documentation for supported schemes, **default** - "BACKWARD". **Valid values:** { **"BACKWARD"** - Use a BACKWARD finite difference transformation method, **"FORWARD""** - Use a FORWARD finite difference transformation method, **"LAGRANGE-RADAU""** - use a collocation transformation method}""")) # Create individual config blocks for vapor(gas) and liquid sides CONFIG.declare("vapor_side", _PhaseCONFIG(doc="vapor side config arguments")) CONFIG.declare("liquid_side", _PhaseCONFIG(doc="liquid side config arguments")) # ========================================================================= def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to build default attributes super().build() # ========================================================================= """ Set argument values for vapor and liquid sides""" # Set flow directions for the control volume blocks # Gas flows from 0 to 1, Liquid flows from 1 to 0 # TODO: Only handling countercurrent flow for now. set_direction_vapor = FlowDirection.forward set_direction_liquid = FlowDirection.backward # ========================================================================= """ Build Control volume 1D for vapor phase and populate vapor control volume""" self.vapor_phase = ControlVolume1DBlock(default={ "transformation_method": self.config.transformation_method, "transformation_scheme": self.config.vapor_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "area_definition": DistributedVars.variant, "property_package": self.config.vapor_side.property_package, "property_package_args": self.config.vapor_side.property_package_args}) self.vapor_phase.add_geometry( flow_direction=set_direction_vapor, length_domain_set=self.config.length_domain_set) self.vapor_phase.add_state_blocks( information_flow=set_direction_vapor, has_phase_equilibrium=False) self.vapor_phase.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_phase_equilibrium=False, has_mass_transfer=True) self.vapor_phase.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_transfer=True) self.vapor_phase.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=self.config.vapor_side.has_pressure_change) self.vapor_phase.apply_transformation() # ========================================================================== """ Build Control volume 1D for liquid phase and populate liquid control volume """ self.liquid_phase = ControlVolume1DBlock(default={ "transformation_method": self.config.transformation_method, "transformation_scheme": self.config.liquid_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "area_definition": DistributedVars.variant, "property_package": self.config.liquid_side.property_package, "property_package_args": self.config.liquid_side.property_package_args}) self.liquid_phase.add_geometry(flow_direction=set_direction_liquid, length_domain_set=self.config. length_domain_set) self.liquid_phase.add_state_blocks( information_flow=set_direction_liquid, has_phase_equilibrium=False) self.liquid_phase.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_phase_equilibrium=False, has_mass_transfer=True) self.liquid_phase.add_energy_balances( balance_type=EnergyBalanceType.enthalpyTotal, has_heat_transfer=True) self.liquid_phase.apply_transformation() # Add Ports for vapor side self.add_inlet_port(name="vapor_inlet", block=self.vapor_phase) self.add_outlet_port(name="vapor_outlet", block=self.vapor_phase) # Add Ports for liquid side self.add_inlet_port(name="liquid_inlet", block=self.liquid_phase) self.add_outlet_port(name="liquid_outlet", block=self.liquid_phase) # ========================================================================== """ Add performace equation method""" self._make_performance() def _make_performance(self): """ Constraints for unit model. Args: None Returns: None """ # ====================================================================== # Custom Sets vap_comp = self.config.vapor_side.property_package.component_list liq_comp = self.config.liquid_side.property_package.component_list equilibrium_comp = vap_comp & liq_comp solvent_comp_list = \ self.config.liquid_side.property_package.solvent_set solute_comp_list = self.config.liquid_side.property_package.solute_set vapor_phase_list_ref = \ self.config.vapor_side.property_package.phase_list liquid_phase_list_ref = \ self.config.liquid_side.property_package.phase_list # Packing parameters self.eps_ref = Param(initialize=0.97,units=None, mutable=True, doc="Packing void space m3/m3") self.packing_specific_area = Param(initialize=250,units=pyunits.m**2 / pyunits.m**3, mutable=True, doc="Packing specific surface area (m2/m3)") self.packing_channel_size = Param(initialize=0.1,units=pyunits.m, mutable=True, doc="Packing channel size (m)") self.hydraulic_diameter = Expression(expr=4 * self.eps_ref / self.packing_specific_area, doc="Hydraulic diameter (m)") # Add the integer indices along vapor phase length domain self.zi = Param(self.vapor_phase.length_domain, mutable=True, doc='''Integer indexing parameter required for transfer across boundaries of a given volume element''') # Set the integer indices along vapor phase length domain for i, x in enumerate(self.vapor_phase.length_domain, 1): self.zi[x] = i # Unit Model Design Variables # Geometry self.diameter_column = Var(domain=Reals, initialize=0.1, units=pyunits.m, doc='Column diameter') self.area_column = Var(domain=Reals, initialize=0.5, units=pyunits.m**2, doc='Column cross-sectional area') self.length_column = Var(domain=Reals, initialize=4.9, units=pyunits.m, doc='Column length') # Hydrodynamics self.velocity_vap = Var(self.flowsheet().time, self.vapor_phase.length_domain, domain=NonNegativeReals, initialize=2, units=pyunits.m / pyunits.s, doc='Vapor superficial velocity') self.velocity_liq = Var(self.flowsheet().time, self.liquid_phase.length_domain, domain=NonNegativeReals, initialize=0.01, units=pyunits.m / pyunits.s, doc='Liquid superficial velocity') self.holdup_liq = Var(self.flowsheet().time, self.liquid_phase.length_domain, initialize=0.001, doc='Volumetric liquid holdup [-]') def rule_holdup_vap(blk, t, x): return blk.eps_ref - blk.holdup_liq[t, x] self.holdup_vap = Expression(self.flowsheet().time, self.vapor_phase.length_domain, rule=rule_holdup_vap, doc='Volumetric vapor holdup [-]') # Define gas velocity at flooding point (m/s) self.gas_velocity_flood = Var(self.flowsheet().time, self.vapor_phase.length_domain, initialize=1, doc='Gas velocity at flooding point') # Flooding fraction def rule_flood_fraction(blk, t, x): return blk.velocity_vap[t, x]/blk.gas_velocity_flood[t, x] self.flood_fraction = Expression(self.flowsheet().time, self.vapor_phase.length_domain, rule=rule_flood_fraction, doc='Flooding fraction (expected to be below 0.8)') # Mass and heat transfer terms # Mass transfer terms self.pressure_equil = Var( self.flowsheet().time, self.vapor_phase.length_domain, equilibrium_comp, domain=NonNegativeReals, initialize=500, units=pyunits.Pa, doc='Equilibruim pressure of diffusing components at interface') self.interphase_mass_transfer = Var( self.flowsheet().time, self.liquid_phase.length_domain, equilibrium_comp, domain=Reals, initialize=0.1, units=pyunits.mol / (pyunits.s * pyunits.m), doc='Rate at which moles of diffusing species transfered into liquid') self.enhancement_factor = Var(self.flowsheet().time, self.liquid_phase.length_domain, units=None, initialize=160, doc='Enhancement factor') # Heat transfer terms self.heat_flux_vap = Var(self.flowsheet().time, self.vapor_phase.length_domain, domain=Reals, initialize=0.0, units=pyunits.J / (pyunits.s * (pyunits.m**3)), doc='Volumetric heat flux in vapor phase') # ===================================================================== # Add performance equations # Inter-facial Area model ([m2/m3]): self.area_interfacial = Var(self.flowsheet().time, self.vapor_phase.length_domain, initialize=0.9, doc='Specific inter-facial area') # --------------------------------------------------------------------- # Geometry constraints # Column area [m2] @self.Constraint(doc="Column cross-sectional area") def column_cross_section_area(blk): return blk.area_column == ( CONST.pi * 0.25 * (blk.diameter_column)**2) # Area of control volume : vapor side and liquid side control_volume_area_definition = ''' column_area * phase_holdup. The void fraction of the vapor phase (volumetric vapor holdup) and that of the liquid phase(volumetric liquid holdup) are lumped into the definition of the cross-sectional area of the vapor-side and liquid-side control volume respectively. Hence, the cross-sectional area of the control volume changes with time and space. ''' if self.config.dynamic: @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, doc=control_volume_area_definition) def vapor_side_area(bk, t, x): return bk.vapor_phase.area[t, x] == ( bk.area_column * bk.holdup_vap[t, x]) @self.Constraint(self.flowsheet().time, self.liquid_phase.length_domain, doc=control_volume_area_definition) def liquid_side_area(bk, t, x): return bk.liquid_phase.area[t, x] == ( bk.area_column * bk.holdup_liq[t, x]) else: self.vapor_phase.area.fix(value(self.area_column)) self.liquid_phase.area.fix(value(self.area_column)) # Pressure consistency in phases @self.Constraint(self.flowsheet().time, self.liquid_phase.length_domain, doc='''Mechanical equilibruim: vapor-side pressure equal liquid -side pressure''') def mechanical_equil(bk, t, x): return bk.liquid_phase.properties[t, x].pressure == \ bk.vapor_phase.properties[t, x].pressure # Length of control volume : vapor side and liquid side @self.Constraint(doc="Vapor side length") def vapor_side_length(blk): return blk.vapor_phase.length == blk.length_column @self.Constraint(doc="Liquid side length") def liquid_side_length(blk): return blk.liquid_phase.length == blk.length_column # --------------------------------------------------------------------- # Hydrodynamic constraints # Vapor superficial velocity @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, doc="Vapor superficial velocity") def eq_velocity_vap(blk, t, x): return blk.velocity_vap[t, x] * blk.area_column * \ blk.vapor_phase.properties[t, x].dens_mol == \ blk.vapor_phase.properties[t, x].flow_mol # Liquid superficial velocity @self.Constraint(self.flowsheet().time, self.liquid_phase.length_domain, doc="Liquid superficial velocity") def eq_velocity_liq(blk, t, x): return blk.velocity_liq[t, x] * blk.area_column * \ blk.liquid_phase.properties[t, x].dens_mol == \ blk.liquid_phase.properties[t, x].flow_mol # --------------------------------------------------------------------- # Mass transfer coefficients # Mass transfer coefficients of diffusing components in vapor phase [mol/m2.s.Pa] self.k_v = Var(self.flowsheet().time, self.vapor_phase.length_domain, equilibrium_comp, doc=' Vapor phase mass transfer coefficient') # Mass transfer coefficients of diffusing components in liquid phase [m/s] self.k_l = Var(self.flowsheet().time, self.liquid_phase.length_domain, equilibrium_comp, doc='Liquid phase mass transfer coefficient') # Intermediate term def rule_phi(blk, t, x, j): if x == self.vapor_phase.length_domain.first(): return Expression.Skip else: zb = self.vapor_phase.length_domain.at(self.zi[x].value - 1) return (blk.enhancement_factor[t, zb] * blk.k_l[t, zb, j] / blk.k_v[t, x, j]) self.phi = Expression( self.flowsheet().time, self.vapor_phase.length_domain, solute_comp_list, rule=rule_phi, doc='Equilibruim partial pressure intermediate term for solute') # Equilibruim partial pressure of diffusing components at interface @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, equilibrium_comp, doc='''Equilibruim partial pressure of diffusing components at interface''') def pressure_at_interface(blk, t, x, j): if x == self.vapor_phase.length_domain.first(): return blk.pressure_equil[t, x, j] == 0.0 else: zb = self.vapor_phase.length_domain.at(self.zi[x].value - 1) lprops = blk.liquid_phase.properties[t, zb] henrycomp = lprops.params.get_component(j).config.henry_component if henrycomp is not None and "Liq" in henrycomp: return blk.pressure_equil[t, x, j] == ( (blk.vapor_phase.properties[t, x].mole_frac_comp[j] * blk.vapor_phase.properties[ t, x].pressure + blk.phi[t, x, j] * lprops.conc_mol_phase_comp_true['Liq',j]) / (1 + blk.phi[t, x, j] / blk.liquid_phase.properties[t, zb].henry['Liq',j])) else: return blk.pressure_equil[t, x, j] == ( lprops.vol_mol_phase['Liq'] * lprops.conc_mol_phase_comp_true['Liq',j] * lprops.pressure_sat_comp[j]) # Mass transfer of diffusing components in vapor phase def rule_mass_transfer(blk, t, x, j): if x == self.vapor_phase.length_domain.first(): return blk.interphase_mass_transfer[t, x, j] == 0.0 else: return blk.interphase_mass_transfer[t, x, j] == ( blk.k_v[t, x, j] * blk.area_interfacial[t, x] * blk.area_column * (blk.vapor_phase.properties[t, x].mole_frac_comp[j] * blk.vapor_phase.properties[t, x].pressure - blk.pressure_equil[t, x, j])) self.mass_transfer_vapor = Constraint(self.flowsheet().time, self.vapor_phase.length_domain, equilibrium_comp, rule=rule_mass_transfer, doc="mass transfer in vapor phase") # Liquid phase mass transfer handle @self.Constraint(self.flowsheet().time, self.liquid_phase.length_domain, self.liquid_phase.properties.phase_component_set, doc="mass transfer to liquid") def liquid_phase_mass_transfer_handle(blk, t, x, p, j): if x == self.liquid_phase.length_domain.last(): return blk.liquid_phase.mass_transfer_term[t, x, p, j] == 0.0 else: zf = self.liquid_phase.length_domain.at(self.zi[x].value + 1) if j in equilibrium_comp: return blk.liquid_phase.mass_transfer_term[t, x, p, j] == \ blk.interphase_mass_transfer[t, zf, j] else: return blk.liquid_phase.mass_transfer_term[t, x, p, j] == \ 0.0 # Vapor phase mass transfer handle @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, self.vapor_phase.properties.phase_component_set, doc="mass transfer from vapor") def vapor_phase_mass_transfer_handle(blk, t, x, p, j): if x == self.vapor_phase.length_domain.first(): return blk.vapor_phase.mass_transfer_term[t, x, p, j] == 0.0 else: if j in equilibrium_comp: return blk.vapor_phase.mass_transfer_term[t, x, p, j] == \ -blk.interphase_mass_transfer[t, x, j] else: return blk.vapor_phase.mass_transfer_term[t, x, p, j] == \ 0.0 # Heat transfer coefficients # Vapor-liquid heat transfer coefficient [J/m2.s.K] self.h_v = Var(self.flowsheet().time, self.vapor_phase.length_domain, initialize=100, doc='''Vapor-liquid heat transfer coefficient''') # Vapor-liquid heat transfer coeff modified by Ackmann factor [J/m.s.K] def rule_heat_transfer_coeff_Ack(blk, t, x): if x == self.vapor_phase.length_domain.first(): return Expression.Skip else: Ackmann_factor =\ sum(blk.vapor_phase.properties[t, x].cp_mol_phase_comp['Vap',j] * blk.interphase_mass_transfer[t, x, j] for j in equilibrium_comp) return Ackmann_factor /\ (1 - exp(-Ackmann_factor / (blk.h_v[t, x] * blk.area_interfacial[t, x] * blk.area_column))) self.h_v_Ack = Expression( self.flowsheet().time, self.vapor_phase.length_domain, rule=rule_heat_transfer_coeff_Ack, doc='Vap-Liq heat transfer coefficient corrected by Ackmann factor') # Heat flux - vapor side [J/s.m] @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, doc="heat transfer - vapor side ") def vapor_phase_volumetric_heat_flux(blk, t, x): if x == self.vapor_phase.length_domain.first(): return blk.heat_flux_vap[t, x] == 0 else: zb = self.vapor_phase.length_domain.at(value(self.zi[x]) - 1) return blk.heat_flux_vap[t, x] == blk.h_v_Ack[t, x] * \ (blk.liquid_phase.properties[t, zb].temperature - blk.vapor_phase.properties[t, x].temperature) # Heat transfer - vapor side [J/s.m] @self.Constraint(self.flowsheet().time, self.vapor_phase.length_domain, doc="heat transfer - vapor side ") def vapor_phase_heat_transfer(blk, t, x): if x == self.vapor_phase.length_domain.first(): return blk.vapor_phase.heat[t, x] == 0 else: zb = self.vapor_phase.length_domain.at(value(self.zi[x]) - 1) return blk.vapor_phase.heat[t, x] == -blk.heat_flux_vap[t, x] - \ (sum(blk.vapor_phase.properties[t, x].enth_mol_phase_comp['Vap',j] * blk.vapor_phase.mass_transfer_term[t, x, 'Vap', j] for j in solute_comp_list)) + \ (sum(blk.liquid_phase.properties[t, zb].enth_mol_phase_comp['Liq',j] * blk.liquid_phase.mass_transfer_term[t, zb, 'Liq', j] for j in solvent_comp_list)) # Heat transfer - liquid side [J/s.m] @self.Constraint(self.flowsheet().time, self.liquid_phase.length_domain, doc="heat transfer - liquid side ") def liquid_phase_heat_transfer(blk, t, x): if x == self.liquid_phase.length_domain.last(): return blk.liquid_phase.heat[t, x] == 0 else: zf = self.vapor_phase.length_domain.at(value(self.zi[x]) + 1) return blk.liquid_phase.heat[t, x] == -blk.vapor_phase.heat[t, zf] # ========================================================================= # Model initialization routine def initialize(blk, vapor_phase_state_args=None, liquid_phase_state_args=None, state_vars_fixed=False, outlvl=idaeslog.NOTSET, solver=None, optarg=None): """ Column initialization. Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = None). optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use IDAES default solver) """ # Set up logger for initialization and solve init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Set solver options opt = get_solver(solver, optarg) dynamic_constraints = [ "pressure_at_interface", "mass_transfer_vapor", "liquid_phase_mass_transfer_handle", "vapor_phase_mass_transfer_handle", "vapor_phase_volumetric_heat_flux", "vapor_phase_heat_transfer", "liquid_phase_heat_transfer"] # --------------------------------------------------------------------- # Deactivate unit model level constraints (asides geometry constraints) for c in blk.component_objects(Constraint, descend_into=True): if c.local_name in dynamic_constraints: c.deactivate() # Fix variables # Interface pressure blk.pressure_equil.fix() # Molar flux blk.interphase_mass_transfer.fix(0.0) blk.vapor_phase.mass_transfer_term.fix(0.0) blk.liquid_phase.mass_transfer_term.fix(0.0) # Heat transfer rate blk.heat_flux_vap.fix(0.0) blk.vapor_phase.heat.fix(0.0) blk.liquid_phase.heat.fix(0.0) # # --------------------------------------------------------------------- # Provide state arguments for property package initialization init_log.info("Step 1: Property Package initialization") vap_comp = blk.config.vapor_side.property_package.component_list liq_apparent_comp = [c[1] for c in blk.liquid_phase.properties.phase_component_set] if vapor_phase_state_args is None: vapor_phase_state_args = { 'flow_mol': blk.vapor_inlet.flow_mol[0].value, 'temperature': blk.vapor_inlet.temperature[0].value, 'pressure': blk.vapor_inlet.pressure[0].value, 'mole_frac_comp': {j: blk.vapor_inlet.mole_frac_comp[0, j].value for j in vap_comp}} if liquid_phase_state_args is None: liquid_phase_state_args = { 'flow_mol': blk.liquid_inlet.flow_mol[0].value, 'temperature': blk.liquid_inlet.temperature[0].value, 'pressure': blk.vapor_inlet.pressure[0].value, 'mole_frac_comp': {j: blk.liquid_inlet.mole_frac_comp[0, j].value for j in liq_apparent_comp}} # Initialize vapor_phase properties block vflag = blk.vapor_phase.properties.initialize( state_args=vapor_phase_state_args, state_vars_fixed=False, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) # Initialize liquid_phase properties block lflag = blk.liquid_phase.properties.initialize( state_args=liquid_phase_state_args, state_vars_fixed=False, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) init_log.info("Step 2: Steady-State isothermal mass balance") blk.vapor_phase.properties.release_state(flags=vflag) blk.liquid_phase.properties.release_state(flags=lflag) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Step 2: {}.".format(idaeslog.condition(res))) assert res.solver.termination_condition == \ TerminationCondition.optimal assert res.solver.status == SolverStatus.ok # --------------------------------------------------------------------- init_log.info('Step 3: Interface equilibrium') # Activate interface pressure constraint blk.pressure_equil.unfix() blk.pressure_at_interface.activate() # ---------------------------------------------------------------------- with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Step 3 complete: {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- init_log.info('Step 4: Isothermal chemical absoption') init_log.info_high("No mass transfer to mass transfer") # Unfix mass transfer terms blk.interphase_mass_transfer.unfix() # Activate mass transfer equation in vapor phase blk.mass_transfer_vapor.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) blk.vapor_phase.mass_transfer_term.unfix() blk.liquid_phase.mass_transfer_term.unfix() blk.vapor_phase_mass_transfer_handle.activate() blk.liquid_phase_mass_transfer_handle.activate() optarg = { "tol": 1e-8, "max_iter": 150, "bound_push":1e-8} opt.options = optarg with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) if res.solver.status != SolverStatus.warning: print('') init_log.info_high( "Step 4 complete: {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- init_log.info('Step 5: Adiabatic chemical absoption') init_log.info_high("Isothermal to Adiabatic ") # Unfix heat transfer terms blk.heat_flux_vap.unfix() blk.vapor_phase.heat.unfix() blk.liquid_phase.heat.unfix() # Activate heat transfer and steady-state energy balance related equations for c in ["vapor_phase_volumetric_heat_flux", "vapor_phase_heat_transfer", "liquid_phase_heat_transfer"]: getattr(blk, c).activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Step 5 complete: {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- if not blk.config.dynamic: init_log.info('Steady-state initialization complete') def fix_initial_condition(blk): """ Initial condition for material and enthalpy balance. Mass balance : Initial condition is determined by fixing n-1 mole fraction and the total molar flowrate Energy balance :Initial condition is determined by fixing the temperature. """ vap_comp = blk.config.vapor_side.property_package.component_list liq_comp = blk.config.liquid_side.property_package.component_list solute_comp_list = blk.config.liquid_side.property_package.solute_set for x in blk.vapor_phase.length_domain: if x != 0: blk.vapor_phase.properties[0, x].temperature.fix() blk.vapor_phase.properties[0, x].flow_mol.fix() for j in vap_comp: if (x != 0 and j not in solute_comp_list): blk.vapor_phase.properties[0, x].mole_frac_comp[j].fix() for x in blk.liquid_phase.length_domain: if x != 1: blk.liquid_phase.properties[0, x].temperature.fix() blk.liquid_phase.properties[0, x].flow_mol.fix() for j in liq_comp: if (x != 1 and j not in solute_comp_list): blk.liquid_phase.properties[0, x].mole_frac_comp[j].fix() def unfix_initial_condition(blk): """ Function to unfix initial condition for material and enthalpy balance. """ vap_comp = blk.config.vapor_side.property_package.component_list liq_comp = blk.config.liquid_side.property_package.component_list solute_comp_list = blk.config.liquid_side.property_package.solute_set for x in blk.vapor_phase.length_domain: if x != 0: blk.vapor_phase.properties[0, x].temperature.unfix() blk.vapor_phase.properties[0, x].flow_mol.unfix() for j in vap_comp: if (x != 0 and j not in solute_comp_list): blk.vapor_phase.properties[0, x].mole_frac_comp[j].unfix() for x in blk.liquid_phase.length_domain: if x != 1: blk.liquid_phase.properties[0, x].temperature.unfix() blk.liquid_phase.properties[0, x].flow_mol.unfix() for j in liq_comp: if (x != 1 and j not in solute_comp_list): blk.liquid_phase.properties[0, x].mole_frac_comp[j].unfix() def make_steady_state_column_profile(blk): """ Steady-state Plot function for Temperature and Solute Pressure profile. """ normalised_column_height = [x for x in blk.vapor_phase.length_domain] simulation_time = [t for t in blk.flowsheet().time] # final time tf = simulation_time[-1] # solute list solute_comp_list = blk.config.liquid_side.property_package.solute_set solute_profile = [] liquid_temperature_profile = [] solute_comp_profile = [] # APPEND RESULTS for j in solute_comp_list: for x in blk.vapor_phase.length_domain: x_liq = blk.liquid_phase.length_domain.at(blk.zi[x].value) solute_comp_profile.append( value(1e-3 * blk.vapor_phase.properties[tf, x].pressure * blk.vapor_phase.properties[tf, x].mole_frac_comp[j])) liquid_temperature_profile.append( value(blk.liquid_phase.properties[tf, x_liq].temperature)) solute_profile.append(solute_comp_profile) # plot properties fontsize = 18 labelsize = 18 fig = plt.figure(figsize=(9, 7)) ax1 = fig.add_subplot(111) ax1.set_title('Steady-state column profile', fontsize=16, fontweight='bold') # plot primary axis lab1 = ax1.plot(normalised_column_height, solute_profile[0], linestyle='--', mec="b", mfc="None", color='b', label='solute partial pressure [kPa]', marker='o') ax1.tick_params(axis='y', labelcolor='b', direction='in', labelsize=labelsize) ax1.tick_params(axis='x', direction='in', labelsize=labelsize) ax1.set_xlabel('Normalise column height from bottom', fontsize=fontsize) ax1.set_ylabel('P_solute [ kPa]', color='b', fontweight='bold', fontsize=fontsize) # plot secondary axis ax2 = ax1.twinx() lab2 = ax2.plot(normalised_column_height, liquid_temperature_profile, color='g', linestyle='-', label='Liquid temperature profile', marker='s') ax2.set_ylabel('T$_{liq}$ [ K ] ', color='g', fontweight='bold', fontsize=fontsize) ax2.tick_params(axis='y', labelcolor='g', direction='in', labelsize=labelsize) # get the labels lab_1 = lab1 + lab2 labels_1 = [lb.get_label() for lb in lab_1] ax1.legend(lab_1, labels_1, loc='lower center', fontsize=fontsize) fig.tight_layout() # show graph plt.show() def make_dynamic_column_profile(blk): """ Dynamic Plot function for Temperature and Solute Pressure profile. """ normalised_column_height = [x for x in blk.vapor_phase.length_domain] simulation_time = [t for t in blk.flowsheet().time] fluegas_flow = [value(blk.vapor_inlet.flow_mol[t]) for t in blk.flowsheet().time] # final time tf = simulation_time[-1] nf = len(simulation_time) # mid-time if nf % 2 == 0: tm = int(nf / 2) else: tm = int(nf / 2 + 1) solute_comp_list = blk.config.liquid_side.property_package.solute_set solute_profile_mid = [] solute_profile_fin = [] liquid_temperature_profile_mid = [] liquid_temperature_profile_fin = [] solute_comp_profile_mid = [] solute_comp_profile_fin = [] # APPEND RESULTS for j in solute_comp_list: for x in blk.vapor_phase.length_domain: x_liq = blk.liquid_phase.length_domain.at(blk.zi[x].value) solute_comp_profile_mid.append( value(1e-3 * blk.vapor_phase.properties[tm, x].pressure * blk.vapor_phase.properties[tm, x].mole_frac_comp[j])) solute_comp_profile_fin.append( value(1e-3 * blk.vapor_phase.properties[tf, x].pressure * blk.vapor_phase.properties[tf, x].mole_frac_comp[j])) liquid_temperature_profile_mid.append( value(blk.liquid_phase.properties[tm, x_liq].temperature)) liquid_temperature_profile_fin.append( value(blk.liquid_phase.properties[tf, x_liq].temperature)) solute_profile_mid.append(solute_comp_profile_mid) solute_profile_fin.append(solute_comp_profile_fin) # plot properties fontsize = 18 labelsize = 18 fig = plt.figure(figsize=(12, 7)) ax1 = fig.add_subplot(211) ax1.set_title( 'Column profile @ {0:6.2f} & {1:6.2f} sec'.format(tm, tf), fontsize=16, fontweight='bold') # plot primary axis lab1 = ax1.plot(normalised_column_height, solute_profile_mid[0], linestyle='--', color='b', label='Solute partial pressure [kPa] @ %d' % tm) lab2 = ax1.plot(normalised_column_height, solute_profile_fin[0], linestyle='-', color='b', label='Solute partial pressure [kPa] @ %d' % tf) ax1.tick_params(axis='y', labelcolor='b', direction='in', labelsize=labelsize) ax1.tick_params(axis='x', direction='in', labelsize=labelsize) ax1.set_xlabel('Normalise column height from bottom', fontsize=fontsize) ax1.set_ylabel('P_solute [ kPa]', color='b', fontweight='bold', fontsize=fontsize) # plot secondary axis ax2 = ax1.twinx() lab3 = ax2.plot( normalised_column_height, liquid_temperature_profile_mid, color='g', linestyle='--', label='Liquid temperature profile @ {0:6.1f}'.format(tm)) lab4 = ax2.plot( normalised_column_height, liquid_temperature_profile_fin, color='g', linestyle='-', label='Liquid temperature profile @ {0:6.1f}'.format(tf)) ax2.set_ylabel('T$_{liq}$ [ K ] ', color='g', fontweight='bold', fontsize=fontsize) ax2.tick_params(axis='y', labelcolor='g', direction='in', labelsize=labelsize) # get the labels lab_1 = lab1 + lab2 + lab3 + lab4 labels_1 = [lb.get_label() for lb in lab_1] ax1.legend(lab_1, labels_1, fontsize=fontsize) # plot flowgas flow ax3 = fig.add_subplot(212) ax3.plot(simulation_time, fluegas_flow, linestyle='--', mec="g", mfc="None", color='g', label='Fluegas flow [mol/s]', marker='o') ax3.tick_params(labelsize=labelsize) ax3.set_xlabel('Simulation time (sec)', fontsize=fontsize) ax3.set_ylabel(' Fv [ mol/s]', color='b', fontweight='bold', fontsize=fontsize) ax3.legend(['Fluegas flow [mol/s]'], fontsize=fontsize) fig.tight_layout() plt.show()
class HeatExchangerData(UnitModelBlockData): """ Simple 0D heat exchange unit. Unit model to transfer heat from one material to another. """ CONFIG = UnitModelBlockData.CONFIG(implicit=True) _make_heat_exchanger_config(CONFIG) def _process_config(self): """Check for configuration errors and alternate config option names. """ config = self.config if config.hot_side_name == config.cold_side_name: raise NameError( "Heatexchanger hot and cold side cannot have the same name '{}'." " Be sure to set both the hot_side_name and cold_side_name.". format(config.hot_side_name)) for o in config: if not (o in self.CONFIG or o in [config.hot_side_name, config.cold_side_name]): raise KeyError( "Heatexchanger config option {} not defined".format(o)) if config.hot_side_name in config: config.hot_side_config.set_value(config[config.hot_side_name]) # Allow access to hot_side_config under the hot_side_name, backward # compatible with the tube and shell notation setattr(config, config.hot_side_name, config.hot_side_config) if config.cold_side_name in config: config.cold_side_config.set_value(config[config.cold_side_name]) # Allow access to hot_side_config under the cold_side_name, backward # compatible with the tube and shell notation setattr(config, config.cold_side_name, config.cold_side_config) if config.cold_side_name in ["hot_side", "side_1"]: raise ConfigurationError( "Cold side name cannot be in ['hot_side', 'side_1'].") if config.hot_side_name in ["cold_side", "side_2"]: raise ConfigurationError( "Hot side name cannot be in ['cold_side', 'side_2'].") def build(self): """ Building model Args: None Returns: None """ ######################################################################## # Call UnitModel.build to setup dynamics and configure # ######################################################################## super().build() self._process_config() config = self.config ######################################################################## # Add control volumes # ######################################################################## hot_side = _make_heater_control_volume( self, config.hot_side_name, config.hot_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) cold_side = _make_heater_control_volume( self, config.cold_side_name, config.cold_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) # Add references to the hot side and cold side, so that we have solid # names to refer to internally. side_1 and side_2 also maintain # compatability with older models. Using add_object_reference keeps # these from showing up when you iterate through pyomo compoents in a # model, so only the user specified control volume names are "seen" if not hasattr(self, "side_1"): add_object_reference(self, "side_1", hot_side) if not hasattr(self, "side_2"): add_object_reference(self, "side_2", cold_side) if not hasattr(self, "hot_side"): add_object_reference(self, "hot_side", hot_side) if not hasattr(self, "cold_side"): add_object_reference(self, "cold_side", cold_side) ######################################################################## # Add variables # ######################################################################## # Use hot side units as basis s1_metadata = config.hot_side_config.property_package.get_metadata() q_units = s1_metadata.get_derived_units("power") u_units = s1_metadata.get_derived_units("heat_transfer_coefficient") a_units = s1_metadata.get_derived_units("area") temp_units = s1_metadata.get_derived_units("temperature") u = self.overall_heat_transfer_coefficient = Var( self.flowsheet().config.time, domain=PositiveReals, initialize=100.0, doc="Overall heat transfer coefficient", units=u_units) a = self.area = Var(domain=PositiveReals, initialize=1000.0, doc="Heat exchange area", units=a_units) self.delta_temperature_in = Var( self.flowsheet().config.time, initialize=10.0, doc="Temperature difference at the hot inlet end", units=temp_units) self.delta_temperature_out = Var( self.flowsheet().config.time, initialize=10.1, doc="Temperature difference at the hot outlet end", units=temp_units) if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: self.crossflow_factor = Var( self.flowsheet().config.time, initialize=1.0, doc="Factor to adjust coutercurrent flow heat " "transfer calculation for cross flow.", ) f = self.crossflow_factor self.heat_duty = Reference(cold_side.heat) ######################################################################## # Add ports # ######################################################################## i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet", block=hot_side, doc="Hot side inlet") i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet", block=cold_side, doc="Cold side inlet") o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet", block=hot_side, doc="Hot side outlet") o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet", block=cold_side, doc="Cold side outlet") # Using Andrew's function for now. I want these port names for backward # compatablity, but I don't want them to appear if you iterate throught # components and add_object_reference hides them from Pyomo. if not hasattr(self, "inlet_1"): add_object_reference(self, "inlet_1", i1) if not hasattr(self, "inlet_2"): add_object_reference(self, "inlet_2", i2) if not hasattr(self, "outlet_1"): add_object_reference(self, "outlet_1", o1) if not hasattr(self, "outlet_2"): add_object_reference(self, "outlet_2", o2) if not hasattr(self, "hot_inlet"): add_object_reference(self, "hot_inlet", i1) if not hasattr(self, "cold_inlet"): add_object_reference(self, "cold_inlet", i2) if not hasattr(self, "hot_outlet"): add_object_reference(self, "hot_outlet", o1) if not hasattr(self, "cold_outlet"): add_object_reference(self, "cold_outlet", o2) ######################################################################## # Add end temperature differnece constraints # ######################################################################## @self.Constraint(self.flowsheet().config.time) def delta_temperature_in_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return (b.delta_temperature_in[t] == hot_side.properties_in[t].temperature - pyunits.convert(cold_side.properties_in[t].temperature, to_units=temp_units)) else: return ( b.delta_temperature_in[t] == hot_side.properties_in[t].temperature - pyunits.convert(cold_side.properties_out[t].temperature, to_units=temp_units)) @self.Constraint(self.flowsheet().config.time) def delta_temperature_out_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return ( b.delta_temperature_out[t] == hot_side.properties_out[t].temperature - pyunits.convert(cold_side.properties_out[t].temperature, to_units=temp_units)) else: return (b.delta_temperature_out[t] == hot_side.properties_out[t].temperature - pyunits.convert(cold_side.properties_in[t].temperature, to_units=temp_units)) ######################################################################## # Add a unit level energy balance # ######################################################################## @self.Constraint(self.flowsheet().config.time) def unit_heat_balance(b, t): return 0 == (hot_side.heat[t] + pyunits.convert(cold_side.heat[t], to_units=q_units)) ######################################################################## # Add delta T calculations using callack function, lots of options, # # and users can provide their own if needed # ######################################################################## config.delta_temperature_callback(self) ######################################################################## # Add Heat transfer equation # ######################################################################## deltaT = self.delta_temperature @self.Constraint(self.flowsheet().config.time) def heat_transfer_equation(b, t): if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: return pyunits.convert(self.heat_duty[t], to_units=q_units) == (f[t] * u[t] * a * deltaT[t]) else: return pyunits.convert(self.heat_duty[t], to_units=q_units) == (u[t] * a * deltaT[t]) ######################################################################## # Add symbols for LaTeX equation rendering # ######################################################################## self.overall_heat_transfer_coefficient.latex_symbol = "U" self.area.latex_symbol = "A" hot_side.heat.latex_symbol = "Q_1" cold_side.heat.latex_symbol = "Q_2" self.delta_temperature.latex_symbol = "\\Delta T" def initialize( self, state_args_1=None, state_args_2=None, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6}, duty=None, ): """ Heat exchanger initialization method. Args: state_args_1 : a dict of arguments to be passed to the property initialization for the hot side (see documentation of the specific property package) (default = {}). state_args_2 : a dict of arguments to be passed to the property initialization for the cold side (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating which solver to use during initialization (default = 'ipopt') duty : an initial guess for the amount of heat transfered. This should be a tuple in the form (value, units), (default = (1000 J/s)) Returns: None """ # Set solver options init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") hot_side = getattr(self, self.config.hot_side_name) cold_side = getattr(self, self.config.cold_side_name) opt = SolverFactory(solver) opt.options = optarg flags1 = hot_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1) init_log.info_high("Initialization Step 1a (hot side) Complete.") flags2 = cold_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2) init_log.info_high("Initialization Step 1b (cold side) Complete.") # --------------------------------------------------------------------- # Solve unit without heat transfer equation # if costing block exists, deactivate if hasattr(self, "costing"): self.costing.deactivate() self.heat_transfer_equation.deactivate() # Get side 1 and side 2 heat units, and convert duty as needed s1_units = hot_side.heat.get_units() s2_units = cold_side.heat.get_units() if duty is None: # Assume 1000 J/s and check for unitless properties if s1_units is None and s2_units is None: # Backwards compatability for unitless properties s1_duty = -1000 s2_duty = 1000 else: s1_duty = pyunits.convert_value(-1000, from_units=pyunits.W, to_units=s1_units) s2_duty = pyunits.convert_value(1000, from_units=pyunits.W, to_units=s2_units) else: # Duty provided with explicit units s1_duty = -pyunits.convert_value( duty[0], from_units=duty[1], to_units=s1_units) s2_duty = pyunits.convert_value(duty[0], from_units=duty[1], to_units=s2_units) cold_side.heat.fix(s2_duty) for i in hot_side.heat: hot_side.heat[i].value = s1_duty with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) cold_side.heat.unfix() self.heat_transfer_equation.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state hot_side.release_state(flags1, outlvl=outlvl) cold_side.release_state(flags2, outlvl=outlvl) init_log.info("Initialization Completed, {}".format( idaeslog.condition(res))) # if costing block exists, activate and initialize if hasattr(self, "costing"): self.costing.activate() costing.initialize(self.costing) def _get_performance_contents(self, time_point=0): var_dict = { "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] } var_dict["HX Area"] = self.area var_dict["Heat Duty"] = self.heat_duty[time_point] if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} expr_dict = {} expr_dict["Delta T Driving"] = self.delta_temperature[time_point] expr_dict["Delta T In"] = self.delta_temperature_in[time_point] expr_dict["Delta T Out"] = self.delta_temperature_out[time_point] return {"vars": var_dict, "exprs": expr_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Hot Inlet": self.inlet_1, "Hot Outlet": self.outlet_1, "Cold Inlet": self.inlet_2, "Cold Outlet": self.outlet_2, }, time_point=time_point, ) def get_costing(self, module=costing, year=None, **kwargs): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year) self.costing = Block() module.hx_costing(self.costing, **kwargs) def calculate_scaling_factors(self): super().calculate_scaling_factors() # We have a pretty good idea that the delta Ts will be between about # 1 and 100 regardless of process of temperature units, so a default # should be fine, so don't warn. Guessing a typical delta t around 10 # the default scaling factor is set to 0.1 sf_dT1 = dict( zip(self.delta_temperature_in.keys(), [ iscale.get_scaling_factor(v, default=0.1) for v in self.delta_temperature_in.values() ])) sf_dT2 = dict( zip(self.delta_temperature_out.keys(), [ iscale.get_scaling_factor(v, default=0.1) for v in self.delta_temperature_out.values() ])) # U depends a lot on the process and units of measure so user should set # this one. sf_u = dict( zip(self.overall_heat_transfer_coefficient.keys(), [ iscale.get_scaling_factor(v, default=1, warning=True) for v in self.overall_heat_transfer_coefficient.values() ])) # Since this depends on the process size this is another scaling factor # the user should always set. sf_a = iscale.get_scaling_factor(self.area, default=1, warning=True) for t, c in self.heat_transfer_equation.items(): iscale.constraint_scaling_transform(c, sf_dT1[t] * sf_u[t] * sf_a) for t, c in self.unit_heat_balance.items(): iscale.constraint_scaling_transform(c, sf_dT1[t] * sf_u[t] * sf_a) for t, c in self.delta_temperature_in_equation.items(): iscale.constraint_scaling_transform(c, sf_dT1[t]) for t, c in self.delta_temperature_out_equation.items(): iscale.constraint_scaling_transform(c, sf_dT2[t]) if hasattr(self, "costing"): # import costing scaling factors costing.calculate_scaling_factors(self.costing)
class FWH0DData(UnitModelBlockData): CONFIG = UnitModelBlockData.CONFIG() _define_feedwater_heater_0D_config(CONFIG) 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 initialize(self, *args, **kwargs): outlvl = kwargs.get("outlvl", idaeslog.NOTSET) init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") config = self.config # shorter ref to config for less line splitting sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # the initialization here isn't straight forward since the heat exchanger # may have 3 stages and they are countercurrent. For simplicity each # stage in initialized with the same cooling water inlet conditions then # the whole feedwater heater is solved together. There are more robust # approaches which can be implimented if the need arises. # initialize desuperheat if include if config.has_desuperheat: if config.has_drain_cooling: _set_port(self.desuperheat.inlet_2, self.cooling.inlet_2) else: _set_port(self.desuperheat.inlet_2, self.condense.inlet_2) self.desuperheat.initialize(*args, **kwargs) self.desuperheat.inlet_1.flow_mol.unfix() if config.has_drain_mixer: _set_port(self.drain_mix.steam, self.desuperheat.outlet_1) else: _set_port(self.condense.inlet_1, self.desuperheat.outlet_1) # fix the steam and fwh inlet for init self.desuperheat.inlet_1.fix() self.desuperheat.inlet_1.flow_mol.unfix() # unfix for extract calc # initialize mixer if included if config.has_drain_mixer: self.drain_mix.steam.fix() self.drain_mix.drain.fix() self.drain_mix.outlet.unfix() self.drain_mix.initialize(*args, **kwargs) _set_port(self.condense.inlet_1, self.drain_mix.outlet) if config.has_desuperheat: self.drain_mix.steam.unfix() else: self.drain_mix.steam.flow_mol.unfix() # Initialize condense section if config.has_drain_cooling: _set_port(self.condense.inlet_2, self.cooling.inlet_2) self.cooling.inlet_2.fix() else: self.condense.inlet_2.fix() if not config.has_drain_mixer and not config.has_desuperheat: self.condense.inlet_1.fix() self.condense.inlet_1.flow_mol.unfix() tempsat = value(self.condense.shell.properties_in[0].temperature_sat) temp = value(self.condense.tube.properties_in[0].temperature) if tempsat - temp < 30: init_log.warning( "The steam sat. temperature ({}) is near the feedwater" " inlet temperature ({})".format(tempsat, temp)) self.condense.initialize(*args, **kwargs) # Initialize drain cooling if included if config.has_drain_cooling: _set_port(self.cooling.inlet_1, self.condense.outlet_1) self.cooling.initialize(*args, **kwargs) # Solve all together opt = SolverFactory(kwargs.get("solver", "ipopt")) opt.options = kwargs.get("oparg", {}) assert degrees_of_freedom(self) == 0 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info("Condensing shell inlet delta T = {}".format( value(self.condense.delta_temperature_in[0]))) init_log.info("Condensing Shell outlet delta T = {}".format( value(self.condense.delta_temperature_out[0]))) init_log.info("Steam Flow = {}".format( value(self.condense.inlet_1.flow_mol[0]))) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) from_json(self, sd=istate, wts=sp)
class HeatExchangerData(UnitModelBlockData): """ Simple 0D heat exchange unit. Unit model to transfer heat from one material to another. """ CONFIG = UnitModelBlockData.CONFIG(implicit=True) _make_heat_exchanger_config(CONFIG) def set_scaling_factor_energy(self, f): """ This function sets scaling_factor_energy for both side_1 and side_2. This factor multiplies the energy balance and heat transfer equations in the heat exchnager. The value of this factor should be about 1/(expected heat duty). Args: f: Energy balance scaling factor """ self.side_1.scaling_factor_energy.value = f self.side_2.scaling_factor_energy.value = f def _process_config(self): """Check for configuration errors and alternate config option names. """ config = self.config if config.hot_side_name == config.cold_side_name: raise NameError( "Heatexchanger hot and cold side cannot have the same name '{}'." " Be sure to set both the hot_side_name and cold_side_name.". format(config.hot_side_name)) for o in config: if not (o in self.CONFIG or o in [config.hot_side_name, config.cold_side_name]): raise KeyError( "Heatexchanger config option {} not defined".format(o)) if config.hot_side_name in config: config.hot_side_config.set_value(config[config.hot_side_name]) # Allow access to hot_side_config under the hot_side_name, backward # compatible with the tube and shell notation setattr(config, config.hot_side_name, config.hot_side_config) if config.cold_side_name in config: config.cold_side_config.set_value(config[config.cold_side_name]) # Allow access to hot_side_config under the cold_side_name, backward # compatible with the tube and shell notation setattr(config, config.cold_side_name, config.cold_side_config) def build(self): """ Building model Args: None Returns: None """ ######################################################################## # Call UnitModel.build to setup dynamics and configure # ######################################################################## super().build() self._process_config() config = self.config ######################################################################## # Add variables # ######################################################################## u = self.overall_heat_transfer_coefficient = Var( self.flowsheet().config.time, domain=PositiveReals, initialize=100.0, doc="Overall heat transfer coefficient", ) a = self.area = Var(domain=PositiveReals, initialize=1000.0, doc="Heat exchange area") self.delta_temperature_in = Var( self.flowsheet().config.time, initialize=10.0, doc="Temperature difference at the hot inlet end", ) self.delta_temperature_out = Var( self.flowsheet().config.time, initialize=10.1, doc="Temperature difference at the hot outlet end", ) if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: self.crossflow_factor = Var( self.flowsheet().config.time, initialize=1.0, doc="Factor to adjust coutercurrent flow heat " "transfer calculation for cross flow.", ) f = self.crossflow_factor ######################################################################## # Add control volumes # ######################################################################## _make_heater_control_volume( self, "side_1", config.hot_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) _make_heater_control_volume( self, "side_2", config.cold_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) # Add named references to side_1 and side_2, side 1 and 2 maintain # backward compatability and are names the user doesn't need to worry # about. The sign convention for duty is heat from side 1 to side 2 is # positive add_object_reference(self, config.hot_side_name, self.side_1) add_object_reference(self, config.cold_side_name, self.side_2) # Add convienient references to heat duty. q = self.heat_duty = Reference(self.side_2.heat) ######################################################################## # Add ports # ######################################################################## # Keep old port names, just for backward compatability self.add_inlet_port(name="inlet_1", block=self.side_1, doc="Hot side inlet") self.add_inlet_port(name="inlet_2", block=self.side_2, doc="Cold side inlet") self.add_outlet_port(name="outlet_1", block=self.side_1, doc="Hot side outlet") self.add_outlet_port(name="outlet_2", block=self.side_2, doc="Cold side outlet") # Using Andrew's function for now, I think Pyomo's refrence has trouble # with scalar (pyomo) components. add_object_reference(self, config.hot_side_name + "_inlet", self.inlet_1) add_object_reference(self, config.cold_side_name + "_inlet", self.inlet_2) add_object_reference(self, config.hot_side_name + "_outlet", self.outlet_1) add_object_reference(self, config.cold_side_name + "_outlet", self.outlet_2) ######################################################################## # Add end temperature differnece constraints # ######################################################################## @self.Constraint(self.flowsheet().config.time) def delta_temperature_in_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return (b.delta_temperature_in[t] == b.side_1.properties_in[t].temperature - b.side_2.properties_in[t].temperature) else: return (b.delta_temperature_in[t] == b.side_1.properties_in[t].temperature - b.side_2.properties_out[t].temperature) @self.Constraint(self.flowsheet().config.time) def delta_temperature_out_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return (b.delta_temperature_out[t] == b.side_1.properties_out[t].temperature - b.side_2.properties_out[t].temperature) else: return (b.delta_temperature_out[t] == b.side_1.properties_out[t].temperature - b.side_2.properties_in[t].temperature) ######################################################################## # Add a unit level energy balance # ######################################################################## @self.Constraint(self.flowsheet().config.time) def unit_heat_balance(b, t): return 0 == self.side_1.heat[t] + self.side_2.heat[t] ######################################################################## # Add delta T calculations using callack function, lots of options, # # and users can provide their own if needed # ######################################################################## config.delta_temperature_callback(self) ######################################################################## # Add Heat transfer equation # ######################################################################## deltaT = self.delta_temperature scale = self.side_1.scaling_factor_energy @self.Constraint(self.flowsheet().config.time) def heat_transfer_equation(b, t): if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: return 0 == (f[t] * u[t] * a * deltaT[t] - q[t]) * scale else: return 0 == (u[t] * a * deltaT[t] - q[t]) * scale ######################################################################## # Add symbols for LaTeX equation rendering # ######################################################################## self.overall_heat_transfer_coefficient.latex_symbol = "U" self.area.latex_symbol = "A" self.side_1.heat.latex_symbol = "Q_1" self.side_2.heat.latex_symbol = "Q_2" self.delta_temperature.latex_symbol = "\\Delta T" def initialize( self, state_args_1=None, state_args_2=None, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6}, duty=1000, ): """ Heat exchanger initialization method. Args: state_args_1 : a dict of arguments to be passed to the property initialization for side_1 (see documentation of the specific property package) (default = {}). state_args_2 : a dict of arguments to be passed to the property initialization for side_2 (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating which solver to use during initialization (default = 'ipopt') duty : an initial guess for the amount of heat transfered (default = 10000) Returns: None """ # Set solver options init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") opt = SolverFactory(solver) opt.options = optarg flags1 = self.side_1.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1) init_log.info_high("Initialization Step 1a (side_1) Complete.") flags2 = self.side_2.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2) init_log.info_high("Initialization Step 1b (side_2) Complete.") # --------------------------------------------------------------------- # Solve unit without heat transfer equation self.heat_transfer_equation.deactivate() self.side_2.heat.fix(duty) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) self.side_2.heat.unfix() self.heat_transfer_equation.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state self.side_1.release_state(flags1, outlvl=outlvl) self.side_2.release_state(flags2, outlvl=outlvl) init_log.info("Initialization Completed, {}".format( idaeslog.condition(res))) def _get_performance_contents(self, time_point=0): var_dict = { "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] } var_dict["HX Area"] = self.area var_dict["Heat Duty"] = self.heat_duty[time_point] if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} expr_dict = {} expr_dict["Delta T Driving"] = self.delta_temperature[time_point] expr_dict["Delta T In"] = self.delta_temperature_in[time_point] expr_dict["Delta T Out"] = self.delta_temperature_out[time_point] return {"vars": var_dict, "exprs": expr_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Hot Inlet": self.inlet_1, "Hot Outlet": self.outlet_1, "Cold Inlet": self.inlet_2, "Cold Outlet": self.outlet_2, }, time_point=time_point, ) def get_costing(self, module=costing): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing() self.costing = Block() module.hx_costing(self.costing)
class ConcreteTubeSideData(UnitModelBlockData): """ConcreteTubeSide 1D Unit Model Class. """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""", ), ) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium term construction flag", doc="""Argument to enable phase equilibrium on the shell side. - True - include phase equilibrium term - False - do not include phase equilibrium term""", ), ) CONFIG.declare( "property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a ParameterBlock object""", ), ) CONFIG.declare( "property_package_args", ConfigValue( default={}, description="Arguments for constructing shell property package", doc="""A dict of arguments to be passed to the PropertyBlockData and used when constructing these (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a dict (see property package for documentation)""", ), ) CONFIG.declare( "transformation_method", ConfigValue( default=useDefault, description="Discretization method to use for DAE transformation", doc="""Discretization method to use for DAE transformation. See Pyomo documentation for supported transformations.""", ), ) CONFIG.declare( "transformation_scheme", ConfigValue( default=useDefault, description="Discretization scheme to use for DAE transformation", doc="""Discretization scheme to use when transformating domain. See Pyomo documentation for supported schemes.""", ), ) CONFIG.declare( "finite_elements", ConfigValue( default=20, domain=int, description="Number of finite elements length domain", doc="""Number of finite elements to use when discretizing length domain (default=20)""", ), ) CONFIG.declare( "collocation_points", ConfigValue( default=5, domain=int, description="Number of collocation points per finite element", doc="""Number of collocation points to use per finite element when discretizing length domain (default=3)""", ), ) CONFIG.declare( "flow_type", ConfigValue( default=HeatExchangerFlowPattern.cocurrent, domain=In(HeatExchangerFlowPattern), description="Flow configuration of concrete tube", doc="""Flow configuration of concrete tube - HeatExchangerFlowPattern.cocurrent: shell and tube flows from 0 to 1 (default) - HeatExchangerFlowPattern.countercurrent: shell side flows from 0 to 1 tube side flows from 1 to 0""", ), ) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # dicretisation if not specified. if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: set_direction_tube = FlowDirection.forward if self.config.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the tube side of the " "co-current concrete tube. " "Defaulting to finite " "difference method on the tube side.") self.config.transformation_method = "dae.finite_difference" if self.config.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the tube side of the " "co-current concrete tube. " "Defaulting to backward finite " "difference on the tube side.") self.config.transformation_scheme = "BACKWARD" elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent: set_direction_tube = FlowDirection.backward if self.config.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the tube side of the " "counter-current concrete tube. " "Defaulting to finite " "difference method on the tube side.") self.config.transformation_method = "dae.finite_difference" if self.config.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the tube side of the " "counter-current concrete tube. " "Defaulting to forward finite " "difference on the tube side.") self.config.transformation_scheme = "BACKWARD" else: raise ConfigurationError( "{} ConcreteTubeSide only supports cocurrent and " "countercurrent flow patterns, but flow_type configuration" " argument was set to {}.".format(self.name, self.config.flow_type)) self.tube = ControlVolume1DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "transformation_method": self.config.transformation_method, "transformation_scheme": self.config.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, }) self.tube.add_geometry(flow_direction=set_direction_tube) self.tube.add_state_blocks( information_flow=set_direction_tube, has_phase_equilibrium=self.config.has_phase_equilibrium, ) # Populate tube self.tube.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=self.config.has_phase_equilibrium, ) self.tube.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True, ) self.tube.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change, ) self.tube.apply_transformation() # Add Ports for tube side self.add_inlet_port(name="tube_inlet", block=self.tube) self.add_outlet_port(name="tube_outlet", block=self.tube) # Add reference to control volume geometry add_object_reference(self, "tube_area", self.tube.area) add_object_reference(self, "tube_length", self.tube.length) self._make_performance() def _make_performance(self): """Constraints for unit model. Args: None Returns: None """ tube_units = self.config.property_package.get_metadata( ).get_derived_units self.d_tube_outer = Var( domain=PositiveReals, initialize=0.011, doc="Outer diameter of tube", units=tube_units("length"), ) self.d_tube_inner = Var( domain=PositiveReals, initialize=0.010, doc="Inner diameter of tube", units=tube_units("length"), ) self.tube_heat_transfer_coefficient = Var( self.flowsheet().config.time, self.tube.length_domain, domain=PositiveReals, initialize=50, doc="Heat transfer coefficient", units=tube_units("heat_transfer_coefficient"), ) self.temperature_wall = Var( self.flowsheet().config.time, self.tube.length_domain, domain=PositiveReals, initialize=298.15, units=tube_units("temperature"), ) # Energy transfer between tube wall and tube @self.Constraint( self.flowsheet().config.time, self.tube.length_domain, doc="Convective heat transfer", ) def tube_heat_transfer_eq(self, t, x): return self.tube.heat[t, x] == self.tube_heat_transfer_coefficient[ t, x] * c.pi * pyunits.convert( self.d_tube_inner, to_units=tube_units("length")) * ( pyunits.convert(self.temperature_wall[t, x], to_units=tube_units("temperature")) - self.tube.properties[t, x].temperature) # Define tube area in terms of tube diameter self.area_calc_tube = Constraint( expr=4 * self.tube_area == c.pi * pyunits.convert( self.d_tube_inner, to_units=tube_units("length"))**2) def initialize(self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): """ Initialization routine for the unit (default solver ipopt). Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") solver = get_solver(solver=solver, options=optarg) flags_tube = self.tube.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) self.tube.release_state(flags_tube) init_log.info("Initialization Complete.") def _get_performance_contents(self, time_point=0): var_dict = {} var_dict["Tube Area"] = self.tube.area var_dict["Tube Outer Diameter"] = self.d_tube_outer var_dict["Tube Inner Diameter"] = self.d_tube_inner var_dict["Tube Length"] = self.tube.length return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Tube Inlet": self.tube_inlet, "Tube Outlet": self.tube_outlet, }, time_point=time_point, ) def calculate_scaling_factors(self): super().calculate_scaling_factors() for i, c in self.tube_heat_transfer_eq.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.tube.heat[i], default=1, warning=True))
class HelmNtuCondenserData(UnitModelBlockData): """ Simple NTU condenser unit model. This model assumes the property pacakages specified are Helmholtz EOS type. """ CONFIG = UnitModelBlockData.CONFIG(implicit=True) _make_heat_exchanger_config(CONFIG) def _process_config(self): """Check for configuration errors and alternate config option names. """ config = self.config if config.hot_side_name == config.cold_side_name: raise NameError( "Condenser hot and cold side cannot have the same name '{}'." " Be sure to set both the hot_side_name and cold_side_name.". format(config.hot_side_name)) for o in config: if not (o in self.CONFIG or o in [config.hot_side_name, config.cold_side_name]): raise KeyError( "Condenser config option {} not defined".format(o)) if config.hot_side_name in config: config.hot_side_config.set_value(config[config.hot_side_name]) # Allow access to hot_side_config under the hot_side_name setattr(config, config.hot_side_name, config.hot_side_config) if config.cold_side_name in config: config.cold_side_config.set_value(config[config.cold_side_name]) # Allow access to hot_side_config under the cold_side_name setattr(config, config.cold_side_name, config.cold_side_config) if config.cold_side_name in ["hot_side", "side_1"]: raise ConfigurationError( "Cold side name cannot be in ['hot_side', 'side_1'].") if config.hot_side_name in ["cold_side", "side_2"]: raise ConfigurationError( "Hot side name cannot be in ['cold_side', 'side_2'].") def build(self): """ Building model Args: None Returns: None """ ######################################################################## # Call UnitModel.build to setup dynamics and configure # ######################################################################## super().build() self._process_config() config = self.config time = self.flowsheet().config.time ######################################################################## # Add control volumes # ######################################################################## hot_side = _make_heater_control_volume( self, config.hot_side_name, config.hot_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) cold_side = _make_heater_control_volume( self, config.cold_side_name, config.cold_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) # Add refernces to the hot side and cold side, so that we have solid # names to refere to internally. side_1 and side_2 also maintain # compatability with older models. Using add_object_reference keeps # these from showing up when you iterate through pyomo compoents in a # model, so only the user specified control volume names are "seen" if not hasattr(self, "side_1"): add_object_reference(self, "side_1", hot_side) if not hasattr(self, "side_2"): add_object_reference(self, "side_2", cold_side) if not hasattr(self, "hot_side"): add_object_reference(self, "hot_side", hot_side) if not hasattr(self, "cold_side"): add_object_reference(self, "cold_side", cold_side) ######################################################################## # Add variables # ######################################################################## # Use hot side units as basis s1_metadata = config.hot_side_config.property_package.get_metadata() f_units = s1_metadata.get_derived_units("flow_mole") cp_units = s1_metadata.get_derived_units("heat_capacity_mole") q_units = s1_metadata.get_derived_units("power") u_units = s1_metadata.get_derived_units("heat_transfer_coefficient") a_units = s1_metadata.get_derived_units("area") temp_units = s1_metadata.get_derived_units("temperature") self.overall_heat_transfer_coefficient = pyo.Var( time, domain=pyo.PositiveReals, initialize=100.0, doc="Overall heat transfer coefficient", units=u_units, ) self.area = pyo.Var( domain=pyo.PositiveReals, initialize=1000.0, doc="Heat exchange area", units=a_units, ) self.heat_duty = pyo.Reference(cold_side.heat) ######################################################################## # Add ports # ######################################################################## i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet", block=hot_side, doc="Hot side inlet") i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet", block=cold_side, doc="Cold side inlet") o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet", block=hot_side, doc="Hot side outlet") o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet", block=cold_side, doc="Cold side outlet") # Using Andrew's function for now. I want these port names for backward # compatablity, but I don't want them to appear if you iterate throught # components and add_object_reference hides them from Pyomo. if not hasattr(self, "inlet_1"): add_object_reference(self, "inlet_1", i1) if not hasattr(self, "inlet_2"): add_object_reference(self, "inlet_2", i2) if not hasattr(self, "outlet_1"): add_object_reference(self, "outlet_1", o1) if not hasattr(self, "outlet_2"): add_object_reference(self, "outlet_2", o2) if not hasattr(self, "hot_inlet"): add_object_reference(self, "hot_inlet", i1) if not hasattr(self, "cold_inlet"): add_object_reference(self, "cold_inlet", i2) if not hasattr(self, "hot_outlet"): add_object_reference(self, "hot_outlet", o1) if not hasattr(self, "cold_outlet"): add_object_reference(self, "cold_outlet", o2) ######################################################################## # Add a unit level energy balance # ######################################################################## @self.Constraint(time, doc="Heat balance equation") def unit_heat_balance(b, t): return 0 == (hot_side.heat[t] + pyunits.convert(cold_side.heat[t], to_units=q_units)) ######################################################################## # Add some useful expressions for condenser performance # ######################################################################## @self.Expression(time, doc="Inlet temperature difference") def delta_temperature_in(b, t): return (hot_side.properties_in[t].temperature - pyunits.convert( cold_side.properties_in[t].temperature, temp_units)) @self.Expression(time, doc="Outlet temperature difference") def delta_temperature_out(b, t): return (hot_side.properties_out[t].temperature - pyunits.convert( cold_side.properties_out[t].temperature, temp_units)) @self.Expression(time, doc="NTU Based temperature difference") def delta_temperature_ntu(b, t): return (hot_side.properties_in[t].temperature_sat - pyunits.convert(cold_side.properties_in[t].temperature, temp_units)) @self.Expression( time, doc="Minimum product of flow rate and heat " "capacity (always on tube side since shell side has phase change)") def mcp_min(b, t): return pyunits.convert( cold_side.properties_in[t].flow_mol * cold_side.properties_in[t].cp_mol_phase['Liq'], f_units * cp_units) @self.Expression(time, doc="Number of transfer units (NTU)") def ntu(b, t): return b.overall_heat_transfer_coefficient[t] * b.area / b.mcp_min[ t] @self.Expression(time, doc="Condenser effectiveness factor") def effectiveness(b, t): return 1 - pyo.exp(-self.ntu[t]) @self.Expression(time, doc="Heat treansfer") def heat_transfer(b, t): return b.effectiveness[t] * b.mcp_min[t] * b.delta_temperature_ntu[ t] ######################################################################## # Add Equations to calculate heat duty based on NTU method # ######################################################################## @self.Constraint(time, doc="Heat transfer rate equation based on NTU method") def heat_transfer_equation(b, t): return (pyunits.convert(cold_side.heat[t], q_units) == self.heat_transfer[t]) @self.Constraint( time, doc="Shell side outlet enthalpy is saturated water enthalpy") def saturation_eqn(b, t): return (hot_side.properties_out[t].enth_mol == hot_side.properties_in[t].enth_mol_sat_phase["Liq"]) def set_initial_condition(self): if self.config.dynamic is True: self.hot_side.material_accumulation[:, :, :].value = 0 self.hot_side.energy_accumulation[:, :].value = 0 self.hot_side.material_accumulation[0, :, :].fix(0) self.hot_side.energy_accumulation[0, :].fix(0) self.cold_side.material_accumulation[:, :, :].value = 0 self.cold_side.energy_accumulation[:, :].value = 0 self.cold_side.material_accumulation[0, :, :].fix(0) self.cold_side.energy_accumulation[0, :].fix(0) def initialize( self, state_args_1=None, state_args_2=None, unfix='hot_flow', outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ Condenser initialization method. The initialization routine assumes fixed area and heat transfer coefficient and adjusts the cooling water flow to condense steam to saturated water at shell side pressure. Args: state_args_1 : a dict of arguments to be passed to the property initialization for hot side (see documentation of the specific property package) (default = None). state_args_2 : a dict of arguments to be passed to the property initialization for cold side (see documentation of the specific property package) (default = None). optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None """ if unfix not in {"hot_flow", "cold_flow", "pressure"}: raise Exception("Condenser free variable must be in 'hot_flow', " "'cold_flow', or 'pressure'") # Set solver options init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") hot_side = getattr(self, self.config.hot_side_name) cold_side = getattr(self, self.config.cold_side_name) # Store initial model specs, restored at the end of initializtion, so # the problem is not altered. This can restore fixed/free vars, # active/inactive constraints, and fixed variable values. sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Create solver opt = get_solver(solver, optarg) flags1 = hot_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1) flags2 = cold_side.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2) init_log.info_high("Initialization Step 1 Complete.") # Solve with all constraints activated self.saturation_eqn.activate() if unfix == 'pressure': hot_side.properties_in[:].pressure.unfix() elif unfix == 'hot_flow': hot_side.properties_in[:].flow_mol.unfix() elif unfix == 'cold_flow': cold_side.properties_in[:].flow_mol.unfix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 4 {}.".format( idaeslog.condition(res))) # Release Inlet state hot_side.release_state(flags1, outlvl) cold_side.release_state(flags2, outlvl) from_json(self, sd=istate, wts=sp) def _get_performance_contents(self, time_point=0): var_dict = { "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] } var_dict["HX Area"] = self.area var_dict["Heat Duty"] = self.heat_duty[time_point] expr_dict = {} expr_dict["Delta T In"] = self.delta_temperature_in[time_point] expr_dict["Delta T Out"] = self.delta_temperature_out[time_point] return {"vars": var_dict, "exprs": expr_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Hot Inlet": self.hot_inlet, "Hot Outlet": self.hot_outlet, "Cold Inlet": self.cold_inlet, "Cold Outlet": self.cold_outlet, }, time_point=time_point, ) def get_costing(self, module=costing): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing() self.costing = pyo.Block() module.hx_costing(self.costing) def calculate_scaling_factors(self): super().calculate_scaling_factors() area_sf_default = 1e-2 overall_heat_transfer_coefficient_sf_default = 1e-2 # Function to set defaults so I don't need to reproduce the same code def _fill_miss_with_default(name, s): try: c = getattr(self, name) except AttributeError: return # it's okay if the attribute doesn't exist, spell careful if iscale.get_scaling_factor(c) is None: for ci in c.values(): if iscale.get_scaling_factor(ci) is None: iscale.set_scaling_factor(ci, s) # Set defaults where scale factors are missing _fill_miss_with_default("area", area_sf_default) _fill_miss_with_default("overall_heat_transfer_coefficient", overall_heat_transfer_coefficient_sf_default) for t, c in self.heat_transfer_equation.items(): sf = iscale.get_scaling_factor(self.cold_side.heat[t]) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.unit_heat_balance.items(): sf = iscale.get_scaling_factor(self.cold_side.heat[t]) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.saturation_eqn.items(): sf = iscale.get_scaling_factor( self.hot_side.properties_out[t].enth_mol) iscale.constraint_scaling_transform(c, sf, overwrite=False)
class PIDControllerData(UnitModelBlockData): CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "pv", ConfigValue( default=None, description="Process variable to be controlled", doc="A Pyomo Var, Expression, or Reference for the measured" " process variable. Should be indexed by time.")) CONFIG.declare( "mv", ConfigValue( default=None, description="Manipulated process variable", doc="A Pyomo Var, Expression, or Reference for the controlled" " process variable. Should be indexed by time.")) CONFIG.declare( "bounded_output", ConfigValue( default=False, description="Flag to bound manipulated variable", doc= """Indicating if the output for the manipulated variable is bounded Default: False. If True, user need to set the lower and upper bound parameters""")) CONFIG.declare( "type", ConfigValue(default="PI", domain=In(['P', 'PI', 'PD', 'PID']), description="Control type", doc="""Controller type options including - P: Proportional only - PI: Proportional and integral only - PD: Proportional and derivative only - PID: Proportional, integral and derivative Default is PI""")) def __init__(self, *args, **kwargs): deprecation_warning( "DEPRECATED: The idaes.power_generation.control.pid_controller.PIDController" " model is deprecated and will be removed. Use" " idaes.generic_models.control.PIDController instead.", version=1.12, ) super().__init__(*args, **kwargs) def build(self): """ Build the PID block """ super().build() # Do nothing if steady-state if self.config.dynamic is True: # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration" " requires 'pv'") if self.config.mv is None: raise ConfigurationError("Controller configuration" " requires 'mv'") # Shorter pointers to time set information time_set = self.flowsheet().time self.pv = Reference(self.config.pv) self.mv = Reference(self.config.mv) # Parameters self.mv_lb = Param(mutable=True, initialize=0.05, doc="Controller output lower bound") self.mv_ub = Param(mutable=True, initialize=1, doc="Controller output upper bound") # Variable for basic controller settings may change with time. self.setpoint = Var(time_set, initialize=0.5, doc="Setpoint") self.gain_p = Var(time_set, initialize=0.1, doc="Gain for proportional part") if self.config.type == 'PI' or self.config.type == 'PID': self.gain_i = Var(time_set, initialize=0.1, doc="Gain for integral part") if self.config.type == 'PD' or self.config.type == 'PID': self.gain_d = Var(time_set, initialize=0.01, doc="Gain for derivative part") self.mv_ref = Var(initialize=0.5, doc="bias value of manipulated variable") if self.config.type == 'P' or self.config.type == 'PI': @self.Expression(time_set, doc="Error expression") def error(b, t): return b.setpoint[t] - b.pv[t] else: self.error = Var(time_set, initialize=0, doc="Error variable") @self.Constraint(time_set, doc="Error variable") def error_eqn(b, t): return b.error[t] == b.setpoint[t] - b.pv[t] if self.config.type == 'PI' or self.config.type == 'PID': self.integral_of_error = Var(time_set, initialize=0, doc="Integral term") self.error_from_integral = DerivativeVar( self.integral_of_error, wrt=self.flowsheet().time, initialize=0) @self.Constraint(time_set, doc="Error calculated by" " derivative of integral") def error_from_integral_eqn(b, t): return b.error[t] == b.error_from_integral[t] if self.config.type == 'PID' or self.config.type == 'PD': self.derivative_of_error = DerivativeVar( self.error, wrt=self.flowsheet().time, initialize=0) @self.Expression(time_set, doc="Proportional output") def mv_p_only(b, t): return b.gain_p[t] * b.error[t] @self.Expression(time_set, doc="Proportional output and reference") def mv_p_only_with_ref(b, t): return b.gain_p[t] * b.error[t] + b.mv_ref if self.config.type == 'PI' or self.config.type == 'PID': @self.Expression(time_set, doc="Integral output") def mv_i_only(b, t): return b.gain_i[t] * b.integral_of_error[t] if self.config.type == 'PD' or self.config.type == 'PID': @self.Expression(time_set, doc="Derivative output") def mv_d_only(b, t): return b.gain_d[t] * b.derivative_of_error[t] @self.Expression(time_set, doc="Unbounded output for manimulated variable") def mv_unbounded(b, t): if self.config.type == 'PID': return (b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[t] * b.integral_of_error[t] + b.gain_d[t] * b.derivative_of_error[t]) elif self.config.type == 'PI': return (b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[t] * b.integral_of_error[t]) elif self.config.type == 'PD': return (b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_d[t] * b.derivative_of_error[t]) else: return b.mv_ref + b.gain_p[t] * b.error[t] @self.Constraint(time_set, doc="Bounded output of manipulated variable") def mv_eqn(b, t): if t == b.flowsheet().time.first(): return Constraint.Skip else: if self.config.bounded_output is True: return (b.mv[t]-b.mv_lb) * \ (1+exp(-4/(b.mv_ub-b.mv_lb) * (b.mv_unbounded[t] - (b.mv_lb + b.mv_ub)/2))) == b.mv_ub-b.mv_lb else: return b.mv[t] == b.mv_unbounded[t] if self.config.bounded_output is True: if self.config.type == 'PI' or self.config.type == 'PID': @self.Expression(time_set, doc="Integral error" " at error 0 and mv_ref") def integral_of_error_ref(b, t): return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref - log((b.mv_ub - b.mv_lb) / (b.mv_ref - b.mv_lb) - 1) / 4 * (b.mv_ub - b.mv_lb)) / b.gain_i[t] @self.Expression(time_set, doc="Integral error at error 0 and" " output value at current mv") def integral_of_error_mv(b, t): return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref - log((b.mv_ub - b.mv_lb) / (b.mv[t] - b.mv_lb) - 1) / 4 * (b.mv_ub - b.mv_lb)) / b.gain_i[t]
class PressureChangerData(UnitModelBlockData): """ Standard Compressor/Expander Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare("material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare("momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare("has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) CONFIG.declare("compressor", ConfigValue( default=True, domain=In([True, False]), description="Compressor flag", doc="""Indicates whether this unit should be considered a compressor (True (default), pressure increase) or an expander (False, pressure decrease).""")) CONFIG.declare("thermodynamic_assumption", ConfigValue( default=ThermodynamicAssumption.isothermal, domain=In(ThermodynamicAssumption), description="Thermodynamic assumption to use", doc="""Flag to set the thermodynamic assumption to use for the unit. - ThermodynamicAssumption.isothermal (default) - ThermodynamicAssumption.isentropic - ThermodynamicAssumption.pump - ThermodynamicAssumption.adiabatic""")) CONFIG.declare("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """ Args: None Returns: None """ # Call UnitModel.build super(PressureChangerData, self).build() # Add a control volume to the unit including setting up dynamics. self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) # Add geomerty variables to control volume if self.config.has_holdup: self.control_volume.add_geometry() # Add inlet and outlet state blocks to control volume self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) # Add mass balance # Set has_equilibrium is False for now # TO DO; set has_equilibrium to True self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=self.config.has_phase_equilibrium) # Add energy balance self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_work_transfer=True) # add momentum balance self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Set Unit Geometry and holdup Volume 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 add_pump(self): """ Add constraints for the incompressible fluid assumption Args: None Returns: None """ self.work_fluid = Var( self.flowsheet().config.time, initialize=1.0, doc="Work required to increase the pressure of the liquid") self.efficiency_pump = Var( self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency") @self.Constraint(self.flowsheet().config.time, doc="Pump fluid work constraint") def fluid_work_calculation(b, t): return b.work_fluid[t] == ( (b.control_volume.properties_out[t].pressure - b.control_volume.properties_in[t].pressure) * b.control_volume.properties_out[t].flow_vol) # Actual work @self.Constraint(self.flowsheet().config.time, doc="Actual mechanical work calculation") def actual_work(b, t): if b.config.compressor: return b.sfe*b.work_fluid[t] == b.sfe*( b.work_mechanical[t]*b.efficiency_pump[t]) else: return b.sfe*b.work_mechanical[t] == b.sfe*( b.work_fluid[t]*b.efficiency_pump[t]) def add_isothermal(self): """ Add constraints for isothermal assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint(self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet temperature") def isothermal(b, t): return b.control_volume.properties_in[t].temperature == \ b.control_volume.properties_out[t].temperature def add_adiabatic(self): """ Add constraints for adiabatic assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint(self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet enthalpy") def adiabatic(b, t): return b.control_volume.properties_in[t].enth_mol == \ b.control_volume.properties_out[t].enth_mol def add_isentropic(self): """ Add constraints for isentropic assumption. Args: None Returns: None """ # Get indexing sets from control volume # Add isentropic variables self.efficiency_isentropic = Var(self.flowsheet().config.time, initialize=0.8, doc="Efficiency with respect to an " "isentropic process [-]") self.work_isentropic = Var(self.flowsheet().config.time, initialize=0.0, doc="Work input to unit if isentropic " "process [-]") # Build isentropic state block tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = False self.properties_isentropic = ( self.config.property_package.state_block_class( self.flowsheet().config.time, doc="isentropic properties at outlet", default=tmp_dict)) # Connect isentropic state block properties @self.Constraint(self.flowsheet().config.time, doc="Pressure for isentropic calculations") def isentropic_pressure(b, t): return b.sfp*b.properties_isentropic[t].pressure == \ b.sfp*b.control_volume.properties_out[t].pressure # This assumes isentropic composition is the same as outlet mb_type = self.config.material_balance_type if mb_type == MaterialBalanceType.useDefault: mb_type = \ self.control_volume._get_representative_property_block() \ .default_material_balance_type() if mb_type == \ MaterialBalanceType.componentPhase: @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Material flows for isentropic properties") def isentropic_material(b, t, p, j): return ( b.properties_isentropic[t].get_material_flow_terms(p, j) == b.control_volume.properties_out[t] .get_material_flow_terms(p, j)) elif mb_type == \ MaterialBalanceType.componentTotal: @self.Constraint(self.flowsheet().config.time, self.config.property_package.component_list, doc="Material flows for isentropic properties") def isentropic_material(b, t, j): return (sum( b.properties_isentropic[t].get_material_flow_terms(p, j) for p in self.config.property_package.phase_list) == sum(b.control_volume.properties_out[t] .get_material_flow_terms(p, j) for p in self.config.property_package.phase_list)) elif mb_type == \ MaterialBalanceType.total: @self.Constraint(self.flowsheet().config.time, doc="Material flows for isentropic properties") def isentropic_material(b, t, p, j): return (sum(sum( b.properties_isentropic[t].get_material_flow_terms(p, j) for j in self.config.property_package.component_list) for p in self.config.property_package.phase_list) == sum(sum(b.control_volume.properties_out[t] .get_material_flow_terms(p, j) for j in self.config.property_package.component_list) for p in self.config.property_package.phase_list)) elif mb_type == \ MaterialBalanceType.elementTotal: raise BalanceTypeNotSupportedError( "{} PressureChanger does not support element balances." .format(self.name)) elif mb_type == \ MaterialBalanceType.none: raise BalanceTypeNotSupportedError( "{} PressureChanger does not support material_balance_type" " = none." .format(self.name)) else: raise BurntToast( "{} PressureChanger received an unexpected argument for " "material_balance_type. This should never happen. Please " "contact the IDAES developers with this bug." .format(self.name)) # This assumes isentropic entropy is the same as inlet @self.Constraint(self.flowsheet().config.time, doc="Isentropic assumption") def isentropic(b, t): return b.properties_isentropic[t].entr_mol == \ b.control_volume.properties_in[t].entr_mol # Isentropic work @self.Constraint(self.flowsheet().config.time, doc="Calculate work of isentropic process") def isentropic_energy_balance(b, t): return b.sfe*b.work_isentropic[t] == b.sfe*( sum(b.properties_isentropic[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) - sum(b.control_volume.properties_in[t] .get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) # Actual work @self.Constraint(self.flowsheet().config.time, doc="Actual mechanical work calculation") def actual_work(b, t): if b.config.compressor: return b.sfe*b.work_isentropic[t] == b.sfe*( b.work_mechanical[t]*b.efficiency_isentropic[t]) else: return b.sfe*b.work_mechanical[t] == b.sfe*( b.work_isentropic[t]*b.efficiency_isentropic[t]) def model_check(blk): """ Check that pressure change matches with compressor argument (i.e. if compressor = True, pressure should increase or work should be positive) Args: None Returns: None """ if blk.config.compressor: # Compressor # Check that pressure does not decrease if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with negative deltaP.' .format(blk.name)) if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with ratioP less than 1.' .format(blk.name)) if any(blk.control_volume.properties_out[t].pressure.fixed and (value(blk.control_volume.properties_in[t].pressure) > value(blk.control_volume.properties_out[t].pressure)) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with pressure decrease.' .format(blk.name)) # Check that work is not negative if any(blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) < 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor maybe set with negative work.' .format(blk.name)) else: # Expander # Check that pressure does not increase if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with positive deltaP.' .format(blk.name)) if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with ratioP greater ' 'than 1.'.format(blk.name)) if any(blk.control_volume.properties_out[t].pressure.fixed and (value(blk.control_volume.properties_in[t].pressure) < value(blk.control_volume.properties_out[t].pressure)) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine maybe set with pressure ', 'increase.'.format(blk.name)) # Check that work is not positive if any(blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) > 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with positive work.' .format(blk.name)) # Run holdup block model checks blk.control_volume.model_check() # Run model checks on isentropic property block try: for t in blk.flowsheet().config.time: blk.properties_in[t].model_check() except AttributeError: pass def initialize(blk, state_args=None, routine=None, outlvl=0, solver='ipopt', optarg={'tol': 1e-6}): ''' General wrapper for pressure changer initialisation routines Keyword Arguments: routine : str stating which initialization routine to execute * None - use routine matching thermodynamic_assumption * 'isentropic' - use isentropic initialization routine * 'isothermal' - use isothermal initialization routine state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' if routine is None: # Use routine for specific type of unit routine = blk.config.thermodynamic_assumption # Call initialisation routine if routine is ThermodynamicAssumption.isentropic: blk.init_isentropic(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) else: # Call the general initialization routine in UnitModelBlockData super(PressureChangerData, blk).initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) def init_isentropic(blk, state_args, outlvl, solver, optarg): ''' Initialisation routine for unit (default solver ipopt) Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' # Set solver options if outlvl > 3: stee = True else: stee = False opt = SolverFactory(solver) opt.options = optarg # --------------------------------------------------------------------- # Initialize Isentropic block blk.control_volume.properties_in.initialize(outlvl=outlvl-1, optarg=optarg, solver=solver, state_args=state_args) if outlvl > 0: logger.info('{} Initialisation Step 1 Complete.'.format(blk.name)) # --------------------------------------------------------------------- # Initialize holdup block flags = blk.control_volume.initialize(outlvl=outlvl-1, optarg=optarg, solver=solver, state_args=state_args) if outlvl > 0: logger.info('{} Initialisation Step 2 Complete.'.format(blk.name)) # --------------------------------------------------------------------- # Solve for isothermal conditions if isinstance( blk.control_volume.properties_in[ blk.flowsheet().config.time[1]].temperature, Var): for t in blk.flowsheet().config.time: blk.control_volume.properties_in[t].temperature.fix() blk.isentropic.deactivate() results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: logger.info('{} Initialisation Step 3 Complete.' .format(blk.name)) else: logger.warning('{} Initialisation Step 3 Failed.' .format(blk.name)) for t in blk.flowsheet().config.time: blk.control_volume.properties_in[t].temperature.unfix() blk.isentropic.activate() elif outlvl > 0: logger.info('{} Initialisation Step 3 Skipped.'.format(blk.name)) # --------------------------------------------------------------------- # Solve unit results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: logger.info('{} Initialisation Step 4 Complete.' .format(blk.name)) else: logger.warning('{} Initialisation Step 4 Failed.' .format(blk.name)) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl-1) if outlvl > 0: logger.info('{} Initialisation Complete.'.format(blk.name)) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "deltaP"): var_dict["Mechanical Work"] = self.work_mechanical[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] if hasattr(self, "ratioP"): var_dict["Pressure Ratio"] = self.deltaP[time_point] if hasattr(self, "efficiency_pump"): var_dict["Efficiency"] = self.deltaP[time_point] if hasattr(self, "efficiency_isentropic"): var_dict["Isentropic Efficiency"] = self.deltaP[time_point] return {"vars": var_dict}
class ReboilerData(UnitModelBlockData): """ Reboiler unit for distillation model. Unit model to reboil the liquid from the bottom tray of the distillation column. """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare("has_boilup_ratio", ConfigValue( default=False, domain=In([True, False]), description="Boilup ratio term construction flag", doc="""Indicates whether terms for boilup ratio should be constructed, **default** - False. **Valid values:** { **True** - include construction of boilup ratio constraint, **False** - exclude construction of boilup ratio constraint}""")) CONFIG.declare("material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare("momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare("has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """Build the model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(ReboilerData, self).build() # Add Control Volume for the Reboiler self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_state_blocks( has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) if self.config.has_boilup_ratio is True: self.boilup_ratio = Var(initialize=0.5, doc="boilup ratio for reboiler") def rule_boilup_ratio(self, t): if hasattr(self.control_volume.properties_out[t], "flow_mol_phase"): return self.boilup_ratio * \ self.control_volume.properties_out[t].\ flow_mol_phase["Liq"] == self.control_volume.\ properties_out[t].flow_mol_phase["Vap"] elif hasattr(self.control_volume.properties_out[t], "flow_mol_phase_comp"): return self.boilup_ratio * \ sum(self.control_volume.properties_out[t]. flow_mol_phase_comp["Liq", i] for i in self.control_volume.properties_out[t]. _params.component_list) == \ sum(self.control_volume.properties_out[t]. flow_mol_phase_comp["Vap", i] for i in self.control_volume.properties_out[t]. _params.component_list) else: raise PropertyNotSupportedError( "Unrecognized names for flow variables encountered " "while building the constraint for reboiler.") self.eq_boilup_ratio = Constraint(self.flowsheet().time, rule=rule_boilup_ratio) self._make_ports() self._make_splits_reboiler() # Add object reference to variables of the control volume # Reference to the heat duty add_object_reference(self, "heat_duty", self.control_volume.heat) # Reference to the pressure drop (if set to True) if self.config.has_pressure_change: add_object_reference(self, "deltaP", self.control_volume.deltaP) def _make_ports(self): # Add Ports for the reboiler # Inlet port (the vapor from the top tray) self.add_inlet_port() # Outlet ports that always exist irrespective of reboiler type self.bottoms = Port(noruleinit=True, doc="Bottoms stream.") self.vapor_reboil = Port(noruleinit=True, doc="Vapor outlet stream that is returned to " "to the bottom tray.") def _make_splits_reboiler(self): # Get dict of Port members and names member_list = self.control_volume.\ properties_out[0].define_port_members() # Create references and populate the reflux, distillate ports for k in member_list: # Create references and populate the intensive variables if "flow" not in k and "frac" not in k and "enth" not in k: if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(member_list[k].local_name) else: var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the reflux port self.bottoms.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_reboil.add(Reference(var), k) elif "frac" in k and ("mole" in k or "mass" in k): # Mole/mass frac is typically indexed index_set = member_list[k].index_set() # if state var is not mole/mass frac by phase if "phase" not in k: # Assuming the state block has the var # "mole_frac_phase_comp". Valid if VLE is supported # Create a string "mole_frac_phase_comp" or # "mass_frac_phase_comp". Cannot directly append phase to # k as the naming convention is phase followed by comp str_split = k.split('_') local_name = '_'.join(str_split[0:2]) + \ "_phase" + "_" + str_split[2] # Rule for liquid fraction def rule_liq_frac(self, t, i): return self.control_volume.properties_out[t].\ component(local_name)["Liq", i] self.e_liq_frac = Expression( self.flowsheet().time, index_set, rule=rule_liq_frac) # Rule for vapor fraction def rule_vap_frac(self, t, i): return self.control_volume.properties_out[t].\ component(local_name)["Vap", i] self.e_vap_frac = Expression( self.flowsheet().time, index_set, rule=rule_vap_frac) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_liq_frac, k) # add the reference and variable name to the # vapor port self.vapor_reboil.add(self.e_vap_frac, k) else: # Assumes mole_frac_phase or mass_frac_phase exist as # state vars in the port and therefore access directly # from the state block. var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the distillate port self.bottoms.add(Reference(var), k) # add the reference and variable name to the boil up port self.vapor_reboil.add(Reference(var), k) elif "flow" in k: if "phase" not in k: # Assumes that here the var is total flow or component # flow. However, need to extract the flow by phase from # the state block. Expects to find the var # flow_mol_phase or flow_mass_phase in the state block. # Check if it is not indexed by component list and this # is total flow if not member_list[k].is_indexed(): # if state var is not flow_mol/flow_mass # by phase local_name = str(member_list[k].local_name) + \ "_phase" # Rule for vap flow def rule_vap_flow(self, t): return self.control_volume.properties_out[t].\ component(local_name)["Vap"] self.e_vap_flow = Expression( self.flowsheet().time, rule=rule_vap_flow) # Rule to link the liq flow to the distillate def rule_bottoms_flow(self, t): return self.control_volume.properties_out[t].\ component(local_name)["Liq"] self.e_bottoms_flow = Expression( self.flowsheet().time, rule=rule_bottoms_flow) else: # when it is flow comp indexed by component list str_split = \ str(member_list[k].local_name).split("_") if len(str_split) == 3 and str_split[-1] == "comp": local_name = str_split[0] + "_" + \ str_split[1] + "_phase_" + "comp" # Get the indexing set i.e. component list index_set = member_list[k].index_set() # Rule for vap flow def rule_vap_flow(self, t, i): return self.control_volume.properties_out[t].\ component(local_name)["Vap", i] self.e_vap_flow = Expression( self.flowsheet().time, index_set, rule=rule_vap_flow) # Rule to link the liq flow to the distillate def rule_bottoms_flow(self, t, i): return self.control_volume.properties_out[t].\ component(local_name)["Liq", i] self.e_bottoms_flow = Expression( self.flowsheet().time, index_set, rule=rule_bottoms_flow) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_bottoms_flow, k) # add the reference and variable name to the # distillate port self.vapor_reboil.add(self.e_vap_flow, k) elif "enth" in k: if "phase" not in k: # assumes total mixture enthalpy (enth_mol or enth_mass) if not member_list[k].is_indexed(): # if state var is not enth_mol/enth_mass # by phase, add _phase string to extract the right # value from the state block local_name = str(member_list[k].local_name) + \ "_phase" else: raise PropertyPackageError( "Enthalpy is indexed but the variable " "name does not reflect the presence of an index. " "Please follow the naming convention outlined " "in the documentation for state variables.") # Rule for vap enthalpy. Setting the enthalpy to the # enth_mol_phase['Vap'] value from the state block def rule_vap_enth(self, t): return self.control_volume.properties_out[t].\ component(local_name)["Vap"] self.e_vap_enth = Expression( self.flowsheet().time, rule=rule_vap_enth) # Rule to link the liq flow to the distillate. # Setting the enthalpy to the # enth_mol_phase['Liq'] value from the state block def rule_bottoms_enth(self, t): return self.control_volume.properties_out[t].\ component(local_name)["Liq"] self.e_bottoms_enth = Expression( self.flowsheet().time, rule=rule_bottoms_enth) # add the reference and variable name to the # distillate port self.bottoms.add(self.e_bottoms_enth, k) # add the reference and variable name to the # distillate port self.vapor_reboil.add(self.e_vap_enth, k) elif "phase" in k: # assumes enth_mol_phase or enth_mass_phase. # This is an intensive property, you create a direct # reference irrespective of the reflux, distillate and # vap_outlet # Rule for vap flow if not member_list[k].is_indexed(): var = self.control_volume.properties_out[:].\ component(member_list[k].local_name) else: var = self.control_volume.properties_out[:].\ component(member_list[k].local_name)[...] # add the reference and variable name to the distillate port self.bottoms.add(Reference(var), k) # add the reference and variable name to the # vapor outlet port self.vapor_reboil.add(Reference(var), k) else: raise PropertyNotSupportedError( "Unrecognized enthalpy state variable encountered " "while building ports for the reboiler. Only total " "mixture enthalpy or enthalpy by phase are supported.") def initialize(self, solver=None, outlvl=0): # TODO: Fix the inlets to the reboiler to the vapor flow from # the top tray or take it as an argument to this method. init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # Initialize the inlet and outlet state blocks self.control_volume.initialize(outlvl=outlvl) if solver is not None: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info( "Initialization Complete, {}.".format(idaeslog.condition(res)) ) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): stream_attributes = {} stream_dict = {"Inlet": "inlet", "Vapor Reboil": "vapor_reboil", "Bottoms": "bottoms"} for n, v in stream_dict.items(): port_obj = getattr(self, v) stream_attributes[n] = {} for k in port_obj.vars: for i in port_obj.vars[k].keys(): if isinstance(i, float): stream_attributes[n][k] = value( port_obj.vars[k][time_point]) else: if len(i) == 2: kname = str(i[1]) else: kname = str(i[1:]) stream_attributes[n][k + " " + kname] = \ value(port_obj.vars[k][time_point, i[1:]]) return DataFrame.from_dict(stream_attributes, orient="columns")
class HeatExchangerData(UnitModelBlockData): """ Simple 0D heat exchange unit. Unit model to transfer heat from one material to another. """ CONFIG = UnitModelBlockData.CONFIG() _make_heat_exchanger_config(CONFIG) def set_scaling_factor_energy(self, f): """ This function sets scaling_factor_energy for both side_1 and side_2. This factor multiplies the energy balance and heat transfer equations in the heat exchnager. The value of this factor should be about 1/(expected heat duty). Args: f: Energy balance scaling factor """ self.side_1.scaling_factor_energy.value = f self.side_2.scaling_factor_energy.value = f def build(self): """ Building model Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() config = self.config # Add variables self.overall_heat_transfer_coefficient = Var( self.flowsheet().config.time, domain=PositiveReals, initialize=100, doc="Overall heat transfer coefficient") self.overall_heat_transfer_coefficient.latex_symbol = "U" self.area = Var(domain=PositiveReals, initialize=1000, doc="Heat exchange area") self.area.latex_symbol = "A" if config.flow_pattern == HeatExchangerFlowPattern.crossflow: self.crossflow_factor = Var( self.flowsheet().config.time, initialize=1, doc="Factor to adjust coutercurrent flow heat transfer " "calculation for cross flow.") if config.delta_temperature_rule == delta_temperature_underwood2_rule: # Define a cube root function that return the real negative root # for the cube root of a negative number. self.cbrt = ExternalFunction(library=functions_lib(), function="cbrt") # Add Control Volumes _make_heater_control_volume(self, "side_1", config.side_1, dynamic=config.dynamic, has_holdup=config.has_holdup) _make_heater_control_volume(self, "side_2", config.side_2, dynamic=config.dynamic, has_holdup=config.has_holdup) # Add Ports self.add_inlet_port(name="inlet_1", block=self.side_1) self.add_inlet_port(name="inlet_2", block=self.side_2) self.add_outlet_port(name="outlet_1", block=self.side_1) self.add_outlet_port(name="outlet_2", block=self.side_2) # Add convienient references to heat duty. add_object_reference(self, "heat_duty", self.side_2.heat) self.side_1.heat.latex_symbol = "Q_1" self.side_2.heat.latex_symbol = "Q_2" @self.Expression(self.flowsheet().config.time, doc="Temperature difference at the side 1 inlet end") def delta_temperature_in(b, t): if b.config.flow_pattern == \ HeatExchangerFlowPattern.countercurrent: return b.side_1.properties_in[t].temperature -\ b.side_2.properties_out[t].temperature elif b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return b.side_1.properties_in[t].temperature -\ b.side_2.properties_in[t].temperature elif b.config.flow_pattern == HeatExchangerFlowPattern.crossflow: return b.side_1.properties_in[t].temperature -\ b.side_2.properties_out[t].temperature else: raise ConfigurationError( "Flow pattern {} not supported".format( b.config.flow_pattern)) @self.Expression(self.flowsheet().config.time, doc="Temperature difference at the side 1 outlet end") def delta_temperature_out(b, t): if b.config.flow_pattern == \ HeatExchangerFlowPattern.countercurrent: return b.side_1.properties_out[t].temperature -\ b.side_2.properties_in[t].temperature elif b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return b.side_1.properties_out[t].temperature -\ b.side_2.properties_out[t].temperature elif b.config.flow_pattern == HeatExchangerFlowPattern.crossflow: return b.side_1.properties_out[t].temperature -\ b.side_2.properties_in[t].temperature # Add a unit level energy balance def unit_heat_balance_rule(b, t): return 0 == self.side_1.heat[t] + self.side_2.heat[t] self.unit_heat_balance = Constraint(self.flowsheet().config.time, rule=unit_heat_balance_rule) # Add heat transfer equation self.delta_temperature = Expression( self.flowsheet().config.time, rule=config.delta_temperature_rule, doc="Temperature difference driving force for heat transfer") self.delta_temperature.latex_symbol = "\\Delta T" if config.flow_pattern == HeatExchangerFlowPattern.crossflow: self.heat_transfer_equation = Constraint( self.flowsheet().config.time, rule=_cross_flow_heat_transfer_rule) else: self.heat_transfer_equation = Constraint( self.flowsheet().config.time, rule=_heat_transfer_rule) def initialize(self, state_args_1=None, state_args_2=None, outlvl=0, solver='ipopt', optarg={'tol': 1e-6}, duty=1000): """ Heat exchanger initialization method. Args: state_args_1 : a dict of arguments to be passed to the property initialization for side_1 (see documentation of the specific property package) (default = {}). state_args_2 : a dict of arguments to be passed to the property initialization for side_2 (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating which solver to use during initialization (default = 'ipopt') duty : an initial guess for the amount of heat transfered (default = 10000) Returns: None """ # Set solver options tee = True if outlvl >= 3 else False opt = SolverFactory(solver) opt.options = optarg flags1 = self.side_1.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, state_args=state_args_1) if outlvl > 0: _log.info('{} Initialization Step 1a (side_1) Complete.'.format( self.name)) flags2 = self.side_2.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, state_args=state_args_2) if outlvl > 0: _log.info('{} Initialization Step 1b (side_2) Complete.'.format( self.name)) # --------------------------------------------------------------------- # Solve unit without heat transfer equation self.heat_transfer_equation.deactivate() self.side_2.heat.fix(duty) results = opt.solve(self, tee=tee, symbolic_solver_labels=True) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: _log.info('{} Initialization Step 2 Complete.'.format( self.name)) else: _log.warning('{} Initialization Step 2 Failed.'.format( self.name)) self.side_2.heat.unfix() self.heat_transfer_equation.activate() # --------------------------------------------------------------------- # Solve unit results = opt.solve(self, tee=tee, symbolic_solver_labels=True) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: _log.info('{} Initialization Step 3 Complete.'.format( self.name)) else: _log.warning('{} Initialization Step 3 Failed.'.format( self.name)) # --------------------------------------------------------------------- # Release Inlet state self.side_1.release_state(flags1, outlvl - 1) self.side_2.release_state(flags2, outlvl - 1) if outlvl > 0: _log.info('{} Initialization Complete.'.format(self.name))
class CSTRData(UnitModelBlockData): """ Standard CSTR Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "has_equilibrium_reactions", ConfigValue( default=False, domain=In([True, False]), description="Equilibrium reaction construction flag", doc="""Indicates whether terms for equilibrium controlled reactions should be constructed, **default** - True. **Valid values:** { **True** - include equilibrium reaction terms, **False** - exclude equilibrium reaction terms.}""")) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) CONFIG.declare( "has_heat_of_reaction", ConfigValue( default=False, domain=In([True, False]), description="Heat of reaction term construction flag", doc="""Indicates whether terms for heat of reaction terms should be constructed, **default** - False. **Valid values:** { **True** - include heat of reaction terms, **False** - exclude heat of reaction terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc= """Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""")) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc= """A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""")) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(CSTRData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args }) self.control_volume.add_geometry() self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_reaction_blocks( has_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=True, has_equilibrium_reactions=self.config.has_equilibrium_reactions, has_phase_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=self.config.has_heat_of_reaction, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add object references add_object_reference(self, "volume", self.control_volume.volume) # Add CSTR performance equation @self.Constraint(self.flowsheet().config.time, self.config.reaction_package.rate_reaction_idx, doc="CSTR performance equation") def cstr_performance_eqn(b, t, r): return b.control_volume.rate_reaction_extent[t, r] == ( b.volume[t] * b.control_volume.reactions[t].reaction_rate[r]) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): add_object_reference(self, "deltaP", self.control_volume.deltaP) def _get_performance_contents(self, time_point=0): var_dict = {"Volume": self.volume[time_point]} if hasattr(self, "heat_duty"): var_dict["Heat Duty"] = self.heat_duty[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] return {"vars": var_dict}
class HeatExchangerData(UnitModelBlockData): """ Simple 0D heat exchange unit. Unit model to transfer heat from one material to another. """ CONFIG = UnitModelBlockData.CONFIG() _make_heat_exchanger_config(CONFIG) def set_scaling_factor_energy(self, f): """ This function sets scaling_factor_energy for both shell and tube. This factor multiplies the energy balance and heat transfer equations in the heat exchnager. The value of this factor should be about 1/(expected heat duty). Args: f: Energy balance scaling factor """ self.shell.scaling_factor_energy.value = f self.tube.scaling_factor_energy.value = f def build(self): """ Building model Args: None Returns: None """ ######################################################################## # Call UnitModel.build to setup dynamics and configure # ######################################################################## super().build() config = self.config ######################################################################## # Add variables # ######################################################################## u = self.overall_heat_transfer_coefficient = Var( self.flowsheet().config.time, domain=PositiveReals, initialize=100.0, doc="Overall heat transfer coefficient") a = self.area = Var(domain=PositiveReals, initialize=1000.0, doc="Heat exchange area") self.delta_temperature_in = Var( self.flowsheet().config.time, initialize=10.0, doc="Temperature difference at the shell inlet end") self.delta_temperature_out = Var( self.flowsheet().config.time, initialize=10.0, doc="Temperature difference at the shell outlet end") if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: self.crossflow_factor = Var( self.flowsheet().config.time, initialize=1.0, doc="Factor to adjust coutercurrent flow heat " "transfer calculation for cross flow.") f = self.crossflow_factor ######################################################################## # Add control volumes # ######################################################################## _make_heater_control_volume(self, "shell", config.shell, dynamic=config.dynamic, has_holdup=config.has_holdup) _make_heater_control_volume(self, "tube", config.tube, dynamic=config.dynamic, has_holdup=config.has_holdup) # Add convienient references to heat duty. q = self.heat_duty = Reference(self.tube.heat) ######################################################################## # Add ports # ######################################################################## self.add_inlet_port(name="inlet_1", block=self.shell) self.add_inlet_port(name="inlet_2", block=self.tube) self.add_outlet_port(name="outlet_1", block=self.shell) self.add_outlet_port(name="outlet_2", block=self.tube) ######################################################################## # Add end temperaure differnece constraints # ######################################################################## @self.Constraint(self.flowsheet().config.time) def delta_temperature_in_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return (b.delta_temperature_in[t] == b.shell.properties_in[t].temperature - b.tube.properties_in[t].temperature) else: return (b.delta_temperature_in[t] == b.shell.properties_in[t].temperature - b.tube.properties_out[t].temperature) @self.Constraint(self.flowsheet().config.time) def delta_temperature_out_equation(b, t): if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent: return (b.delta_temperature_out[t] == b.shell.properties_out[t].temperature - b.tube.properties_out[t].temperature) else: return (b.delta_temperature_out[t] == b.shell.properties_out[t].temperature - b.tube.properties_in[t].temperature) ######################################################################## # Add a unit level energy balance # ######################################################################## @self.Constraint(self.flowsheet().config.time) def unit_heat_balance(b, t): return 0 == self.shell.heat[t] + self.tube.heat[t] ######################################################################## # Add delta T calculations using callack function, lots of options, # # and users can provide their own if needed # ######################################################################## config.delta_temperature_callback(self) ######################################################################## # Add Heat transfer equation # ######################################################################## deltaT = self.delta_temperature scale = self.shell.scaling_factor_energy @self.Constraint(self.flowsheet().config.time) def heat_transfer_equation(b, t): if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: return 0 == (f[t] * u[t] * a * deltaT[t] - q[t]) * scale else: return 0 == (u[t] * a * deltaT[t] - q[t]) * scale ######################################################################## # Add symbols for LaTeX equation rendering # ######################################################################## self.overall_heat_transfer_coefficient.latex_symbol = "U" self.area.latex_symbol = "A" self.shell.heat.latex_symbol = "Q_1" self.tube.heat.latex_symbol = "Q_2" self.delta_temperature.latex_symbol = "\\Delta T" def initialize(self, state_args_1=None, state_args_2=None, outlvl=0, solver='ipopt', optarg={'tol': 1e-6}, duty=1000): """ Heat exchanger initialization method. Args: state_args_1 : a dict of arguments to be passed to the property initialization for shell (see documentation of the specific property package) (default = {}). state_args_2 : a dict of arguments to be passed to the property initialization for tube (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating which solver to use during initialization (default = 'ipopt') duty : an initial guess for the amount of heat transfered (default = 10000) Returns: None """ # Set solver options tee = True if outlvl >= 3 else False opt = SolverFactory(solver) opt.options = optarg flags1 = self.shell.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, state_args=state_args_1) if outlvl > 0: _log.info('{} Initialization Step 1a (shell) Complete.'.format( self.name)) flags2 = self.tube.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, state_args=state_args_2) if outlvl > 0: _log.info('{} Initialization Step 1b (tube) Complete.'.format( self.name)) # --------------------------------------------------------------------- # Solve unit without heat transfer equation self.heat_transfer_equation.deactivate() self.tube.heat.fix(duty) results = opt.solve(self, tee=tee, symbolic_solver_labels=True) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: _log.info('{} Initialization Step 2 Complete.'.format( self.name)) else: _log.warning('{} Initialization Step 2 Failed.'.format( self.name)) self.tube.heat.unfix() self.heat_transfer_equation.activate() # --------------------------------------------------------------------- # Solve unit results = opt.solve(self, tee=tee, symbolic_solver_labels=True) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: _log.info('{} Initialization Step 3 Complete.'.format( self.name)) else: _log.warning('{} Initialization Step 3 Failed.'.format( self.name)) # --------------------------------------------------------------------- # Release Inlet state self.shell.release_state(flags1, outlvl - 1) self.tube.release_state(flags2, outlvl - 1) if outlvl > 0: _log.info('{} Initialization Complete.'.format(self.name)) def _get_performance_contents(self, time_point=0): var_dict = { "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] } var_dict["HX Area"] = self.area var_dict["Heat Duty"] = self.heat_duty[time_point] if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} expr_dict = {} expr_dict["Delta T Driving"] = self.delta_temperature[time_point] expr_dict["Delta T In"] = self.delta_temperature_in[time_point] expr_dict["Delta T Out"] = self.delta_temperature_out[time_point] return {"vars": var_dict, "exprs": expr_dict} def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Shell Inlet": self.inlet_1, "Shell Outlet": self.outlet_1, "Tube Inlet": self.inlet_2, "Tube Outlet": self.outlet_2 }, time_point=time_point) def get_costing(self, module=costing): if not hasattr(self.flowsheet(), 'costing'): self.flowsheet().get_costing() self.costing = Block() module.hx_costing(self.costing)
class WaterPipeData(UnitModelBlockData): """ Water or steam pipe Unit Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of material balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=True, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "contraction_expansion_at_end", ConfigValue(default='None', domain=In(['None', 'contraction', 'expansion']), description='Any contraction or expansion at the end', doc='Define if pressure drop due to contraction' ' or expansion needs to be considered')) CONFIG.declare( "water_phase", ConfigValue(default='Liq', domain=In(['Liq', 'Vap']), description='Water phase', doc='''Define water phase for property calls, mixed phase not supported''')) def build(self): """ Begin building model """ # Call UnitModel.build to setup dynamics super(WaterPipeData, 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, ) 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 Volume self._set_geometry() # Construct performance equations self._make_performance() def _set_geometry(self): """ Define the geometry of the unit as necessary """ # Number of pipe self.number_of_pipes = Var(initialize=4, doc="Number of pipes") # Length of pipe self.length = Var(initialize=10.0, doc="Total length of the straight pipe") # Elevation change of pipe, elevation of outlet - elevation of inlet self.elevation_change = Var( initialize=10.0, doc="Change of elevation from inlet to outlet") # Inside diameter of pipe self.diameter = Var(initialize=0.6, doc="Inside diameter of pipe") if not self.config.contraction_expansion_at_end == "None": self.area_ratio = Var( initialize=1, doc="Cross section area ratio of exit end to pipe") # Volume constraint @self.Constraint(self.flowsheet().config.time, doc="Total volume of all pipes") def volume_eqn(b, t): return b.volume[t] == 0.25 * const.pi * b.diameter**2 \ * b.length * b.number_of_pipes def _make_performance(self): """ Define constraints which describe the behaviour of the unit model. """ phase = self.config.water_phase # Add performance variables # Velocity of fluid inside pipe self.velocity = Var(self.flowsheet().config.time, initialize=10.0, doc='Fluid velocity inside pipe') # Reynolds number self.N_Re = Var(self.flowsheet().config.time, initialize=10000.0, doc='Reynolds number') # Darcy friction factor self.friction_factor_darcy = Var(self.flowsheet().config.time, initialize=0.005, doc='Darcy friction factor') # Correction factor for pressure drop due to friction self.fcorrection_dp = Var(initialize=1.0, doc="Correction factor for pressure drop") # Pressure change due to friction self.deltaP_friction = Var(self.flowsheet().config.time, initialize=-1.0, doc='Pressure change due to friction') # Pressure change due to gravity self.deltaP_gravity = Var(self.flowsheet().config.time, initialize=0.0, doc='Pressure change due to gravity') # Pressure change due to area change at end self.deltaP_area_change = Var(self.flowsheet().config.time, initialize=0.0, doc='Pressure change due to area change') # Equation for calculating velocity @self.Constraint(self.flowsheet().config.time, doc="Velocity of fluid inside pipe") def velocity_eqn(b, t): return b.velocity[t]*0.25*const.pi*b.diameter**2\ * b.number_of_pipes == \ b.control_volume.properties_in[t].flow_vol # Equation for calculating Reynolds number @self.Constraint(self.flowsheet().config.time, doc="Reynolds number") def Reynolds_number_eqn(b, t): return b.N_Re[t] * \ b.control_volume.properties_in[t].visc_d_phase[phase] == \ b.diameter * b.velocity[t] *\ b.control_volume.properties_in[t].dens_mass_phase[phase] # Friction factor expression depending on laminar or turbulent flow @self.Constraint(self.flowsheet().config.time, doc="Darcy friction factor as" " a function of Reynolds number") def friction_factor_darcy_eqn(b, t): return b.friction_factor_darcy[t] * b.N_Re[t]**(0.25) == \ 0.3164 * b.fcorrection_dp # Pressure change equation for friction @self.Constraint(self.flowsheet().config.time, doc="Pressure change due to friction") def pressure_change_friction_eqn(b, t): return b.deltaP_friction[t] * b.diameter == \ - 0.5 * b.control_volume.properties_in[t].\ dens_mass_phase[phase] * \ b.velocity[t]**2 * b.friction_factor_darcy[t] * b.length # Pressure change equation for gravity @self.Constraint(self.flowsheet().config.time, doc="Pressure change due to gravity") def pressure_change_gravity_eqn(b, t): return b.deltaP_gravity[t] == -const.acceleration_gravity * \ b.control_volume.properties_in[t].dens_mass_phase[phase]\ * b.elevation_change # Pressure change equation for contraction or expansion @self.Constraint(self.flowsheet().config.time, doc="Pressure change due to gravity") def pressure_change_area_change_eqn(b, t): if self.config.contraction_expansion_at_end == "contraction": return b.deltaP_area_change[t] == - (0.1602*b.area_ratio**2 - 0.646*b.area_ratio + 1.4858) * \ 0.5 * b.control_volume.properties_out[t].\ dens_mass_phase[phase] * (b.velocity[t]/b.area_ratio)**2 \ + 0.5*b.control_volume.properties_out[t].\ dens_mass_phase[phase]*b.velocity[t]**2 elif self.config.contraction_expansion_at_end == "expansion": return b.deltaP_area_change[t] == \ b.control_volume.properties_out[t].dens_mass_phase[phase]\ * b.velocity[t]**2*(b.area_ratio-1)/b.area_ratio**2 else: return b.deltaP_area_change[t] == 0 # Total pressure change equation @self.Constraint(self.flowsheet().config.time, doc="Pressure drop") def pressure_change_total_eqn(b, t): return b.deltaP[t] == (b.deltaP_friction[t] + b.deltaP_gravity[t] + b.deltaP_area_change[t]) def set_initial_condition(self): if self.config.dynamic is True: self.control_volume.material_accumulation[:, :, :].value = 0 self.control_volume.energy_accumulation[:, :].value = 0 self.control_volume.material_accumulation[0, :, :].fix(0) self.control_volume.energy_accumulation[0, :].fix(0) def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, solver='ipopt', optarg={'tol': 1e-6}): ''' WaterPipe initialization routine. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) for the control_volume of the model to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") opt = SolverFactory(solver) opt.options = optarg flags = blk.control_volume.initialize(outlvl=outlvl + 1, optarg=optarg, solver=solver, state_args=state_args) init_log.info_high("Initialization Step 1 Complete.") # Fix outlet enthalpy and pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].pressure.fix( value(blk.control_volume.properties_in[t].pressure)) blk.pressure_change_total_eqn.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) # Unfix outlet enthalpy and pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].pressure.unfix() blk.pressure_change_total_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) blk.control_volume.release_state(flags, outlvl) init_log.info("Initialization Complete.") def calculate_scaling_factors(self): pass