class TrayData(UnitModelBlockData): """ Tray unit for distillation model. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue(domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. Flash units do not support dynamic behavior.""")) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Indicates whether holdup terms should be constructed or not. **default** - False. Flash units do not have defined volume, thus this must be False.""")) CONFIG.declare( "is_feed_tray", ConfigValue(default=False, domain=Bool, description="flag to indicate feed tray.", doc="""indicates if this is a feed tray and constructs corresponding ports, **default** - False. **Valid values:** { **True** - feed tray, **False** - conventional tray with no feed inlet}""")) CONFIG.declare( "has_liquid_side_draw", ConfigValue( default=False, domain=Bool, description="liquid side draw construction flag.", doc="""indicates if there is a liquid side draw from the tray, **default** - False. **Valid values:** { **True** - include a liquid side draw from the tray, **False** - exclude a liquid side draw from the tray.}""")) CONFIG.declare( "has_vapor_side_draw", ConfigValue( default=False, domain=Bool, description="vapor side draw construction flag.", doc="""indicates if there is a vapor side draw from the tray, **default** - False. **Valid values:** { **True** - include a vapor side draw from the tray, **False** - exclude a vapor side draw from the tray.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue(default=False, domain=Bool, description="heat duty to/from tray construction flag.", doc="""indicates if there is heat duty to/from the tray, **default** - False. **Valid values:** { **True** - include a heat duty term, **False** - exclude a heat duty term.}""")) 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.}""")) 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(TrayData, self).build() # Create the inlet list to build inlet state blocks if self.config.is_feed_tray: inlet_list = ["feed", "liq", "vap"] else: inlet_list = ["liq", "vap"] # Create a dict to set up the inlet state blocks state_block_args = dict(**self.config.property_package_args) state_block_args["has_phase_equilibrium"] = True state_block_args["defined_state"] = True for i in inlet_list: state_obj = self.config.property_package.build_state_block( self.flowsheet().time, doc="State block for " + i + "_inlet to tray", default=state_block_args) setattr(self, "properties_in_" + i, state_obj) # Create a dict to set up the mixed outlet state blocks mixed_block_args = dict(**self.config.property_package_args) mixed_block_args["has_phase_equilibrium"] = True mixed_block_args["defined_state"] = False self.properties_out = self.config.property_package.\ build_state_block(self.flowsheet().time, doc="State block for mixed outlet from tray", default=mixed_block_args) self._add_material_balance() self._add_energy_balance() self._add_pressure_balance() # 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._add_ports() def _add_material_balance(self): """Method to construct the mass balance equation.""" @self.Constraint(self.flowsheet().time, self.config.property_package.component_list, doc="material balance") def material_mixing_equations(b, t, j): if self.config.is_feed_tray: return 0 == sum( self.properties_in_feed[t].get_material_flow_terms(p, j) + self.properties_in_liq[t].get_material_flow_terms(p, j) + self.properties_in_vap[t].get_material_flow_terms(p, j) - self.properties_out[t].get_material_flow_terms(p, j) for p in b.config.property_package.phase_list) else: return 0 == sum( self.properties_in_liq[t].get_material_flow_terms(p, j) + self.properties_in_vap[t].get_material_flow_terms(p, j) - self.properties_out[t].get_material_flow_terms(p, j) for p in b.config.property_package.phase_list) def _add_energy_balance(self): """Method to construct the energy balance equation.""" if self.config.has_heat_transfer: units_meta = self.config.property_package.get_metadata() self.heat_duty = Var(self.flowsheet().time, initialize=0, doc="Heat duty for the tray", units=units_meta.get_derived_units("power")) @self.Constraint(self.flowsheet().time, doc="energy balance") def enthalpy_mixing_equations(b, t): if self.config.is_feed_tray: if self.config.has_heat_transfer: return 0 == ( sum(self.properties_in_feed[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) + sum(self.properties_in_liq[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) + sum(self.properties_in_vap[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) - sum(self.properties_out[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) + \ self.heat_duty[t] else: return 0 == ( sum(self.properties_in_feed[t].get_enthalpy_flow_terms( p) for p in b.config.property_package.phase_list) + sum(self.properties_in_liq[t].get_enthalpy_flow_terms( p) for p in b.config.property_package.phase_list) + sum(self.properties_in_vap[t].get_enthalpy_flow_terms( p) for p in b.config.property_package.phase_list) - sum(self.properties_out[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) else: if self.config.has_heat_transfer: return 0 == ( sum(self.properties_in_liq[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) + sum(self.properties_in_vap[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) - sum(self.properties_out[t]. get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) + \ self.heat_duty[t] else: return 0 == (sum( self.properties_in_liq[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list ) + sum( self.properties_in_vap[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) - sum( self.properties_out[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) def _add_pressure_balance(self): """Method to construct the pressure balance.""" if self.config.has_pressure_change: units_meta = self.config.property_package.get_metadata() self.deltaP = Var(self.flowsheet().time, initialize=0, doc="Pressure drop across tray", units=units_meta.get_derived_units("pressure")) @self.Constraint(self.flowsheet().time, doc="pressure balance for tray") def pressure_drop_equation(self, t): if self.config.has_pressure_change: return self.properties_out[t].pressure == \ self.properties_in_liq[t].pressure - self.deltaP[t] else: return self.properties_out[t].pressure == \ self.properties_in_liq[t].pressure def _add_ports(self): """Method to construct the ports for the tray.""" # Add feed inlet port if self.config.is_feed_tray: self.add_inlet_port(name="feed", block=self.properties_in_feed) # Add liquid and vapor inlet ports self.add_inlet_port(name="liq_in", block=self.properties_in_liq) self.add_inlet_port(name="vap_in", block=self.properties_in_vap) # Add liquid outlet port self.liq_out = Port(noruleinit=True, doc="liquid outlet from tray") # Add liquid side draw port if selected if self.config.has_liquid_side_draw: self.liq_side_sf = Var( initialize=0.01, doc="split fraction for the liquid side draw") self.liq_side_draw = Port(noruleinit=True, doc="liquid side draw.") self._make_phase_split( port=self.liq_side_draw, phase=self._liquid_set, has_liquid_side_draw=self.config.has_liquid_side_draw, side_sf=self.liq_side_sf) # Populate the liquid outlet port with the remaining liquid # after the side draw self._make_phase_split(port=self.liq_out, phase=self._liquid_set, side_sf=1 - self.liq_side_sf) else: # Populate the liquid outlet port when no liquid side draw self._make_phase_split(port=self.liq_out, phase=self._liquid_set, side_sf=1) # Add the vapor outlet port self.vap_out = Port(noruleinit=True, doc="vapor outlet from tray") # Add vapor side draw port if selected if self.config.has_vapor_side_draw: self.vap_side_sf = Var( initialize=0.01, doc="split fraction for the vapor side draw") self.vap_side_draw = Port(noruleinit=True, doc="vapor side draw.") self._make_phase_split( port=self.vap_side_draw, phase=self._vapor_set, has_vapor_side_draw=self.config.has_vapor_side_draw, side_sf=self.vap_side_sf) # Populate the vapor outlet port with the remaining vapor # after the vapor side draw self._make_phase_split(port=self.vap_out, phase=self._vapor_set, side_sf=1 - self.vap_side_sf) else: # Populate the vapor outlet port when no vapor side draw self._make_phase_split(port=self.vap_out, phase=self._vapor_set, side_sf=1) def _make_phase_split(self, port=None, phase=None, has_liquid_side_draw=False, has_vapor_side_draw=False, side_sf=None): """Method to split and populate the outlet ports with corresponding phase values from the mixed stream outlet block.""" member_list = self.properties_out[0].define_port_members() 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.properties_out[:].\ component(local_name) else: var = self.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the port ref = Reference(var) setattr(self, "_" + k + "_ref", ref) port.add(ref, 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.properties_out[0], "mole_frac_phase_comp") and \ hasattr(self.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.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 local_name: if hasattr(self.properties_out[0], "mass_frac_phase_comp") and \ hasattr(self.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.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 mole fraction def rule_mole_frac(self, t, i): if not flow_phase_comp: sum_flow_comp = sum( self.properties_out[t].component( local_name_frac)[p, i] * self.properties_out[t].component( local_name_flow)[p] for p in phase) return sum_flow_comp / sum( self.properties_out[t].component( local_name_flow)[p] for p in phase) else: sum_flow_comp = sum( self.properties_out[t].component( local_name_flow)[p, i] for p in phase) return sum_flow_comp / sum( self.properties_out[t].component( local_name_flow)[p, i] for p in phase for i in self.config.property_package.component_list) # add the reference and variable name to the port expr = Expression(self.flowsheet().time, index_set, rule=rule_mole_frac) self.add_component("e_mole_frac_" + port.local_name, expr) port.add(expr, 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.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the port ref = Reference(var) setattr(self, "_" + k + "_" + port.local_name + "_ref", ref) port.add(ref, 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 to link the flow to the port def rule_flow(self, t): return sum(self.properties_out[t].component( local_name_flow)[p] for p in phase) * (side_sf) # add the reference and variable name to the port expr = Expression(self.flowsheet().time, rule=rule_flow) self.add_component("e_flow_" + port.local_name, expr) port.add(expr, k) 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 to link the flow to the port def rule_flow(self, t, i): return sum(self.properties_out[t].component( local_name_flow)[p, i] for p in phase) * (side_sf) expr = Expression(self.flowsheet().time, index_set, rule=rule_flow) self.add_component("e_flow_" + port.local_name, expr) port.add(expr, local_name) elif "phase" in local_name: # flow is indexed by phase and comp # Get the indexing sets i.e. component list and phase list component_set = self.config.\ property_package.component_list phase_set = self.config.\ property_package.phase_list def rule_flow(self, t, p, i): if (phase is self._liquid_set and p in self._liquid_set) or \ (phase is self._vapor_set and p in self._vapor_set) : # pass appropriate phase flow values to port return (self.properties_out[t].component( local_name)[p, i]) * (side_sf) else: # return small number for phase that should not # be in the appropriate port. For example, # the state vars will be flow_mol_phase_comp # which will include all phases. The liq port # should have the correct references to the liq # phase flow but the vapor phase flow should be 0. return 1e-8 expr = Expression(self.flowsheet().time, phase_set, component_set, rule=rule_flow) self.add_component("e_" + local_name + port.local_name, expr) port.add(expr, k) else: raise PropertyPackageError( "Unrecognized flow state variable encountered " "while building ports for the tray. Please follow " "the naming convention outlined in the documentation " "for state variables.") 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_phase = 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 to link the phase enthalpy to the port. def rule_enth(self, t): return sum(self.properties_out[t].component( local_name_phase)[p] for p in phase) expr = Expression(self.flowsheet().time, rule=rule_enth) self.add_component("e_enth_" + port.local_name, expr) # add the reference and variable name to the port port.add(expr, 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 if not member_list[k].is_indexed(): var = self.properties_out[:].\ component(local_name) else: var = self.properties_out[:].\ component(local_name)[...] # add the reference and variable name to the port ref = Reference(var) setattr(self, "_" + k + "_" + port.local_name + "_ref", ref) port.add(ref, k) else: raise PropertyNotSupportedError( "Unrecognized enthalpy state variable encountered " "while building ports for the tray. Only total " "mixture enthalpy or enthalpy by phase are supported.") def initialize(self, state_args_feed=None, state_args_liq=None, state_args_vap=None, hold_state_liq=False, hold_state_vap=False, solver=None, optarg=None, outlvl=idaeslog.NOTSET): # TODO: # 1. Initialization for dynamic mode. Currently not supported. # 2. Handle unfixed side split fraction vars # 3. Better logic to handle and fix state vars. init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") init_log.info("Begin initialization.") solverobj = get_solver(solver, optarg) if self.config.has_liquid_side_draw: if not self.liq_side_sf.fixed: raise ConfigurationError( "Liquid side draw split fraction not fixed but " "has_liquid_side_draw set to True.") if self.config.has_vapor_side_draw: if not self.vap_side_sf.fixed: raise ConfigurationError( "Vapor side draw split fraction not fixed but " "has_vapor_side_draw set to True.") # Create initial guess if not provided by using current values if self.config.is_feed_tray and state_args_feed is None: state_args_feed = {} state_args_liq = {} state_args_vap = {} state_dict = (self.properties_in_feed[ self.flowsheet().time.first()].define_port_members()) for k in state_dict.keys(): if "flow" in k: if state_dict[k].is_indexed(): state_args_feed[k] = {} state_args_liq[k] = {} state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_feed[k][m] = \ value(state_dict[k][m]) state_args_liq[k][m] = \ value(0.1 * state_dict[k][m]) state_args_vap[k][m] = \ value(0.1 * state_dict[k][m]) else: state_args_feed[k] = value(state_dict[k]) state_args_liq[k] = 0.1 * value(state_dict[k]) state_args_vap[k] = 0.1 * value(state_dict[k]) else: if state_dict[k].is_indexed(): state_args_feed[k] = {} state_args_liq[k] = {} state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_feed[k][m] = \ value(state_dict[k][m]) state_args_liq[k][m] = \ value(state_dict[k][m]) state_args_vap[k][m] = \ value(state_dict[k][m]) else: state_args_feed[k] = value(state_dict[k]) state_args_liq[k] = value(state_dict[k]) state_args_vap[k] = value(state_dict[k]) # Create initial guess if not provided by using current values if not self.config.is_feed_tray and state_args_liq is None: state_args_liq = {} state_dict = (self.properties_in_liq[ self.flowsheet().time.first()].define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args_liq[k] = {} for m in state_dict[k].keys(): state_args_liq[k][m] = \ value(state_dict[k][m]) else: state_args_liq[k] = value(state_dict[k]) # Create initial guess if not provided by using current values if not self.config.is_feed_tray and state_args_vap is None: state_args_vap = {} state_dict = (self.properties_in_vap[ self.flowsheet().time.first()].define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_vap[k][m] = \ value(state_dict[k][m]) else: state_args_vap[k] = value(state_dict[k]) if self.config.is_feed_tray: feed_flags = self.properties_in_feed.initialize( outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_feed, state_vars_fixed=False) liq_in_flags = self.properties_in_liq. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_liq, state_vars_fixed=False) vap_in_flags = self.properties_in_vap. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_vap, state_vars_fixed=False) # state args to initialize the mixed outlet state block state_args_mixed = {} if self.config.is_feed_tray: # if feed tray, initialize the mixed state block at # the same condition. state_args_mixed = state_args_feed else: # if not feed tray, initialize mixed state block at average of # vap/liq inlets except pressure. While this is crude, it # will work for most combination of state vars. state_dict = (self.properties_in_liq[ self.flowsheet().time.first()].define_port_members()) for k in state_dict.keys(): if k == "pressure": # Take the lowest pressure and this is the liq inlet state_args_mixed[k] = value( self.properties_in_liq[0].component( state_dict[k].local_name)) elif state_dict[k].is_indexed(): state_args_mixed[k] = {} for m in state_dict[k].keys(): if "flow" in k: state_args_mixed[k][m] = \ value(self.properties_in_liq[0]. component(state_dict[k].local_name)[m]) \ + value(self.properties_in_vap[0]. component(state_dict[k].local_name)[m]) else: state_args_mixed[k][m] = \ 0.5 * (value(self.properties_in_liq[0]. component(state_dict[k]. local_name)[m]) + value(self.properties_in_vap[0]. component(state_dict[k].local_name)[m])) else: if "flow" in k: state_args_mixed[k] = \ value(self.properties_in_liq[0]. component(state_dict[k].local_name)) +\ value(self.properties_in_vap[0]. component(state_dict[k].local_name)) else: state_args_mixed[k] = \ 0.5 * (value(self.properties_in_liq[0]. component(state_dict[k].local_name)) + value(self.properties_in_vap[0]. component(state_dict[k].local_name))) # Initialize the mixed outlet state block self.properties_out. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=False, state_args=state_args_mixed, state_vars_fixed=False) # Deactivate energy balance self.enthalpy_mixing_equations.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].temperature.\ fix(state_args_mixed["temperature"]) except AttributeError: init_log.warning("Trying to fix outlet temperature " "during initialization but temperature attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") # Deactivate pressure balance self.pressure_drop_equation.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].pressure.\ fix(state_args_mixed["pressure"]) except AttributeError: init_log.warning("Trying to fix outlet pressure " "during initialization but pressure attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solverobj.solve(self, tee=slc.tee) init_log.info("Mass balance solve {}.".format(idaeslog.condition(res))) # Activate energy balance self.enthalpy_mixing_equations.activate() try: self.properties_out[:].temperature.unfix() except AttributeError: pass with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solverobj.solve(self, tee=slc.tee) init_log.info("Mass and energy balance solve {}.".format( idaeslog.condition(res))) # Activate pressure balance self.pressure_drop_equation.activate() try: self.properties_out[:].pressure.unfix() except AttributeError: pass if degrees_of_freedom(self) == 0: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solverobj.solve(self, tee=slc.tee) init_log.info("Mass, energy and pressure balance solve {}.".format( idaeslog.condition(res))) else: raise Exception("State vars fixed but degrees of freedom " "for tray block is not zero during " "initialization.") if not check_optimal_termination(res): raise InitializationError( f"{self.name} failed to initialize successfully. Please check " f"the output logs for more information.") init_log.info("Initialization complete, status {}.".format( idaeslog.condition(res))) if not self.config.is_feed_tray: if not hold_state_vap: self.properties_in_vap.release_state(flags=vap_in_flags, outlvl=outlvl) if not hold_state_liq: self.properties_in_liq.release_state(flags=liq_in_flags, outlvl=outlvl) if hold_state_liq and hold_state_vap: return liq_in_flags, vap_in_flags elif hold_state_vap: return vap_in_flags elif hold_state_liq: return liq_in_flags else: self.properties_in_liq.release_state(flags=liq_in_flags, outlvl=outlvl) self.properties_in_vap.release_state(flags=vap_in_flags, outlvl=outlvl) return feed_flags
def _define_turbine_multistage_config(config): config.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag", doc= "Only False, in a dynamic flowsheet this is psuedo-steady-state.", ), ) config.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag", doc= "Only False, in a dynamic flowsheet this is psuedo-steady-state.", ), ) 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( "num_parallel_inlet_stages", ConfigValue( default=4, domain=int, description= "Number of parallel inlet stages to simulate partial arc " "admission. Default=4", ), ) config.declare( "throttle_valve_function", ConfigValue( default=ValveFunctionType.linear, domain=In(ValveFunctionType), description= "Valve function type, if custom provide an expression rule", doc= """The type of valve function, if custom provide an expression rule with the valve_function_rule argument. **default** - ValveFunctionType.linear **Valid values** - { ValveFunctionType.linear, ValveFunctionType.quick_opening, ValveFunctionType.equal_percentage, ValveFunctionType.custom}""", ), ) config.declare( "throttle_valve_function_callback", ConfigValue( default=None, description="A callback to add a custom valve function to the " "throttle valves or None. If a callback is provided, it should " "take the valve block data as an argument and add a " "valve_function expressions to it. Default=None", ), ) config.declare( "num_hp", ConfigValue( default=2, domain=int, description= "Number of high pressure stages not including inlet stage", doc="Number of high pressure stages not including inlet stage", ), ) config.declare( "num_ip", ConfigValue( default=10, domain=int, description="Number of intermediate pressure stages", doc="Number of intermediate pressure stages", ), ) config.declare( "num_lp", ConfigValue( default=5, domain=int, description= "Number of low pressure stages not including outlet stage", doc="Number of low pressure stages not including outlet stage", ), ) config.declare( "hp_split_locations", ConfigList( default=[], domain=int, description="Locations of splitters in HP section", doc="A list of index locations of splitters in the HP section. The " "indexes indicate after which stage to include splitters. 0 is " "between the inlet stage and the first regular HP stage.", ), ) config.declare( "ip_split_locations", ConfigList( default=[], domain=int, description="Locations of splitters in IP section", doc="A list of index locations of splitters in the IP section. The " "indexes indicate after which stage to include splitters.", ), ) config.declare( "lp_split_locations", ConfigList( default=[], domain=int, description="Locations of splitter in LP section", doc="A list of index locations of splitters in the LP section. The " "indexes indicate after which stage to include splitters.", ), ) config.declare( "hp_disconnect", ConfigList( default=[], domain=int, description="HP Turbine stages to not connect to next with an arc.", doc="HP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "ip_disconnect", ConfigList( default=[], domain=int, description="IP Turbine stages to not connect to next with an arc.", doc="IP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "lp_disconnect", ConfigList( default=[], domain=int, description="LP Turbine stages to not connect to next with an arc.", doc="LP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "hp_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, hp split index: number of splitter outlets, if not 2", ), ) config.declare( "ip_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, ip split index: number of splitter outlets, if not 2", ), ) config.declare( "lp_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, lp split index: number of splitter outlets, if not 2", ), )
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}
def _trf_config(): """ Generate the configuration dictionary. The user may change the configuration options during the instantiation of the trustregion solver: >>> optTRF = SolverFactory('trustregion', ... solver='ipopt', ... maximum_iterations=50, ... minimum_radius=1e-5, ... verbose=True) The user may also update the configuration after instantiation: >>> optTRF = SolverFactory('trustregion') >>> optTRF._CONFIG.trust_radius = 0.5 The user may also update the configuration as part of the solve call: >>> optTRF = SolverFactory('trustregion') >>> optTRF.solve(model, decision_variables, trust_radius=0.5) Returns ------- CONFIG : ConfigDict This holds all configuration options to be passed to the TRF solver. """ CONFIG = ConfigDict('TrustRegion') ### Solver options CONFIG.declare( 'solver', ConfigValue(default='ipopt', description='Solver to use. Default = ``ipopt``.')) CONFIG.declare( 'keepfiles', ConfigValue(default=False, domain=Bool, description="Optional. Whether or not to " "write files of sub-problems for use in debugging. " "Default = False.")) CONFIG.declare( 'tee', ConfigValue(default=False, domain=Bool, description="Optional. Sets the ``tee`` " "for sub-solver(s) utilized. " "Default = False.")) ### Trust Region specific options CONFIG.declare( 'verbose', ConfigValue(default=False, domain=Bool, description="Optional. When True, print each " "iteration's relevant information to the console " "as well as to the log. " "Default = False.")) CONFIG.declare( 'trust_radius', ConfigValue(default=1.0, domain=PositiveFloat, description="Initial trust region radius ``delta_0``. " "Default = 1.0.")) CONFIG.declare( 'minimum_radius', ConfigValue( default=1e-6, domain=PositiveFloat, description="Minimum allowed trust region radius ``delta_min``. " "Default = 1e-6.")) CONFIG.declare( 'maximum_radius', ConfigValue( default=CONFIG.trust_radius * 100, domain=PositiveFloat, description="Maximum allowed trust region radius. If trust region " "radius reaches maximum allowed, solver will exit. " "Default = 100 * trust_radius.")) CONFIG.declare( 'maximum_iterations', ConfigValue(default=50, domain=PositiveInt, description="Maximum allowed number of iterations. " "Default = 50.")) ### Termination options CONFIG.declare( 'feasibility_termination', ConfigValue( default=1e-5, domain=PositiveFloat, description= "Feasibility measure termination tolerance ``epsilon_theta``. " "Default = 1e-5.")) CONFIG.declare( 'step_size_termination', ConfigValue( default=CONFIG.feasibility_termination, domain=PositiveFloat, description="Step size termination tolerance ``epsilon_s``. " "Matches the feasibility termination tolerance by default.")) ### Switching Condition options CONFIG.declare( 'minimum_feasibility', ConfigValue(default=1e-4, domain=PositiveFloat, description="Minimum feasibility measure ``theta_min``. " "Default = 1e-4.")) CONFIG.declare( 'switch_condition_kappa_theta', ConfigValue( default=0.1, domain=In(NumericRange(0, 1, 0, (False, False))), description="Switching condition parameter ``kappa_theta``. " "Contained in open set (0, 1). " "Default = 0.1.")) CONFIG.declare( 'switch_condition_gamma_s', ConfigValue(default=2.0, domain=PositiveFloat, description="Switching condition parameter ``gamma_s``. " "Must satisfy: ``gamma_s > 1/(1+mu)`` where ``mu`` " "is contained in set (0, 1]. " "Default = 2.0.")) ### Trust region update/ratio test parameters CONFIG.declare( 'radius_update_param_gamma_c', ConfigValue( default=0.5, domain=In(NumericRange(0, 1, 0, (False, False))), description="Lower trust region update parameter ``gamma_c``. " "Default = 0.5.")) CONFIG.declare( 'radius_update_param_gamma_e', ConfigValue( default=2.5, domain=In(NumericRange(1, None, 0)), description="Upper trust region update parameter ``gamma_e``. " "Default = 2.5.")) CONFIG.declare( 'ratio_test_param_eta_1', ConfigValue(default=0.05, domain=In(NumericRange(0, 1, 0, (False, False))), description="Lower ratio test parameter ``eta_1``. " "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. " "Default = 0.05.")) CONFIG.declare( 'ratio_test_param_eta_2', ConfigValue(default=0.2, domain=In(NumericRange(0, 1, 0, (False, False))), description="Lower ratio test parameter ``eta_2``. " "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. " "Default = 0.2.")) ### Filter CONFIG.declare( 'maximum_feasibility', ConfigValue( default=50.0, domain=PositiveFloat, description="Maximum allowable feasibility measure ``theta_max``. " "Parameter for use in filter method." "Default = 50.0.")) CONFIG.declare( 'param_filter_gamma_theta', ConfigValue( default=0.01, domain=In(NumericRange(0, 1, 0, (False, False))), description="Fixed filter parameter ``gamma_theta`` within (0, 1). " "Default = 0.01")) CONFIG.declare( 'param_filter_gamma_f', ConfigValue( default=0.01, domain=In(NumericRange(0, 1, 0, (False, False))), description="Fixed filter parameter ``gamma_f`` within (0, 1). " "Default = 0.01")) return CONFIG
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=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("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.}""")) CONFIG.declare("length_domain_set", ConfigValue( default=[0.0, 1.0], domain=list_of_floats, 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().config.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 # TODO : Need to add units self.volume = Var(initialize=1, doc="Reactor 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 != '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} var_dict = {"Length": self.length} var_dict = {"Area": self.area} return {"vars": var_dict}
class PyomoCyIpoptSolver(object): CONFIG = ConfigBlock("cyipopt") CONFIG.declare("tee", ConfigValue( default=False, domain=bool, description="Stream solver output to console", )) CONFIG.declare("load_solutions", ConfigValue( default=True, domain=bool, description="Store the final solution into the original Pyomo model", )) CONFIG.declare("return_nlp", ConfigValue( default=False, domain=bool, description="Return the results object and the underlying nlp" " NLP object from the solve call.", )) CONFIG.declare("options", ConfigBlock(implicit=True)) CONFIG.declare("intermediate_callback", ConfigValue( default=None, description="Set the function that will be called each" " iteration." )) def __init__(self, **kwds): """Create an instance of the CyIpoptSolver. You must provide a problem_interface that corresponds to the abstract class CyIpoptProblemInterface options can be provided as a dictionary of key value pairs """ self.config = self.CONFIG(kwds) def _set_model(self, model): self._model = model def available(self, exception_flag=False): return numpy_available and cyipopt_available def license_is_valid(self): return True def version(self): return tuple(int(_) for _ in cyipopt.__version__.split('.')) def solve(self, model, **kwds): config = self.config(kwds, preserve_implicit=True) if not isinstance(model, Block): raise ValueError("PyomoCyIpoptSolver.solve(model): model " "must be a Pyomo Block") # If this is a Pyomo model / block, then we need to create # the appropriate PyomoNLP, then wrap it in a CyIpoptNLP grey_box_blocks = list(model.component_data_objects( egb.ExternalGreyBoxBlock, active=True)) if grey_box_blocks: # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) else: nlp = pyomo_nlp.PyomoNLP(model) problem = CyIpoptNLP(nlp, intermediate_callback=config.intermediate_callback) xl = problem.x_lb() xu = problem.x_ub() gl = problem.g_lb() gu = problem.g_ub() nx = len(xl) ng = len(gl) cyipopt_solver = cyipopt.Problem( n=nx, m=ng, problem_obj=problem, lb=xl, ub=xu, cl=gl, cu=gu ) # check if we need scaling obj_scaling, x_scaling, g_scaling = problem.scaling_factors() if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)): # need to set scaling factors if obj_scaling is None: obj_scaling = 1.0 if x_scaling is None: x_scaling = np.ones(nx) if g_scaling is None: g_scaling = np.ones(ng) try: set_scaling = cyipopt_solver.set_problem_scaling except AttributeError: # Fall back to pre-1.0.0 API set_scaling = cyipopt_solver.setProblemScaling set_scaling(obj_scaling, x_scaling, g_scaling) # add options try: add_option = cyipopt_solver.add_option except AttributeError: # Fall back to pre-1.0.0 API add_option = cyipopt_solver.addOption for k, v in config.options.items(): add_option(k, v) timer = TicTocTimer() try: # We preemptively set up the TeeStream, even if we aren't # going to use it: the implementation is such that the # context manager does nothing (i.e., doesn't start up any # processing threads) until afer a client accesses # STDOUT/STDERR with TeeStream(sys.stdout) as _teeStream: if config.tee: try: fd = sys.stdout.fileno() except (io.UnsupportedOperation, AttributeError): # If sys,stdout doesn't have a valid fileno, # then create one using the TeeStream fd = _teeStream.STDOUT.fileno() else: fd = None with redirect_fd(fd=1, output=fd, synchronize=False): x, info = cyipopt_solver.solve(problem.x_init()) solverStatus = SolverStatus.ok except: msg = "Exception encountered during cyipopt solve:" logger.error(msg, exc_info=sys.exc_info()) solverStatus = SolverStatus.unknown raise wall_time = timer.toc(None) results = SolverResults() if config.load_solutions: nlp.set_primals(x) nlp.set_duals(info['mult_g']) nlp.load_state_into_pyomo( bound_multipliers=(info['mult_x_L'], info['mult_x_U'])) else: soln = results.solution.add() soln.variable.update( (i, {'Value':j, 'ipopt_zL_out': zl, 'ipopt_zU_out': zu}) for i,j,zl,zu in zip( nlp.variable_names(), x, info['mult_x_L'], info['mult_x_U'] ) ) soln.constraint.update( (i, {'Dual':j}) for i,j in zip( nlp.constraint_names(), info['mult_g'])) results.problem.name = model.name obj = next(model.component_data_objects(Objective, active=True)) if obj.sense == minimize: results.problem.sense = ProblemSense.minimize results.problem.upper_bound = info['obj_val'] else: results.problem.sense = ProblemSense.maximize results.problem.lower_bound = info['obj_val'] results.problem.number_of_objectives = 1 results.problem.number_of_constraints = ng results.problem.number_of_variables = nx results.problem.number_of_binary_variables = 0 results.problem.number_of_integer_variables = 0 results.problem.number_of_continuous_variables = nx # TODO: results.problem.number_of_nonzeros results.solver.name = 'cyipopt' results.solver.return_code = info['status'] results.solver.message = info['status_msg'] results.solver.wallclock_time = wall_time status_enum = _cyipopt_status_enum[info['status_msg']] results.solver.termination_condition = _ipopt_term_cond[status_enum] results.solver.status = TerminationCondition.to_solver_status( results.solver.termination_condition) if config.return_nlp: return results, nlp return results # # Support "with" statements. # def __enter__(self): return self def __exit__(self, t, v, traceback): pass
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 MultiStart(object): """Solver wrapper that initializes at multiple starting points. # TODO: also return appropriate duals For theoretical underpinning, see https://www.semanticscholar.org/paper/How-many-random-restarts-are-enough-Dick-Wong/55b248b398a03dc1ac9a65437f88b835554329e0 Keyword arguments below are specified for the ``solve`` function. """ CONFIG = ConfigBlock("MultiStart") CONFIG.declare( "strategy", ConfigValue( default="rand", domain=In([ "rand", "midpoint_guess_and_bound", "rand_guess_and_bound", "rand_distributed" ]), description="Specify the restart strategy. Defaults to rand.", doc="""Specify the restart strategy. - "rand": random choice between variable bounds - "midpoint_guess_and_bound": midpoint between current value and farthest bound - "rand_guess_and_bound": random choice between current value and farthest bound - "rand_distributed": random choice among evenly distributed values """)) CONFIG.declare( "solver", ConfigValue(default="ipopt", description="solver to use, defaults to ipopt")) CONFIG.declare( "solver_args", ConfigValue( default={}, description="Dictionary of keyword arguments to pass to the solver." )) CONFIG.declare( "iterations", ConfigValue( default=10, description="Specify the number of iterations, defaults to 10. " "If -1 is specified, the high confidence stopping rule will be used" )) CONFIG.declare( "stopping_mass", ConfigValue( default=0.5, description="Maximum allowable estimated missing mass of optima.", doc="""Maximum allowable estimated missing mass of optima for the high confidence stopping rule, only used with the random strategy. The lower the parameter, the stricter the rule. Value bounded in (0, 1].""")) CONFIG.declare( "stopping_delta", ConfigValue( default=0.5, description= "1 minus the confidence level required for the stopping rule.", doc= """1 minus the confidence level required for the stopping rule for the high confidence stopping rule, only used with the random strategy. The lower the parameter, the stricter the rule. Value bounded in (0, 1].""")) CONFIG.declare( "suppress_unbounded_warning", ConfigValue( default=False, domain=bool, description= "True to suppress warning for skipping unbounded variables.")) CONFIG.declare( "HCS_max_iterations", ConfigValue( default=1000, description= "Maximum number of iterations before interrupting the high confidence stopping rule." )) CONFIG.declare( "HCS_tolerance", ConfigValue( default=0, description= "Tolerance on HCS objective value equality. Defaults to Python float equality precision." )) __doc__ = add_docstring_list(__doc__, CONFIG) def available(self, exception_flag=True): """Check if solver is available. TODO: For now, it is always available. However, sub-solvers may not always be available, and so this should reflect that possibility. """ return True def solve(self, model, **kwds): # initialize keyword args config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) # initialize the solver solver = SolverFactory(config.solver) # Model sense objectives = model.component_data_objects(Objective, active=True) obj = next(objectives, None) if next(objectives, None) is not None: raise RuntimeError( "Multistart solver is unable to handle model with multiple active objectives." ) if obj is None: raise RuntimeError( "Multistart solver is unable to handle model with no active objective." ) # store objective values and objective/result information for best # solution obtained objectives = [] obj_sign = 1 if obj.sense == minimize else -1 best_objective = float('inf') * obj_sign best_model = model best_result = None try: # create temporary variable list for value transfer tmp_var_list_name = unique_component_name(model, "_vars_list") setattr( model, tmp_var_list_name, list(model.component_data_objects(ctype=Var, descend_into=True))) best_result = result = solver.solve(model, **config.solver_args) if (result.solver.status is SolverStatus.ok and result.solver.termination_condition is tc.optimal): obj_val = value(model.obj.expr) best_objective = obj_val objectives.append(obj_val) num_iter = 0 max_iter = config.iterations # if HCS rule is specified, reinitialize completely randomly until # rule specifies stopping using_HCS = config.iterations == -1 HCS_completed = False if using_HCS: assert config.strategy == "rand", \ "High confidence stopping rule requires rand strategy." max_iter = config.HCS_max_iterations while num_iter < max_iter: if using_HCS and should_stop(objectives, config.stopping_mass, config.stopping_delta, config.HCS_tolerance): HCS_completed = True break num_iter += 1 # at first iteration, solve the originally passed model m = model.clone() if num_iter > 1 else model reinitialize_variables(m, config) result = solver.solve(m, **config.solver_args) if (result.solver.status is SolverStatus.ok and result.solver.termination_condition is tc.optimal): obj_val = value(m.obj.expr) objectives.append(obj_val) if obj_val * obj_sign < obj_sign * best_objective: # objective has improved best_objective = obj_val best_model = m best_result = result if num_iter == 1: # if it's the first iteration, set the best_model and # best_result regardless of solution status in case the # model is infeasible. best_model = m best_result = result if using_HCS and not HCS_completed: logger.warning( "High confidence stopping rule was unable to complete " "after %s iterations. To increase this limit, change the " "HCS_max_iterations flag." % num_iter) # if no better result was found than initial solve, then return # that without needing to copy variables. if best_model is model: return best_result # reassign the given models vars to the new models vars orig_var_list = getattr(model, tmp_var_list_name) best_soln_var_list = getattr(best_model, tmp_var_list_name) for orig_var, new_var in zip(orig_var_list, best_soln_var_list): if not orig_var.is_fixed(): orig_var.value = new_var.value return best_result finally: # Remove temporary variable list delattr(model, tmp_var_list_name) def __enter__(self): return self def __exit__(self, t, v, traceback): pass
class Electrodialysis0DData(UnitModelBlockData): """ 0D Electrodialysis Model """ # CONFIG are options for the unit model CONFIG = ConfigBlock() # CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. The filtration unit does not support dynamic behavior, thus this must be False.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Indicates whether holdup terms should be constructed or not. **default** - False. The filtration unit does not have defined volume, thus this must be False.""", ), ) CONFIG.declare( "operation_mode", ConfigValue( default="Constant_Current", domain=In(["Constant_Current", "Constant_Voltage"]), description="The electrical operation mode. To be selected between Constant Current and Constant Voltage", ), ) 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.}""", ), ) # # TODO: Consider adding the EnergyBalanceType config using the following code ''' CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.none, 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( "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): # build always starts by calling super().build() # This triggers a lot of boilerplate in the background for you super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition # Create essential sets. self.membrane_set = Set(initialize=["cem", "aem"]) # Create unit model parameters and vars self.water_density = Param( initialize=1000, mutable=False, units=pyunits.kg * pyunits.m**-3, doc="density of water", ) self.cell_pair_num = Var( initialize=1, domain=NonNegativeIntegers, bounds=(1, 10000), units=pyunits.dimensionless, doc="cell pair number in a stack", ) # electrodialysis cell dimensional properties self.cell_width = Var( initialize=0.1, bounds=(1e-3, 1e2), units=pyunits.meter, doc="The width of the electrodialysis cell, denoted as b in the model description", ) self.cell_length = Var( initialize=0.5, bounds=(1e-3, 1e2), units=pyunits.meter, doc="The length of the electrodialysis cell, denoted as l in the model description", ) self.spacer_thickness = Var( initialize=0.0001, units=pyunits.meter, doc="The distance between the concecutive aem and cem", ) # Material and Operational properties self.membrane_thickness = Var( self.membrane_set, initialize=0.0001, bounds=(1e-6, 1e-1), units=pyunits.meter, doc="Membrane thickness", ) self.solute_diffusivity_membrane = Var( self.membrane_set, self.config.property_package.ion_set | self.config.property_package.solute_set, initialize=1e-10, bounds=(1e-16, 1e-6), units=pyunits.meter**2 * pyunits.second**-1, doc="Solute (ionic and neutral) diffusivity in the membrane phase", ) self.ion_trans_number_membrane = Var( self.membrane_set, self.config.property_package.ion_set, bounds=(0, 1), units=pyunits.dimensionless, doc="Ion transference number in the membrane phase", ) self.water_trans_number_membrane = Var( self.membrane_set, initialize=5, bounds=(0, 50), units=pyunits.dimensionless, doc="Transference number of water in membranes", ) self.water_permeability_membrane = Var( self.membrane_set, initialize=1e-14, units=pyunits.meter * pyunits.second**-1 * pyunits.pascal**-1, doc="Water permeability coefficient", ) self.membrane_surface_resistance = Var( self.membrane_set, initialize=2e-4, bounds=(1e-6, 1), units=pyunits.ohm * pyunits.meter**2, doc="Surface resistance of membrane", ) self.electrodes_resistance = Var( initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=pyunits.ohm * pyunits.meter**2, doc="areal resistance of TWO electrode compartments of a stack", ) self.current = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 1000), units=pyunits.amp, doc="Current across a cell-pair or stack", ) self.voltage = Var( self.flowsheet().config.time, initialize=100, bounds=(0, 1000), units=pyunits.volt, doc="Voltage across a stack, declared under the 'Constant Voltage' mode only", ) self.current_utilization = Var( initialize=1, bounds=(0, 1), units=pyunits.dimensionless, doc="The current utilization including water electro-osmosis and ion diffusion", ) # Performance metrics self.current_efficiency = Var( self.flowsheet().config.time, initialize=0.9, bounds=(0, 1), units=pyunits.dimensionless, doc="The overall current efficiency for deionizaiton", ) self.power_electrical = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 12100), domain=NonNegativeReals, units=pyunits.watt, doc="Electrical power consumption of a stack", ) self.specific_power_electrical = Var( self.flowsheet().config.time, initialize=10, bounds=(0, 1000), domain=NonNegativeReals, units=pyunits.kW * pyunits.hour * pyunits.meter**-3, doc="Diluate-volume-flow-rate-specific electrical power consumption", ) # TODO: consider adding more performance as needed. # Fluxes Vars for constructing mass transfer terms self.elec_migration_flux_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_in of a component across the membrane driven by electrical migration", ) self.elec_migration_flux_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_out of a component across the membrane driven by electrical migration", ) self.nonelec_flux_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_in of a component across the membrane driven by non-electrical forces", ) self.nonelec_flux_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1, doc="Molar flux_out of a component across the membrane driven by non-electrical forces", ) # Build control volume for the dilute channel self.diluate_channel = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, } ) self.diluate_channel.add_state_blocks(has_phase_equilibrium=False) self.diluate_channel.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) self.diluate_channel.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False ) # # TODO: Consider adding energy balances # Build control volume for the concentrate channel self.concentrate_channel = ControlVolume0DBlock( default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, } ) self.concentrate_channel.add_state_blocks(has_phase_equilibrium=False) self.concentrate_channel.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True ) self.concentrate_channel.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False ) # # TODO: Consider adding energy balances # Add ports (creates inlets and outlets for each channel) self.add_inlet_port(name="inlet_diluate", block=self.diluate_channel) self.add_outlet_port(name="outlet_diluate", block=self.diluate_channel) self.add_inlet_port(name="inlet_concentrate", block=self.concentrate_channel) self.add_outlet_port(name="outlet_concentrate", block=self.concentrate_channel) # Build Constraints @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, doc="Current-Voltage relationship", ) def eq_current_voltage_relation(self, t, p): surface_resistance_cp = ( self.membrane_surface_resistance["aem"] + self.membrane_surface_resistance["cem"] + self.spacer_thickness / ( 0.5 * ( self.concentrate_channel.properties_in[ t ].electrical_conductivity_phase[p] + self.concentrate_channel.properties_out[ t ].electrical_conductivity_phase[p] + self.diluate_channel.properties_in[ t ].electrical_conductivity_phase[p] + self.diluate_channel.properties_out[ t ].electrical_conductivity_phase[p] ) ) ) return ( self.current[t] * ( surface_resistance_cp * self.cell_pair_num + self.electrodes_resistance ) == self.voltage[t] * self.cell_width * self.cell_length ) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for electrical migration flux_in", ) def eq_elec_migration_flux_in(self, t, p, j): if j == "H2O": return self.elec_migration_flux_in[t, p, j] == ( self.water_trans_number_membrane["cem"] + self.water_trans_number_membrane["aem"] ) * ( self.current[t] / (self.cell_width * self.cell_length) / Constants.faraday_constant ) elif j in self.config.property_package.ion_set: return self.elec_migration_flux_in[t, p, j] == ( self.ion_trans_number_membrane["cem", j] - self.ion_trans_number_membrane["aem", j] ) * ( self.current_utilization * self.current[t] / (self.cell_width * self.cell_length) ) / ( self.config.property_package.charge_comp[j] * Constants.faraday_constant ) else: return self.elec_migration_flux_out[t, p, j] == 0 @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for electrical migration flux_out", ) def eq_elec_migration_flux_out(self, t, p, j): if j == "H2O": return self.elec_migration_flux_out[t, p, j] == ( self.water_trans_number_membrane["cem"] + self.water_trans_number_membrane["aem"] ) * ( self.current[t] / (self.cell_width * self.cell_length) / Constants.faraday_constant ) elif j in self.config.property_package.ion_set: return self.elec_migration_flux_out[t, p, j] == ( self.ion_trans_number_membrane["cem", j] - self.ion_trans_number_membrane["aem", j] ) * ( self.current_utilization * self.current[t] / (self.cell_width * self.cell_length) ) / ( self.config.property_package.charge_comp[j] * Constants.faraday_constant ) else: return self.elec_migration_flux_out[t, p, j] == 0 @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for non-electrical flux_in", ) def eq_nonelec_flux_in(self, t, p, j): if j == "H2O": return self.nonelec_flux_in[ t, p, j ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["cem"] + self.water_permeability_membrane["aem"] ) * ( self.concentrate_channel.properties_in[t].pressure_osm_phase[p] - self.diluate_channel.properties_in[t].pressure_osm_phase[p] ) else: return self.nonelec_flux_in[t, p, j] == -( self.solute_diffusivity_membrane["cem", j] / self.membrane_thickness["cem"] + self.solute_diffusivity_membrane["aem", j] / self.membrane_thickness["aem"] ) * ( self.concentrate_channel.properties_in[t].conc_mol_phase_comp[p, j] - self.diluate_channel.properties_in[t].conc_mol_phase_comp[p, j] ) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Equation for non-electrical flux_out", ) def eq_nonelec_flux_out(self, t, p, j): if j == "H2O": return self.nonelec_flux_out[ t, p, j ] == self.water_density / self.config.property_package.mw_comp[j] * ( self.water_permeability_membrane["cem"] + self.water_permeability_membrane["aem"] ) * ( self.concentrate_channel.properties_out[t].pressure_osm_phase[p] - self.diluate_channel.properties_out[t].pressure_osm_phase[p] ) else: return self.nonelec_flux_out[t, p, j] == -( self.solute_diffusivity_membrane["cem", j] / self.membrane_thickness["cem"] + self.solute_diffusivity_membrane["aem", j] / self.membrane_thickness["aem"] ) * ( self.concentrate_channel.properties_out[t].conc_mol_phase_comp[p, j] - self.diluate_channel.properties_out[t].conc_mol_phase_comp[p, j] ) # Add constraints for mass transfer terms (diluate_channel) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term for the diluate channel", ) def eq_mass_transfer_term_diluate(self, t, p, j): return self.diluate_channel.mass_transfer_term[t, p, j] == -0.5 * ( self.elec_migration_flux_in[t, p, j] + self.elec_migration_flux_out[t, p, j] + self.nonelec_flux_in[t, p, j] + self.nonelec_flux_out[t, p, j] ) * (self.cell_width * self.cell_length) # Add constraints for mass transfer terms (concentrate_channel) @self.Constraint( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term for the concentrate channel", ) def eq_mass_transfer_term_concentrate(self, t, p, j): return self.concentrate_channel.mass_transfer_term[t, p, j] == 0.5 * ( self.elec_migration_flux_in[t, p, j] + self.elec_migration_flux_out[t, p, j] + self.nonelec_flux_in[t, p, j] + self.nonelec_flux_out[t, p, j] ) * (self.cell_width * self.cell_length) # Add isothermal condition @self.Constraint( self.flowsheet().config.time, doc="Isothermal condition for the diluate channel", ) def eq_isothermal_diluate(self, t): return ( self.diluate_channel.properties_in[t].temperature == self.diluate_channel.properties_out[t].temperature ) @self.Constraint( self.flowsheet().config.time, doc="Isothermal condition for the concentrate channel", ) def eq_isothermal_concentrate(self, t): return ( self.concentrate_channel.properties_in[t].temperature == self.concentrate_channel.properties_out[t].temperature ) @self.Constraint( self.flowsheet().config.time, doc="Electrical power consumption of a stack", ) def eq_power_electrical(self, t): return self.power_electrical[t] == self.current[t] * self.voltage[t] @self.Constraint( self.flowsheet().config.time, doc="Diluate_volume_flow_rate_specific electrical power consumption of a stack", ) def eq_specific_power_electrical(self, t): return ( pyunits.convert( self.specific_power_electrical[t], pyunits.watt * pyunits.second * pyunits.meter**-3, ) * self.diluate_channel.properties_out[t].flow_vol_phase["Liq"] == self.current[t] * self.voltage[t] ) @self.Constraint( self.flowsheet().config.time, doc="Overall current efficiency evaluation", ) def eq_current_efficiency(self, t): return ( self.current_efficiency[t] * self.current[t] == sum( self.diluate_channel.properties_in[t].flow_mol_phase_comp["Liq", j] * self.config.property_package.charge_comp[j] - self.diluate_channel.properties_out[t].flow_mol_phase_comp[ "Liq", j ] * self.config.property_package.charge_comp[j] for j in self.config.property_package.cation_set ) * Constants.faraday_constant ) # initialize method def initialize_build( blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None ): """ General wrapper for pressure changer initialization routines 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) 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") # Set solver options opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Set the outlet has the same intial condition of the inlet. for k in blk.keys(): for j in blk[k].config.property_package.component_list: blk[k].diluate_channel.properties_out[0].flow_mol_phase_comp[ "Liq", j ] = value( blk[k] .diluate_channel.properties_in[0] .flow_mol_phase_comp["Liq", j] ) blk[k].concentrate_channel.properties_out[0].flow_mol_phase_comp[ "Liq", j ] = value( blk[k] .concentrate_channel.properties_in[0] .flow_mol_phase_comp["Liq", j] ) # Initialize diluate_channel block flags_diluate = blk.diluate_channel.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, hold_state=True, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Initialize concentrate_side block flags_concentrate = blk.concentrate_channel.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, # inlet var hold_state=True, ) init_log.info_high("Initialization Step 2 Complete.") # --------------------------------------------------------------------- # 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 state blk.diluate_channel.release_state(flags_diluate, outlvl) init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) blk.concentrate_channel.release_state(flags_concentrate, outlvl) init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) def calculate_scaling_factors(self): super().calculate_scaling_factors() # Var scaling # The following Vars' sf are allowed to be provided by users or set by default. if ( iscale.get_scaling_factor(self.solute_diffusivity_membrane, warning=True) is None ): iscale.set_scaling_factor(self.solute_diffusivity_membrane, 1e10) if iscale.get_scaling_factor(self.membrane_thickness, warning=True) is None: iscale.set_scaling_factor(self.membrane_thickness, 1e4) if ( iscale.get_scaling_factor(self.water_permeability_membrane, warning=True) is None ): iscale.set_scaling_factor(self.water_permeability_membrane, 1e14) if iscale.get_scaling_factor(self.cell_length, warning=True) is None: iscale.set_scaling_factor(self.cell_length, 1e1) if iscale.get_scaling_factor(self.cell_width, warning=True) is None: iscale.set_scaling_factor(self.cell_width, 1e1) if iscale.get_scaling_factor(self.spacer_thickness, warning=True) is None: iscale.set_scaling_factor(self.spacer_thickness, 1e4) if ( iscale.get_scaling_factor(self.membrane_surface_resistance, warning=True) is None ): iscale.set_scaling_factor(self.membrane_surface_resistance, 1e4) if iscale.get_scaling_factor(self.electrodes_resistance, warning=True) is None: iscale.set_scaling_factor(self.electrodes_resistance, 1e4) if iscale.get_scaling_factor(self.current, warning=True) is None: iscale.set_scaling_factor(self.current, 1) if iscale.get_scaling_factor(self.voltage, warning=True) is None: iscale.set_scaling_factor(self.voltage, 1e-1) # The folloing Vars are built for constructing constraints and their sf are computed from other Vars. iscale.set_scaling_factor( self.elec_migration_flux_in, iscale.get_scaling_factor(self.current) * iscale.get_scaling_factor(self.cell_length) ** -1 * iscale.get_scaling_factor(self.cell_width) ** -1 * 1e5, ) iscale.set_scaling_factor( self.elec_migration_flux_out, iscale.get_scaling_factor(self.current) * iscale.get_scaling_factor(self.cell_length) ** -1 * iscale.get_scaling_factor(self.cell_width) ** -1 * 1e5, ) iscale.set_scaling_factor( self.power_electrical, iscale.get_scaling_factor(self.current) * iscale.get_scaling_factor(self.voltage), ) for ind, c in self.specific_power_electrical.items(): iscale.set_scaling_factor( self.specific_power_electrical[ind], 3.6e6 * iscale.get_scaling_factor(self.current[ind]) * iscale.get_scaling_factor(self.voltage[ind]) * iscale.get_scaling_factor( self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"] ) ** -1, ) # Constraint scaling for ind, c in self.eq_current_voltage_relation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.membrane_surface_resistance) ) for ind, c in self.eq_power_electrical.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.power_electrical) ) for ind, c in self.eq_specific_power_electrical.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.specific_power_electrical[ind]) ) for ind, c in self.eq_elec_migration_flux_in.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.elec_migration_flux_in) ) for ind, c in self.eq_elec_migration_flux_out.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.elec_migration_flux_out) ) for ind, c in self.eq_nonelec_flux_in.items(): if ind[2] == "H2O": sf = ( 1e-3 * 0.018 * iscale.get_scaling_factor(self.water_permeability_membrane) * iscale.get_scaling_factor( self.concentrate_channel.properties_in[ ind[0] ].pressure_osm_phase[ind[1]] ) ) sf = ( iscale.get_scaling_factor(self.solute_diffusivity_membrane) / iscale.get_scaling_factor(self.membrane_thickness) * iscale.get_scaling_factor( self.concentrate_channel.properties_in[ind[0]].conc_mol_phase_comp[ ind[1], ind[2] ] ) ) iscale.set_scaling_factor(self.nonelec_flux_in[ind], sf) iscale.constraint_scaling_transform(c, sf) for ind, c in self.eq_nonelec_flux_out.items(): if ind[2] == "H2O": sf = ( 1e-3 * 0.018 * iscale.get_scaling_factor(self.water_permeability_membrane) * iscale.get_scaling_factor( self.concentrate_channel.properties_out[ ind[0] ].pressure_osm_phase[ind[1]] ) ) else: sf = ( iscale.get_scaling_factor(self.solute_diffusivity_membrane) / iscale.get_scaling_factor(self.membrane_thickness) * iscale.get_scaling_factor( self.concentrate_channel.properties_out[ ind[0] ].conc_mol_phase_comp[ind[1], ind[2]] ) ) iscale.set_scaling_factor(self.nonelec_flux_out[ind], sf) iscale.constraint_scaling_transform(c, sf) for ind, c in self.eq_mass_transfer_term_diluate.items(): iscale.constraint_scaling_transform( c, min( iscale.get_scaling_factor(self.elec_migration_flux_in[ind]), iscale.get_scaling_factor( self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind] ), iscale.get_scaling_factor(self.nonelec_flux_out[ind]), ), ) for ind, c in self.eq_mass_transfer_term_concentrate.items(): iscale.constraint_scaling_transform( c, min( iscale.get_scaling_factor(self.elec_migration_flux_in[ind]), iscale.get_scaling_factor( self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind] ), iscale.get_scaling_factor(self.nonelec_flux_out[ind]), ), ) for ind, c in self.eq_power_electrical.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.power_electrical[ind]), ) for ind, c in self.eq_specific_power_electrical.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.specific_power_electrical[ind]) * iscale.get_scaling_factor( self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"] ), ) for ind, c in self.eq_current_efficiency.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.current[ind]) ) for ind, c in self.eq_isothermal_diluate.items(): iscale.constraint_scaling_transform( c, self.diluate_channel.properties_in[ind].temperature ) for ind, c in self.eq_isothermal_concentrate.items(): iscale.constraint_scaling_transform( c, self.concentrate_channel.properties_in[ind].temperature ) def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Diluate Channel Inlet": self.inlet_diluate, "Concentrate Channel Inlet": self.inlet_concentrate, "Diluate Channel Outlet": self.outlet_diluate, "Concentrate Channel Outlet": self.outlet_concentrate, }, time_point=time_point, ) def _get_performance_contents(self, time_point=0): return { "vars": { "Electrical power consumption(Watt)": self.power_electrical[time_point], "Specific electrical power consumption (kWh/m**3)": self.specific_power_electrical[ time_point ], "Current efficiency for deionzation": self.current_efficiency[ time_point ], }, "exprs": {}, "params": {}, }
def pyros_config(): CONFIG = ConfigDict('PyROS') # ================================================ # === Options common to all solvers # ================================================ CONFIG.declare( 'time_limit', ConfigValue( default=None, domain=NonNegativeFloat, description="Optional. Default = None. " "Total allotted time for the execution of the PyROS solver in seconds " "(includes time spent in sub-solvers). 'None' is no time limit.")) CONFIG.declare( 'keepfiles', ConfigValue( default=False, domain=bool, description= "Optional. Default = False. Whether or not to write files of sub-problems for use in debugging. " "Must be paired with a writable directory supplied via ``subproblem_file_directory``." )) CONFIG.declare( 'tee', ConfigValue( default=False, domain=bool, description= "Optional. Default = False. Sets the ``tee`` for all sub-solvers utilized." )) CONFIG.declare( 'load_solution', ConfigValue( default=True, domain=bool, description="Optional. Default = True. " "Whether or not to load the final solution of PyROS into the model object." )) # ================================================ # === Required User Inputs # ================================================ CONFIG.declare( "first_stage_variables", ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description= "Required. List of ``Var`` objects referenced in ``model`` representing the design variables." )) CONFIG.declare( "second_stage_variables", ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description= "Required. List of ``Var`` referenced in ``model`` representing the control variables." )) CONFIG.declare( "uncertain_params", ConfigValue( default=[], domain=InputDataStandardizer(Param, _ParamData), description= "Required. List of ``Param`` referenced in ``model`` representing the uncertain parameters. MUST be ``mutable``. " "Assumes entries are provided in consistent order with the entries of 'nominal_uncertain_param_vals' input." )) CONFIG.declare( "uncertainty_set", ConfigValue( default=None, domain=uncertainty_sets, description= "Required. ``UncertaintySet`` object representing the uncertainty space " "that the final solutions will be robust against.")) CONFIG.declare( "local_solver", ConfigValue( default=None, domain=SolverResolvable(), description= "Required. ``Solver`` object to utilize as the primary local NLP solver." )) CONFIG.declare( "global_solver", ConfigValue( default=None, domain=SolverResolvable(), description= "Required. ``Solver`` object to utilize as the primary global NLP solver." )) # ================================================ # === Optional User Inputs # ================================================ CONFIG.declare( "objective_focus", ConfigValue( default=ObjectiveType.nominal, domain=ValidEnum(ObjectiveType), description= "Optional. Default = ``ObjectiveType.nominal``. Choice of objective function to optimize in the master problems. " "Choices are: ``ObjectiveType.worst_case``, ``ObjectiveType.nominal``. See Note for details." )) CONFIG.declare( "nominal_uncertain_param_vals", ConfigValue( default=[], domain=list, description= "Optional. Default = deterministic model ``Param`` values. List of nominal values for all uncertain parameters. " "Assumes entries are provided in consistent order with the entries of ``uncertain_params`` input." )) CONFIG.declare( "decision_rule_order", ConfigValue( default=0, domain=In([0, 1, 2]), description= "Optional. Default = 0. Order of decision rule functions for handling second-stage variable recourse. " "Choices are: '0' for constant recourse (a.k.a. static approximation), '1' for affine recourse " "(a.k.a. affine decision rules), '2' for quadratic recourse.")) CONFIG.declare( "solve_master_globally", ConfigValue( default=False, domain=bool, description= "Optional. Default = False. 'True' for the master problems to be solved with the user-supplied global solver(s); " "or 'False' for the master problems to be solved with the user-supplied local solver(s). " )) CONFIG.declare( "max_iter", ConfigValue( default=-1, domain=PositiveIntOrMinusOne, description= "Optional. Default = -1. Iteration limit for the GRCS algorithm. '-1' is no iteration limit." )) CONFIG.declare( "robust_feasibility_tolerance", ConfigValue( default=1e-4, domain=NonNegativeFloat, description= "Optional. Default = 1e-4. Relative tolerance for assessing robust feasibility violation during separation phase." )) CONFIG.declare( "separation_priority_order", ConfigValue( default={}, domain=dict, description= "Optional. Default = {}. Dictionary mapping inequality constraint names to positive integer priorities for separation. " "Constraints not referenced in the dictionary assume a priority of 0 (lowest priority)." )) CONFIG.declare( "progress_logger", ConfigValue( default="pyomo.contrib.pyros", domain=a_logger, description= "Optional. Default = \"pyomo.contrib.pyros\". The logger object to use for reporting." )) CONFIG.declare( "backup_local_solvers", ConfigValue( default=[], domain=SolverResolvable(), description= "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup " "whenever primary local NLP solver fails to identify solution to a sub-problem." )) CONFIG.declare( "backup_global_solvers", ConfigValue( default=[], domain=SolverResolvable(), description= "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup " "whenever primary global NLP solver fails to identify solution to a sub-problem." )) CONFIG.declare( "subproblem_file_directory", ConfigValue( default=None, domain=str, description= "Optional. Path to a directory where subproblem files and " "logs will be written in the case that a subproblem fails to solve." )) # ================================================ # === Advanced Options # ================================================ CONFIG.declare( "bypass_local_separation", ConfigValue( default=False, domain=bool, description= "This is an advanced option. Default = False. 'True' to only use global solver(s) during separation; " "'False' to use local solver(s) at intermediate separations, " "using global solver(s) only before termination to certify robust feasibility. " )) CONFIG.declare( "bypass_global_separation", ConfigValue( default=False, domain=bool, description= "This is an advanced option. Default = False. 'True' to only use local solver(s) during separation; " "however, robustness of the final result will not be guaranteed. Use to expedite PyROS run when " "global solver(s) cannot (efficiently) solve separation problems.") ) CONFIG.declare( "p_robustness", ConfigValue( default={}, domain=dict, description= "This is an advanced option. Default = {}. Whether or not to add p-robustness constraints to the master problems. " "If the dictionary is empty (default), then p-robustness constraints are not added. " "See Note for how to specify arguments.")) return CONFIG
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 FlowsheetBlockData(ProcessBlockData): """ The FlowsheetBlockData Class forms the base class for all IDAES process flowsheet models. The main purpose of this class is to automate the tasks common to all flowsheet models and ensure that the necessary attributes of a flowsheet model are present. The most signfiicant role of the FlowsheetBlockData class is to automatically create the time domain for the flowsheet. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("dynamic", ConfigValue( default=useDefault, domain=In([useDefault, True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent or False, **True** - set as a dynamic model, **False** - set as a steady-state model.}""")) CONFIG.declare("time", ConfigValue( default=None, domain=is_time_domain, description="Flowsheet time domain", doc="""Pointer to the time domain for the flowsheet. Users may provide an existing time domain from another flowsheet, otherwise the flowsheet will search for a parent with a time domain or create a new time domain and reference it here.""")) CONFIG.declare("time_set", ConfigValue( default=[0], domain=list_of_floats, description="Set of points for initializing time domain", doc="""Set of points for initializing time domain. This should be a list of floating point numbers, **default** - [0].""")) CONFIG.declare("default_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Default property package to use in flowsheet", doc="""Indicates the default property package to be used by models within this flowsheet if not otherwise specified, **default** - None. **Valid values:** { **None** - no default property package, **a ParameterBlock object**.}""")) def build(self): """ General build method for FlowsheetBlockData. This method calls a number of sub-methods which automate the construction of expected attributes of flowsheets. Inheriting models should call `super().build`. Args: None Returns: None """ super(FlowsheetBlockData, self).build() # Set up dynamic flag and time domain self._setup_dynamics() def is_flowsheet(self): """ Method which returns True to indicate that this component is a flowsheet. Args: None Returns: True """ return True # TODO [Qi]: this should be implemented as a transformation def model_check(self): """ This method runs model checks on all unit models in a flowsheet. This method searches for objects which inherit from UnitModelBlockData and executes the model_check method if it exists. Args: None Returns: None """ _log.info("Executing model checks.") for o in self.component_objects(descend_into=False): if isinstance(o, UnitModelBlockData): try: o.model_check() except AttributeError: _log.warning('{} Model/block has no model check. To ' 'correct this, add a model_check method to ' 'the associated unit model class' .format(o.name)) def _setup_dynamics(self): # Look for parent flowsheet fs = self.flowsheet() # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == useDefault: if fs is None: # No parent, so default to steady-state and warn user _log.warning('{} is a top level flowhseet, but dynamic flag ' 'set to useDefault. Dynamic ' 'flag set to False by default' .format(self.name)) self.config.dynamic = False else: # Get dynamic flag from parent flowsheet self.config.dynamic = fs.config.dynamic # Check for case when dynamic=True, but parent dynamic=False elif self.config.dynamic is True: if fs is not None and fs.config.dynamic is False: raise DynamicError( '{} trying to declare a dynamic model within ' 'a steady-state flowsheet. This is not ' 'supported by the IDAES framework. Try ' 'creating a dynamic flowsheet instead, and ' 'declaring some models as steady-state.' .format(self.name)) if self.config.time is not None: # Validate user provided time domain if (self.config.dynamic is True and not isinstance(self.config.time, ContinuousSet)): raise DynamicError( '{} was set as a dynamic flowsheet, but time domain ' 'provided was not a ContinuousSet.'.format(self.name)) else: # If no parent flowsheet, set up time domain if fs is None: # Create time domain if self.config.dynamic: # Check if time_set has at least two points if len(self.config.time_set) < 2: # Check if time_set is at default value if self.config.time_set == [0.0]: # If default, set default end point to be 1.0 self.config.time_set = [0.0, 1.0] else: # Invalid user input, raise Excpetion raise DynamicError( "Flowsheet provided with invalid " "time_set attribute - must have at " "least two values (start and end).") # For dynamics, need a ContinuousSet self.time = ContinuousSet(initialize=self.config.time_set) else: # For steady-state, use an ordered Set self.time = pe.Set(initialize=self.config.time_set, ordered=True) # Set time config argument as reference to time domain self.config.time = self.time else: # Set time config argument to parent time self.config.time = fs.config.time
class MixerData(UnitModelBlockData): """ This is a general purpose model for a Mixer block with the IDAES modeling framework. This block can be used either as a stand-alone Mixer unit operation, or as a sub-model within another unit operation. This model creates a number of StateBlocks to represent the incoming streams, then writes a set of phase-component material balances, an overall enthalpy balance and a momentum balance (2 options) linked to a mixed-state StateBlock. The mixed-state StateBlock can either be specified by the user (allowing use as a sub-model), or created by the Mixer. When being used as a sub-model, Mixer should only be used when a set of new StateBlocks are required for the streams to be mixed. It should not be used to mix streams from mutiple ControlVolumes in a single unit model - in these cases the unit model developer should write their own mixing equations. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. Mixer blocks are always steady-state.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Mixer blocks do not contain holdup, thus this must be False.""", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for mixer", 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( "inlet_list", ConfigValue( domain=list_of_strings, description="List of inlet names", doc="""A list containing names of inlets, **default** - None. **Valid values:** { **None** - use num_inlets argument, **list** - a list of names to use for inlets.}""", ), ) CONFIG.declare( "num_inlets", ConfigValue( domain=int, description="Number of inlets to unit", doc="""Argument indicating number (int) of inlets to construct, not used if inlet_list arg is provided, **default** - None. **Valid values:** { **None** - use inlet_list arg instead, or default to 2 if neither argument provided, **int** - number of inlets to create (will be named with sequential integers from 1 to num_inlets).}""", ), ) 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( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Calculate phase equilibrium in mixed stream", doc="""Argument indicating whether phase equilibrium should be calculated for the resulting mixed stream, **default** - False. **Valid values:** { **True** - calculate phase equilibrium in mixed stream, **False** - do not calculate equilibrium in mixed stream.}""", ), ) CONFIG.declare( "energy_mixing_type", ConfigValue( default=MixingType.extensive, domain=MixingType, description="Method to use when mixing energy flows", doc="""Argument indicating what method to use when mixing energy flows of incoming streams, **default** - MixingType.extensive. **Valid values:** { **MixingType.none** - do not include energy mixing equations, **MixingType.extensive** - mix total enthalpy flows of each phase.}""", ), ) CONFIG.declare( "momentum_mixing_type", ConfigValue( default=MomentumMixingType.minimize, domain=MomentumMixingType, description="Method to use when mixing momentum/pressure", doc="""Argument indicating what method to use when mixing momentum/ pressure of incoming streams, **default** - MomentumMixingType.minimize. **Valid values:** { **MomentumMixingType.none** - do not include momentum mixing equations, **MomentumMixingType.minimize** - mixed stream has pressure equal to the minimimum pressure of the incoming streams (uses smoothMin operator), **MomentumMixingType.equality** - enforces equality of pressure in mixed and all incoming streams., **MomentumMixingType.minimize_and_equality** - add constraints for pressure equal to the minimum pressure of the inlets and constraints for equality of pressure in mixed and all incoming streams. When the model is initially built, the equality constraints are deactivated. This option is useful for switching between flow and pressure driven simulations.}""", ), ) CONFIG.declare( "mixed_state_block", ConfigValue( default=None, domain=is_state_block, description="Existing StateBlock to use as mixed stream", doc="""An existing state block to use as the outlet stream from the Mixer block, **default** - None. **Valid values:** { **None** - create a new StateBlock for the mixed stream, **StateBlock** - a StateBock to use as the destination for the mixed stream.} """, ), ) CONFIG.declare( "construct_ports", ConfigValue( default=True, domain=In([True, False]), description="Construct inlet and outlet Port objects", doc="""Argument indicating whether model should construct Port objects linked to all inlet states and the mixed state, **default** - True. **Valid values:** { **True** - construct Ports for all states, **False** - do not construct Ports.""", ), ) def build(self): """ General build method for MixerData. This method calls a number of sub-methods which automate the construction of expected attributes of unit models. Inheriting models should call `super().build`. Args: None Returns: None """ # Call super.build() super(MixerData, self).build() # Call setup methods from ControlVolumeBlockData self._get_property_package() self._get_indexing_sets() # Create list of inlet names inlet_list = self.create_inlet_list() # Build StateBlocks inlet_blocks = self.add_inlet_state_blocks(inlet_list) if self.config.mixed_state_block is None: mixed_block = self.add_mixed_state_block() else: mixed_block = self.get_mixed_state_block() mb_type = self.config.material_balance_type if mb_type == MaterialBalanceType.useDefault: t_ref = self.flowsheet().config.time.first() mb_type = mixed_block[t_ref].default_material_balance_type() if mb_type != MaterialBalanceType.none: self.add_material_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block, mb_type=mb_type) else: raise BurntToast("{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format( self.name)) if self.config.energy_mixing_type == MixingType.extensive: self.add_energy_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif self.config.energy_mixing_type == MixingType.none: pass else: raise ConfigurationError( "{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) # Add to try/expect to catch cases where pressure is not supported # by properties. try: if self.config.momentum_mixing_type == MomentumMixingType.minimize: self.add_pressure_minimization_equations( inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif (self.config.momentum_mixing_type == MomentumMixingType.equality): self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif (self.config.momentum_mixing_type == MomentumMixingType.minimize_and_equality): self.add_pressure_minimization_equations( inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.pressure_equality_constraints.deactivate() elif self.config.momentum_mixing_type == MomentumMixingType.none: pass else: raise ConfigurationError( "{} recieved unrecognised value for " "momentum_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) except PropertyNotSupportedError: raise PropertyNotSupportedError( "{} The property package supplied for this unit does not " "appear to support pressure, which is required for momentum " "mixing. Please set momentum_mixing_type to " "MomentumMixingType.none or provide a property package which " "supports pressure.".format(self.name)) self.add_port_objects(inlet_list, inlet_blocks, mixed_block) def create_inlet_list(self): """ Create list of inlet stream names based on config arguments. Returns: list of strings """ if (self.config.inlet_list is not None and self.config.num_inlets is not None): # If both arguments provided and not consistent, raise Exception if len(self.config.inlet_list) != self.config.num_inlets: raise ConfigurationError( "{} Mixer provided with both inlet_list and " "num_inlets arguments, which were not consistent (" "length of inlet_list was not equal to num_inlets). " "PLease check your arguments for consistency, and " "note that it is only necessary to provide one of " "these arguments.".format(self.name)) elif self.config.inlet_list is None and self.config.num_inlets is None: # If no arguments provided for inlets, default to num_inlets = 2 self.config.num_inlets = 2 # Create a list of names for inlet StateBlocks if self.config.inlet_list is not None: inlet_list = self.config.inlet_list else: inlet_list = [ "inlet_" + str(n) for n in range(1, self.config.num_inlets + 1) ] return inlet_list def add_inlet_state_blocks(self, inlet_list): """ Construct StateBlocks for all inlet streams. Args: list of strings to use as StateBlock names Returns: list of StateBlocks """ # Setup StateBlock argument dict tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["defined_state"] = True # Create empty list to hold StateBlocks for return inlet_blocks = [] # Create an instance of StateBlock for all inlets for i in inlet_list: i_obj = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties at inlet", default=tmp_dict, ) setattr(self, i + "_state", i_obj) inlet_blocks.append(getattr(self, i + "_state")) return inlet_blocks def add_mixed_state_block(self): """ Constructs StateBlock to represent mixed stream. Returns: New StateBlock object """ # Setup StateBlock argument dict tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium tmp_dict["defined_state"] = False self.mixed_state = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties of mixed stream", default=tmp_dict, ) return self.mixed_state def get_mixed_state_block(self): """ Validates StateBlock provided in user arguments for mixed stream. Returns: The user-provided StateBlock or an Exception """ # Sanity check to make sure method is not called when arg missing if self.config.mixed_state_block is None: raise BurntToast("{} get_mixed_state_block method called when " "mixed_state_block argument is None. This should " "not happen.".format(self.name)) # Check that the user-provided StateBlock uses the same prop pack if (self.config.mixed_state_block[self.flowsheet().config.time.first( )].config.parameters != self.config.property_package): raise ConfigurationError( "{} StateBlock provided in mixed_state_block argument " "does not come from the same property package as " "provided in the property_package argument. All " "StateBlocks within a Mixer must use the same " "property package.".format(self.name)) return self.config.mixed_state_block def add_material_mixing_equations(self, inlet_blocks, mixed_block, mb_type): """ Add material mixing equations. """ pp = self.config.property_package # Get phase component list(s) pc_set = pp.get_phase_component_set() if mb_type == MaterialBalanceType.componentPhase: # Create equilibrium generation term and constraints if required if self.config.has_phase_equilibrium is True: # Get units from property package units = {} for u in ["holdup", "time"]: try: units[u] = pp.get_metadata().default_units[u] except KeyError: units[u] = "-" try: self.phase_equilibrium_generation = Var( self.flowsheet().config.time, pp.phase_equilibrium_idx, domain=Reals, doc="Amount of generation in unit by phase " "equilibria [{}/{}]".format(units["holdup"], units["time"]), ) except AttributeError: raise PropertyNotSupportedError( "{} Property package does not contain a list of phase " "equilibrium reactions (phase_equilibrium_idx), " "thus does not support phase equilibrium.".format( self.name)) # Define terms to use in mixing equation def phase_equilibrium_term(b, t, p, j): if self.config.has_phase_equilibrium: sd = {} for r in pp.phase_equilibrium_idx: if pp.phase_equilibrium_list[r][0] == j: if (pp.phase_equilibrium_list[r][1][0] == p): sd[r] = 1 elif (pp.phase_equilibrium_list[r][1][1] == p): sd[r] = -1 else: sd[r] = 0 else: sd[r] = 0 return sum(b.phase_equilibrium_generation[t, r] * sd[r] for r in pp.phase_equilibrium_idx) else: return 0 # Write phase-component balances @self.Constraint( self.flowsheet().config.time, pc_set, doc="Material mixing equations", ) def material_mixing_equations(b, t, p, j): if (p, j) in pc_set: return 0 == ( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) + phase_equilibrium_term(b, t, p, j)) else: return Constraint.Skip elif mb_type == MaterialBalanceType.componentTotal: # Write phase-component balances @self.Constraint( self.flowsheet().config.time, self.config.property_package.component_list, doc="Material mixing equations", ) def material_mixing_equations(b, t, j): return 0 == sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for p in b.config.property_package.phase_list if (p, j) in pc_set) elif mb_type == MaterialBalanceType.total: # Write phase-component balances @self.Constraint(self.flowsheet().config.time, doc="Material mixing equations") def material_mixing_equations(b, t): return 0 == sum( sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for j in b.config.property_package.component_list if (p, j) in pc_set) for p in b.config.property_package.phase_list) elif mb_type == MaterialBalanceType.elementTotal: raise ConfigurationError("{} Mixers do not support elemental " "material balances.".format(self.name)) elif mb_type == MaterialBalanceType.none: pass else: raise BurntToast( "{} Mixer received unrecognised value for " "material_balance_type. This should not happen, " "please report this bug to the IDAES developers.".format( self.name)) def add_energy_mixing_equations(self, inlet_blocks, mixed_block): """ Add energy mixing equations (total enthalpy balance). """ @self.Constraint(self.flowsheet().config.time, doc="Energy balances") def enthalpy_mixing_equations(b, t): return 0 == (sum( sum(inlet_blocks[i][t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) for i in range(len(inlet_blocks))) - sum(mixed_block[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) def add_pressure_minimization_equations(self, inlet_blocks, mixed_block): """ Add pressure minimization equations. This is done by sequential comparisons of each inlet to the minimum pressure so far, using the IDAES smooth minimum fuction. """ if not hasattr(self, "inlet_idx"): self.inlet_idx = RangeSet(len(inlet_blocks)) # Add variables self.minimum_pressure = Var( self.flowsheet().config.time, self.inlet_idx, doc="Variable for calculating " "minimum inlet pressure", ) self.eps_pressure = Param( mutable=True, initialize=1e-3, domain=PositiveReals, doc="Smoothing term for " "minimum inlet pressure", ) # Calculate minimum inlet pressure @self.Constraint( self.flowsheet().config.time, self.inlet_idx, doc="Calculation for minimum inlet pressure", ) def minimum_pressure_constraint(b, t, i): if i == self.inlet_idx.first(): return self.minimum_pressure[t, i] == ( inlet_blocks[i - 1][t].pressure) else: return self.minimum_pressure[t, i] == (smooth_min( self.minimum_pressure[t, i - 1], inlet_blocks[i - 1][t].pressure, self.eps_pressure, )) # Set inlet pressure to minimum pressure @self.Constraint(self.flowsheet().config.time, doc="Link pressure to control volume") def mixture_pressure(b, t): return mixed_block[t].pressure == ( self.minimum_pressure[t, self.inlet_idx.last()]) def add_pressure_equality_equations(self, inlet_blocks, mixed_block): """ Add pressure equality equations. Note that this writes a number of constraints equal to the number of inlets, enforcing equality between all inlets and the mixed stream. """ if not hasattr(self, "inlet_idx"): self.inlet_idx = RangeSet(len(inlet_blocks)) # Create equality constraints @self.Constraint( self.flowsheet().config.time, self.inlet_idx, doc="Calculation for minimum inlet pressure", ) def pressure_equality_constraints(b, t, i): return mixed_block[t].pressure == inlet_blocks[i - 1][t].pressure def add_port_objects(self, inlet_list, inlet_blocks, mixed_block): """ Adds Port objects if required. Args: a list of inlet StateBlock objects a mixed state StateBlock object Returns: None """ if self.config.construct_ports is True: # Add ports for p in inlet_list: i_state = getattr(self, p + "_state") self.add_port(name=p, block=i_state, doc="Inlet Port") self.add_port(name="outlet", block=mixed_block, doc="Outlet Port") def model_check(blk): """ This method executes the model_check methods on the associated state blocks (if they exist). This method is generally called by a unit model as part of the unit's model_check method. Args: None Returns: None """ # Try property block model check for t in blk.flowsheet().config.time: try: inlet_list = blk.create_inlet_list() for i in inlet_list: i_block = getattr(blk, i + "_state") i_block[t].model_check() except AttributeError: _log.warning( "{} Mixer inlet property block has no model " "checks. To correct this, add a model_check " "method to the associated StateBlock class.".format( blk.name)) try: if blk.config.mixed_state_block is None: blk.mixed_state[t].model_check() else: blk.config.mixed_state_block.model_check() except AttributeError: _log.warning("{} Mixer outlet property block has no " "model checks. To correct this, add a " "model_check method to the associated " "StateBlock class.".format(blk.name)) def use_minimum_inlet_pressure_constraint(self): """Activate the mixer pressure = mimimum inlet pressure constraint and deactivate the mixer pressure and all inlet pressures are equal constraints. This should only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality. """ if (self.config.momentum_mixing_type != MomentumMixingType.minimize_and_equality): _log.warning( """use_minimum_inlet_pressure_constraint() can only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality""") return self.minimum_pressure_constraint.activate() self.pressure_equality_constraints.deactivate() def use_equal_pressure_constraint(self): """Deactivate the mixer pressure = mimimum inlet pressure constraint and activate the mixer pressure and all inlet pressures are equal constraints. This should only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality. """ if (self.config.momentum_mixing_type != MomentumMixingType.minimize_and_equality): _log.warning( """use_equal_pressure_constraint() can only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality""") return self.minimum_pressure_constraint.deactivate() self.pressure_equality_constraints.activate() def initialize(blk, outlvl=6, optarg={}, solver="ipopt", hold_state=False): """ Initialization routine for mixer (default solver ipopt) Keyword Arguments: outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') hold_state : flag indicating whether the initialization routine should unfix any state variables fixed during initialization, **default** - False. **Valid values:** **True** - states variables are not unfixed, and a dict of returned containing flags for which states were fixed during initialization, **False** - state variables are unfixed after initialization by calling the release_state method. Returns: If hold_states is True, returns a dict containing flags for which states were fixed during initialization. """ 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 inlet state blocks flags = {} inlet_list = blk.create_inlet_list() i_block_list = [] for i in inlet_list: i_block = getattr(blk, i + "_state") i_block_list.append(i_block) flags[i] = {} flags[i] = i_block.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True, ) # Initialize mixed state block if blk.config.mixed_state_block is None: mblock = blk.mixed_state else: mblock = blk.config.mixed_state_block o_flags = {} # Calculate initial guesses for mixed stream state for t in blk.flowsheet().config.time: # Iterate over state vars as defined by property package s_vars = mblock[t].define_state_vars() for s in s_vars: i_vars = [] for k in s_vars[s]: # Record whether variable was fixed or not o_flags[t, s, k] = s_vars[s][k].fixed # If fixed, use current value # otherwise calculate guess from mixed state if not s_vars[s][k].fixed: for i in range(len(i_block_list)): i_vars.append( getattr(i_block_list[i][t], s_vars[s].local_name)) if s == "pressure": # If pressure, use minimum as initial guess mblock[t].pressure.value = min( i_block_list[i][t].pressure.value for i in range(len(i_block_list))) elif "flow" in s: # If a "flow" variable (i.e. extensive), sum inlets for k in s_vars[s]: s_vars[s][k].value = sum( i_vars[i][k].value for i in range(len(i_block_list))) else: # Otherwise use average of inlets for k in s_vars[s]: s_vars[s][k].value = sum( i_vars[i][k].value for i in range( len(i_block_list))) / len(i_block_list) mblock.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False, ) # Revert fixed status of variables to what they were before for t in blk.flowsheet().config.time: s_vars = mblock[t].define_state_vars() for s in s_vars: for k in s_vars[s]: s_vars[s][k].fixed = o_flags[t, s, k] if blk.config.mixed_state_block is None: if (hasattr(blk, "pressure_equality_constraints") and blk.pressure_equality_constraints.active is True): blk.pressure_equality_constraints.deactivate() for t in blk.flowsheet().config.time: sys_press = getattr(blk, blk.create_inlet_list()[0] + "_state")[t].pressure blk.mixed_state[t].pressure.fix(sys_press.value) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) blk.pressure_equality_constraints.activate() for t in blk.flowsheet().config.time: blk.mixed_state[t].pressure.unfix() else: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) else: init_log.info("Initialization Complete.") if hold_state is True: return flags else: blk.release_state(flags, outlvl=outlvl) def release_state(blk, flags, outlvl=idaeslog.NOTSET): """ Method to release state variables fixed during initialization. Keyword Arguments: flags : dict containing information of which state variables were fixed during initialization, and should now be unfixed. This dict is returned by initialize if hold_state = True. outlvl : sets output level of logging Returns: None """ inlet_list = blk.create_inlet_list() for i in inlet_list: i_block = getattr(blk, i + "_state") i_block.release_state(flags[i], outlvl=outlvl + 1) def _get_stream_table_contents(self, time_point=0): io_dict = {} inlet_list = self.create_inlet_list() for i in inlet_list: io_dict[i] = getattr(self, i + "_state") if self.config.mixed_state_block is None: io_dict["Outlet"] = self.mixed_state else: io_dict["Outlet"] = self.config.mixed_state_block return create_stream_table_dataframe(io_dict, time_point=time_point) def calculate_scaling_factors(self): super().calculate_scaling_factors() mb_type = self.config.material_balance_type if hasattr(self, "material_mixing_equations"): if mb_type == MaterialBalanceType.componentPhase: for (t, p, j), c in self.material_mixing_equations.items(): flow_term = self.mixed_state[t].get_material_flow_terms( p, j) s = iscale.get_scaling_factor(flow_term, default=1) iscale.constraint_scaling_transform(c, s) elif mb_type == MaterialBalanceType.componentTotal: for (t, j), c in self.material_mixing_equations.items(): for i, p in enumerate( self.config.property_package.phase_list): ft = self.mixed_state[t].get_material_flow_terms(p, j) if i == 0: s = iscale.get_scaling_factor(ft, default=1) else: _s = iscale.get_scaling_factor(ft, default=1) s = _s if _s < s else s iscale.constraint_scaling_transform(c, s) elif mb_type == MaterialBalanceType.total: pc_set = self.config.property_package.get_phase_component_set() for t, c in self.material_mixing_equations.items(): for i, (p, j) in enumerate(pc_set): ft = self.mixed_state[t].get_material_flow_terms(p, j) if i == 0: s = iscale.get_scaling_factor(ft, default=1) else: _s = iscale.get_scaling_factor(ft, default=1) s = _s if _s < s else s iscale.constraint_scaling_transform(c, s)
class ProductData(UnitModelBlockData): """ Standard Product Block Class """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. Product blocks are always steady-state.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Product blocks do not contain holdup, thus this must be False.""", ), ) 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. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(ProductData, self).build() # Add State Block self.properties = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties in product", default={ "defined_state": True, "has_phase_equilibrium": False, **self.config.property_package_args, }, ) # Add references to all state vars s_vars = self.properties[ self.flowsheet().config.time.first()].define_state_vars() for s in s_vars: l_name = s_vars[s].local_name if s_vars[s].is_indexed(): slicer = self.properties[:].component(l_name)[...] else: slicer = self.properties[:].component(l_name) r = Reference(slicer) setattr(self, s, r) # Add outlet port self.add_port(name="inlet", block=self.properties, doc="Inlet Port") def initialize(blk, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6}): """ This method calls the initialization method of the state block. 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 which solver to use during initialization (default = 'ipopt') Returns: None """ # --------------------------------------------------------------------- # Initialize state block init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") blk.properties.initialize(outlvl=outlvl, optarg=optarg, solver=solver, **state_args) init_log.info("Initialization Complete.") def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe({"Inlet": self.inlet}, time_point=time_point)
class HelmSplitterData(UnitModelBlockData): """ This is a basic stream splitter which splits flow into outlet streams based on split fractions. This does not do phase seperation, and assumes that you are using a Helmholtz EOS propery package with P-H state variables. In dynamic mode this uses a pseudo-steady-state model. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", )) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for mixer", 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( "outlet_list", ConfigValue( domain=list_of_strings, description="List of outlet names", doc="""A list containing names of outlets, **default** - None. **Valid values:** { **None** - use num_outlets argument, **list** - a list of names to use for outlets.}""", ), ) CONFIG.declare( "num_outlets", ConfigValue( domain=int, description="Number of outlets to unit", doc="""Argument indicating number (int) of outlets to construct, not used if outlet_list arg is provided, **default** - None. **Valid values:** { **None** - use outlet_list arg instead, or default to 2 if neither argument provided, **int** - number of outlets to create (will be named with sequential integers from 1 to num_outlets).}""", ), ) def build(self): """ Build a splitter. Args: None Returns: None """ time = self.flowsheet().config.time super().build() self._get_property_package() self.create_outlet_list() self.add_inlet_state_and_port() self.add_outlet_state_blocks() self.add_outlet_port_objects() self.split_fraction = Var(time, self.outlet_list, initialize=1.0 / len(self.outlet_list), doc="Split fractions for outlet streams") @self.Constraint(time, doc="Splt constraint") def sum_split(b, t): return 1 == sum(self.split_fraction[t, o] for o in self.outlet_list) @self.Constraint(time, self.outlet_list, doc="Pressure constraint") def pressure_eqn(b, t, o): o_block = getattr(self, "{}_state".format(o)) return self.mixed_state[t].pressure == o_block[t].pressure @self.Constraint(time, self.outlet_list, doc="Enthalpy constraint") def enthalpy_eqn(b, t, o): o_block = getattr(self, "{}_state".format(o)) return self.mixed_state[t].enth_mol == o_block[t].enth_mol @self.Constraint(time, self.outlet_list, doc="Flow constraint") def flow_eqn(b, t, o): o_block = getattr(self, "{}_state".format(o)) sf = self.split_fraction[t, o] return self.mixed_state[t].flow_mol * sf == o_block[t].flow_mol def add_inlet_state_and_port(self): tmp_dict = dict(**self.config.property_package_args) tmp_dict["defined_state"] = True self.mixed_state = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties of mixed (inlet) stream", default=tmp_dict, ) self.add_port(name="inlet", block=self.mixed_state, doc="Inlet Port") def create_outlet_list(self): """ Create list of outlet stream names based on config arguments. Returns: list of strings """ config = self.config if config.outlet_list is not None and config.num_outlets is not None: # If both arguments provided and not consistent, raise Exception if len(config.outlet_list) != config.num_outlets: raise ConfigurationError( "{} Splitter provided with both outlet_list and " "num_outlets arguments, which were not consistent (" "length of outlet_list was not equal to num_outlets). " "Please check your arguments for consistency, and " "note that it is only necessry to provide one of " "these arguments.".format(self.name)) elif (config.outlet_list is None and config.num_outlets is None): # If no arguments provided for outlets, default to num_outlets = 2 config.num_outlets = 2 # Create a list of names for outlet StateBlocks if config.outlet_list is not None: outlet_list = self.config.outlet_list else: outlet_list = [ "outlet_{}".format(n) for n in range(1, config.num_outlets + 1) ] self.outlet_list = outlet_list def add_outlet_state_blocks(self): """ Construct StateBlocks for all outlet streams. Args: None Returns: list of StateBlocks """ # Setup StateBlock argument dict tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["defined_state"] = False # Create empty list to hold StateBlocks for return self.outlet_blocks = {} # Create an instance of StateBlock for all outlets for o in self.outlet_list: o_obj = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties at outlet", default=tmp_dict, ) setattr(self, o + "_state", o_obj) self.outlet_blocks[o] = o_obj def add_outlet_port_objects(self): """ Adds outlet Port objects if required. Args: None Returns: None """ self.outlet_ports = {} for p in self.outlet_list: self.add_port(name=p, block=self.outlet_blocks[p], doc="Outlet") self.outlet_ports[p] = getattr(self, p) def initialize(self, outlvl=idaeslog.NOTSET, optarg={}, solver=None): """ Initialization routine for splitter Keyword Arguments: outlvl: sets output level of initialization routine optarg: solver options dictionary object (default={}) solver: str indicating whcih solver to use during initialization (default = None, use default solver) Returns: If hold_states is True, returns a dict containing flags for which states were fixed during initialization. """ 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) # sp is what to save to make sure state after init is same as the start sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # check for fixed outlet flows and use them to calculate fixed split # fractions for t in self.flowsheet().config.time: for o in self.outlet_list: if self.outlet_blocks[o][t].flow_mol.fixed: self.split_fraction[t, o].fix( value(self.mixed_state[t] / self.outlet_blocks[o][t].flow_mol)) # fix or unfix split fractions so n - 1 are fixed for t in self.flowsheet().config.time: # see how many split fractions are fixed n = sum(1 for o in self.outlet_list if self.split_fraction[t, o].fixed) # if number of outlets - 1 we're good if n == len(self.outlet_list) - 1: continue # if too mant are fixed un fix the first, generally assume that is # the main flow, and is the calculated split fraction if n == len(self.outlet_list): self.split_fraction[t, self.outlet_list[0]].unfix() # if not enough fixed, start fixing from the back until there are # are enough for o in reversed(self.outlet_list): if not self.split_fraction[t, o].fixed: self.split_fraction[t, o].fix() n += 1 if n == len(self.outlet_list) - 1: break # This model is really simple so it should easily solve without much # effort to initialize self.inlet.fix() for o, p in self.outlet_ports.items(): p.unfix() 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("Initialization Complete: {}".format( idaeslog.condition(res))) from_json(self, sd=istate, wts=sp) def calculate_scaling_factors(self): super().calculate_scaling_factors() for (t, i), c in self.pressure_eqn.items(): o_block = getattr(self, "{}_state".format(i)) s = iscale.get_scaling_factor(o_block[t].pressure) iscale.constraint_scaling_transform(c, s) for (t, i), c in self.enthalpy_eqn.items(): o_block = getattr(self, "{}_state".format(i)) s = iscale.get_scaling_factor(o_block[t].enth_mol) iscale.constraint_scaling_transform(c, s) for (t, i), c in self.flow_eqn.items(): o_block = getattr(self, "{}_state".format(i)) s = iscale.get_scaling_factor(o_block[t].flow_mol) iscale.constraint_scaling_transform(c, s)
class HelmMixerData(UnitModelBlockData): """ This is a Helmholtz EOS specific mixed unit model. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. Mixer blocks are always steady-state.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Mixer blocks do not contain holdup, thus this must be False.""", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for mixer", 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( "inlet_list", ConfigValue( domain=list_of_strings, description="List of inlet names", doc="""A list containing names of inlets, **default** - None. **Valid values:** { **None** - use num_inlets argument, **list** - a list of names to use for inlets.}""", ), ) CONFIG.declare( "num_inlets", ConfigValue( domain=int, description="Number of inlets to unit", doc="""Argument indicating number (int) of inlets to construct, not used if inlet_list arg is provided, **default** - None. **Valid values:** { **None** - use inlet_list arg instead, or default to 2 if neither argument provided, **int** - number of inlets to create (will be named with sequential integers from 1 to num_inlets).}""", ), ) CONFIG.declare( "momentum_mixing_type", ConfigValue( default=MomentumMixingType.minimize, domain=MomentumMixingType, description="Method to use when mixing momentum/pressure", doc="""Argument indicating what method to use when mixing momentum/ pressure of incoming streams, **default** - MomentumMixingType.minimize. **Valid values:** { **MomentumMixingType.none** - do not include momentum mixing equations, **MomentumMixingType.minimize** - mixed stream has pressure equal to the minimimum pressure of the incoming streams (uses smoothMin operator), **MomentumMixingType.equality** - enforces equality of pressure in mixed and all incoming streams., **MomentumMixingType.minimize_and_equality** - add constraints for pressure equal to the minimum pressure of the inlets and constraints for equality of pressure in mixed and all incoming streams. When the model is initially built, the equality constraints are deactivated. This option is useful for switching between flow and pressure driven simulations.}""", ), ) def build(self): """ General build method for MixerData. This method calls a number of sub-methods which automate the construction of expected attributes of unit models. Inheriting models should call `super().build`. Args: None Returns: None """ # Call super.build() super().build() self._get_property_package() # Create list of inlet names inlet_list = self.create_inlet_list() # Build StateBlocks self.add_inlet_state_blocks() self.add_mixed_state_block() @self.Constraint(self.flowsheet().config.time) def mass_balance(b, t): return self.mixed_state[t].flow_mol == sum( self.inlet_blocks[i][t].flow_mol for i in self.inlet_list) @self.Constraint(self.flowsheet().config.time) def energy_balance(b, t): return self.mixed_state[t].enth_mol*self.mixed_state[t].flow_mol == \ sum(self.inlet_blocks[i][t].enth_mol * self.inlet_blocks[i][t].flow_mol for i in self.inlet_list ) mmx_type = self.config.momentum_mixing_type if mmx_type == MomentumMixingType.minimize: self.add_pressure_minimization_equations() elif mmx_type == MomentumMixingType.equality: self.add_pressure_equality_equations() elif mmx_type == MomentumMixingType.minimize_and_equality: self.add_pressure_minimization_equations() self.add_pressure_equality_equations() self.use_minimum_inlet_pressure_constraint() self.add_port_objects() def create_inlet_list(self): """ Create list of inlet stream names based on config arguments. Returns: list of strings """ if (self.config.inlet_list is not None and self.config.num_inlets is not None): # If both arguments provided and not consistent, raise Exception if len(self.config.inlet_list) != self.config.num_inlets: raise ConfigurationError( "{} Mixer provided with both inlet_list and " "num_inlets arguments, which were not consistent (" "length of inlet_list was not equal to num_inlets). " "PLease check your arguments for consistency, and " "note that it is only necessary to provide one of " "these arguments.".format(self.name)) elif self.config.inlet_list is None and self.config.num_inlets is None: # If no arguments provided for inlets, default to num_inlets = 2 self.config.num_inlets = 2 # Create a list of names for inlet StateBlocks if self.config.inlet_list is not None: inlet_list = self.config.inlet_list else: inlet_list = [ "inlet_{}".format(n) for n in range(1, self.config.num_inlets + 1) ] self.inlet_list = inlet_list def add_inlet_state_blocks(self): """ Construct StateBlocks for all inlet streams. Args: list of strings to use as StateBlock names Returns: list of StateBlocks """ # Setup StateBlock argument dict tmp_dict = dict(**self.config.property_package_args) tmp_dict["defined_state"] = True # Create empty list to hold StateBlocks for return self.inlet_blocks = {} # Create an instance of StateBlock for all inlets for i in self.inlet_list: i_obj = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties at inlet", default=tmp_dict, ) setattr(self, "{}_state".format(i), i_obj) self.inlet_blocks[i] = i_obj def add_mixed_state_block(self): """ Constructs StateBlock to represent mixed stream. Returns: New StateBlock object """ # Setup StateBlock argument dict tmp_dict = dict(**self.config.property_package_args) tmp_dict["defined_state"] = False self.mixed_state = self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Material properties of mixed stream", default=tmp_dict, ) return self.mixed_state def add_pressure_minimization_equations(self): """ Add pressure minimization equations. This is done by sequential comparisons of each inlet to the minimum pressure so far, using the IDAES smooth minimum function. """ units_meta = self.config.property_package.get_metadata() self.eps_pressure = Param( mutable=True, initialize=1e-3, domain=PositiveReals, doc="Smoothing term for minimum inlet pressure", units=units_meta.get_derived_units("pressure")) # Calculate minimum inlet pressure @self.Expression( self.flowsheet().config.time, self.inlet_list, doc="Calculation for minimum inlet pressure", ) def minimum_pressure(b, t, i): if i == self.inlet_list[0]: return self.inlet_blocks[i][t].pressure else: pi = self.inlet_list[self.inlet_list.index(i) - 1] prev_p = self.minimum_pressure[t, pi] this_p = self.inlet_blocks[i][t].pressure return smooth_min(this_p, prev_p, self.eps_pressure) # Set inlet pressure to minimum pressure @self.Constraint(self.flowsheet().config.time, doc="Link pressure to control volume") def minimum_pressure_constraint(b, t): return self.mixed_state[t].pressure == ( self.minimum_pressure[t, self.inlet_list[-1]]) def add_pressure_equality_equations(self): """ Add pressure equality equations. Note that this writes a number of constraints equal to the number of inlets, enforcing equality between all inlets and the mixed stream. """ # Create equality constraints @self.Constraint( self.flowsheet().config.time, self.inlet_list, doc="Calculation for minimum inlet pressure", ) def pressure_equality_constraints(b, t, i): return self.mixed_state[t].pressure == self.inlet_blocks[i][ t].pressure def add_port_objects(self): """ Adds Port objects if required. Args: a list of inlet StateBlock objects a mixed state StateBlock object Returns: None """ for p in self.inlet_list: self.add_port(name=p, block=self.inlet_blocks[p], doc="Inlet Port") self.add_port(name="outlet", block=self.mixed_state, doc="Outlet Port") def use_minimum_inlet_pressure_constraint(self): """Activate the mixer pressure = mimimum inlet pressure constraint and deactivate the mixer pressure and all inlet pressures are equal constraints. This should only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality. """ if (self.config.momentum_mixing_type != MomentumMixingType.minimize_and_equality): _log.warning( """use_minimum_inlet_pressure_constraint() can only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality""") return self.minimum_pressure_constraint.activate() self.pressure_equality_constraints.deactivate() def use_equal_pressure_constraint(self): """Deactivate the mixer pressure = mimimum inlet pressure constraint and activate the mixer pressure and all inlet pressures are equal constraints. This should only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality. """ if (self.config.momentum_mixing_type != MomentumMixingType.minimize_and_equality): _log.warning( """use_equal_pressure_constraint() can only be used when momentum_mixing_type == MomentumMixingType.minimize_and_equality""") return self.minimum_pressure_constraint.deactivate() self.pressure_equality_constraints.activate() def initialize(self, outlvl=idaeslog.NOTSET, optarg={}, solver=None): """ Initialization routine for mixer. Keyword Arguments: outlvl : sets output level of initialization routine optarg : solver options dictionary object (default={}) solver : str indicating whcih 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) # This shouldn't require too much initializtion, just fixing inlets # and solving should always work. # sp is what to save to make sure state after init is same as the start sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) for b in self.inlet_blocks.values(): for bdat in b.values(): bdat.pressure.fix() bdat.enth_mol.fix() bdat.flow_mol.fix() for t, v in self.outlet.pressure.items(): if not v.fixed: v.value = min([ value(self.inlet_blocks[i][t].pressure) for i in self.inlet_blocks ]) self.outlet.unfix() if (hasattr(self, "pressure_equality_constraints") and self.pressure_equality_constraints.active): # If using the equal pressure constraint fix the outlet and free # the inlet pressures, this is typical for pressure driven flow for i, b in self.inlet_blocks.items(): for bdat in b.values(): bdat.pressure.unfix() self.outlet.pressure.fix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) from_json(self, sd=istate, wts=sp) def calculate_scaling_factors(self): super().calculate_scaling_factors() for t, c in self.mass_balance.items(): s = iscale.get_scaling_factor(self.mixed_state[t].flow_mol) iscale.constraint_scaling_transform(c, s) for t, c in self.energy_balance.items(): s = iscale.get_scaling_factor(self.mixed_state[t].enth_mol) s *= iscale.get_scaling_factor(self.mixed_state[t].flow_mol) iscale.constraint_scaling_transform(c, s) if hasattr(self, "minimum_pressure_constraint"): for t, c in self.minimum_pressure_constraint.items(): s = iscale.get_scaling_factor(self.mixed_state[t].pressure) iscale.constraint_scaling_transform(c, s) if hasattr(self, "pressure_equality_constraints"): for (t, i), c in self.pressure_equality_constraints.items(): s = iscale.get_scaling_factor(self.mixed_state[t].pressure) iscale.constraint_scaling_transform(c, s)
class GibbsReactorData(UnitModelBlockData): """ Standard Gibbs Reactor Unit Model Class This model assume all possible reactions reach equilibrium such that the system partial molar Gibbs free energy is minimized. Since some species mole flow rate might be very small, the natural log of the species molar flow rate is used. Instead of specifying the system Gibbs free energy as an objective function, the equations for zero partial derivatives of the grand function with Lagrangian multiple terms with repect to product species mole flow rates and the multiples are specified as constraints. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc= """Gibbs reactors do not support dynamic models, thus this must be False.""")) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag", doc="""Gibbs reactors do not have defined volume, thus this must be False.""")) 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_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.}""")) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(GibbsReactorData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_total_element_balances() self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add performance equations # Add Lagrangian multiplier variables self.lagrange_mult = Var(self.flowsheet().config.time, self.config.property_package.element_list, domain=Reals, initialize=100, doc="Lagrangian multipliers") # Use Lagrangian multiple method to derive equations for Out_Fi # Use RT*lagrange as the Lagrangian multiple such that lagrange is in # a similar order of magnitude as log(Yi) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Gibbs energy minimisation constraint") def gibbs_minimization(b, t, p, j): # Use natural log of species mole flow to avoid Pyomo solver # warnings of reaching infeasible point return 0 == ( b.control_volume.properties_out[t].gibbs_mol_phase_comp[p, j] + sum(b.lagrange_mult[t, e] * b.control_volume.properties_out[t]. config.parameters.element_comp[j][e] for e in b.config.property_package.element_list)) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): add_object_reference(self, "deltaP", self.control_volume.deltaP) def _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}
class DowncomerData(UnitModelBlockData): """ Downcomer Unit Class """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue(domain=DefaultBool, default=useDefault, 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.}""")) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=Bool, description="Holdup construction flag", doc="""Indicates whether holdup terms should be constructed or not. Must be True if dynamic = True, **default** - False. **Valid values:** { **True** - construct holdup terms, **False** - do not construct holdup terms}""")) 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( "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() # no phase transitions in the unit - handeled by Helmholtz EoS 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) 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 """ units_meta = self.config.property_package.get_metadata() # Number of downcomers self.number_downcomers = Var(initialize=4, doc="Number of downcomers for the boiler") # Height of downcomer self.height = Var(initialize=10.0, doc="Height of downcomer", units=units_meta.get_derived_units("length")) # Inside diameter of downcomer self.diameter = Var(initialize=0.6, doc="Inside diameter of downcomer", units=units_meta.get_derived_units("length")) # Volume constraint @self.Constraint(self.flowsheet().time, doc="Downcomer volume of all pipes") def volume_eqn(b, t): return b.volume[t] == 0.25*const.pi*b.diameter**2*b.height \ * b.number_downcomers def _make_performance(self): """ Define constraints which describe the behaviour of the unit model. """ units_meta = self.config.property_package.get_metadata() # Add performance variables # Velocity of fluid inside downcomer pipe self.velocity = Var(self.flowsheet().time, initialize=10.0, doc='Liquid water velocity inside downcomer', units=units_meta.get_derived_units("velocity")) # Reynolds number self.N_Re = Var(self.flowsheet().time, initialize=10000.0, doc='Reynolds number') # Darcy friction factor (turbulent flow) self.friction_factor_darcy = Var(self.flowsheet().time, initialize=0.005, doc='Darcy friction factor') # Pressure change due to friction self.deltaP_friction = Var( self.flowsheet().time, initialize=-1.0, doc='Pressure change due to friction', units=units_meta.get_derived_units("pressure")) # Pressure change due to gravity self.deltaP_gravity = Var( self.flowsheet().time, initialize=100.0, doc='Pressure change due to gravity', units=units_meta.get_derived_units("pressure")) # Equation for calculating velocity @self.Constraint(self.flowsheet().time, doc="Velocity of fluid inside downcomer") def velocity_eqn(b, t): return b.velocity[t]*0.25*const.pi*b.diameter**2 \ * b.number_downcomers \ == b.control_volume.properties_in[t].flow_vol # Equation for calculating Reynolds number @self.Constraint(self.flowsheet().time, doc="Reynolds number") def Reynolds_number_eqn(b, t): return b.N_Re[t] * \ b.control_volume.properties_in[t].visc_d_phase["Liq"] == \ b.diameter * b.velocity[t] *\ b.control_volume.properties_in[t].dens_mass_phase["Liq"] # Friction factor expression depending on laminar or turbulent flow @self.Constraint(self.flowsheet().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 # Pressure change equation for friction, # -1/2*density*velocity^2*fD/diameter*height @self.Constraint(self.flowsheet().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["Liq"] * \ b.velocity[t]**2 * b.friction_factor_darcy[t] * b.height # Pressure change equation for gravity, density*gravity*height g_units = units_meta.get_derived_units("acceleration") @self.Constraint(self.flowsheet().time, doc="Pressure change due to gravity") def pressure_change_gravity_eqn(b, t): return b.deltaP_gravity[t] == \ b.control_volume.properties_in[t].dens_mass_phase["Liq"] \ * pyunits.convert(const.acceleration_gravity, to_units=g_units) * b.height # Total pressure change equation @self.Constraint(self.flowsheet().time, doc="Pressure drop") def pressure_change_total_eqn(b, t): return b.deltaP[t] == (b.deltaP_friction[t] + b.deltaP_gravity[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): ''' Downcomer 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=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( outlvl=outlvl + 1, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") # make sure 0 DoF if degrees_of_freedom(blk) != 0: raise ConfigurationError( "Incorrect degrees of freedom when initializing {}: dof = {}". format(blk.name, degrees_of_freedom(blk))) # 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_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().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 + 1) init_log.info("Initialization Complete.") def calculate_scaling_factors(self): # set a default Reynolds number scaling for v in self.N_Re.values(): if iscale.get_scaling_factor(v, warning=True) is None: iscale.set_scaling_factor(v, 1e-4) for v in self.friction_factor_darcy.values(): if iscale.get_scaling_factor(v, warning=True) is None: iscale.set_scaling_factor(v, 100) for v in self.deltaP_gravity.values(): if iscale.get_scaling_factor(v, warning=True) is None: iscale.set_scaling_factor(v, 1e-3) for v in self.deltaP_friction.values(): if iscale.get_scaling_factor(v, warning=True) is None: iscale.set_scaling_factor(v, 1e-3) for t, c in self.volume_eqn.items(): sf = iscale.get_scaling_factor(self.volume[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.Reynolds_number_eqn.items(): sf = iscale.get_scaling_factor(self.N_Re[t], default=1, warning=True) sf *= iscale.get_scaling_factor( self.control_volume.properties_in[t].visc_d_phase["Liq"], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.pressure_change_friction_eqn.items(): sf = iscale.get_scaling_factor(self.deltaP_friction[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.pressure_change_gravity_eqn.items(): sf = iscale.get_scaling_factor(self.deltaP_gravity[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False) for t, c in self.pressure_change_total_eqn.items(): sf = iscale.get_scaling_factor(self.deltaP[t], default=1, warning=True) iscale.constraint_scaling_transform(c, sf, overwrite=False)
class PIDBlockData(ProcessBlockData): CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "pv", ConfigValue( default=None, description="Measured process variable", doc="A Pyomo Var, Expression, or Reference for the measured" " process variable. Should be indexed by time.")) CONFIG.declare( "output", ConfigValue( default=None, description="Controlled process variable", doc="A Pyomo Var, Expression, or Reference for the controlled" " process variable. Should be indexed by time.")) CONFIG.declare( "upper", ConfigValue( default=1.0, domain=float, description="Output upper limit", doc="The upper limit for the controller output, default=1")) CONFIG.declare( "lower", ConfigValue( default=0.0, domain=float, description="Output lower limit", doc="The lower limit for the controller output, default=0")) CONFIG.declare( "calculate_initial_integral", ConfigValue( default=True, domain=bool, description="Calculate the initial integral term value if true, " " otherwise provide a variable err_i0, which can be fixed", doc="Calculate the initial integral term value if true, otherwise" " provide a variable err_i0, which can be fixed, default=True")) CONFIG.declare( "pid_form", ConfigValue(default=PIDForm.velocity, domain=In(PIDForm), description="Velocity or standard form", doc="Velocity or standard form")) # TODO<jce> options for P, PI, and PD, you can currently do PI by setting # the derivative time to 0, this class should handle PI and PID # controllers. Proportional, only controllers are sufficiently # different that another class should be implemented. # TODO<jce> Anti-windup the integral term can keep accumulating error when # the controller output is at a bound. This can cause trouble, # and ways to deal with it should be implemented # TODO<jce> Implement way to better deal with the integral term for setpoint # changes (see bumpless). I need to look into the more, but this # would basically use the calculation like the one already used # for the first time point to calculate integral error to keep the # controller output from suddenly jumping in response to a set # point change or transition from manual to automatic control. def _build_standard(self, time_set, t0): # Want to fix the output variable at the first time step to make # solving easier. This calculates the initial integral error to line up # with the initial output value, keeps the controller from initially # jumping. if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # integral error @self.Expression(time_set, doc="Integral error") def err_i(b, t_end): return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) * (t - time_set.prev(t)) / 2.0 for t in time_set if t <= t_end and t > t0) # Calculate the unconstrained controller output @self.Expression(time_set, doc="Unconstrained controller output") def unconstrained_output(b, t): return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] + b.time_d[t] * b.err_d[t]) @self.Expression(doc="Initial integral error at the end") def err_i_end(b): return b.err_i[time_set.last()] def _build_velocity(self, time_set, t0): if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # Calculate the unconstrained controller output @self.Expression(time_set, doc="Unconstrained controller output") def unconstrained_output(b, t): if t == t0: # do the standard first step so I have a previous time # for the rest of the velocity form return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i0 + b.time_d[t] * b.err_d[t]) tb = time_set.prev(t) # time back a step return self.output[tb] + self.gain[t] * ( b.pterm[t] - b.pterm[tb] + (t - tb) / b.time_i[t] * (b.err[t] + b.err[tb]) / 2 + b.time_d[t] * (b.err_d[t] - b.err_d[tb])) @self.Expression(doc="Initial integral error at the end") def err_i_end(b): tl = time_set.last() return b.time_i[tl]*(b.output[tl] - b.gain[tl]*b.pterm[tl]\ - b.gain[tl]*b.time_d[tl]*b.err_d[tl])/b.gain[tl] def build(self): """ Build the PID block """ if isinstance(self.flowsheet().time, ContinuousSet): # time may not be a continuous set if you have a steady state model # in the steady state model case obviously the controller should # not be active, but you can still add it. if 'scheme' not in self.flowsheet().time.get_discretization_info(): # if you have a dynamic model, must do time discretization # before adding the PID model raise RunTimeError( "PIDBlock must be added after time discretization") super().build() # do the ProcessBlockData voodoo for config # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration requires 'pv'") if self.config.output is None: raise ConfigurationError( "Controller configuration requires 'output'") # Shorter pointers to time set information time_set = self.flowsheet().time t0 = time_set.first() # Variable for basic controller settings may change with time. self.setpoint = pyo.Var(time_set, doc="Setpoint") self.gain = pyo.Var(time_set, doc="Controller gain") self.time_i = pyo.Var(time_set, doc="Integral time") self.time_d = pyo.Var(time_set, doc="Derivative time") # Make the initial derivative term a variable so you can set it. This # should let you carry on from the end of another time period self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0) self.err_d0.fix() if not self.config.calculate_initial_integral: self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0) self.err_i0.fix() # Make references to the output and measured variables self.pv = pyo.Reference(self.config.pv) # No duplicate self.output = pyo.Reference(self.config.output) # No duplicate # Create an expression for error from setpoint @self.Expression(time_set, doc="Setpoint error") def err(b, t): return self.setpoint[t] - self.pv[t] # Use expressions to allow the some future configuration @self.Expression(time_set) def pterm(b, t): return -self.pv[t] @self.Expression(time_set) def dterm(b, t): return -self.pv[t] @self.Expression(time_set) def iterm(b, t): return self.err[t] # Output limits parameter self.limits = pyo.Param(["l", "h"], mutable=True, doc="controller output limits", initialize={ "l": self.config.lower, "h": self.config.upper }) # Smooth min and max are used to limit output, smoothing parameter here self.smooth_eps = pyo.Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") # This is ugly, but want integral and derivative error as expressions, # nice implementation with variables is harder to initialize and solve @self.Expression(time_set, doc="Derivative error.") def err_d(b, t): if t == t0: return self.err_d0 else: return (b.dterm[t] - b.dterm[time_set.prev(t)])\ /(t - time_set.prev(t)) if self.config.pid_form == PIDForm.standard: self._build_standard(time_set, t0) else: self._build_velocity(time_set, t0) # Add the controller output constraint and limit it with smooth min/max e = self.smooth_eps h = self.limits["h"] l = self.limits["l"] @self.Constraint(time_set, doc="Controller output constraint") def output_constraint(b, t): if t == t0: return pyo.Constraint.Skip else: return self.output[t] ==\ smooth_min( smooth_max(self.unconstrained_output[t], l, e), h, e)
class BatteryStorageData(UnitModelBlockData): """ Wind plant using turbine powercurve and resource data. Unit model to convert wind resource into electricity. """ CONFIG = ConfigBlock() CONFIG.declare("dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Battery does not support dynamic models, thus this must be False.""")) CONFIG.declare("has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag", doc="""Battery does not have defined volume, thus this must be False.""")) def build(self): """Building model This model does not use the flowsheet's time domain. Instead, it only models a single timestep, with initial conditions provided by `initial_state_of_charge` and `initial_energy_throughput`. The model calculates change in stored energy across a single time step using the power flow variables, `power_in` and `power_out`, and the `dr_hr` parameter. Args: None Returns: None """ super().build() # Design variables and parameters self.nameplate_power = Var(within=NonNegativeReals, initialize=0.0, bounds=(0, 1e6), doc="Nameplate power of battery energy storage", units=pyunits.kW) self.nameplate_energy = Var(within=NonNegativeReals, initialize=0.0, bounds=(0, 1e7), doc="Nameplate energy of battery energy storage", units=pyunits.kWh) self.charging_eta = Param(within=NonNegativeReals, mutable=True, initialize=1, doc="Charging efficiency, (0, 1]") self.discharging_eta = Param(within=NonNegativeReals, mutable=True, initialize=1, doc="Discharging efficiency, (0, 1]") self.degradation_rate = Param(within=NonNegativeReals, mutable=True, initialize=0.8/3800, doc="Degradation rate, [0, 2.5e-3]", units=pyunits.hr/pyunits.hr) # Initial conditions self.initial_state_of_charge = Var(within=NonNegativeReals, initialize=0.0, doc="State of charge at t - 1, [0, self.nameplate_energy]", units=pyunits.kWh) self.initial_energy_throughput = Var(within=NonNegativeReals, initialize=0.0, doc="Cumulative energy throughput at t - 1", units=pyunits.kWh) # Power flows and energy storage self.dt = Param(within=NonNegativeReals, initialize=1, doc="Time step for converting between electricity power flows and stored energy", units=pyunits.hr) self.elec_in = Var(within=NonNegativeReals, initialize=0.0, doc="Energy in", units=pyunits.kW) self.elec_out = Var(within=NonNegativeReals, initialize=0.0, doc="Energy out", units=pyunits.kW) self.state_of_charge = Var(within=NonNegativeReals, initialize=0.0, doc="State of charge (energy), [0, self.nameplate_energy]", units=pyunits.kWh) self.energy_throughput = Var(within=NonNegativeReals, initialize=0.0, doc="Cumulative energy throughput", units=pyunits.kWh) # Ports self.power_in = Port(noruleinit=True, doc="A port for electricity inflow") self.power_in.add(self.elec_in, "electricity") self.power_out = Port(noruleinit=True, doc="A port for electricity outflow") self.power_out.add(self.elec_out, "electricity") @self.Constraint(self.flowsheet().config.time) def state_evolution(b): return b.state_of_charge == b.initial_state_of_charge + ( b.charging_eta * b.dt * b.elec_in - b.dt / b.discharging_eta * b.elec_out) @self.Constraint(self.flowsheet().config.time) def accumulate_energy_throughput(b): return b.energy_throughput == b.initial_energy_throughput + b.dt * (b.elec_in + b.elec_out) / 2 @self.Constraint(self.flowsheet().config.time) def state_of_charge_bounds(b): return b.state_of_charge <= b.nameplate_energy - b.degradation_rate * b.energy_throughput @self.Constraint(self.flowsheet().config.time) def power_bound_in(b): return b.elec_in <= b.nameplate_power @self.Constraint(self.flowsheet().config.time) def power_bound_out(b): return b.elec_out <= b.nameplate_power
class StateBlockData(ProcessBlockData): """ This is the base class for state block data objects. These are blocks that contain the Pyomo components associated with calculating a set of thermophysical and transport properties for a given material. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "parameters", ConfigValue( domain=is_physical_parameter_block, description="""A reference to an instance of the Property Parameter Block associated with this property package.""")) CONFIG.declare( "defined_state", ConfigValue( default=False, domain=Bool, description="Flag indicating if incoming state is fully defined", doc="""Flag indicating whether the state should be considered fully defined, and thus whether constraints such as sum of mass/mole fractions should be included, **default** - False. **Valid values:** { **True** - state variables will be fully defined, **False** - state variables will not be fully defined.}""")) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=True, domain=Bool, description="Phase equilibrium constraint flag", doc="""Flag indicating whether phase equilibrium constraints should be constructed in this state block, **default** - True. **Valid values:** { **True** - StateBlock should calculate phase equilibrium, **False** - StateBlock should not calculate phase equilibrium.}""")) def __init__(self, *args, **kwargs): self._lock_attribute_creation = False super().__init__(*args, **kwargs) def lock_attribute_creation_context(self): """Returns a context manager that does not allow attributes to be created while in the context and allows attributes to be created normally outside the context. """ return _lock_attribute_creation_context(self) def is_property_constructed(self, attr): """Returns True if the attribute ``attr`` already exists, or false if it would be added in ``__getattr__``, or does not exist. Args: attr (str): Attribute name to check Return: True if the attribute is already constructed, False otherwise """ with self.lock_attribute_creation_context(): return hasattr(self, attr) @property def component_list(self): return self.parent_component()._return_component_list() @property def phase_list(self): return self.parent_component()._return_phase_list() @property def phase_component_set(self): return self.parent_component()._return_phase_component_set() @property def has_inherent_reactions(self): return self.parent_component()._has_inherent_reactions() @property def include_inherent_reactions(self): return self.parent_component()._include_inherent_reactions() def build(self): """ General build method for StateBlockDatas. Args: None Returns: None """ super(StateBlockData, self).build() add_object_reference(self, "_params", self.config.parameters) # TODO: Deprecate this at some point # Backwards compatability check for old-style property packages self._params._validate_parameter_block() @property def params(self): return self._params def define_state_vars(self): """ Method that returns a dictionary of state variables used in property package. Implement a placeholder method which returns an Exception to force users to overload this. """ raise NotImplementedError('{} property package has not implemented the' ' define_state_vars method. Please contact ' 'the property package developer.') def define_port_members(self): """ Method used to specify components to populate Ports with. Defaults to define_state_vars, and developers should overload as required. """ return self.define_state_vars() def define_display_vars(self): """ Method used to specify components to use to generate stream tables and other outputs. Defaults to define_state_vars, and developers should overload as required. """ return self.define_state_vars() def get_material_flow_terms(self, *args, **kwargs): """ Method which returns a valid expression for material flow to use in the material balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_material_flow_terms method. Please ' 'contact the property package developer.') def get_material_density_terms(self, *args, **kwargs): """ Method which returns a valid expression for material density to use in the material balances . """ raise NotImplementedError('{} property package has not implemented the' ' get_material_density_terms method. Please ' 'contact the property package developer.') def get_material_diffusion_terms(self, *args, **kwargs): """ Method which returns a valid expression for material diffusion to use in the material balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_material_diffusion_terms method. ' 'Please contact the property package ' 'developer.') def get_enthalpy_flow_terms(self, *args, **kwargs): """ Method which returns a valid expression for enthalpy flow to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_enthalpy_flow_terms method. Please ' 'contact the property package developer.') def get_energy_density_terms(self, *args, **kwargs): """ Method which returns a valid expression for enthalpy density to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_energy_density_terms method. Please ' 'contact the property package developer.') def get_energy_diffusion_terms(self, *args, **kwargs): """ Method which returns a valid expression for energy diffusion to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_energy_diffusion_terms method. ' 'Please contact the property package ' 'developer.') def get_material_flow_basis(self, *args, **kwargs): """ Method which returns an Enum indicating the basis of the material flow term. """ return MaterialFlowBasis.other def calculate_bubble_point_temperature(self, *args, **kwargs): """ Method which computes the bubble point temperature for a multi- component mixture given a pressure and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_bubble_point_temperature method.' ' Please contact the property package ' 'developer.') def calculate_dew_point_temperature(self, *args, **kwargs): """ Method which computes the dew point temperature for a multi- component mixture given a pressure and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_dew_point_temperature method.' ' Please contact the property package ' 'developer.') def calculate_bubble_point_pressure(self, *args, **kwargs): """ Method which computes the bubble point pressure for a multi- component mixture given a temperature and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_bubble_point_pressure method.' ' Please contact the property package ' 'developer.') def calculate_dew_point_pressure(self, *args, **kwargs): """ Method which computes the dew point pressure for a multi- component mixture given a temperature and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_dew_point_pressure method.' ' Please contact the property package ' 'developer.') def __getattr__(self, attr): """ This method is used to avoid generating unnecessary property calculations in state blocks. __getattr__ is called whenever a property is called for, and if a propery does not exist, it looks for a method to create the required property, and any associated components. Create a property calculation if needed. Return an attrbute error if attr == 'domain' or starts with a _ . The error for _ prevents a recursion error if trying to get a function to create a property and that function doesn't exist. Pyomo also ocasionally looks for things that start with _ and may not exist. Pyomo also looks for the domain attribute, and it may not exist. This works by creating a property calculation by calling the "_"+attr function. A list of __getattr__ calls is maintained in self.__getattrcalls to check for recursive loops which maybe useful for debugging. This list is cleared after __getattr__ completes successfully. Args: attr: an attribute to create and return. Should be a property component. """ if self._lock_attribute_creation: raise AttributeError( f"{attr} does not exist, and attribute creation is locked.") def clear_call_list(self, attr): """Local method for cleaning up call list when a call is handled. Args: attr: attribute currently being handled """ if self.__getattrcalls[-1] == attr: if len(self.__getattrcalls) <= 1: del self.__getattrcalls else: del self.__getattrcalls[-1] else: raise PropertyPackageError( "{} Trying to remove call {} from __getattr__" " call list, however this is not the most " "recent call in the list ({}). This indicates" " a bug in the __getattr__ calls. Please " "contact the IDAES developers with this bug.".format( self.name, attr, self.__getattrcalls[-1])) # Check that attr is not something we shouldn't touch if attr == "domain" or attr.startswith("_"): # Don't interfere with anything by getting attributes that are # none of my business raise PropertyPackageError( '{} {} does not exist, but is a protected ' 'attribute. Check the naming of your ' 'components to avoid any reserved names'.format( self.name, attr)) if attr == "config": try: self._get_config_args() return self.config except: raise BurntToast("{} getattr method was triggered by a call " "to the config block, but _get_config_args " "failed. This should never happen.") # Check for recursive calls try: # Check if __getattrcalls is initialized self.__getattrcalls except AttributeError: # Initialize it self.__getattrcalls = [attr] else: # Check to see if attr already appears in call list if attr in self.__getattrcalls: # If it does, indicates a recursive loop. if attr == self.__getattrcalls[-1]: # attr method is calling itself self.__getattrcalls.append(attr) raise PropertyPackageError( '{} _{} made a recursive call to ' 'itself, indicating a potential ' 'recursive loop. This is generally ' 'caused by the {} method failing to ' 'create the {} component.'.format( self.name, attr, attr, attr)) else: self.__getattrcalls.append(attr) raise PropertyPackageError( '{} a potential recursive loop has been ' 'detected whilst trying to construct {}. ' 'A method was called, but resulted in a ' 'subsequent call to itself, indicating a ' 'recursive loop. This may be caused by a ' 'method trying to access a component out ' 'of order for some reason (e.g. it is ' 'declared later in the same method). See ' 'the __getattrcalls object for a list of ' 'components called in the __getattr__ ' 'sequence.'.format(self.name, attr)) # If not, add call to list self.__getattrcalls.append(attr) # Get property information from properties metadata try: m = self.config.parameters.get_metadata().properties if m is None: raise PropertyPackageError( '{} property package get_metadata()' ' method returned None when trying to create ' '{}. Please contact the developer of the ' 'property package'.format(self.name, attr)) except KeyError: # If attr not in metadata, assume package does not # support property clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by property package (property is ' 'not listed in package metadata properties).'.format( self.name, attr)) # Get method name from resulting properties try: if m[attr]['method'] is None: # If method is none, property should be constructed # by property package, so raise PropertyPackageError clear_call_list(self, attr) raise PropertyPackageError( '{} {} should be constructed automatically ' 'by property package, but is not present. ' 'This can be caused by methods being called ' 'out of order.'.format(self.name, attr)) elif m[attr]['method'] is False: # If method is False, package does not support property # Raise NotImplementedError clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by property package ' '(property method is listed as False in ' 'package property metadata).'.format(self.name, attr)) elif isinstance(m[attr]['method'], str): # Try to get method name in from PropertyBlock object try: f = getattr(self, m[attr]['method']) except AttributeError: # If fails, method does not exist clear_call_list(self, attr) raise PropertyPackageError( '{} {} package property metadata method ' 'returned a name that does not correspond' ' to any method in the property package. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) else: # Otherwise method name is invalid clear_call_list(self, attr) raise PropertyPackageError( '{} {} package property metadata method ' 'returned invalid value for method name. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) except KeyError: # No method key - raise Exception # Need to use an AttributeError so Pyomo.DAE will handle this clear_call_list(self, attr) raise PropertyNotSupportedError( '{} package property metadata method ' 'does not contain a method for {}. ' 'Please select a package which supports ' 'the necessary properties for your process.'.format( self.name, attr)) # Call attribute if it is callable # If this fails, it should return a meaningful error. if callable(f): try: f() except Exception: # Clear call list and reraise error clear_call_list(self, attr) raise else: # If f is not callable, inform the user and clear call list clear_call_list(self, attr) raise PropertyPackageError( '{} tried calling attribute {} in order to create ' 'component {}. However the method is not callable.'.format( self.name, f, attr)) # Clear call list, and return comp = getattr(self, attr) clear_call_list(self, attr) return comp def calculate_scaling_factors(self): super().calculate_scaling_factors() # Get scaling factor defaults, if no scaling factor set for v in self.component_data_objects((Constraint, Var, Expression), descend_into=False): if iscale.get_scaling_factor(v) is None: # don't replace if set name = v.getname().split("[")[0] index = v.index() sf = self.config.parameters.get_default_scaling(name, index) if sf is not None: iscale.set_scaling_factor(v, sf)
class ReactionBlockData(ReactionBlockDataBase): """ Heterogeneous reaction package for methane reacting with Fe2O3 based OC """ # Create Class ConfigBlock CONFIG = ConfigBlock() CONFIG.declare( "parameters", ConfigValue(domain=is_reaction_parameter_block, description=""" A reference to an instance of the Reaction Parameter Block associated with this property package. """)) CONFIG.declare( "solid_state_block", ConfigValue(domain=is_state_block, description=""" A reference to an instance of a StateBlock for the solid phase with which this reaction block should be associated. """)) CONFIG.declare( "gas_state_block", ConfigValue(domain=is_state_block, description=""" A reference to an instance of a StateBlock for the gas phase with which this reaction block should be associated. """)) CONFIG.declare( "has_equilibrium", 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.} """)) def build(self): """ Callable method for Block construction """ super(ReactionBlockDataBase, self).build() # Object references to the corresponding state blocks and parameters add_object_reference(self, "_params", self.config.parameters) add_object_reference(self, "solid_state_ref", self.config.solid_state_block[self.index()]) add_object_reference(self, "gas_state_ref", self.config.gas_state_block[self.index()]) # Object reference for parameters if needed by CV1D # Reaction stoichiometry add_object_reference( self, "rate_reaction_stoichiometry", self.config.parameters.rate_reaction_stoichiometry) # Heat of reaction add_object_reference(self, "dh_rxn", self.config.parameters.dh_rxn) # Rate constant method def _k_rxn(self): self.k_rxn = Var(self._params.rate_reaction_idx, domain=Reals, initialize=1, doc='Rate constant ' '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') def rate_constant_eqn(b, j): if j == 'R1': return 1e6 * self.k_rxn[j] == \ 1e6 * (self._params.k0_rxn[j] * exp(-self._params.energy_activation[j] / (self._params.gas_const * self.solid_state_ref.temperature))) else: return Constraint.Skip try: # Try to build constraint self.rate_constant_eqn = Constraint(self._params.rate_reaction_idx, rule=rate_constant_eqn) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.k_rxn) self.del_component(self.rate_constant_eqn) raise # Conversion of oxygen carrier def _OC_conv(self): self.OC_conv = Var(domain=Reals, initialize=0.0, doc='Fraction of metal oxide converted') def OC_conv_eqn(b): return 1e6 * b.OC_conv * \ (b.solid_state_ref.mass_frac_comp['Fe3O4'] + (b.solid_state_ref._params.mw_comp['Fe3O4'] / b.solid_state_ref._params.mw_comp['Fe2O3']) * (b._params.rate_reaction_stoichiometry ['R1', 'Sol', 'Fe3O4'] / -b._params.rate_reaction_stoichiometry ['R1', 'Sol', 'Fe2O3']) * b.solid_state_ref.mass_frac_comp['Fe2O3']) == \ 1e6 * b.solid_state_ref.mass_frac_comp['Fe3O4'] try: # Try to build constraint self.OC_conv_eqn = Constraint(rule=OC_conv_eqn) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.OC_conv) self.del_component(self.OC_conv_eqn) # Conversion of oxygen carrier reformulated def _OC_conv_temp(self): self.OC_conv_temp = Var(domain=Reals, initialize=1.0, doc='Reformulation term for' 'X to help eqn scaling') def OC_conv_temp_eqn(b): return 1e3 * b.OC_conv_temp**3 == 1e3 * (1 - b.OC_conv)**2 try: # Try to build constraint self.OC_conv_temp_eqn = Constraint(rule=OC_conv_temp_eqn) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.OC_conv_temp) self.del_component(self.OC_conv_temp_eqn) # General rate of reaction method def _reaction_rate(self): self.reaction_rate = Var(self._params.rate_reaction_idx, domain=Reals, initialize=0, doc="Gen. rate of reaction [mol_rxn/m3.s]") def rate_rule(b, r): return b.reaction_rate[ r] * 1e4 == b._params._scale_factor_rxn * 1e4 * ( b.solid_state_ref.mass_frac_comp['Fe2O3'] * (1 - b.solid_state_ref.particle_porosity) * b.solid_state_ref.dens_mass_skeletal * (b._params.a_vol / (b.solid_state_ref._params.mw_comp['Fe2O3'])) * 3 * b._params.rxn_stoich_coeff[r] * b.k_rxn[r] * (((b.gas_state_ref.dens_mol_comp['CH4']**2 + b._params.eps **2)**0.5)**b._params.rxn_order[r]) * b.OC_conv_temp / (b._params.dens_mol_sol * b._params.grain_radius) / (-b._params.rate_reaction_stoichiometry['R1', 'Sol', 'Fe2O3'])) try: # Try to build constraint self.gen_rate_expression = Constraint( self._params.rate_reaction_idx, rule=rate_rule) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.reaction_rate) self.del_component(self.gen_rate_expression) raise def get_reaction_rate_basis(b): return MaterialFlowBasis.molar def model_check(blk): """ Model checks for property block """ # Check temperature bounds if value(blk.temperature) < blk.temperature.lb: _log.error('{} Temperature set below lower bound.'.format( blk.name)) if value(blk.temperature) > blk.temperature.ub: _log.error('{} Temperature set above upper bound.'.format( blk.name))
class GDPbbSolver(object): """ A branch and bound-based solver for Generalized Disjunctive Programming (GDP) problems The GDPbb solver solves subproblems relaxing certain disjunctions, and builds up a tree of potential active disjunctions. By exploring promising branches, it eventually results in an optimal configuration of disjunctions. Keyword arguments below are specified for the ``solve`` function. """ CONFIG = ConfigBlock("gdpbb") CONFIG.declare( "solver", ConfigValue(default="baron", description="Subproblem solver to use, defaults to baron")) CONFIG.declare( "solver_args", ConfigBlock( implicit=True, description="Block of keyword arguments to pass to the solver.")) CONFIG.declare( "tee", ConfigValue(default=False, domain=bool, description="Flag to stream solver output to console.")) CONFIG.declare( "check_sat", ConfigValue( default=False, domain=bool, description= "When True, GDPBB will check satisfiability via the pyomo.contrib.satsolver interface at each node" )) CONFIG.declare( "logger", ConfigValue( default='pyomo.contrib.gdpbb', description="The logger object or name to use for reporting.", domain=a_logger)) CONFIG.declare( "time_limit", ConfigValue( default=600, domain=PositiveInt, description="Time limit (seconds, default=600)", doc="Seconds allowed until terminated. Note that the time limit can" "currently only be enforced between subsolver invocations. You may" "need to set subsolver time limits as well.")) @deprecated("GDPbb has been merged into GDPopt. " "You can use the algorithm using GDPopt with strategy='LBB'.", logger="pyomo.solvers", version='TBD', remove_in='TBD') def __init__(self, *args, **kwargs): super(GDPbbSolver, self).__init__(*args, **kwargs) def available(self, exception_flag=True): """Check if solver is available. TODO: For now, it is always available. However, sub-solvers may not always be available, and so this should reflect that possibility. """ return True def version(self): return __version__ def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) return SolverFactory('gdpopt').solve( model, strategy='LBB', minlp_solver=config.solver, minlp_solver_args=config.solver_args, tee=config.tee, check_sat=config.check_sat, logger=config.logger, time_limit=config.time_limit) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total', is_main_timer=True), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver)) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str( config.solver)) setup_results_object(solve_data, config) # clone original model for root node of branch and bound root = solve_data.working_model = solve_data.original_model.clone() # get objective sense process_objective(solve_data, config) objectives = solve_data.original_model.component_data_objects( Objective, active=True) obj = next(objectives, None) solve_data.results.problem.sense = obj.sense # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the relaxed disjunctions root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active) root.GDPbb_utils.deactivated_constraints = ComponentSet([ constr for disjunction in root.GDPbb_utils.unenforced_disjunctions for disjunct in disjunction.disjuncts for constr in disjunct.component_data_objects(ctype=Constraint, active=True) if constr.body.polynomial_degree() not in (1, 0) ]) # Deactivate nonlinear constraints in unenforced disjunctions for constr in root.GDPbb_utils.deactivated_constraints: constr.deactivate() # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation. if not hasattr(root, 'BigM'): root.BigM = Suffix() # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed # to True/False values. # TODO this should also be done within the loop, but we aren't handling it right now. # Should affect efficiency, but not correctness. root.GDPbb_utils.disjuncts_fixed_True = ComponentSet() # Only find top-level (non-nested) disjunctions for disjunction in root.component_data_objects(Disjunction, active=True): fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 1 ] fixed_false_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 0 ] for disjunct in fixed_false_disjuncts: disjunct.deactivate() if len(fixed_false_disjuncts) == len( disjunction.disjuncts) - 1: # all but one disjunct in the disjunction is fixed to False. Remaining one must be true. if not fixed_true_disjuncts: fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct not in fixed_false_disjuncts ] # Reactivate the fixed-true disjuncts for disjunct in fixed_true_disjuncts: newly_activated = ComponentSet() for constr in disjunct.component_data_objects(Constraint): if constr in root.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint root.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. root.GDPbb_utils.deactivated_constraints -= newly_activated root.GDPbb_utils.disjuncts_fixed_True.add(disjunct) if fixed_true_disjuncts: assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (disjunction.name, ) root.GDPbb_utils.unenforced_disjunctions.remove( disjunction) # Check satisfiability if config.check_sat and satisfiable(root, config.logger) is False: # Problem is not satisfiable. Problem is infeasible. obj_value = obj_sign * float('inf') else: # solve the root node config.logger.info("Solving the root node.") obj_value, result, var_values = self.subproblem_solve( root, config) if obj_sign * obj_value == float('inf'): config.logger.info( "Model was found to be infeasible at the root node. Elapsed %.2f seconds." % get_main_elapsed_time(solve_data.timing)) if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = float('inf') solve_data.results.problem.upper_bound = None else: solve_data.results.problem.lower_bound = None solve_data.results.problem.upper_bound = float('-inf') solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = 0 solve_data.results.solver.termination_condition = tc.infeasible return solve_data.results # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] total_nodes_counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush(heap, ((obj_sign * obj_value, disjunctions_left, -total_nodes_counter), root, result, var_values)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop( heap) incumbent_obj_value, disjunctions_left, _ = sort_tuple config.logger.info( "Exploring node with LB %.10g and %s inactive disjunctions." % (incumbent_obj_value, disjunctions_left)) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. original_model = solve_data.original_model for orig_var, val in zip( original_model.GDPbb_utils.variable_list, incumbent_var_values): orig_var.value = val solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition return solve_data.results # Pick the next disjunction to branch on next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[ 0] config.logger.info("Branching on disjunction %s" % next_disjunction.name) assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (next_disjunction.name, ) new_nodes_counter = 0 for i, disjunct in enumerate(next_disjunction.disjuncts): # Create one branch for each of the disjuncts on the disjunction if any(disj.indicator_var.fixed and disj.indicator_var.value == 1 for disj in next_disjunction.disjuncts if disj is not disjunct): # If any other disjunct is fixed to 1 and an xor relationship applies, # then this disjunct cannot be activated. continue # Check time limit if get_main_elapsed_time( solve_data.timing) >= config.time_limit: if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = incumbent_obj_value solve_data.results.problem.upper_bound = float( 'inf') else: solve_data.results.problem.lower_bound = float( '-inf') solve_data.results.problem.upper_bound = incumbent_obj_value config.logger.info('GDPopt unable to converge bounds ' 'before time limit of {} seconds. ' 'Elapsed: {} seconds'.format( config.time_limit, get_main_elapsed_time( solve_data.timing))) config.logger.info( 'Final bound values: LB: {} UB: {}'.format( solve_data.results.problem.lower_bound, solve_data.results.problem.upper_bound)) solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = tc.maxTimeLimit return solve_data.results # Branch on the disjunct child = incumbent_model.clone() # TODO I am leaving the old branching system in place, but there should be # something better, ideally that deals with nested disjunctions as well. disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop( 0) child_disjunct = disjunction_to_branch.disjuncts[i] child_disjunct.indicator_var.fix(1) # Deactivate (and fix to 0) other disjuncts on the disjunction for disj in disjunction_to_branch.disjuncts: if disj is not child_disjunct: disj.deactivate() # Activate nonlinear constraints on the newly fixed child disjunct newly_activated = ComponentSet() for constr in child_disjunct.component_data_objects( Constraint): if constr in child.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint child.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. child.GDPbb_utils.deactivated_constraints -= newly_activated child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct) if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True: # If the disjunct was already branched to True from a parent disjunct branching, just pass # through the incumbent value without resolving. The solution should be the same as the parent. total_nodes_counter += 1 ordering_tuple = (obj_sign * incumbent_obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, incumbent_var_values)) new_nodes_counter += 1 continue if config.check_sat and satisfiable( child, config.logger) is False: # Problem is not satisfiable. Skip this disjunct. continue obj_value, result, var_values = self.subproblem_solve( child, config) total_nodes_counter += 1 ordering_tuple = (obj_sign * obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, var_values)) new_nodes_counter += 1 config.logger.info( "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % (new_nodes_counter, disjunctions_left - 1, len(heap))) @staticmethod def validate_model(model): # Validates that model has only exclusive disjunctions for d in model.component_data_objects(ctype=Disjunction, active=True): if not d.xor: raise ValueError('GDPbb solver unable to handle ' 'non-exclusive disjunctions') objectives = model.component_data_objects(Objective, active=True) obj = next(objectives, None) if next(objectives, None) is not None: raise RuntimeError( "GDPbb solver is unable to handle model with multiple active objectives." ) if obj is None: raise RuntimeError( "GDPbb solver is unable to handle model with no active objective." ) @staticmethod def subproblem_solve(gdp, config): subproblem = gdp.clone() TransformationFactory('gdp.bigm').apply_to(subproblem) main_obj = next( subproblem.component_data_objects(Objective, active=True)) obj_sign = 1 if main_obj.sense == minimize else -1 try: result = SolverFactory(config.solver).solve( subproblem, **config.solver_args) except RuntimeError as e: config.logger.warning( "Solver encountered RuntimeError. Treating as infeasible. " "Msg: %s\n%s" % (str(e), traceback.format_exc())) var_values = [ v.value for v in subproblem.GDPbb_utils.variable_list ] return obj_sign * float('inf'), SolverResults(), var_values var_values = [v.value for v in subproblem.GDPbb_utils.variable_list] term_cond = result.solver.termination_condition if result.solver.status is SolverStatus.ok and any( term_cond == valid_cond for valid_cond in (tc.optimal, tc.locallyOptimal, tc.feasible)): return value(main_obj.expr), result, var_values elif term_cond == tc.unbounded: return obj_sign * float('-inf'), result, var_values elif term_cond == tc.infeasible: return obj_sign * float('inf'), result, var_values else: config.logger.warning("Unknown termination condition of %s" % term_cond) return obj_sign * float('inf'), result, var_values def __enter__(self): return self def __exit__(self, t, v, traceback): pass
class ReactionParameterData(ReactionParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for superheated steam. """ # Create Class ConfigBlock CONFIG = ConfigBlock() CONFIG.declare( "gas_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the gas phase.", domain=is_physical_parameter_block)) CONFIG.declare( "solid_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the solid phase.", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def build(self): ''' Callable method for Block construction. ''' super(ReactionParameterBlock, self).build() self._reaction_block_class = ReactionBlock # Create Phase objects self.Vap = VaporPhase() self.Sol = SolidPhase() # Create Component objects self.CH4 = Component() self.CO2 = Component() self.H2O = Component() self.Fe2O3 = Component() self.Fe3O4 = Component() self.Al2O3 = Component() # Component list subsets self.gas_component_list = Set(initialize=['CO2', 'H2O', 'CH4']) self.sol_component_list = Set(initialize=['Fe2O3', 'Fe3O4', 'Al2O3']) # Reaction Index self.rate_reaction_idx = Set(initialize=["R1"]) # Gas Constant self.gas_const = Param(within=PositiveReals, mutable=False, default=8.314459848e-3, doc='Gas Constant [kJ/mol.K]') # Smoothing factor self.eps = Param(mutable=True, default=1e-8, doc='Smoothing Factor') # Reaction rate scale factor self._scale_factor_rxn = Param(mutable=True, default=1, doc='Scale Factor for reaction eqn.' 'Used to help initialization routine') # Reaction Stoichiometry self.rate_reaction_stoichiometry = { ("R1", "Vap", "CH4"): -1, ("R1", "Vap", "CO2"): 1, ("R1", "Vap", "H2O"): 2, ("R1", "Sol", "Fe2O3"): -12, ("R1", "Sol", "Fe3O4"): 8, ("R1", "Sol", "Al2O3"): 0 } # Reaction stoichiometric coefficient self.rxn_stoich_coeff = Param(self.rate_reaction_idx, default=12, mutable=True, doc='Reaction stoichiometric' 'coefficient [-]') # Standard Heat of Reaction - kJ/mol_rxn dh_rxn_dict = {"R1": 136.5843} self.dh_rxn = Param(self.rate_reaction_idx, initialize=dh_rxn_dict, doc="Heat of reaction [kJ/mol]") # ------------------------------------------------------------------------- """ Reaction properties that can be estimated""" # Particle grain radius within OC particle self.grain_radius = Var(domain=Reals, initialize=2.6e-7, doc='Representative particle grain' 'radius within OC particle [m]') self.grain_radius.fix() # Molar density OC particle self.dens_mol_sol = Var(domain=Reals, initialize=32811, doc='Molar density of OC particle [mol/m^3]') self.dens_mol_sol.fix() # Available volume for reaction - from EPAT report (1-ep)' self.a_vol = Var(domain=Reals, initialize=0.28, doc='Available reaction vol. per vol. of OC') self.a_vol.fix() # Activation Energy self.energy_activation = Var(self.rate_reaction_idx, domain=Reals, initialize=4.9e1, doc='Activation energy [kJ/mol]') self.energy_activation.fix() # Reaction order self.rxn_order = Var(self.rate_reaction_idx, domain=Reals, initialize=1.3, doc='Reaction order in gas species [-]') self.rxn_order.fix() # Pre-exponential factor self.k0_rxn = Var(self.rate_reaction_idx, domain=Reals, initialize=8e-4, doc='Pre-exponential factor' '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') self.k0_rxn.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'k_rxn': { 'method': '_k_rxn', 'units': 'mol^(1-N_reaction)m^(3*N_reaction -2)/s]' }, 'OC_conv': { 'method': "_OC_conv", 'units': None }, 'OC_conv_temp': { 'method': "_OC_conv_temp", 'units': None }, 'reaction_rate': { 'method': "_reaction_rate", 'units': 'mol_rxn/m3.s' } }) obj.add_default_units({ 'time': 's', 'length': 'm', 'mass': 'kg', 'amount': 'mol', 'temperature': 'K', 'energy': 'kJ' })
class IdealParameterData(PhysicalParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for BTX system. """ # Config block for the _IdealStateBlock CONFIG = PhysicalParameterBlock.CONFIG() CONFIG.declare( "valid_phase", ConfigValue(default=('Vap', 'Liq'), domain=In(['Liq', 'Vap', ('Vap', 'Liq'), ('Liq', 'Vap')]), description="Flag indicating the valid phase", doc="""Flag indicating the valid phase for a given set of conditions, and thus corresponding constraints should be included, **default** - ('Vap', 'Liq'). **Valid values:** { **'Liq'** - Liquid only, **'Vap'** - Vapor only, **('Vap', 'Liq')** - Vapor-liquid equilibrium, **('Liq', 'Vap')** - Vapor-liquid equilibrium,}""")) def build(self): ''' Callable method for Block construction. ''' super(IdealParameterData, self).build() self.state_block_class = IdealStateBlock # List of valid phases in property package if self.config.valid_phase == ('Liq', 'Vap') or \ self.config.valid_phase == ('Vap', 'Liq'): self.phase_list = Set(initialize=['Liq', 'Vap'], ordered=True) elif self.config.valid_phase == 'Liq': self.phase_list = Set(initialize=['Liq']) else: self.phase_list = Set(initialize=['Vap']) @classmethod def define_metadata(cls, obj): """Define properties supported and units.""" obj.add_properties({ 'flow_mol': { 'method': None, 'units': 'mol/s' }, 'mole_frac_comp': { 'method': None, 'units': 'none' }, 'temperature': { 'method': None, 'units': 'K' }, 'pressure': { 'method': None, 'units': 'Pa' }, 'flow_mol_phase': { 'method': None, 'units': 'mol/s' }, 'dens_mol_phase': { 'method': '_dens_mol_phase', 'units': 'mol/m^3' }, 'pressure_sat': { 'method': '_pressure_sat', 'units': 'Pa' }, 'mole_frac_phase_comp': { 'method': '_mole_frac_phase', 'units': 'no unit' }, 'energy_internal_mol_phase_comp': { 'method': '_energy_internal_mol_phase_comp', 'units': 'J/mol' }, 'energy_internal_mol_phase': { 'method': '_enenrgy_internal_mol_phase', 'units': 'J/mol' }, 'enth_mol_phase_comp': { 'method': '_enth_mol_phase_comp', 'units': 'J/mol' }, 'enth_mol_phase': { 'method': '_enth_mol_phase', 'units': 'J/mol' }, 'entr_mol_phase_comp': { 'method': '_entr_mol_phase_comp', 'units': 'J/mol' }, 'entr_mol_phase': { 'method': '_entr_mol_phase', 'units': 'J/mol' }, 'temperature_bubble': { 'method': '_temperature_bubble', 'units': 'K' }, 'temperature_dew': { 'method': '_temperature_dew', 'units': 'K' }, 'pressure_bubble': { 'method': '_pressure_bubble', 'units': 'Pa' }, 'pressure_dew': { 'method': '_pressure_dew', 'units': 'Pa' }, 'fug_vap': { 'method': '_fug_vap', 'units': 'Pa' }, 'fug_liq': { 'method': '_fug_liq', 'units': 'Pa' }, 'dh_vap': { 'method': '_dh_vap', 'units': 'J/mol' }, 'ds_vap': { 'method': '_ds_vap', 'units': 'J/mol.K' } }) obj.add_default_units({ 'time': 's', 'length': 'm', 'mass': 'g', 'amount': 'mol', 'temperature': 'K', 'energy': 'J', 'holdup': 'mol' })
class ReverseOsmosis1DData(_ReverseOsmosisBaseData): """Standard 1D Reverse Osmosis Unit Model Class.""" CONFIG = _ReverseOsmosisBaseData.CONFIG() CONFIG.declare( "area_definition", ConfigValue( default=DistributedVars.uniform, domain=In(DistributedVars), description="Argument for defining form of area variable", doc="""Argument defining whether area variable should be spatially variant or not. **default** - DistributedVars.uniform. **Valid values:** { DistributedVars.uniform - area does not vary across spatial domain, DistributedVars.variant - area can vary over the domain and is indexed by time and space.}""")) 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 transforming domain. See Pyomo documentation for supported schemes.""")) CONFIG.declare( "finite_elements", ConfigValue( default=20, domain=int, description="Number of finite elements in 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=5)""")) def _process_config(self): if self.config.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the " "reverse osmosis module. " "Defaulting to finite " "difference method.") self.config.transformation_method = "dae.finite_difference" if self.config.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the " "reverse osmosis module." "Defaulting to backward finite " "difference.") self.config.transformation_scheme = "BACKWARD" def build(self): """ Build 1D RO model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Check configuration errors self._process_config() # Build 1D Control volume for feed side self.feed_side = feed_side = ControlVolume1DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "area_definition": self.config.area_definition, "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 }) # Add geometry to feed side feed_side.add_geometry() # Add state blocks to feed side feed_side.add_state_blocks(has_phase_equilibrium=False) # Populate feed side feed_side.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) feed_side.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Apply transformation to feed side feed_side.apply_transformation() add_object_reference(self, 'length_domain', self.feed_side.length_domain) self.first_element = self.length_domain.first() self.difference_elements = Set(ordered=True, initialize=(x for x in self.length_domain if x != self.first_element)) # Add inlet/outlet ports for feed side self.add_inlet_port(name="inlet", block=feed_side) self.add_outlet_port(name="retentate", block=feed_side) # Make indexed stateblock and separate stateblock for permeate-side and permeate outlet, respectively. tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = False # these blocks are not inlets self.permeate_side = self.config.property_package.state_block_class( self.flowsheet().config.time, self.length_domain, doc="Material properties of permeate along permeate channel", default=tmp_dict) self.mixed_permeate = self.config.property_package.state_block_class( self.flowsheet().config.time, doc="Material properties of mixed permeate exiting the module", default=tmp_dict) # Membrane interface: indexed state block self.feed_side.properties_interface = self.config.property_package.state_block_class( self.flowsheet().config.time, self.length_domain, doc="Material properties of feed-side membrane interface", default=tmp_dict) # Add port to mixed_permeate self.add_port(name="permeate", block=self.mixed_permeate) # ========================================================================== """ Add references to control volume geometry.""" add_object_reference(self, 'length', feed_side.length) add_object_reference(self, 'area_cross', feed_side.area) # Add reference to pressure drop for feed side only if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): add_object_reference(self, 'dP_dx', feed_side.deltaP) self._make_performance() self._add_expressions() def _make_performance(self): """ Variables and constraints for unit model. Args: None Returns: None """ solvent_set = self.config.property_package.solvent_set solute_set = self.config.property_package.solute_set # Units units_meta = \ self.config.property_package.get_metadata().get_derived_units # ========================================================================== self.width = Var(initialize=1, bounds=(1e-1, 1e3), domain=NonNegativeReals, units=units_meta('length'), doc='Membrane width') super()._make_performance() # mass transfer def mass_transfer_phase_comp_initialize(b, t, x, p, j): return value( self.feed_side.properties[t, x].get_material_flow_terms( 'Liq', j) * self.recovery_mass_phase_comp[t, 'Liq', j]) self.mass_transfer_phase_comp = Var( self.flowsheet().config.time, self.length_domain, self.config.property_package.phase_list, self.config.property_package.component_list, initialize=mass_transfer_phase_comp_initialize, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=units_meta('mass') * units_meta('time')**-1 * units_meta('length')**-1, doc='Mass transfer to permeate') if self.config.has_pressure_change: self.deltaP = Var(self.flowsheet().config.time, initialize=-1e5, bounds=(-1e6, 0), domain=NegativeReals, units=units_meta('pressure'), doc='Pressure drop across unit') # ========================================================================== # Mass transfer term equation @self.Constraint(self.flowsheet().config.time, self.difference_elements, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_transfer_term(b, t, x, p, j): return b.mass_transfer_phase_comp[ t, x, p, j] == -b.feed_side.mass_transfer_term[t, x, p, j] # ========================================================================== # Mass flux = feed mass transfer equation @self.Constraint(self.flowsheet().config.time, self.difference_elements, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_flux_equal_mass_transfer(b, t, x, p, j): return b.flux_mass_phase_comp[ t, x, p, j] * b.width == -b.feed_side.mass_transfer_term[t, x, p, j] # ========================================================================== # Mass flux equations (Jw and Js) # ========================================================================== # Final permeate mass flow rate (of solvent and solute) --> Mp,j, final = sum(Mp,j) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Permeate mass flow rates exiting unit") def eq_permeate_production(b, t, p, j): return (b.mixed_permeate[t].get_material_flow_terms(p, j) == sum( b.permeate_side[t, x].get_material_flow_terms(p, j) for x in b.difference_elements)) # ========================================================================== # Feed and permeate-side mass transfer connection --> Mp,j = Mf,transfer = Jj * W * L/n @self.Constraint(self.flowsheet().config.time, self.difference_elements, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer from feed to permeate") def eq_connect_mass_transfer(b, t, x, p, j): return (b.permeate_side[t, x].get_material_flow_terms( p, j) == -b.feed_side.mass_transfer_term[t, x, p, j] * b.length / b.nfe) ## ========================================================================== # Pressure drop if (self.config.pressure_change_type == PressureChangeType.fixed_per_unit_length or self.config.pressure_change_type == PressureChangeType.calculated): @self.Constraint(self.flowsheet().config.time, doc='Pressure drop across unit') def eq_pressure_drop(b, t): return (b.deltaP[t] == sum(b.dP_dx[t, x] * b.length / b.nfe for x in b.difference_elements)) if (self.config.pressure_change_type == PressureChangeType.fixed_per_stage and self.config.has_pressure_change): @self.Constraint(self.flowsheet().config.time, self.length_domain, doc='Fixed pressure drop across unit') def eq_pressure_drop(b, t, x): return b.deltaP[t] == b.length * b.dP_dx[t, x] ## ========================================================================== # Feed-side isothermal conditions # NOTE: this could go on the feed_side block, but that seems to hurt initialization # in the tests for this unit @self.Constraint(self.flowsheet().config.time, self.difference_elements, doc="Isothermal assumption for feed channel") def eq_feed_isothermal(b, t, x): return b.feed_side.properties[t, b.first_element].temperature == \ b.feed_side.properties[t, x].temperature def initialize_build(blk, initialize_guess=None, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, fail_on_warning=False, ignore_dof=False): """ Initialization routine for 1D-RO unit. Keyword Arguments: initialize_guess : a dict of guesses for solvent_recovery, solute_recovery, and cp_modulus. These guesses offset the initial values for the retentate, permeate, and membrane interface state blocks from the inlet feed (default = {'deltaP': -1e4, 'solvent_recovery': 0.5, 'solute_recovery': 0.01, 'cp_modulus': 1.1}) state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for the inlet feed side state block (see documentation of the specific property package) (default = None). outlvl : sets output level of initialization routine solver : str indicating which solver to use during initialization (default = None, use default solver) optarg : solver options dictionary object (default=None, use default solver options) fail_on_warning : boolean argument to fail or only produce warning upon unsuccessful solve (default=False) ignore_dof : boolean argument to ignore when DOF != 0 (default=False) 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) source = blk.feed_side.properties[blk.flowsheet().config.time.first(), blk.first_element] state_args = blk._get_state_args(source, blk.mixed_permeate[0], initialize_guess, state_args) # --------------------------------------------------------------------- # Step 1: Initialize feed_side, permeate_side, and mixed_permeate blocks flags_feed_side = blk.feed_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args['feed_side'], hold_state=True) init_log.info("Initialization Step 1 Complete") if not ignore_dof: check_dof(blk, fail_flag=fail_on_warning, logger=init_log) # --------------------------------------------------------------------- # Initialize other state blocks # base properties on inlet state block flag_feed_side_properties_interface = blk.feed_side.properties_interface.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args['interface']) flags_permeate_side = blk.permeate_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args['permeate']) flags_mixed_permeate = blk.mixed_permeate.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args['permeate']) init_log.info("Initialization Step 2 Complete.") # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) # occasionally it might be worth retrying a solve if not check_optimal_termination(res): init_log.warn( "Trouble solving ReverseOsmosis1D unit model, trying one more time" ) res = opt.solve(blk, tee=slc.tee) check_solve(res, logger=init_log, fail_flag=fail_on_warning, checkpoint='Initialization Step 3') # --------------------------------------------------------------------- # Release Inlet state blk.feed_side.release_state(flags_feed_side, outlvl) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.dens_solvent) is None: sf = iscale.get_scaling_factor( self.feed_side.properties[0, 0].dens_mass_phase['Liq']) iscale.set_scaling_factor(self.dens_solvent, sf) super().calculate_scaling_factors() # these variables should have user input, if not there will be a warning if iscale.get_scaling_factor(self.width) is None: sf = iscale.get_scaling_factor(self.width, default=1, warning=True) iscale.set_scaling_factor(self.width, sf) if iscale.get_scaling_factor(self.length) is None: sf = iscale.get_scaling_factor(self.length, default=10, warning=True) iscale.set_scaling_factor(self.length, sf) # setting scaling factors for variables # will not override if the user provides the scaling factor ## default of 1 set by ControlVolume1D if iscale.get_scaling_factor(self.area_cross) == 1: iscale.set_scaling_factor(self.area_cross, 100) for (t, x, p, j), v in self.mass_transfer_phase_comp.items(): sf = (iscale.get_scaling_factor( self.feed_side.properties[t, x].get_material_flow_terms(p, j)) / iscale.get_scaling_factor(self.feed_side.length)) * value( self.nfe) if iscale.get_scaling_factor(v) is None: iscale.set_scaling_factor(v, sf) v = self.feed_side.mass_transfer_term[t, x, p, j] if iscale.get_scaling_factor(v) is None: iscale.set_scaling_factor(v, sf) if hasattr(self, 'deltaP'): for v in self.deltaP.values(): if iscale.get_scaling_factor(v) is None: iscale.set_scaling_factor(v, 1e-4) if hasattr(self, 'dP_dx'): for v in self.feed_side.pressure_dx.values(): iscale.set_scaling_factor(v, 1e-5) else: for v in self.feed_side.pressure_dx.values(): iscale.set_scaling_factor(v, 1e5)
class TranslatorData(UnitModelBlockData): """ Standard Translator Block Class """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Translator blocks are always steady-state.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Translator blocks do not contain holdup.""", ), ) CONFIG.declare( "outlet_state_defined", ConfigValue( default=True, domain=Bool, description="Indicated whether outlet state will be fully defined", doc="""Indicates whether unit model will fully define outlet state. If False, the outlet property package will enforce constraints such as sum of mole fractions and phase equilibrium. **default** - True. **Valid values:** { **True** - outlet state will be fully defined, **False** - outlet property package should enforce sumation and equilibrium constraints.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Indicates whether outlet is in phase equilibrium", doc="""Indicates whether outlet property package should enforce phase equilibrium constraints. **default** - False. **Valid values:** { **True** - outlet property package should calculate phase equilibrium, **False** - outlet property package should notcalculate phase equilibrium.} """, ), ) CONFIG.declare( "inlet_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for incoming stream", doc="""Property parameter object used to define property calculations for the incoming stream, **default** - None. **Valid values:** { **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "inlet_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package " "of the incoming stream", doc="""A ConfigBlock with arguments to be passed to the property block associated with the incoming stream, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "outlet_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for outgoing stream", doc="""Property parameter object used to define property calculations for the outgoing stream, **default** - None. **Valid values:** { **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "outlet_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package " "of the outgoing stream", doc="""A ConfigBlock with arguments to be passed to the property block associated with the outgoing stream, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) def build(self): """ Begin building model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(TranslatorData, self).build() # Check construction argumnet consistency if (self.config.outlet_state_defined and self.config.has_phase_equilibrium): raise ConfigurationError( "{} cannot calcuate phase equilibrium (has_phase_equilibrium " "= True) when outlet state is set to be fully defined (" "outlet_state_defined = True).".format(self.name) ) # Add State Blocks self.properties_in = self.config.inlet_property_package.build_state_block( self.flowsheet().time, doc="Material properties in incoming stream", default={ "defined_state": True, "has_phase_equilibrium": False, **self.config.inlet_property_package_args, }, ) self.properties_out = self.config.outlet_property_package.build_state_block( self.flowsheet().time, doc="Material properties in outgoing stream", default={ "defined_state": self.config.outlet_state_defined, "has_phase_equilibrium": self.config.has_phase_equilibrium, **self.config.outlet_property_package_args, }, ) # Add outlet port self.add_port(name="inlet", block=self.properties_in, doc="Inlet Port") self.add_port(name="outlet", block=self.properties_out, doc="Outlet Port") def initialize( blk, state_args_in=None, state_args_out=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ This method calls the initialization method of the state blocks. Keyword Arguments: state_args_in : a dict of arguments to be passed to the inlet property package (to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_out : a dict of arguments to be passed to the outlet property package (to provide an initial state for initialization (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) Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize state block flags = blk.properties_in.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_in, hold_state=True, ) blk.properties_out.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_out, ) if degrees_of_freedom(blk) == 0: with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info("Initialization Complete {}." .format(idaeslog.condition(res))) else: init_log.warning("Initialization incomplete. Degrees of freedom " "were not zero. Please provide sufficient number " "of constraints linking the state variables " "between the two state blocks.") blk.properties_in.release_state(flags=flags, outlvl=outlvl)
class PhysicalParameterData(PhysicalParameterBlock): """ Property Parameter Block Class. """ # Config block for the _IdealStateBlock CONFIG = PhysicalParameterBlock.CONFIG() CONFIG.declare("valid_phase", ConfigValue( default=('Vap', 'Liq'), domain=In(['Liq', 'Vap', ('Vap', 'Liq'), ('Liq', 'Vap')]), description="Flag indicating the valid phase", doc="""Flag indicating the valid phase for a given set of conditions, and thus corresponding constraints should be included, **default** - ('Vap', 'Liq'). **Valid values:** { **'Liq'** - Liquid only, **'Vap'** - Vapor only, **('Vap', 'Liq')** - Vapor-liquid equilibrium, **('Liq', 'Vap')** - Vapor-liquid equilibrium,}""")) CONFIG.declare("Cp", ConfigValue( default=0.035, domain=float, description="Constant pressure heat capacity in MJ/(kmol K)", doc="""Value for the constant pressure heat capacity, **default** = 0.035 MJ/(kmol K)""")) def build(self): ''' Callable method for Block construction. ''' super(PhysicalParameterData, self).build() self._state_block_class = IdealStateBlock # List of valid phases and components in property package if self.config.valid_phase == ('Liq', 'Vap') or \ self.config.valid_phase == ('Vap', 'Liq'): self.Liq = LiquidPhase() self.Vap = VaporPhase() elif self.config.valid_phase == 'Liq': self.Liq = LiquidPhase() else: self.Vap = VaporPhase() self.CH4 = Component() self.CO = Component() self.H2 = Component() self.CH3OH = Component() self.phase_equilibrium_idx = Set(initialize=[1, 2, 3, 4]) self.phase_equilibrium_list = \ {1: ["CH4", ("Vap", "Liq")], 2: ["CO", ("Vap", "Liq")], 3: ["H2", ("Vap", "Liq")], 4: ["CH3OH", ("Vap", "Liq")]} # Antoine coefficients assume pressure in mmHG and temperature in K self.vapor_pressure_coeff = {('CH4', 'A'): 15.2243, ('CH4', 'B'): 897.84, ('CH4', 'C'): -7.16, ('CO', 'A'): 14.3686, ('CO', 'B'): 530.22, ('CO', 'C'): -13.15, ('H2', 'A'): 13.6333, ('H2', 'B'): 164.9, ('H2', 'C'): 3.19, ('CH3OH', 'A'): 18.5875, ('CH3OH', 'B'): 3626.55, ('CH3OH', 'C'): -34.29} Cp = self.config.Cp Cv = value(Cp - pyunits.convert(Constants.gas_constant, pyunits.MJ/pyunits.kmol/pyunits.K)) gamma = Cp / Cv self.gamma = Param(within=NonNegativeReals, mutable=True, default=gamma, doc='Ratio of Cp to Cv') self.Cp = Param(within=NonNegativeReals, mutable=True, default=Cp, units=pyunits.MJ/pyunits.kmol/pyunits.K, doc='Constant pressure heat capacity') @classmethod def define_metadata(cls, obj): """Define properties supported and units.""" obj.add_properties( {'flow_mol': {'method': None}, 'mole_frac': {'method': None}, 'temperature': {'method': None}, 'pressure': {'method': None}, 'flow_mol_phase': {'method': None}, 'density_mol': {'method': '_density_mol'}, 'vapor_pressure': {'method': '_vapor_pressure'}, 'mole_frac_phase': {'method': '_mole_frac_phase'}, 'enthalpy_comp_liq': {'method': '_enthalpy_comp_liq'}, 'enthalpy_comp_vap': {'method': '_enthalpy_comp_vap'}, 'enthalpy_liq': {'method': '_enthalpy_liq'}, 'enthalpy_vap': {'method': '_enthalpy_vap'}}) obj.add_default_units({'time': pyunits.s, 'length': pyunits.m, 'mass': pyunits.Gg, # yields base units MJ, MPa 'amount': pyunits.kmol, 'temperature': pyunits.hK})
def _make_heater_config_block(config): """ Declare configuration options for HeaterData block. """ config.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, 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.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_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_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 _add_OA_configs(CONFIG): CONFIG.declare("init_strategy", ConfigValue( default="set_covering", domain=In(valid_init_strategies.keys()), description="Initialization strategy to use.", doc="Selects the initialization strategy to use when generating " "the initial cuts to construct the master problem." )) CONFIG.declare("custom_init_disjuncts", ConfigList( # domain=ComponentSets of Disjuncts, default=None, description="List of disjunct sets to use for initialization." )) CONFIG.declare("max_slack", ConfigValue( default=1000, domain=NonNegativeFloat, description="Upper bound on slack variables for OA" )) CONFIG.declare("OA_penalty_factor", ConfigValue( default=1000, domain=NonNegativeFloat, description="Penalty multiplication term for slack variables on the " "objective value." )) CONFIG.declare("set_cover_iterlim", ConfigValue( default=8, domain=NonNegativeInt, description="Limit on the number of set covering iterations." )) CONFIG.declare("call_before_master_solve", ConfigValue( default=_DoNothing, description="callback hook before calling the master problem solver" )) CONFIG.declare("call_after_master_solve", ConfigValue( default=_DoNothing, description="callback hook after a solution of the master problem" )) CONFIG.declare("call_before_subproblem_solve", ConfigValue( default=_DoNothing, description="callback hook before calling the subproblem solver" )) CONFIG.declare("call_after_subproblem_solve", ConfigValue( default=_DoNothing, description="callback hook after a solution of the " "nonlinear subproblem" )) CONFIG.declare("call_after_subproblem_feasible", ConfigValue( default=_DoNothing, description="callback hook after feasible solution of " "the nonlinear subproblem" )) CONFIG.declare("algorithm_stall_after", ConfigValue( default=2, description="number of non-improving master iterations after which " "the algorithm will stall and exit." )) CONFIG.declare("round_discrete_vars", ConfigValue( default=True, description="flag to round subproblem discrete variable values to the " "nearest integer. Rounding is done before fixing disjuncts." )) CONFIG.declare("force_subproblem_nlp", ConfigValue( default=False, description="Force subproblems to be NLP, even if discrete variables " "exist." )) CONFIG.declare("mip_presolve", ConfigValue( default=True, description="Flag to enable or diable GDPopt MIP presolve. " "Default=True.", domain=bool )) CONFIG.declare("subproblem_presolve", ConfigValue( default=True, description="Flag to enable or disable subproblem presolve. " "Default=True.", domain=bool )) CONFIG.declare("max_fbbt_iterations", ConfigValue( default=3, description="Maximum number of feasibility-based bounds tightening " "iterations to do during NLP subproblem preprocessing.", domain=PositiveInt )) CONFIG.declare("tighten_nlp_var_bounds", ConfigValue( default=False, description="Whether or not to do feasibility-based bounds tightening " "on the variables in the NLP subproblem before solving it.", domain=bool )) CONFIG.declare("calc_disjunctive_bounds", ConfigValue( default=False, description="Calculate special disjunctive variable bounds for GLOA. " "False by default.", domain=bool )) CONFIG.declare("obbt_disjunctive_bounds", ConfigValue( default=False, description="Use optimality-based bounds tightening rather than " "feasibility-based bounds tightening to compute disjunctive variable " "bounds. False by default.", domain=bool )) return CONFIG