def test_In(self): c = ConfigBlock() c.declare('a', ConfigValue(None, In([1,3,5]))) self.assertEqual(c.a, None) c.a = 3 self.assertEqual(c.a, 3) with self.assertRaises(ValueError): c.a = 2 self.assertEqual(c.a, 3) with self.assertRaises(ValueError): c.a = {} self.assertEqual(c.a, 3) with self.assertRaises(ValueError): c.a = '1' self.assertEqual(c.a, 3) c.declare('b', ConfigValue(None, In([1,3,5], int))) self.assertEqual(c.b, None) c.b = 3 self.assertEqual(c.b, 3) with self.assertRaises(ValueError): c.b = 2 self.assertEqual(c.b, 3) with self.assertRaises(ValueError): c.b = {} self.assertEqual(c.b, 3) c.b = '1' self.assertEqual(c.b, 1)
def _make_pem_electrolyzer_config_block(config): config.declare("dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""PEM Electrolyzer 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="""Gibbs reactors do not have defined volume, 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, **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 _define_feedwater_heater_0D_config(config): config.declare( "has_drain_mixer", ConfigValue( default=True, domain=In([True, False]), description="Add a mixer to the inlet of the condensing section", doc="""Add a mixer to the inlet of the condensing section to add water from the drain of another feedwaterheater to the steam, if True""", ), ) config.declare( "has_desuperheat", ConfigValue( default=True, domain=In([True, False]), description="Add a mixer desuperheat section to the heat exchanger", doc="Add a mixer desuperheat section to the heat exchanger", ), ) config.declare( "has_drain_cooling", ConfigValue( default=True, domain=In([True, False]), description= "Add a section after condensing section cool condensate.", doc="Add a section after condensing section to cool condensate.", ), ) 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("condense", HeatExchangerData.CONFIG()) config.declare("desuperheat", HeatExchangerData.CONFIG()) config.declare("cooling", HeatExchangerData.CONFIG())
def _define_config(config): config.compressor = False config.get("compressor")._default = False config.get("compressor")._domain = In([False]) config.material_balance_type = MaterialBalanceType.componentTotal config.get( "material_balance_type")._default = MaterialBalanceType.componentTotal config.thermodynamic_assumption = ThermodynamicAssumption.adiabatic config.get("thermodynamic_assumption" )._default = ThermodynamicAssumption.adiabatic config.get("thermodynamic_assumption")._domain = In( [ThermodynamicAssumption.adiabatic]) config.declare( "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( "valve_function_rule", ConfigValue( default=None, description= "This is a rule that returns a time indexed valve function expression.", doc= """This is a rule that returns a time indexed valve function expression. This is required only if valve_function==ValveFunctionType.custom""", ), ) config.declare( "phase", ConfigValue( default="Vap", domain=In(("Vap", "Liq")), description='Expected phase of fluid in valve in {"Liq", "Vap"}', ), )
def _add_fp_configs(CONFIG): """Adds the feasibility pump-related configurations. Parameters ---------- CONFIG : ConfigBlock The specific configurations for MindtPy. """ CONFIG.declare('fp_cutoffdecr', ConfigValue( default=1E-1, domain=PositiveFloat, description='Additional relative decrement of cutoff value for the original objective function.' )) CONFIG.declare('fp_iteration_limit', ConfigValue( default=20, domain=PositiveInt, description='Feasibility pump iteration limit', doc='Number of maximum iterations in the feasibility pump methods.' )) # TODO: integrate this option CONFIG.declare('fp_projcuts', ConfigValue( default=True, description='Whether to add cut derived from regularization of MIP solution onto NLP feasible set.', domain=bool )) CONFIG.declare('fp_transfercuts', ConfigValue( default=True, description='Whether to transfer cuts from the Feasibility Pump MIP to main MIP in selected strategy (all except from the round in which the FP MIP became infeasible).', domain=bool )) CONFIG.declare('fp_projzerotol', ConfigValue( default=1E-4, domain=PositiveFloat, description='Tolerance on when to consider optimal value of regularization problem as zero, which may trigger the solution of a Sub-NLP.' )) CONFIG.declare('fp_mipgap', ConfigValue( default=1E-2, domain=PositiveFloat, description='Optimality tolerance (relative gap) to use for solving MIP regularization problem.' )) CONFIG.declare('fp_discrete_only', ConfigValue( default=True, description='Only calculate the distance among discrete variables in regularization problems.', domain=bool )) CONFIG.declare('fp_main_norm', ConfigValue( default='L1', domain=In(['L1', 'L2', 'L_infinity']), description='Different forms of objective function MIP regularization problem.' )) CONFIG.declare('fp_norm_constraint', ConfigValue( default=True, description='Whether to add the norm constraint to FP-NLP', domain=bool )) CONFIG.declare('fp_norm_constraint_coef', ConfigValue( default=1, domain=PositiveFloat, description='The coefficient in the norm constraint, correspond to the Beta in the paper.' ))
def _make_heat_exchanger_config(config): """ Declare configuration options for HeatExchangerData block. """ config.declare("side_1", ConfigBlock( implicit=True, description="Config block for side_1", doc="""A config block used to construct the side_1 control volume.""")) config.declare("side_2", ConfigBlock( implicit=True, description="Config block for side_2", doc="""A config block used to construct the side_2 control volume.""")) _make_heater_config_block(config.side_1) _make_heater_config_block(config.side_2) config.declare("delta_temperature_callback", ConfigValue( default=delta_temperature_lmtd_callback, description="Callback for for temperature difference calculations")) config.declare("flow_pattern", ConfigValue( default=HeatExchangerFlowPattern.countercurrent, domain=In(HeatExchangerFlowPattern), description="Heat exchanger flow pattern", doc="""Heat exchanger flow pattern, **default** - HeatExchangerFlowPattern.countercurrent. **Valid values:** { **HeatExchangerFlowPattern.countercurrent** - countercurrent flow, **HeatExchangerFlowPattern.cocurrent** - cocurrent flow, **HeatExchangerFlowPattern.crossflow** - cross flow, factor times countercurrent temperature difference.}"""))
class IonData(SoluteData): """ Component type for ionic species. These can exist only in AqueousPhases, and are always solutes. """ CONFIG = SoluteData.CONFIG() # Remove valid_phase_types argument, as ions are aqueous phase only CONFIG.__delitem__("valid_phase_types") # Set as not having a vapor pressure has_psat = CONFIG.get("has_vapor_pressure") has_psat.set_value(False) has_psat.set_default_value(False) has_psat.set_domain(In([False])) CONFIG.declare("charge", ConfigValue(domain=int, doc="Charge of ionic species.")) def _is_phase_valid(self, phase): return phase.is_aqueous_phase() def _is_aqueous_phase_valid(self): return True def _add_to_electrolyte_component_list(self): """ Special case method for adding references to new Component in component_lists for electrolyte systems, New Component types should overload this method """ raise NotImplementedError( "{} The IonData component class is inteded as a base class for " "the AnionData and CationData classes, and should not be used " "directly".format(self.name))
class CompressorData(PressureChangerData): # Pressure changer with isentropic turbine options CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = True CONFIG.get("compressor")._default = True CONFIG.get("compressor")._domain = In([True]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.isentropic
class EnergyRecoveryDeviceData(PumpIsothermalData): """ Turbine-type isothermal energy recovery device """ # switch compressor to False CONFIG = PumpIsothermalData.CONFIG() CONFIG.get("compressor")._default = False CONFIG.get("compressor")._domain = In([False]) CONFIG.compressor = False
def _add_loa_configs(CONFIG): """Adds the LOA-related configurations. Parameters ---------- CONFIG : ConfigBlock The specific configurations for MindtPy. """ CONFIG.declare( 'level_coef', ConfigValue( default=0.5, domain=PositiveFloat, description='The coefficient in the regularization main problem' 'represents how much the linear approximation of the MINLP problem is trusted.' )) CONFIG.declare( 'solution_limit', ConfigValue( default=10, domain=PositiveInt, description= 'The solution limit for the regularization problem since it does not need to be solved to optimality.' )) CONFIG.declare( 'add_cuts_at_incumbent', ConfigValue( default=False, description= 'Whether to add lazy cuts to the main problem at the incumbent solution found in the branch & bound tree', domain=bool)) CONFIG.declare( 'reduce_level_coef', ConfigValue( default=False, description= 'Whether to reduce level coefficient in ROA single tree when regularization problem is infeasible.', domain=bool)) CONFIG.declare( 'use_bb_tree_incumbent', ConfigValue( default=False, description= 'Whether to use the incumbent solution of branch & bound tree in ROA single tree when regularization problem is infeasible.', domain=bool)) CONFIG.declare( 'sqp_lag_scaling_coef', ConfigValue( default='fixed', domain=In(['fixed', 'variable_dependent']), description='The coefficient used to scale the L2 norm in sqp_lag.' ))
def _get_GDPopt_config(): _supported_strategies = { 'LOA', # Logic-based outer approximation 'GLOA', # Global logic-based outer approximation 'LBB', # Logic-based branch-and-bound 'RIC', # Relaxation with Integer Cuts } CONFIG = ConfigBlock("GDPopt") CONFIG.declare( "iterlim", ConfigValue(default=100, domain=NonNegativeInt, description="Iteration limit.")) 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.")) CONFIG.declare( "strategy", ConfigValue(default=None, domain=In(_supported_strategies), description="Decomposition strategy to use.")) CONFIG.declare( "tee", ConfigValue(default=False, description="Stream output to terminal.", domain=bool)) CONFIG.declare( "logger", ConfigValue( default='pyomo.contrib.gdpopt', description="The logger object or name to use for reporting.", domain=a_logger)) _add_OA_configs(CONFIG) _add_BB_configs(CONFIG) _add_subsolver_configs(CONFIG) _add_tolerance_configs(CONFIG) return CONFIG
def __init__(self, subproblem_solver): self._subproblem_solver = pe.SolverFactory(subproblem_solver) if isinstance(self._subproblem_solver, PersistentSolver): self._using_persistent_solver = True else: self._using_persistent_solver = False self._relaxations = ComponentSet() self._relaxations_not_tracking_solver = ComponentSet() self._relaxations_with_added_cuts = ComponentSet() self._pyomo_model = None self.options = ConfigBlock() self.options.declare( 'feasibility_tol', ConfigValue(default=1e-6, domain=NonNegativeFloat, doc='Tolerance below which cuts will not be added')) self.options.declare( 'max_iter', ConfigValue(default=30, domain=NonNegativeInt, doc='Maximum number of iterations')) self.options.declare( 'keep_cuts', ConfigValue( default=False, domain=In([True, False]), doc='Whether or not to keep the cuts generated after the solve' )) self.options.declare( 'time_limit', ConfigValue(default=float('inf'), domain=NonNegativeFloat, doc='Time limit in seconds')) self.subproblem_solver_options = ConfigBlock(implicit=True)
class IntegerToBinary(IsomorphicTransformation): """Reformulate integer variables to binary variables and constraints. This transformation may be safely applied multiple times to the same model. """ CONFIG = ConfigBlock("contrib.integer_to_binary") CONFIG.declare( "strategy", ConfigValue( default='base2', domain=In('base2', ), description="Reformulation method", # TODO: eventually we will support other methods, but not yet. )) CONFIG.declare( "ignore_unused", ConfigValue( default=False, domain=bool, description= "Ignore variables that do not appear in (potentially) active constraints. " "These variables are unlikely to be passed to the solver.")) CONFIG.declare( "relax_integrality", ConfigValue( default=True, domain=bool, description="Relax the integrality of the integer variables " "after adding in the binary variables and constraints.")) def _apply_to(self, model, **kwds): """Apply the transformation to the given model.""" config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) integer_vars = list(v for v in model.component_data_objects( ctype=Var, descend_into=(Block, Disjunct)) if v.is_integer() and not v.fixed) if len(integer_vars) == 0: logger.info( "Model has no free integer variables. No reformulation needed." ) return vars_on_constr = ComponentSet() for c in model.component_data_objects(ctype=Constraint, descend_into=(Block, Disjunct), active=True): vars_on_constr.update( v for v in identify_variables(c.body, include_fixed=False) if v.is_integer()) if config.ignore_unused: num_vars_not_on_constr = len(integer_vars) - len(vars_on_constr) if num_vars_not_on_constr > 0: logger.info( "%s integer variables on the model are not attached to any constraints. " "Ignoring unused variables.") integer_vars = list(vars_on_constr) logger.info("Reformulating integer variables using the %s strategy." % config.strategy) # Set up reformulation block blk_name = unique_component_name(model, "_int_to_binary_reform") reform_block = Block( doc="Holds variables and constraints for reformulating " "integer variables to binary variables.") setattr(model, blk_name, reform_block) reform_block.int_var_set = RangeSet(0, len(integer_vars) - 1) reform_block.new_binary_var = Var( Any, domain=Binary, dense=False, initialize=0, doc="Binary variable with index (int_var_idx, idx)") reform_block.integer_to_binary_constraint = Constraint( reform_block.int_var_set, doc="Equality constraints mapping the binary variable values " "to the integer variable value.") # check that variables are bounded for idx, int_var in enumerate(integer_vars): if not (int_var.has_lb() and int_var.has_ub()): raise ValueError( "Integer variable %s is missing an " "upper or lower bound. LB: %s; UB: %s. " "Integer to binary reformulation does not support unbounded integer variables." % (int_var.name, int_var.lb, int_var.ub)) # do the reformulation highest_power = int(floor(log(value(int_var.ub - int_var.lb), 2))) # TODO potentially fragile due to floating point reform_block.integer_to_binary_constraint.add( idx, expr=int_var == sum(reform_block.new_binary_var[idx, pwr] * (2**pwr) for pwr in range(0, highest_power + 1)) + int_var.lb) # Relax the original integer variable if config.relax_integrality: int_var.domain = Reals logger.info("Reformulated %s integer variables using " "%s binary variables and %s constraints." % (len(integer_vars), len(reform_block.new_binary_var), len(reform_block.integer_to_binary_constraint)))
class CoagulationFlocculationData(UnitModelBlockData): """ Zero order Coagulation-Flocculation model based on Jar Tests """ # 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("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.}""")) # NOTE: This option is temporarily disabled ''' 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("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare("chemical_additives", ConfigValue( default={}, domain=dict, description="""Dictionary of chemical additives used in coagulation process, along with their molecular weights, the moles of salt produced per mole of chemical added, and the molecular weights of the salt produced by the chemical additive with the format of: \n {'chem_name_1': {'parameter_data': { 'mw_additive': (value, units), 'moles_salt_per_mole_additive': value, 'mw_salt': (value, units) } }, 'chem_name_2': {'parameter_data': { 'mw_additive': (value, units), 'moles_salt_per_mole_additive': value, 'mw_salt': (value, units) } }, } """)) def build(self): # build always starts by calling super().build() # This triggers a lot of boilerplate in the background for you super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta = self.config.property_package.get_metadata().get_derived_units # check the optional config arg 'chemical_additives' common_msg = "The 'chemical_additives' dict MUST contain a dict of 'parameter_data' for " + \ "each chemical name. That 'parameter_data' dict MUST contain 'mw_chem', " + \ "'moles_salt_per_mole_additive', and 'mw_salt' as keys. Users are also " + \ "required to provide the values for the molecular weights and the units " + \ "within a tuple arg. Example format provided below.\n\n" + \ "{'chem_name_1': \n" + \ " {'parameter_data': \n" + \ " {'mw_additive': (value, units), \n" + \ " 'moles_salt_per_mole_additive': value, \n" + \ " 'mw_salt': (value, units)} \n" + \ " }, \n" + \ "}\n\n" mw_adds = {} mw_salts = {} molar_rat = {} for j in self.config.chemical_additives: if type(self.config.chemical_additives[j]) != dict: raise ConfigurationError("\n Did not provide a 'dict' for chemical \n" + common_msg) if 'parameter_data' not in self.config.chemical_additives[j]: raise ConfigurationError("\n Did not provide a 'parameter_data' for chemical \n" + common_msg) if 'mw_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_additive' for chemical \n" + common_msg) if 'moles_salt_per_mole_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'moles_salt_per_mole_additive' for chemical \n" + common_msg) if 'mw_salt' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_salt' for chemical \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_additive']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_additive' \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_salt']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_salt' \n" + common_msg) if not isinstance(self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'], (int,float)): raise ConfigurationError("\n Did not provide a number for 'moles_salt_per_mole_additive' \n" + common_msg) #Populate temp dicts for parameter and variable setting mw_adds[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_additive'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_additive'][1], to_units=pyunits.kg/pyunits.mol) mw_salts[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_salt'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_salt'][1], to_units=pyunits.kg/pyunits.mol) molar_rat[j] = self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'] # Add unit variables # Linear relationship between TSS (mg/L) and Turbidity (NTU) # TSS (mg/L) = Turbidity (NTU) * slope + intercept # Default values come from the following paper: # H. Rugner, M. Schwientek,B. Beckingham, B. Kuch, P. Grathwohl, # Environ. Earth Sci. 69 (2013) 373-380. DOI: 10.1007/s12665-013-2307-1 self.slope = Var( self.flowsheet().config.time, initialize=1.86, bounds=(1e-8, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Slope relation between TSS (mg/L) and Turbidity (NTU)') self.intercept = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Intercept relation between TSS (mg/L) and Turbidity (NTU)') self.initial_turbidity_ntu = Var( self.flowsheet().config.time, initialize=50, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Initial measured Turbidity (NTU) from Jar Test') self.final_turbidity_ntu = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Final measured Turbidity (NTU) from Jar Test') self.chemical_doses = Var( self.flowsheet().config.time, self.config.chemical_additives.keys(), initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Dosages of the set of chemical additives') self.chemical_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_adds, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the set of chemical additives') self.salt_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_salts, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the produced salts from chemical additives') self.salt_from_additive_mole_ratio = Param( self.config.chemical_additives.keys(), mutable=True, initialize=molar_rat, domain=NonNegativeReals, units=pyunits.mol/pyunits.mol, doc='Moles of the produced salts from 1 mole of chemical additives') # Build control volume for feed side self.control_volume = ControlVolume0DBlock(default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_state_blocks( has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) # NOTE: This checks for if an energy_balance_type is defined if hasattr(self.config, "energy_balance_type"): self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=False) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False) # Add ports self.add_inlet_port(name='inlet', block=self.control_volume) self.add_outlet_port(name='outlet', block=self.control_volume) # Check _phase_component_set for required items if ('Liq', 'TDS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TDS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'Sludge') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','Sludge') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'TSS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TSS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) # -------- Add constraints --------- # Adds isothermal constraint if no energy balance present if not hasattr(self.config, "energy_balance_type"): @self.Constraint(self.flowsheet().config.time, doc="Isothermal condition") def eq_isothermal(self, t): return (self.control_volume.properties_out[t].temperature == self.control_volume.properties_in[t].temperature) # Constraint for tss loss rate based on measured final turbidity self.tss_loss_rate = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time loss rate of TSS based on the measured final turbidity') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tss_loss_rate(self, t): tss_out = pyunits.convert(self.slope[t]*self.final_turbidity_ntu[t] + self.intercept[t], to_units=units_meta('mass')*units_meta('length')**-3) input_rate = self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS'] exit_rate = self.control_volume.properties_out[t].flow_vol_phase['Liq']*tss_out return (self.tss_loss_rate[t] == input_rate - exit_rate) # Constraint for tds gain rate based on 'chemical_doses' and 'chemical_additives' if self.config.chemical_additives: self.tds_gain_rate = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time gain rate of TDS based on the chemicals added for coagulation') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tds_gain_rate(self, t): sum = 0 for j in self.config.chemical_additives.keys(): chem_dose = pyunits.convert(self.chemical_doses[t, j], to_units=units_meta('mass')*units_meta('length')**-3) chem_dose = chem_dose/self.chemical_mw[j] * \ self.salt_from_additive_mole_ratio[j] * \ self.salt_mw[j]*self.control_volume.properties_out[t].flow_vol_phase['Liq'] sum = sum+chem_dose return (self.tds_gain_rate[t] == sum) # Add constraints for mass transfer terms @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_transfer_term(self, t, p, j): if (p, j) == ('Liq', 'TSS'): return self.control_volume.mass_transfer_term[t, p, j] == -self.tss_loss_rate[t] elif (p, j) == ('Liq', 'Sludge'): return self.control_volume.mass_transfer_term[t, p, j] == self.tss_loss_rate[t] elif (p, j) == ('Liq', 'TDS'): if self.config.chemical_additives: return self.control_volume.mass_transfer_term[t, p, j] == self.tds_gain_rate[t] else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0 else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0 # Return a scalar expression for the inlet concentration of TSS def compute_inlet_tss_mass_concentration(self, t): """ Function to generate an expression that would represent the mass concentration of TSS at the inlet port of the unit. Inlet ports are generally established upstream, but this will be useful for establishing the inlet TSS when an upstream TSS is unknown. This level of inlet TSS is based off of measurements made of Turbidity during the Jar Test. Keyword Arguments: self : this unit model object t : time index on the flowsheet Returns: Expression Recover the numeric value by using 'value(Expression)' """ units_meta = self.config.property_package.get_metadata().get_derived_units return pyunits.convert(self.slope[t]*self.initial_turbidity_ntu[t] + self.intercept[t], to_units=units_meta('mass')*units_meta('length')**-3) # Return a scale expression for the inlet mass flow rate of TSS def compute_inlet_tss_mass_flow(self, t): """ Function to generate an expression that would represent the mass flow rate of TSS at the inlet port of the unit. Inlet ports are generally established upstream, but this will be useful for establishing the inlet TSS when an upstream TSS is unknown. This level of inlet TSS is based off of measurements made of Turbidity during the Jar Test. Keyword Arguments: self : this unit model object t : time index on the flowsheet Returns: Expression Recover the numeric value by using 'value(Expression)' """ return self.control_volume.properties_in[t].flow_vol_phase['Liq']*self.compute_inlet_tss_mass_concentration(t) # Function to automate fixing of the Turbidity v TSS relation params to defaults def fix_tss_turbidity_relation_defaults(self): self.slope.fix() self.intercept.fix() # 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) # --------------------------------------------------------------------- # Initialize holdup block flags = blk.control_volume.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # --------------------------------------------------------------------- # 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 2 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl + 1) init_log.info( "Initialization Complete: {}".format(idaeslog.condition(res)) ) def calculate_scaling_factors(self): super().calculate_scaling_factors() units_meta = self.config.property_package.get_metadata().get_derived_units # scaling factors for turbidity relationship # Supressing warning (these factors are not very important) if iscale.get_scaling_factor(self.slope) is None: sf = iscale.get_scaling_factor(self.slope, default=1, warning=False) iscale.set_scaling_factor(self.slope, sf) if iscale.get_scaling_factor(self.intercept) is None: sf = iscale.get_scaling_factor(self.intercept, default=1, warning=False) iscale.set_scaling_factor(self.intercept, sf) # scaling factors for turbidity measurements and chemical doses # Supressing warning if iscale.get_scaling_factor(self.initial_turbidity_ntu) is None: sf = iscale.get_scaling_factor(self.initial_turbidity_ntu, default=1, warning=False) iscale.set_scaling_factor(self.initial_turbidity_ntu, sf) if iscale.get_scaling_factor(self.final_turbidity_ntu) is None: sf = iscale.get_scaling_factor(self.final_turbidity_ntu, default=1, warning=False) iscale.set_scaling_factor(self.final_turbidity_ntu, sf) if iscale.get_scaling_factor(self.chemical_doses) is None: sf = iscale.get_scaling_factor(self.chemical_doses, default=1, warning=False) iscale.set_scaling_factor(self.chemical_doses, sf) # set scaling for tss_loss_rate if iscale.get_scaling_factor(self.tss_loss_rate) is None: sf = 0 for t in self.control_volume.properties_in: sf += value(self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS']) sf = sf / len(self.control_volume.properties_in) if sf < 0.01: sf = 0.01 iscale.set_scaling_factor(self.tss_loss_rate, 1/sf) for ind, c in self.eq_tss_loss_rate.items(): iscale.constraint_scaling_transform(c, 1/sf) # set scaling for tds_gain_rate if self.config.chemical_additives: if iscale.get_scaling_factor(self.tds_gain_rate) is None: sf = 0 for t in self.control_volume.properties_in: sum = 0 for j in self.config.chemical_additives.keys(): chem_dose = pyunits.convert(self.chemical_doses[t, j], to_units=units_meta('mass')*units_meta('length')**-3) chem_dose = chem_dose/self.chemical_mw[j] * \ self.salt_from_additive_mole_ratio[j] * \ self.salt_mw[j]*self.control_volume.properties_in[t].flow_vol_phase['Liq'] sum = sum+chem_dose sf += value(sum) sf = sf / len(self.control_volume.properties_in) if sf < 0.001: sf = 0.001 iscale.set_scaling_factor(self.tds_gain_rate, 1/sf) for ind, c in self.eq_tds_gain_rate.items(): iscale.constraint_scaling_transform(c, 1/sf) # set scaling for mass transfer terms for ind, c in self.eq_mass_transfer_term.items(): if ind[2] == "TDS": if self.config.chemical_additives: sf = iscale.get_scaling_factor(self.tds_gain_rate) else: sf = 1 elif ind[2] == "TSS": sf = iscale.get_scaling_factor(self.tss_loss_rate) elif ind[2] == "Sludge": sf = iscale.get_scaling_factor(self.tss_loss_rate) else: sf = 1 iscale.constraint_scaling_transform(c, sf) iscale.set_scaling_factor(self.control_volume.mass_transfer_term[ind] , sf) # set scaling factors for control_volume.properties_in based on control_volume.properties_out for t in self.control_volume.properties_in: if iscale.get_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase) is None: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase) iscale.set_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase, sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp) is None: for ind in self.control_volume.properties_in[t].flow_mass_phase_comp: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp[ind], sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp) is None: for ind in self.control_volume.properties_in[t].mass_frac_phase_comp: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp[ind], sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase) is None: for ind in self.control_volume.properties_in[t].flow_vol_phase: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_vol_phase[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase[ind], sf) # update scaling for control_volume.properties_out for t in self.control_volume.properties_out: if iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase) is None: iscale.set_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase, 1e-3) # need to update scaling factors for TSS, Sludge, and TDS to account for the # expected change in their respective values from the loss/gain rates for ind in self.control_volume.properties_out[t].flow_mass_phase_comp: if ind[1] == "TSS": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) if ind[1] == "Sludge": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) for ind in self.control_volume.properties_out[t].mass_frac_phase_comp: if ind[1] == "TSS": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) if ind[1] == "Sludge": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og))
class TrustRegionSolver(OptSolver): """ A trust region filter method for black box / glass box optimizaiton Solves nonlinear optimization problems containing external function calls through automatic construction of reduced models (ROM), also known as surrogate models. Currently implements linear and quadratic reduced models. See Eason, Biegler (2016) AIChE Journal for more details Arguments: """ # + param.CONFIG.generte_yaml_template() CONFIG = ConfigBlock('Trust Region') CONFIG.declare( 'solver', ConfigValue(default='ipopt', description='solver to use, defaults to ipopt', doc='')) CONFIG.declare( 'solver_options', ConfigBlock(implicit=True, description='options to pass to the subproblem solver', doc='')) # Initialize trust radius CONFIG.declare( 'trust radius', ConfigValue(default=1.0, domain=PositiveFloat, description='', doc='')) # Initialize sample region CONFIG.declare( 'sample region', ConfigValue(default=True, domain=bool, description='', doc='')) # Initialize sample radius # TODO do we need to keep the if statement? if CONFIG.sample_region: default_sample_radius = 0.1 else: default_sample_radius = CONFIG.trust_radius / 2.0 CONFIG.declare( 'sample radius', ConfigValue(default=default_sample_radius, domain=PositiveFloat, description='', doc='')) # Initialize radius max CONFIG.declare( 'radius max', ConfigValue(default=1000.0 * CONFIG.trust_radius, domain=PositiveFloat, description='', doc='')) # Termination tolerances CONFIG.declare( 'ep i', ConfigValue(default=1e-5, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'ep delta', ConfigValue(default=1e-5, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'ep chi', ConfigValue(default=1e-3, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'delta min', ConfigValue(default=1e-6, domain=PositiveFloat, description='delta min <= ep delta', doc='')) CONFIG.declare( 'max it', ConfigValue(default=20, domain=PositiveInt, description='', doc='')) # Compatibility Check Parameters CONFIG.declare( 'kappa delta', ConfigValue(default=0.8, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'kappa mu', ConfigValue(default=1.0, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'mu', ConfigValue(default=0.5, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'ep compatibility', ConfigValue(default=CONFIG.ep_i, domain=PositiveFloat, description='Suggested value: ep compatibility == ep i', doc='')) CONFIG.declare( 'compatibility penalty', ConfigValue(default=0.0, domain=NonNegativeFloat, description='', doc='')) # Criticality Check Parameters CONFIG.declare( 'criticality check', ConfigValue(default=0.1, domain=PositiveFloat, description='', doc='')) # Trust region update parameters CONFIG.declare( 'gamma c', ConfigValue(default=0.5, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'gamma e', ConfigValue(default=2.5, domain=PositiveFloat, description='', doc='')) # Switching Condition CONFIG.declare( 'gamma s', ConfigValue(default=2.0, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'kappa theta', ConfigValue(default=0.1, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'theta min', ConfigValue(default=1e-4, domain=PositiveFloat, description='', doc='')) # Filter CONFIG.declare( 'gamma f', ConfigValue( default=0.01, domain=PositiveFloat, description='gamma_f and gamma_theta in (0,1) are fixed parameters', doc='')) CONFIG.declare( 'gamma theta', ConfigValue( default=0.01, domain=PositiveFloat, description='gamma_f and gamma_theta in (0,1) are fixed parameters', doc='')) CONFIG.declare( 'theta max', ConfigValue(default=50, domain=PositiveInt, description='', doc='')) # Ratio test parameters (for theta steps) CONFIG.declare( 'eta1', ConfigValue(default=0.05, domain=PositiveFloat, description='', doc='')) CONFIG.declare( 'eta2', ConfigValue(default=0.2, domain=PositiveFloat, description='', doc='')) # Output level (replace with real printlevels!!!) CONFIG.declare( 'print variables', ConfigValue(default=False, domain=bool, description='', doc='')) # Sample Radius reset parameter CONFIG.declare( 'sample radius adjust', ConfigValue(default=0.5, domain=PositiveFloat, description='', doc='')) # Default romtype CONFIG.declare( 'reduced model type', ConfigValue(default=1, domain=In([0, 1]), description='0 = Linear, 1 = Quadratic', doc='')) def __init__(self, **kwds): # set persistent config options tmp_kwds = {'type': kwds.pop('type', 'trustregion')} self.config = self.CONFIG(kwds, preserve_implicit=True) # # Call base class constructor # tmp_kwds['solver'] = self.config.solver OptSolver.__init__(self, **tmp_kwds) 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 a 3-tuple describing the solver version.""" return __version__ def solve(self, model, eflist, **kwds): # set customized config parameters self._local_config = self.config(kwds, preserve_implicit=True) # first store all data we will need to change in original model as a tuple # [0]=Var component, [1]=external function list, [2]=config block model._tmp_trf_data = (list(model.component_data_objects(Var)), eflist, self._local_config) # now clone the model inst = model.clone() # call TRF on cloned model TRF(inst, inst._tmp_trf_data[1], inst._tmp_trf_data[2]) # copy potentially changed variable values back to original model and return for inst_var, orig_var in zip(inst._tmp_trf_data[0], model._tmp_trf_data[0]): orig_var.set_value(value(inst_var))
class PFRData(UnitModelBlockData): """ Standard Plug Flow Reactor Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_equilibrium_reactions", ConfigValue( default=False, domain=Bool, description="Equilibrium reaction construction flag", doc="""Indicates whether terms for equilibrium controlled reactions should be constructed, **default** - True. **Valid values:** { **True** - include equilibrium reaction terms, **False** - exclude equilibrium reaction terms.}""")) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) CONFIG.declare( "has_heat_of_reaction", ConfigValue( default=False, domain=Bool, description="Heat of reaction term construction flag", doc="""Indicates whether terms for heat of reaction terms should be constructed, **default** - False. **Valid values:** { **True** - include heat of reaction terms, **False** - exclude heat of reaction terms.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=Bool, description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc= """Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""")) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc= """A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""")) CONFIG.declare( "length_domain_set", ConfigValue( default=[0.0, 1.0], domain=ListOf(float), description="List of points to use to initialize length domain", doc= """A list of values to be used when constructing the length domain of the reactor. Point must lie between 0.0 and 1.0, **default** - [0.0, 1.0]. **Valid values:** { a list of floats}""")) CONFIG.declare( "transformation_method", ConfigValue( default="dae.finite_difference", description="Method to use for DAE transformation", doc="""Method to use to transform domain. Must be a method recognised by the Pyomo TransformationFactory, **default** - "dae.finite_difference".""")) CONFIG.declare( "transformation_scheme", ConfigValue(default="BACKWARD", description="Scheme to use for DAE transformation", doc="""Scheme to use when transformating domain. See Pyomo documentation for supported schemes, **default** - "BACKWARD".""")) CONFIG.declare( "finite_elements", ConfigValue( default=20, description= "Number of finite elements to use for DAE transformation", doc="""Number of finite elements to use when transforming length domain, **default** - 20.""")) CONFIG.declare( "collocation_points", ConfigValue( default=3, description="No. collocation points to use for DAE transformation", doc="""Number of collocation points to use when transforming length domain, **default** - 3.""")) def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(PFRData, self).build() # Build Control Volume self.control_volume = ControlVolume1DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args, "transformation_method": self.config.transformation_method, "transformation_scheme": self.config.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points }) self.control_volume.add_geometry( length_domain_set=self.config.length_domain_set) self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_reaction_blocks( has_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=True, has_equilibrium_reactions=self.config.has_equilibrium_reactions, has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=self.config.has_heat_of_reaction, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) self.control_volume.apply_transformation() # Add Ports self.add_inlet_port() self.add_outlet_port() # Add PFR performance equation @self.Constraint(self.flowsheet().time, self.control_volume.length_domain, self.config.reaction_package.rate_reaction_idx, doc="PFR performance equation") def performance_eqn(b, t, x, r): return b.control_volume.rate_reaction_extent[t, x, r] == ( b.control_volume.reactions[t, x].reaction_rate[r] * b.control_volume.area) # Set references to balance terms at unit level add_object_reference(self, "length", self.control_volume.length) add_object_reference(self, "area", self.control_volume.area) # Add volume variable for full reactor units = self.config.property_package.get_metadata() self.volume = Var(initialize=1, doc="Reactor Volume", units=units.get_derived_units("volume")) self.geometry = Constraint(expr=self.volume == self.area * self.length) if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat[...]) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP[...]) def _get_performance_contents(self, time_point=0): var_dict = {"Volume": self.volume} var_dict = {"Length": self.length} var_dict = {"Area": self.area} return {"vars": var_dict} def get_costing(self, year=None, module=costing, **kwargs): if not hasattr(self.flowsheet(), "costing"): self.flowsheet().get_costing(year=year, module=module) self.costing = Block() units_meta = ( self.config.property_package.get_metadata().get_derived_units) self.diameter = Var(initialize=1, units=units_meta('length'), doc='vessel diameter') self.diameter_eq = Constraint( expr=self.volume == (self.length * const.pi * self.diameter**2) / 4) module.pfr_costing(self.costing, **kwargs)
class GenericReactionParameterData(ReactionParameterBlock): """ General Reaction Parameter Block Class """ CONFIG = ReactionParameterBlock.CONFIG() CONFIG.declare( "reaction_basis", ConfigValue( default=MaterialFlowBasis.molar, domain=In(MaterialFlowBasis), doc="Basis of reactions", description="Argument indicating basis of reaction terms. Should be " "an instance of a MaterialFlowBasis Enum")) CONFIG.declare("rate_reactions", ConfigBlock(implicit=True, implicit_domain=rate_rxn_config)) CONFIG.declare( "equilibrium_reactions", ConfigBlock(implicit=True, implicit_domain=equil_rxn_config)) # Base units of measurement CONFIG.declare( "base_units", ConfigValue( default={}, domain=dict, description="Base units for property package", doc="Dict containing definition of base units of measurement to use " "with property package.")) # User-defined default scaling factors CONFIG.declare( "default_scaling_factors", ConfigValue( domain=dict, description="User-defined default scaling factors", doc="Dict of user-defined properties and associated default " "scaling factors")) def build(self): ''' Callable method for Block construction. ''' # Call super.build() to initialize Block # In this case we are replicating the super.build to get around a # chicken-and-egg problem # The super.build tries to validate units, but they have not been set # and cannot be set until the config block is created by super.build super(ReactionParameterBlock, self).build() self.default_scaling_factor = {} # Set base units of measurement self.get_metadata().add_default_units(self.config.base_units) # TODO: Need way to tie reaction package to a specfic property package self._validate_property_parameter_units() self._validate_property_parameter_properties() # Call configure method to set construction arguments self.configure() # Build core components self._reaction_block_class = GenericReactionBlock # Alias associated property package to keep line length down ppack = self.config.property_package if not hasattr(ppack, "_electrolyte") or not ppack._electrolyte: pc_set = ppack._phase_component_set elif ppack.config.state_components.name == "true": pc_set = ppack.true_phase_component_set elif ppack.config.state_components.name == "apparent": pc_set = ppack.apparent_phase_component_set else: raise BurntToast() # Construct rate reaction attributes if required if len(self.config.rate_reactions) > 0: # Construct rate reaction index self.rate_reaction_idx = Set( initialize=self.config.rate_reactions.keys()) # Construct rate reaction stoichiometry dict self.rate_reaction_stoichiometry = {} for r, rxn in self.config.rate_reactions.items(): for p, j in pc_set: self.rate_reaction_stoichiometry[(r, p, j)] = 0 if rxn.stoichiometry is None: raise ConfigurationError( "{} rate reaction {} was not provided with a " "stoichiometry configuration argument.".format( self.name, r)) else: for k, v in rxn.stoichiometry.items(): if k[0] not in ppack.phase_list: raise ConfigurationError( "{} stoichiometry for rate reaction {} " "included unrecognised phase {}.".format( self.name, r, k[0])) if k[1] not in ppack.component_list: raise ConfigurationError( "{} stoichiometry for rate reaction {} " "included unrecognised component {}.".format( self.name, r, k[1])) self.rate_reaction_stoichiometry[(r, k[0], k[1])] = v # Check that a method was provided for the rate form if rxn.rate_form is None: _log.debug( "{} rate reaction {} was not provided with a " "rate_form configuration argument. This is suitable " "for processes using stoichiometric reactors, but not " "for those using unit operations which rely on " "reaction rate.".format(self.name, r)) # Construct equilibrium reaction attributes if required if len(self.config.equilibrium_reactions) > 0: # Construct equilibrium reaction index self.equilibrium_reaction_idx = Set( initialize=self.config.equilibrium_reactions.keys()) # Construct equilibrium reaction stoichiometry dict self.equilibrium_reaction_stoichiometry = {} for r, rxn in self.config.equilibrium_reactions.items(): for p, j in pc_set: self.equilibrium_reaction_stoichiometry[(r, p, j)] = 0 if rxn.stoichiometry is None: raise ConfigurationError( "{} equilibrium reaction {} was not provided with a " "stoichiometry configuration argument.".format( self.name, r)) else: for k, v in rxn.stoichiometry.items(): if k[0] not in ppack.phase_list: raise ConfigurationError( "{} stoichiometry for equilibrium reaction {} " "included unrecognised phase {}.".format( self.name, r, k[0])) if k[1] not in ppack.component_list: raise ConfigurationError( "{} stoichiometry for equilibrium reaction {} " "included unrecognised component {}.".format( self.name, r, k[1])) self.equilibrium_reaction_stoichiometry[(r, k[0], k[1])] = v # Check that a method was provided for the equilibrium form if rxn.equilibrium_form is None: raise ConfigurationError( "{} equilibrium reaction {} was not provided with a " "equilibrium_form configuration argument.".format( self.name, r)) # Add a master reaction index which includes both types of reactions if (len(self.config.rate_reactions) > 0 and len(self.config.equilibrium_reactions) > 0): self.reaction_idx = Set( initialize=(self.rate_reaction_idx | self.equilibrium_reaction_idx)) elif len(self.config.rate_reactions) > 0: self.reaction_idx = Set(initialize=self.rate_reaction_idx) elif len(self.config.equilibrium_reactions) > 0: self.reaction_idx = Set(initialize=self.equilibrium_reaction_idx) else: raise BurntToast("{} Generic property package failed to construct " "master reaction Set. This should not happen. " "Please contact the IDAES developers with this " "bug".format(self.name)) # Construct blocks to contain parameters for each reaction for r in self.reaction_idx: self.add_component("reaction_" + str(r), Block()) # Build parameters if len(self.config.rate_reactions) > 0: for r in self.rate_reaction_idx: rblock = getattr(self, "reaction_" + r) r_config = self.config.rate_reactions[r] order_init = {} for p, j in pc_set: if "reaction_order" in r_config.parameter_data: try: order_init[p, j] = r_config.parameter_data[ "reaction_order"][p, j] except KeyError: order_init[p, j] = 0 else: # Assume elementary reaction and use stoichiometry try: if r_config.stoichiometry[p, j] < 0: # These are reactants, but order is -ve stoic order_init[p, j] = -r_config.stoichiometry[p, j] else: # Anything else is a product, not be included order_init[p, j] = 0 except KeyError: order_init[p, j] = 0 rblock.reaction_order = Var(pc_set, initialize=order_init, doc="Reaction order", units=None) for val in self.config.rate_reactions[r].values(): try: val.build_parameters(rblock, self.config.rate_reactions[r]) except AttributeError: pass if len(self.config.equilibrium_reactions) > 0: for r in self.equilibrium_reaction_idx: rblock = getattr(self, "reaction_" + r) r_config = self.config.equilibrium_reactions[r] order_init = {} for p, j in pc_set: if "reaction_order" in r_config.parameter_data: try: order_init[p, j] = r_config.parameter_data[ "reaction_order"][p, j] except KeyError: order_init[p, j] = 0 else: # Assume elementary reaction and use stoichiometry try: # Here we use the stoic. coeff. directly # However, solids should be excluded as they # normally do not appear in the equilibrium # relationship pobj = ppack.get_phase(p) if not pobj.is_solid_phase(): order_init[p, j] = r_config.stoichiometry[p, j] else: order_init[p, j] = 0 except KeyError: order_init[p, j] = 0 rblock.reaction_order = Var(pc_set, initialize=order_init, doc="Reaction order", units=None) for val in self.config.equilibrium_reactions[r].values(): try: val.build_parameters( rblock, self.config.equilibrium_reactions[r]) except AttributeError: pass except KeyError as err: # This likely arises from mismatched true and apparent # species sets. Reaction packages must use the same # basis as the associated thermo properties # Raise an exception to inform the user raise PropertyPackageError( "{} KeyError encountered whilst constructing " "reaction parameters. This may be due to " "mismatched state_components between the " "Reaction Package and the associated Physical " "Property Package - Reaction Packages must use the" "same basis (true or apparent species) as the " "Physical Property Package.".format(self.name), err) # As a safety check, make sure all Vars in reaction blocks are fixed for v in self.component_objects(Var, descend_into=True): for i in v: if v[i].value is None: raise ConfigurationError( "{} parameter {} was not assigned" " a value. Please check your configuration " "arguments.".format(self.name, v.local_name)) v[i].fix() # Set default scaling factors if self.config.default_scaling_factors is not None: self.default_scaling_factor.update( self.config.default_scaling_factors) # Finally, call populate_default_scaling_factors method to fill blanks iscale.populate_default_scaling_factors(self) def configure(self): """ Placeholder method to allow users to specify config arguments via a class. The user class should inherit from this one and implement a configure() method which sets the values of the desired config arguments. Args: None Returns: None """ pass def parameters(self): """ Placeholder method to allow users to specify parameters via a class. The user class should inherit from this one and implement a parameters() method which creates the required components. Args: None Returns: None """ pass @classmethod def define_metadata(cls, obj): """Define properties supported and units.""" obj.add_properties({ 'dh_rxn': { 'method': '_dh_rxn' }, 'k_eq': { 'method': '_k_eq' }, 'log_k_eq': { 'method': '_log_k_eq' }, 'k_rxn': { 'method': '_k_rxn' }, 'reaction_rate': { 'method': "_reaction_rate" } })
class EquilibriumReactorData(UnitModelBlockData): """ Standard Equilibrium Reactor Unit Model 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. Equilibrium Reactors 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. Equilibrium reactors do not have defined volume, thus this must be False.""")) 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_rate_reactions", ConfigValue( default=True, domain=In([True, False]), description="Rate reaction construction flag", doc="""Indicates whether terms for rate controlled reactions should be constructed, along with constraints equating these to zero, **default** - True. **Valid values:** { **True** - include rate reaction terms, **False** - exclude rate reaction terms.}""")) CONFIG.declare( "has_equilibrium_reactions", ConfigValue( default=True, 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 term construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** - True. **Valid values:** { **True** - include phase equilibrium term, **False** - exclude phase equlibirum 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_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_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare( "reaction_package", ConfigValue( default=None, domain=is_reaction_parameter_block, description="Reaction package to use for control volume", doc= """Reaction parameter object used to define reaction calculations, **default** - None. **Valid values:** { **None** - no reaction package, **ReactionParameterBlock** - a ReactionParameterBlock object.}""")) CONFIG.declare( "reaction_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing reaction packages", doc= """A ConfigBlock with arguments to be passed to a reaction block(s) and used when constructing these, **default** - None. **Valid values:** { see reaction package for documentation.}""")) def build(self): """ Begin building model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(EquilibriumReactorData, self).build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args, "reaction_package": self.config.reaction_package, "reaction_package_args": self.config.reaction_package_args }) # No need for control volume geometry self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_reaction_blocks( has_equilibrium=self.config.has_equilibrium_reactions) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_rate_reactions=self.config.has_rate_reactions, has_equilibrium_reactions=self.config.has_equilibrium_reactions, has_phase_equilibrium=self.config.has_phase_equilibrium) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_of_reaction=self.config.has_heat_of_reaction, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() if self.config.has_rate_reactions: # Add equilibrium reactor performance equation @self.Constraint(self.flowsheet().config.time, self.config.reaction_package.rate_reaction_idx, doc="Rate reaction equilibrium constraint") def rate_reaction_constraint(b, t, r): # Set kinetic reaction rates to zero return b.control_volume.reactions[t].reaction_rate[r] == 0 # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): add_object_reference(self, "heat_duty", self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): add_object_reference(self, "deltaP", self.control_volume.deltaP) def _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 WaterFlashData(UnitModelBlockData): """ Simplified Flash Unit Model Class, only for IAPWS with mixed state """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue(default=False, domain=In([False]), description="Dynamic model flag", doc="""Indicates whether the model is dynamic""")) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), 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( "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 """ super(WaterFlashData, self).build() self.mixed_state = self.config.property_package.build_state_block( self.flowsheet().time, default=self.config.property_package_args) self.add_port("inlet", self.mixed_state) self.vap_state = self.config.property_package.build_state_block( self.flowsheet().time, default=self.config.property_package_args) self.liq_state = self.config.property_package.build_state_block( self.flowsheet().time, default=self.config.property_package_args) self.add_port("vap_outlet", self.vap_state) self.add_port("liq_outlet", self.liq_state) # vapor outlet state @self.Constraint(self.flowsheet().time) def vap_material_balance(b, t): return 1e-4*b.mixed_state[t].flow_mol*b.mixed_state[t].vapor_frac == \ b.vap_state[t].flow_mol*1e-4 @self.Constraint(self.flowsheet().time) def vap_enthalpy_balance(b, t): return b.mixed_state[t].enth_mol_phase["Vap"]*1e-4 == \ b.vap_state[t].enth_mol*1e-4 @self.Constraint(self.flowsheet().time) def vap_pressure_balance(b, t): return b.mixed_state[t].pressure*1e-6 == \ b.vap_state[t].pressure*1e-6 # liquid outlet state @self.Constraint(self.flowsheet().time) def liq_material_balance(b, t): return 1e-4*b.mixed_state[t].flow_mol*(1 - b.mixed_state[t].vapor_frac)\ == b.liq_state[t].flow_mol*1e-4 @self.Constraint(self.flowsheet().time) def liq_enthalpy_balance(b, t): return 1e-4*b.mixed_state[t].enth_mol_phase["Liq"] == \ b.liq_state[t].enth_mol*1e-4 @self.Constraint(self.flowsheet().time) def liq_pressure_balance(b, t): return b.mixed_state[t].pressure*1e-6 == \ b.liq_state[t].pressure*1e-6 def initialize(blk, state_args_water_steam=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Drum initialization routine. Keyword Arguments: state_args_water_steam : 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") init_log.info_low("Starting initialization...") # fix FeedWater Inlet flags_fw = fix_state_vars(blk.mixed_state, state_args_water_steam) blk.mixed_state.initialize(solver=solver, optarg=optarg, outlvl=outlvl) # initialize outlet states for t in blk.flowsheet().time: blk.vap_state[t].flow_mol = value(blk.mixed_state[t].flow_mol * blk.mixed_state[t].vapor_frac) blk.vap_state[t].enth_mol = value( blk.mixed_state[t].enth_mol_phase["Vap"]) blk.vap_state[t].pressure = value(blk.mixed_state[t].pressure) blk.vap_state[t].vapor_frac = 1 blk.liq_state[t].flow_mol = value( blk.mixed_state[t].flow_mol * (1 - blk.mixed_state[t].vapor_frac)) blk.liq_state[t].enth_mol = value( blk.mixed_state[t].enth_mol_phase["Liq"]) blk.liq_state[t].pressure = value(blk.mixed_state[t].pressure) blk.liq_state[t].vapor_frac = 0 # unfix variables revert_state_vars(blk.mixed_state, flags_fw) init_log.info_low("Initialization Complete.") def set_initial_condition(self): pass def calculate_scaling_factors(self): pass
class SeparatorData(UnitModelBlockData): """ This is a general purpose model for a Separator block with the IDAES modeling framework. This block can be used either as a stand-alone Separator unit operation, or as a sub-model within another unit operation. This model creates a number of StateBlocks to represent the outgoing streams, then writes a set of phase-component material balances, an overall enthalpy balance (2 options), 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 Separator. When being used as a sub-model, Separator should only be used when a set of new StateBlocks are required for the streams to be separated. It should not be used to separate streams to go to mutiple ControlVolumes in a single unit model - in these cases the unit model developer should write their own splitting 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. 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 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).}""")) CONFIG.declare( "split_basis", ConfigValue( default=SplittingType.totalFlow, domain=SplittingType, description="Basis for splitting stream", doc="""Argument indicating basis to use for splitting mixed stream, **default** - SplittingType.totalFlow. **Valid values:** { **SplittingType.totalFlow** - split based on total flow (split fraction indexed only by time and outlet), **SplittingType.phaseFlow** - split based on phase flows (split fraction indexed by time, outlet and phase), **SplittingType.componentFlow** - split based on component flows (split fraction indexed by time, outlet and components), **SplittingType.phaseComponentFlow** - split based on phase-component flows ( split fraction indexed by both time, outlet, phase and components).}""")) CONFIG.declare( "energy_split_basis", ConfigValue( default=EnergySplittingType.equal_temperature, domain=EnergySplittingType, description="Type of constraint to write for energy splitting", doc="""Argument indicating basis to use for splitting energy this is not used for when ideal_separation == True. **default** - EnergySplittingType.equal_temperature. **Valid values:** { **EnergySplittingType.equal_temperature** - outlet temperatures equal inlet **EnergySplittingType.equal_molar_enthalpy** - oulet molar enthalpies equal inlet}""")) CONFIG.declare( "ideal_separation", ConfigValue( default=True, domain=In([True, False]), description="Ideal splitting flag", doc="""Argument indicating whether ideal splitting should be used. Ideal splitting assumes perfect spearation of material, and attempts to avoid duplication of StateBlocks by directly partitioning outlet flows to ports, **default** - True. **Valid values:** { **True** - use ideal splitting methods, **False** - use explicit splitting equations with split fractions.}""")) CONFIG.declare( "ideal_split_map", ConfigValue( domain=dict, description="Ideal splitting partitioning map", doc="""Dictionary containing information on how extensive variables should be partitioned when using ideal splitting (ideal_separation = True). **default** - None. **Valid values:** { **dict** with keys of indexing set members and values indicating which outlet this combination of keys should be partitioned to. E.g. {("Vap", "H2"): "outlet_1"}}""")) CONFIG.declare( "mixed_state_block", ConfigValue( domain=is_state_block, description="Existing StateBlock to use as mixed stream", doc="""An existing state block to use as the source stream from the Separator block, **default** - None. **Valid values:** { **None** - create a new StateBlock for the mixed stream, **StateBlock** - a StateBock to use as the source 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 the mixed state and all outlet states, **default** - True. **Valid values:** { **True** - construct Ports for all states, **False** - do not construct Ports.""")) def build(self): """ General build method for SeparatorData. 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(SeparatorData, self).build() # Call setup methods from ControlVolumeBlockData self._get_property_package() self._get_indexing_sets() # Create list of inlet names outlet_list = self.create_outlet_list() if self.config.mixed_state_block is None: mixed_block = self.add_mixed_state_block() else: mixed_block = self.get_mixed_state_block() # Add inlet port self.add_inlet_port_objects(mixed_block) # Construct splitter based on ideal_separation argument if self.config.ideal_separation: # Use ideal partitioning method self.partition_outlet_flows(mixed_block, outlet_list) else: # Otherwise, Build StateBlocks for outlet outlet_blocks = self.add_outlet_state_blocks(outlet_list) # Add split fractions self.add_split_fractions(outlet_list) # Construct splitting equations self.add_material_splitting_constraints(mixed_block) self.add_energy_splitting_constraints(mixed_block) self.add_momentum_splitting_constraints(mixed_block) # Construct outlet port objects self.add_outlet_port_objects(outlet_list, outlet_blocks) def create_outlet_list(self): """ Create list of outlet stream names based on config arguments. Returns: list of strings """ if (self.config.outlet_list is not None and self.config.num_outlets is not None): # If both arguments provided and not consistent, raise Exception if len(self.config.outlet_list) != self.config.num_outlets: raise ConfigurationError( "{} Separator 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 (self.config.outlet_list is None and self.config.num_outlets is None): # If no arguments provided for outlets, default to num_outlets = 2 self.config.num_outlets = 2 # Create a list of names for outlet StateBlocks if self.config.outlet_list is not None: outlet_list = self.config.outlet_list else: outlet_list = [ 'outlet_' + str(n) for n in range(1, self.config.num_outlets + 1) ] return outlet_list def add_outlet_state_blocks(self, outlet_list): """ Construct StateBlocks for all outlet 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["parameters"] = self.config.property_package tmp_dict["defined_state"] = False # Create empty list to hold StateBlocks for return outlet_blocks = [] # Create an instance of StateBlock for all outlets for o in outlet_list: o_obj = self.config.property_package.state_block_class( self.flowsheet().config.time, doc="Material properties at outlet", default=tmp_dict) setattr(self, o + "_state", o_obj) outlet_blocks.append(getattr(self, o + "_state")) return outlet_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"] = False tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = True self.mixed_state = self.config.property_package.state_block_class( 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 Separator must use the same " "property package.".format(self.name)) return self.config.mixed_state_block def add_inlet_port_objects(self, mixed_block): """ Adds inlet Port object if required. Args: a mixed state StateBlock object Returns: None """ if self.config.construct_ports is True: self.add_port(name="inlet", block=mixed_block, doc="Inlet Port") def add_outlet_port_objects(self, outlet_list, outlet_blocks): """ Adds outlet Port objects if required. Args: a list of outlet StateBlock objects Returns: None """ if self.config.construct_ports is True: # Add ports for p in outlet_list: o_state = getattr(self, p + "_state") self.add_port(name=p, block=o_state, doc="Outlet Port") def add_split_fractions(self, outlet_list): """ Creates outlet Port objects and tries to partiton mixed stream flows between these Args: StateBlock representing the mixed flow to be split a list of names for outlets Returns: None """ self.outlet_idx = Set(initialize=outlet_list) if self.config.split_basis == SplittingType.totalFlow: sf_idx = [self.flowsheet().config.time, self.outlet_idx] sf_sum_idx = [self.flowsheet().config.time] elif self.config.split_basis == SplittingType.phaseFlow: sf_idx = [ self.flowsheet().config.time, self.outlet_idx, self.config.property_package.phase_list ] sf_sum_idx = [ self.flowsheet().config.time, self.config.property_package.phase_list ] elif self.config.split_basis == SplittingType.componentFlow: sf_idx = [ self.flowsheet().config.time, self.outlet_idx, self.config.property_package.component_list ] sf_sum_idx = [ self.flowsheet().config.time, self.config.property_package.component_list ] elif self.config.split_basis == SplittingType.phaseComponentFlow: sf_idx = [ self.flowsheet().config.time, self.outlet_idx, self.config.property_package.phase_list, self.config.property_package.component_list ] sf_sum_idx = [ self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list ] else: raise BurntToast("{} split_basis has unexpected value. This " "should not happen.".format(self.name)) # Create split fraction variable self.split_fraction = Var(*sf_idx, initialize=0.5, doc="Outlet split fractions") # Add constraint that split fractions sum to 1 def sum_sf_rule(b, t, *args): return 1 == sum(b.split_fraction[t, o, args] for o in self.outlet_idx) self.sum_split_frac = Constraint(*sf_sum_idx, rule=sum_sf_rule) def add_material_splitting_constraints(self, mixed_block): """ Creates constraints for splitting the material flows """ def sf(t, o, p, j): if self.config.split_basis == SplittingType.totalFlow: return self.split_fraction[t, o] elif self.config.split_basis == SplittingType.phaseFlow: return self.split_fraction[t, o, p] elif self.config.split_basis == SplittingType.componentFlow: return self.split_fraction[t, o, j] elif self.config.split_basis == SplittingType.phaseComponentFlow: return self.split_fraction[t, o, p, j] @self.Constraint(self.flowsheet().config.time, self.outlet_idx, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Material splitting equations") def material_splitting_eqn(b, t, o, p, j): o_block = getattr(self, o + "_state") return (sf(t, o, p, j) * mixed_block[t].get_material_flow_terms( p, j) == o_block[t].get_material_flow_terms(p, j)) def add_energy_splitting_constraints(self, mixed_block): """ Creates constraints for splitting the energy flows - done by equating temperatures in outlets. """ if self.config.energy_split_basis == \ EnergySplittingType.equal_temperature: @self.Constraint(self.flowsheet().config.time, self.outlet_idx, doc="Temperature equality constraint") def temperature_equality_eqn(b, t, o): o_block = getattr(self, o + "_state") return mixed_block[t].temperature == o_block[t].temperature elif self.config.energy_split_basis == \ EnergySplittingType.equal_molar_enthalpy: @self.Constraint(self.flowsheet().config.time, self.outlet_idx, doc="Molar enthalpy equality constraint") def molar_enthalpy_equality_eqn(b, t, o): o_block = getattr(self, o + "_state") return mixed_block[t].enth_mol == o_block[t].enth_mol def add_momentum_splitting_constraints(self, mixed_block): """ Creates constraints for splitting the momentum flows - done by equating pressures in outlets. """ @self.Constraint(self.flowsheet().config.time, self.outlet_idx, doc="Pressure equality constraint") def pressure_equality_eqn(b, t, o): o_block = getattr(self, o + "_state") return mixed_block[t].pressure == o_block[t].pressure def partition_outlet_flows(self, mb, outlet_list): """ Creates outlet Port objects and tries to partiton mixed stream flows between these Args: StateBlock representing the mixed flow to be split a list of names for outlets Returns: None """ # Check arguments if self.config.construct_ports is False: raise ConfigurationError("{} cannot have and ideal separator " "(ideal_separation = True) with " "construct_ports = False.".format( self.name)) if self.config.split_basis == SplittingType.totalFlow: raise ConfigurationError("{} cannot do an ideal separation based " "on total flow.".format(self.name)) if self.config.ideal_split_map is None: raise ConfigurationError( "{} was not provided with an " "ideal_split_map argument which is " "necessary for doing an ideal_separation.".format(self.name)) # Validate split map split_map = self.config.ideal_split_map idx_list = [] if self.config.split_basis == SplittingType.phaseFlow: for p in self.config.property_package.phase_list: idx_list.append((p)) if len(idx_list) != len(split_map): raise ConfigurationError( "{} ideal_split_map does not match with " "split_basis chosen. ideal_split_map must" " have a key for each combination of indices.".format( self.name)) for k in idx_list: if k not in split_map: raise ConfigurationError( "{} ideal_split_map does not match with " "split_basis chosen. ideal_split_map must" " have a key for each combination of indices.".format( self.name)) elif self.config.split_basis == SplittingType.componentFlow: for j in self.config.property_package.component_list: idx_list.append((j)) if len(idx_list) != len(split_map): raise ConfigurationError( "{} ideal_split_map does not match with " "split_basis chosen. ideal_split_map must" " have a key for each component.".format(self.name)) elif self.config.split_basis == SplittingType.phaseComponentFlow: for p in self.config.property_package.phase_list: for j in self.config.property_package.component_list: idx_list.append((p, j)) if len(idx_list) != len(split_map): raise ConfigurationError( "{} ideal_split_map does not match with " "split_basis chosen. ideal_split_map must" " have a key for each phase-component pair.".format( self.name)) # Check that no. outlets matches split_basis if len(outlet_list) != len(idx_list): raise ConfigurationError( "{} Cannot perform ideal separation. Must have one " "outlet for each possible combination of the " "chosen split_basis.".format(self.name)) # Create tolerance Parameter for 0 flow outlets self.eps = Param(default=1e-8, mutable=True) # Get list of port members s_vars = mb[self.flowsheet().config.time.first()].define_port_members() # Add empty Port objects for o in outlet_list: p_obj = Port(noruleinit=True, doc="Outlet Port") setattr(self, o, p_obj) # Iterate over members to create References or Expressions for s in s_vars: # Get local variable name of component l_name = s_vars[s].local_name if l_name == "pressure" or l_name == "temperature": # Assume outlets same as mixed flow - make Reference e_obj = Reference(mb[:].component(l_name)) elif (l_name.startswith("mole_frac") or l_name.startswith("mass_frac")): # Mole and mass frac need special handling if l_name.endswith("_phase"): def e_rule(b, t, p, j): if self.config.split_basis == \ SplittingType.phaseFlow: s_check = split_map[p] elif self.config.split_basis == \ SplittingType.componentFlow: s_check = split_map[j] elif self.config.split_basis == \ SplittingType.phaseComponentFlow: s_check = split_map[p, j] else: raise BurntToast( "{} This should not happen. Please " "report this bug to the IDAES " "developers.".format(self.name)) if s_check == o: return mb[t].component(l_name)[p, j] else: return self.eps e_obj = Expression( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, rule=e_rule) else: if self.config.split_basis == \ SplittingType.componentFlow: def e_rule(b, t, j): if split_map[j] == o: return mb[t].component(l_name)[j] # else: return self.eps else: def e_rule(b, t, j): try: mfp = mb[t].component(l_name + "_phase") except AttributeError: raise AttributeError( "{} Cannot use ideal splitting with " "this property package. Package uses " "indexed port member {} which does not" " have the correct indexing sets, and " "an equivalent variable with correct " "indexing sets is not available.". format(self.name, s)) for p in self.config.property_package.phase_list: if self.config.split_basis == \ SplittingType.phaseFlow: s_check = split_map[p] elif self.config.split_basis == \ SplittingType.phaseComponentFlow: s_check = split_map[p, j] else: raise BurntToast( "{} This should not happen. Please" " report this bug to the IDAES " "developers.".format(self.name)) if s_check == o: return mfp[p, j] # else: return self.eps e_obj = Expression( self.flowsheet().config.time, self.config.property_package.component_list, rule=e_rule) elif l_name.endswith("_phase_comp"): def e_rule(b, t, p, j): if self.config.split_basis == \ SplittingType.phaseFlow: s_check = split_map[p] elif self.config.split_basis == \ SplittingType.componentFlow: s_check = split_map[j] elif self.config.split_basis == \ SplittingType.phaseComponentFlow: s_check = split_map[p, j] else: raise BurntToast( "{} This should not happen. Please" " report this bug to the IDAES " "developers.".format(self.name)) if s_check == o: return mb[t].component(l_name)[p, j] else: return self.eps e_obj = Expression( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, rule=e_rule) elif l_name.endswith("_phase"): if self.config.split_basis == \ SplittingType.phaseFlow: def e_rule(b, t, p): if split_map[p] == o: return mb[t].component(l_name)[p] else: return self.eps else: def e_rule(b, t, p): try: mfp = mb[t].component(l_name + "_comp") except AttributeError: raise AttributeError( "{} Cannot use ideal splitting with this " "property package. Package uses indexed " "port member {} which does not have the " "correct indexing sets, and an equivalent " "variable with correct indexing sets is " "not available.".format(self.name, s)) for j in self.config.property_package.component_list: if self.config.split_basis == \ SplittingType.componentFlow: s_check = split_map[j] elif self.config.split_basis == \ SplittingType.phaseComponentFlow: s_check = split_map[p, j] else: raise BurntToast( "{} This should not happen. Please" " report this bug to the IDAES " "developers.".format(self.name)) if s_check == o: return mfp[p, j] # else: return self.eps e_obj = Expression(self.flowsheet().config.time, self.config.property_package.phase_list, rule=e_rule) elif l_name.endswith("_comp"): if self.config.split_basis == \ SplittingType.componentFlow: def e_rule(b, t, j): if split_map[j] == o: return mb[t].component(l_name)[j] else: return self.eps elif self.config.split_basis == \ SplittingType.phaseFlow: def e_rule(b, t, j): try: mfp = mb[t].component("{0}_phase{1}".format( l_name[:-5], s[-5:])) except AttributeError: raise AttributeError( "{} Cannot use ideal splitting with this " "property package. Package uses indexed " "port member {} which does not have the " "correct indexing sets, and an equivalent " "variable with correct indexing sets is " "not available.".format(self.name, s)) for p in self.config.property_package.phase_list: if self.config.split_basis == \ SplittingType.phaseFlow: s_check = split_map[p] elif self.config.split_basis == \ SplittingType.phaseComponentFlow: s_check = split_map[p, j] else: raise BurntToast( "{} This should not happen. Please" " report this bug to the IDAES " "developers.".format(self.name)) if s_check == o: return mfp[p, j] # else: return self.eps e_obj = Expression( self.flowsheet().config.time, self.config.property_package.component_list, rule=e_rule) else: # Not a recognised state, check for indexing sets if mb[self.flowsheet().config.time.first()].component( l_name).is_indexed(): # Is indexed, assume indexes match and partition def e_rule(b, t, k): if split_map[k] == o: try: return mb[t].component(l_name)[k] except KeyError: raise KeyError( "{} Cannot use ideal splitting with" " this property package. Package uses " "indexed port member {} which does not" " have suitable indexing set(s).". format(self.name, s)) else: return self.eps # TODO : Reusing indexing set from first port member. # TODO : Not sure how good of an idea this is. e_obj = Expression( self.flowsheet().config.time, mb[self.flowsheet().config.time.first()].component( l_name).index_set(), rule=e_rule) else: # Is not indexed, look for indexed equivalent try: if self.config.split_basis == \ SplittingType.phaseFlow: def e_rule(b, t): for p in self.config.property_package.phase_list: if split_map[p] == o: return mb[t].component(l_name + "_phase")[p] # else return self.eps elif self.config.split_basis == \ SplittingType.componentFlow: def e_rule(b, t): for j in self.config.property_package.component_list: if split_map[j] == o: return mb[t].component(l_name + "_comp")[j] # else return self.eps elif self.config.split_basis == \ SplittingType.phaseComponentFlow: def e_rule(b, t): for p in self.config.property_package.phase_list: for j in self.config.property_package.component_list: if split_map[p, j] == o: return (mb[t].component( l_name + "_phase_comp")[p, j]) # else return self.eps except AttributeError: raise AttributeError( "{} Cannot use ideal splitting with this " "property package. Package uses unindexed " "port member {} which does not have an " "equivalent indexed form.".format( self.name, s)) e_obj = Expression(self.flowsheet().config.time, rule=e_rule) # Add Reference/Expression object to Separator model object setattr(self, "_" + o + "_" + l_name + "_ref", e_obj) # Add member to Port object p_obj.add(e_obj, s) 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: 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('{} Separator inlet state block has no ' 'model check. To correct this, add a ' 'model_check method to the associated ' 'StateBlock class.'.format(blk.name)) try: outlet_list = blk.create_outlet_list() for o in outlet_list: o_block = getattr(blk, o + "_state") o_block[t].model_check() except AttributeError: _log.warning( '{} Separator outlet state block has no ' 'model checks. To correct this, add a model_check' ' method to the associated StateBlock class.'.format( blk.name)) def initialize(blk, outlvl=0, optarg={}, solver='ipopt', hold_state=False): ''' Initialisation routine for separator (default solver ipopt) Keyword Arguments: outlvl : sets output level of initialisation routine. **Valid values:** **0** - no output (default), **1** - return solver state for each step in routine, **2** - include solver output infomation (tee=True) optarg : solver options dictionary object (default=None) 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. ''' # Set solver options if outlvl > 1: stee = True else: stee = False opt = SolverFactory(solver) opt.options = optarg # Initialize mixed state block if blk.config.mixed_state_block is not None: mblock = blk.config.mixed_state_block else: mblock = blk.mixed_state flags = mblock.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, hold_state=True) if blk.config.ideal_separation: # If using ideal splitting, initialisation should be complete return flags # Initialize outlet StateBlocks outlet_list = blk.create_outlet_list() for o in outlet_list: # Get corresponding outlet StateBlock o_block = getattr(blk, o + "_state") for t in blk.flowsheet().config.time: # Calculate values for state variables s_vars = o_block[t].define_state_vars() for v in s_vars: m_var = getattr(mblock[t], s_vars[v].local_name) if "flow" in v: # If a "flow" variable, is extensive # Apply split fraction try: for k in s_vars[v]: if (k is None or blk.config.split_basis == SplittingType.totalFlow): s_vars[v][k].value = value( m_var[k] * blk.split_fraction[(t, o)]) else: s_vars[v][k].value = value( m_var[k] * blk.split_fraction[(t, o) + k]) except KeyError: raise KeyError( "{} state variable and split fraction " "indexing sets do not match. The in-built" " initialization routine for Separators " "relies on the split fraction and state " "variable indexing sets matching to " "calculate initial guesses for extensive " "variables. In other cases users will " "need to provide their own initial " "guesses".format(blk.name)) else: # Otherwise intensive, equate to mixed stream for k in s_vars[v]: s_vars[v][k].value = m_var[k].value # Call initialization routine for outlet StateBlock o_block.initialize(outlvl=outlvl - 1, optarg=optarg, solver=solver, hold_state=False) if blk.config.mixed_state_block is None: results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: _log.info('{} Initialisation Complete.'.format(blk.name)) else: _log.warning('{} Initialisation Failed.'.format(blk.name)) else: _log.info('{} Initialisation Complete.'.format(blk.name)) if hold_state is True: return flags else: blk.release_state(flags, outlvl=outlvl - 1) def release_state(blk, flags, outlvl=0): ''' Method to release state variables fixed during initialisation. 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 ''' if blk.config.mixed_state_block is None: mblock = blk.mixed_state else: mblock = blk.config.mixed_state_block mblock.release_state(flags, outlvl=outlvl - 1)
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=ListOf(str), 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=Bool, 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=Bool, 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().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().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().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().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 = mixed_block.phase_component_set # Get units metadata units = pp.get_metadata() flow_basis = mixed_block[ self.flowsheet().time.first()].get_material_flow_basis() if flow_basis == MaterialFlowBasis.molar: flow_units = units.get_derived_units("flow_mole") elif flow_basis == MaterialFlowBasis.mass: flow_units = units.get_derived_units("flow_mass") else: # Let this pass for now with no units flow_units = None if mb_type == MaterialBalanceType.componentPhase: # Create equilibrium generation term and constraints if required if self.config.has_phase_equilibrium is True: try: self.phase_equilibrium_generation = Var( self.flowsheet().time, pp.phase_equilibrium_idx, domain=Reals, doc="Amount of generation in unit by phase equilibria", units=flow_units) 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().time, pc_set, doc="Material mixing equations", ) def material_mixing_equations(b, t, p, j): 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)) elif mb_type == MaterialBalanceType.componentTotal: # Write phase-component balances @self.Constraint( self.flowsheet().time, mixed_block.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 mixed_block.phase_list if (p, j) in pc_set) elif mb_type == MaterialBalanceType.total: # Write phase-component balances @self.Constraint(self.flowsheet().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 mixed_block.component_list if (p, j) in pc_set) for p in mixed_block.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().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 mixed_block.phase_list) for i in range(len(inlet_blocks))) - sum(mixed_block[t].get_enthalpy_flow_terms(p) for p in mixed_block.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)) # Get units metadata units = self.config.property_package.get_metadata() # Add variables self.minimum_pressure = Var( self.flowsheet().time, self.inlet_idx, doc="Variable for calculating minimum inlet pressure", units=units.get_derived_units("pressure")) self.eps_pressure = Param( mutable=True, initialize=1e-3, domain=PositiveReals, doc="Smoothing term for minimum inlet pressure", units=units.get_derived_units("pressure")) # Calculate minimum inlet pressure @self.Constraint( self.flowsheet().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().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().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().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=idaeslog.NOTSET, optarg=None, solver=None, hold_state=False): """ Initialization routine for mixer. Keyword Arguments: 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) 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") # Create solver opt = get_solver(solver, 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().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().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().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().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) 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 mb_type == MaterialBalanceType.useDefault: t_ref = self.flowsheet().time.first() mb_type = self.mixed_state[t_ref].default_material_balance_type() 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, default=1, warning=True) iscale.constraint_scaling_transform(c, s) 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, overwrite=False) elif mb_type == MaterialBalanceType.componentTotal: for (t, j), c in self.material_mixing_equations.items(): for i, p in enumerate(self.mixed_state.phase_list): try: ft = self.mixed_state[t].get_material_flow_terms( p, j) except (KeyError, AttributeError): continue # component not in phase 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, overwrite=False) elif mb_type == MaterialBalanceType.total: pc_set = self.mixed_state.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, overwrite=False) if hasattr(self, "enthalpy_mixing_equations"): for t, c in self.enthalpy_mixing_equations.items(): def scale_gen(): for v in self.mixed_state[t].phase_list: yield self.mixed_state[t].get_enthalpy_flow_terms(p) s = iscale.min_scaling_factor(scale_gen(), default=1) iscale.constraint_scaling_transform(c, s, overwrite=False)
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=None, solver=None): """ Initialization routine for splitter Keyword Arguments: 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: 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, overwrite=False) 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, overwrite=False) 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, overwrite=False)
class ZeroOrderBaseData(UnitModelBlockData): """ Standard base class for zero order unit models. This class contains the basic consistency checks and common methods for zero order type models. """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""All zero-order models are steady-state only""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Zero order models do not include holdup""", ), ) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** {see property package for documentation.}""", ), ) CONFIG.declare( "database", ConfigValue( description= "An instance of a WaterTAP Database to use for parameters."), ) CONFIG.declare( "process_subtype", ConfigValue( description= "Process subtype to use when looking up parameters from database." ), ) def build(self): super().build() # Set a placeholder attributes # Placeholder for technology type string self._tech_type = None # Attribute indicating what parameters need to be fixed in the model self._has_recovery_removal = False self._fixed_perf_vars = [] # Place holders for assigning methods self._initialize = None # used to link to initization routine self._scaling = None # used to link to scaling routine self._get_Q = None # used to provide inlet volumetric flow # Attributed for storing contents of reporting output self._stream_table_dict = {} self._perf_var_dict = {} # Check that property package meets requirements if self.config.property_package.phase_list != ["Liq"]: raise ConfigurationError( f"{self.name} configured with invalid property package. " f"Zero-order models only support property packages with a " f"single phase named 'Liq'.") if not hasattr( self.config.property_package, "solvent_set" ) or self.config.property_package.solvent_set != ["H2O"]: raise ConfigurationError( f"{self.name} configured with invalid property package. " f"Zero-order models only support property packages which " f"include 'H2O' as the only Solvent.") if not hasattr(self.config.property_package, "solute_set"): raise ConfigurationError( f"{self.name} configured with invalid property package. " f"Zero-order models require property packages to declare all " f"dissolved species as Solutes.") if (len(self.config.property_package.solute_set) != len(self.config.property_package.component_list) - 1): raise ConfigurationError( f"{self.name} configured with invalid property package. " f"Zero-order models only support `H2O` as a solvent and all " f"other species as Solutes.") def initialize_build(self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): """ Placeholder initialization routine, raises NotImplementedError """ if self._initialize is None or not callable(self._initialize): raise NotImplementedError() else: self._initialize(self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None) def calculate_scaling_factors(self): """ Placeholder scaling routine, should be overloaded by derived classes """ super().calculate_scaling_factors() if callable(self._scaling): self._scaling(self) def load_parameters_from_database(self, use_default_removal=False): """ Method to load parameters for from database. Args: use_default_removal - (optional) indicate whether to use defined default removal fraction if no specific value defined in database Returns: None """ # Get parameter dict from database if self._tech_type is None: raise NotImplementedError( f"{self.name} derived zero order unit model has not " f"implemented the _tech_type attribute. This is required " f"to identify the database file to load parameters from.") # Get parameter dict from database pdict = self.config.database.get_unit_operation_parameters( self._tech_type, subtype=self.config.process_subtype) if self._has_recovery_removal: self.set_recovery_and_removal(pdict, use_default_removal) for v in self._fixed_perf_vars: self.set_param_from_data(v, pdict) def set_recovery_and_removal(self, data, use_default_removal=False): """ Common utility method for setting values of recovery and removal fractions. Args: data - dict of parameter values to use when fixing variables use_default_removal - (optional) indicate whether to use defined default removal fraction if no specific value defined in database Returns: None """ try: self.set_param_from_data(self.recovery_frac_mass_H2O, data) except KeyError: if self.recovery_frac_mass_H2O[:].fixed: pass else: raise for t, j in self.removal_frac_mass_solute: self.set_param_from_data( self.removal_frac_mass_solute[t, j], data, index=j, use_default_removal=use_default_removal, ) def set_param_from_data(self, parameter, data, index=None, use_default_removal=False): """ General method for setting parameter values from a dict of data returned from a database. Args: parameter - a Pyomo Var to be fixed to value from database data - dict of parameter values from database index - (optional) index to fix if parameter is an IndexedVar use_default_removal - (optional) indicate whether to use defined default removal fraction if no specific value defined in database Returns: None Raises: KeyError if values cannot be found for parameter in data dict """ pname = parameter.parent_component().local_name try: pdata = data[pname] except KeyError: raise KeyError( f"{self.name} - database provided does not contain an entry " f"for {pname} for technology.") if index is not None: try: pdata = pdata[index] except KeyError: if pname == "removal_frac_mass_solute" and use_default_removal: try: pdata = data["default_removal_frac_mass_solute"] index = "default" except KeyError: raise KeyError( f"{self.name} - database provided does not " f"contain an entry for {pname} with index {index} " f"for technology and no default removal was " f"specified.") else: raise KeyError( f"{self.name} - database provided does not contain " f"an entry for {pname} with index {index} for " f"technology.") try: val = pdata["value"] except KeyError: raise KeyError( f"{self.name} - no value provided for {pname} (index: " f"{index}) in database.") try: units = getattr(pyunits, pdata["units"]) except KeyError: raise KeyError( f"{self.name} - no units provided for {pname} (index: " f"{index}) in database.") parameter.fix(val * units) _log.info_high(f"{parameter.name} fixed to value {val} {str(units)}") def get_inlet_flow(self, t): return self._get_Q(self, t) def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe(self._stream_table_dict, time_point=time_point) def _get_performance_contents(self, time_point=0): var_dict = {} for k, v in self._perf_var_dict.items(): if k in ["Solute Removal", "Reaction Extent", "Rejection"]: for j, vd in v[time_point, :].wildcard_items(): var_dict[f"{k} [{j}]"] = vd elif v.is_indexed(): var_dict[k] = v[time_point] else: var_dict[k] = v return {"vars": var_dict}
class HelmIsentropicTurbineData(BalanceBlockData): """ Basic isentropic 0D turbine model. This inherits the heater block to get a lot of unit model boilerplate and the mass balance, enegy balance and pressure equations. This model is intended to be used only with Helmholtz EOS property pacakges in mixed or single phase mode with P-H state vars. Since this inherits BalanceBlockData, and only operates in steady-state or pseudo-steady-state (for dynamic models) the following mass, energy and pressure equations are implicitly writen. 1) Mass Balance: 0 = flow_mol_in[t] - flow_mol_out[t] 2) Energy Balance: 0 = (flow_mol[t]*h_mol[t])_in - (flow_mol[t]*h_mol[t])_out + Q_in + W_in 3) Pressure: 0 = P_in[t] + deltaP[t] - P_out[t] """ CONFIG = BalanceBlockData.CONFIG() # For dynamics assume pseudo-steady-state CONFIG.dynamic = False CONFIG.get("dynamic")._default = False CONFIG.get("dynamic")._domain = In([False]) CONFIG.has_holdup = False CONFIG.get("has_holdup")._default = False CONFIG.get("has_holdup")._domain = In([False]) # Rest of config to make this function like a turbine CONFIG.has_pressure_change = True CONFIG.get("has_pressure_change")._default = True CONFIG.get("has_pressure_change")._domain = In([True]) CONFIG.has_work_transfer = True CONFIG.get("has_work_transfer")._default = True CONFIG.get("has_work_transfer")._domain = In([True]) CONFIG.has_heat_transfer = False CONFIG.get("has_heat_transfer")._default = False CONFIG.get("has_heat_transfer")._domain = In([False]) def build(self): """ Add model equations to the unit model. This is called by a default block construnction rule when the unit model is created. """ super().build() # Basic unit model build/read config config = self.config # shorter config pointer # The thermodynamic expression writer object, te, writes expressions # including external function calls to calculate thermodynamic quantities # from a set of state variables. _assert_properties(config.property_package) te = ThermoExpr(blk=self, parameters=config.property_package) eff = self.efficiency_isentropic = pyo.Var( self.flowsheet().config.time, initialize=0.9, doc="Isentropic efficiency") eff.fix() pratio = self.ratioP = pyo.Var(self.flowsheet().config.time, initialize=0.7, doc="Ratio of outlet to inlet pressure") # Some shorter refernces to property blocks properties_in = self.control_volume.properties_in properties_out = self.control_volume.properties_out @self.Expression(self.flowsheet().config.time, doc="Outlet isentropic enthalpy") def h_is(b, t): return te.h(s=properties_in[t].entr_mol, p=properties_out[t].pressure) @self.Expression(self.flowsheet().config.time, doc="Isentropic enthalpy change") def delta_enth_isentropic(b, t): return self.h_is[t] - properties_in[t].enth_mol @self.Expression(self.flowsheet().config.time, doc="Isentropic work") def work_isentropic(b, t): return properties_in[t].flow_mol * (properties_in[t].enth_mol - self.h_is[t]) @self.Expression(self.flowsheet().config.time, doc="Outlet enthalpy") def h_o(b, t): # Early access to the outlet enthalpy and work return properties_in[t].enth_mol - eff[t] * ( properties_in[t].enth_mol - self.h_is[t]) @self.Constraint(self.flowsheet().config.time) def eq_work(b, t): # Work from energy balance return properties_out[t].enth_mol == self.h_o[t] @self.Constraint(self.flowsheet().config.time) def eq_pressure_ratio(b, t): return (pratio[t] * properties_in[t].pressure == properties_out[t].pressure) @self.Expression(self.flowsheet().config.time) def work_mechanical(b, t): return b.control_volume.work[t] def _get_performance_contents(self, time_point=0): """This returns a dictionary of quntities to be used in IDAES unit model report generation routines. """ pc = super()._get_performance_contents(time_point=time_point) return pc def initialize( self, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ For simplicity this initialization requires you to set values for the efficency, inlet, and one of pressure ratio, pressure change or outlet pressure. """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # Create solver slvr = get_solver(solver, optarg) # Store original specification so initialization doesn't change the model # This will only resore the values of varaibles that were originally fixed sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Check for alternate pressure specs for t in self.flowsheet().config.time: if self.outlet.pressure[t].fixed: self.ratioP[t] = pyo.value(self.outlet.pressure[t] / self.inlet.pressure[t]) elif self.control_volume.deltaP[t].fixed: self.ratioP[t] = pyo.value( (self.control_volume.deltaP[t] + self.inlet.pressure[t]) / self.inlet.pressure[t]) # Fix the variables we base the initializtion on and free the rest. # This requires good values to be provided for pressure, efficency, # and inlet conditions, but it is simple and reliable. self.inlet.fix() self.outlet.unfix() self.ratioP.fix() self.deltaP.unfix() self.efficiency_isentropic.fix() for t in self.flowsheet().config.time: self.outlet.pressure[t] = pyo.value(self.inlet.pressure[t] * self.ratioP[t]) self.deltaP[t] = pyo.value(self.outlet.pressure[t] - self.inlet.pressure[t]) self.outlet.enth_mol[t] = pyo.value(self.h_o[t]) self.control_volume.work[t] = pyo.value( self.inlet.flow_mol[t] * self.inlet.enth_mol[t] - self.outlet.flow_mol[t] * self.outlet.enth_mol[t]) self.outlet.flow_mol[t] = pyo.value(self.inlet.flow_mol[t]) # Solve the model (should be already solved from above) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = slvr.solve(self, tee=slc.tee) from_json(self, sd=istate, wts=sp) def calculate_scaling_factors(self): super().calculate_scaling_factors() for t, c in self.eq_pressure_ratio.items(): s = iscale.get_scaling_factor( self.control_volume.properties_in[t].pressure) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.eq_work.items(): s = iscale.get_scaling_factor(self.control_volume.work[t]) iscale.constraint_scaling_transform(c, s, overwrite=False)
rxn_config.declare( "stoichiometry", ConfigValue(domain=dict, description="Stoichiometry of reaction", doc="Dict describing stoichiometry of reaction")) rxn_config.declare( "heat_of_reaction", ConfigValue( description="Method for calculating specific heat of reaction", doc="Valid Python class containing instructions on how to calculate " "the heat of reaction for this reaction.")) rxn_config.declare( "concentration_form", ConfigValue( default=None, domain=In(ConcentrationForm), description="Form to use for concentration terms in reaction equation", doc= "ConcentrationForm Enum indicating what form to use for concentration " "terms when constructing reaction equation.")) rxn_config.declare( "parameter_data", ConfigValue( default={}, domain=dict, description="Dict containing initialization data for parameters")) rate_rxn_config = rxn_config() rate_rxn_config.declare( "rate_constant", ConfigValue(
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.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "has_heat_transfer", ConfigValue( default=False, domain=In([True, False]), description="Heat transfer term construction flag", doc= """Indicates whether terms for heat transfer should be constructed, **default** - False. **Valid values:** { **True** - include heat transfer terms, **False** - exclude heat transfer terms.}""")) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) CONFIG.declare( "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 _add_subsolver_configs(CONFIG): """Adds the subsolver-related configurations. Parameters ---------- CONFIG : ConfigBlock The specific configurations for MindtPy. """ CONFIG.declare( 'nlp_solver', ConfigValue( default='ipopt', domain=In(['ipopt', 'gams', 'baron']), description='NLP subsolver name', doc= 'Which NLP subsolver is going to be used for solving the nonlinear' 'subproblems.')) CONFIG.declare( 'nlp_solver_args', ConfigBlock( implicit=True, description='NLP subsolver options', doc='Which NLP subsolver options to be passed to the solver while ' 'solving the nonlinear subproblems.')) CONFIG.declare( 'mip_solver', ConfigValue( default='glpk', domain=In([ 'gurobi', 'cplex', 'cbc', 'glpk', 'gams', 'gurobi_persistent', 'cplex_persistent' ]), description='MIP subsolver name', doc='Which MIP subsolver is going to be used for solving the mixed-' 'integer main problems.')) CONFIG.declare( 'mip_solver_args', ConfigBlock( implicit=True, description='MIP subsolver options', doc='Which MIP subsolver options to be passed to the solver while ' 'solving the mixed-integer main problems.')) CONFIG.declare( 'mip_solver_mipgap', ConfigValue(default=1E-4, domain=PositiveFloat, description='Mipgap passed to MIP solver.')) CONFIG.declare( 'threads', ConfigValue(default=0, domain=NonNegativeInt, description='Threads', doc='Threads used by MIP solver and NLP solver.')) CONFIG.declare( 'regularization_mip_threads', ConfigValue( default=0, domain=NonNegativeInt, description='regularization MIP threads', doc= 'Threads used by MIP solver to solve regularization main problem.') ) CONFIG.declare( 'solver_tee', ConfigValue( default=False, description= 'Stream the output of MIP solver and NLP solver to terminal.', domain=bool)) CONFIG.declare( 'mip_solver_tee', ConfigValue(default=False, description='Stream the output of MIP solver to terminal.', domain=bool)) CONFIG.declare( 'nlp_solver_tee', ConfigValue(default=False, description='Stream the output of nlp solver to terminal.', domain=bool)) CONFIG.declare( 'mip_regularization_solver', ConfigValue( default=None, domain=In([ 'gurobi', 'cplex', 'cbc', 'glpk', 'gams', 'gurobi_persistent', 'cplex_persistent' ]), description='MIP subsolver for regularization problem', doc= 'Which MIP subsolver is going to be used for solving the regularization problem.' ))
class EvaporatorData(UnitModelBlockData): """ Evaporator model for MVC """ # CONFIG are options for the unit model, this simple model only has the mandatory config options 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( "property_package_feed", ConfigValue( 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_feed", 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( "property_package_vapor", ConfigValue( 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_vapor", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.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.}""", ), ) def build(self): super().build() if self.config.property_package_feed is None: raise ConfigurationError( "Users must provide a feed property package to the evaporator unit model" ) if self.config.property_package_vapor is None: raise ConfigurationError( "Users must provide a vapor property package to the evaporator unit model" ) # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta_feed = ( self.config.property_package_feed.get_metadata().get_derived_units) # Add shared unit model variables self.U = Var( initialize=1e3, bounds=(10, 1e4), units=pyunits.J * pyunits.s**-1 * pyunits.m**-2 * pyunits.K**-1, ) self.area = Var(initialize=1e2, bounds=(1e-1, 1e4), units=pyunits.m**2) self.delta_temperature_in = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) self.delta_temperature_out = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) self.lmtd = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) # Add feed_side block self.feed_side = Block() # Add unit variables to feed self.feed_side.heat_transfer = Var(initialize=1e4, bounds=(1, 1e10), units=pyunits.J * pyunits.s**-1) # Add feed_side state blocks # Feed state block tmp_dict = dict(**self.config.property_package_args_feed) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package_feed tmp_dict["defined_state"] = True # feed inlet defined self.feed_side.properties_feed = ( self.config.property_package_feed.state_block_class( self.flowsheet().config.time, doc="Material properties of feed inlet", default=tmp_dict, )) # Brine state block tmp_dict["defined_state"] = False # brine outlet not yet defined self.feed_side.properties_brine = ( self.config.property_package_feed.state_block_class( self.flowsheet().config.time, doc="Material properties of brine outlet", default=tmp_dict, )) # Vapor state block tmp_dict = dict(**self.config.property_package_args_vapor) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package_vapor tmp_dict["defined_state"] = False # vapor outlet not yet defined self.feed_side.properties_vapor = ( self.config.property_package_vapor.state_block_class( self.flowsheet().config.time, doc="Material properties of vapor outlet", default=tmp_dict, )) # Add condenser self.condenser = Condenser( default={"property_package": self.config.property_package_vapor}) # Add ports - oftentimes users interact with these rather than the state blocks self.add_port(name="inlet_feed", block=self.feed_side.properties_feed) self.add_port(name="outlet_brine", block=self.feed_side.properties_brine) self.add_port(name="outlet_vapor", block=self.feed_side.properties_vapor) self.add_port(name="inlet_condenser", block=self.condenser.control_volume.properties_in) self.add_port(name="outlet_condenser", block=self.condenser.control_volume.properties_out) ### FEED SIDE CONSTRAINTS ### # Mass balance @self.feed_side.Constraint( self.flowsheet().time, self.config.property_package_feed.component_list, doc="Mass balance", ) def eq_mass_balance(b, t, j): lb = b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].lb b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].fix(lb) if j == "H2O": return ( b.properties_feed[t].flow_mass_phase_comp["Liq", "H2O"] == b.properties_brine[t].flow_mass_phase_comp["Liq", "H2O"] + b.properties_vapor[t].flow_mass_phase_comp["Vap", "H2O"]) else: return (b.properties_feed[t].flow_mass_phase_comp["Liq", j] == b.properties_brine[t].flow_mass_phase_comp["Liq", j]) # Energy balance @self.feed_side.Constraint(self.flowsheet().time, doc="Energy balance") def eq_energy_balance(b, t): return (b.heat_transfer + b.properties_feed[t].enth_flow == b.properties_brine[t].enth_flow + b.properties_vapor[t].enth_flow_phase["Vap"]) # Brine pressure @self.feed_side.Constraint(self.flowsheet().time, doc="Brine pressure") def eq_brine_pressure(b, t): return b.properties_brine[t].pressure == b.properties_brine[ t].pressure_sat # Vapor pressure @self.feed_side.Constraint(self.flowsheet().time, doc="Vapor pressure") def eq_vapor_pressure(b, t): return b.properties_vapor[t].pressure == b.properties_brine[ t].pressure # Vapor temperature @self.feed_side.Constraint(self.flowsheet().time, doc="Vapor temperature") def eq_vapor_temperature(b, t): return (b.properties_vapor[t].temperature == b.properties_brine[t].temperature) # return b.properties_vapor[t].temperature == 0.5*(b.properties_out[t].temperature + b.properties_in[t].temperature) ### EVAPORATOR CONSTRAINTS ### # Temperature difference in @self.Constraint(self.flowsheet().time, doc="Temperature difference in") def eq_delta_temperature_in(b, t): return (b.delta_temperature_in == b.condenser.control_volume.properties_in[t].temperature - b.feed_side.properties_brine[t].temperature) # Temperature difference out @self.Constraint(self.flowsheet().time, doc="Temperature difference out") def eq_delta_temperature_out(b, t): return (b.delta_temperature_out == b.condenser.control_volume.properties_out[t].temperature - b.feed_side.properties_brine[t].temperature) # log mean temperature @self.Constraint(self.flowsheet().time, doc="Log mean temperature difference") def eq_lmtd(b, t): dT_in = b.delta_temperature_in dT_out = b.delta_temperature_out temp_units = pyunits.get_units(dT_in) dT_avg = (dT_in + dT_out) / 2 # external function that ruturns the real root, for the cuberoot of negitive # numbers, so it will return without error for positive and negitive dT. b.cbrt = ExternalFunction(library=functions_lib(), function="cbrt", arg_units=[temp_units**3]) return b.lmtd == b.cbrt((dT_in * dT_out * dT_avg)) * temp_units # Heat transfer between feed side and condenser @self.Constraint(self.flowsheet().time, doc="Heat transfer balance") def eq_heat_balance(b, t): return b.feed_side.heat_transfer == -b.condenser.control_volume.heat[ t] # Evaporator heat transfer @self.Constraint(self.flowsheet().time, doc="Evaporator heat transfer") def eq_evaporator_heat(b, t): return b.feed_side.heat_transfer == b.U * b.area * b.lmtd def initialize(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) # --------------------------------------------------------------------- # Initialize feed side flags_feed = blk.feed_side.properties_feed.initialize(solver=solver, optarg=optarg, hold_state=True) init_log.info_high("Initialization Step 1 Complete.") # # --------------------------------------------------------------------- # # Initialize brine # Set state_args from inlet state if state_args is None: state_args = {} state_dict = blk.feed_side.properties_feed[ blk.flowsheet().config.time.first()].define_port_members() for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args[k] = {} for m in state_dict[k].keys(): state_args[k][m] = state_dict[k][m].value else: state_args[k] = state_dict[k].value blk.feed_side.properties_brine.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args) state_args_vapor = {} state_args_vapor["pressure"] = 0.5 * state_args["pressure"] state_args_vapor["temperature"] = state_args["temperature"] state_args_vapor["flow_mass_phase_comp"] = { ("Liq", "H2O"): blk.feed_side.properties_vapor[0].flow_mass_phase_comp["Liq", "H2O"].lb, ("Vap", "H2O"): state_args["flow_mass_phase_comp"][("Liq", "H2O")], } blk.feed_side.properties_vapor.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_vapor, ) init_log.info_high("Initialization Step 2 Complete.") # intialize condenser state_args_condenser = state_args_vapor state_args_condenser["flow_mass_phase_comp"][("Vap", "H2O")] = ( 0.5 * state_args_condenser["flow_mass_phase_comp"][("Vap", "H2O")]) state_args_condenser["pressure"] = blk.feed_side.properties_brine[ 0].pressure_sat.value state_args_condenser["temperature"] = state_args["temperature"] + 5 blk.condenser.initialize(state_args=state_args_condenser) # assert False # flags_condenser_cv = blk.condenser.initialize(state_args=state_args_condenser,hold_state=True) init_log.info_high("Initialization Step 3 Complete.") # --------------------------------------------------------------------- # Deactivate heat transfer balance # blk.eq_heat_balance.deactivate() # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 4 {}.".format( idaeslog.condition(res))) # --------------------------------------------------------------------- # Release feed and condenser inlet states blk.feed_side.properties_feed.release_state(flags_feed, outlvl=outlvl) # blk.condenser.control_volume.release_state(flags_condenser_cv, outlvl=outlvl) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) def _get_performance_contents(self, time_point=0): var_dict = { "Heat transfer": self.feed_side.heat_transfer, "Evaporator temperature": self.feed_side.properties_brine[0].temperature, "Evaporator pressure": self.feed_side.properties_brine[0].pressure, } return {"vars": var_dict} def calculate_scaling_factors(self): super().calculate_scaling_factors() self.condenser.calculate_scaling_factors() if iscale.get_scaling_factor(self.feed_side.heat_transfer) is None: sf = iscale.get_scaling_factor( self.feed_side.properties_vapor[0].enth_flow_phase["Vap"]) iscale.set_scaling_factor(self.feed_side.heat_transfer, sf)
def _get_MindtPy_config(): """Set up the configurations for MindtPy. Returns ------- CONFIG : ConfigBlock The specific configurations for MindtPy """ CONFIG = ConfigBlock('MindtPy') CONFIG.declare( 'iteration_limit', ConfigValue( default=50, domain=NonNegativeInt, description='Iteration limit', doc='Number of maximum iterations in the decomposition methods.')) CONFIG.declare( 'stalling_limit', ConfigValue( default=15, domain=PositiveInt, description='Stalling limit', doc= 'Stalling limit for primal bound progress in the decomposition methods.' )) 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.')) CONFIG.declare( 'strategy', ConfigValue( default='OA', domain=In(['OA', 'ECP', 'GOA', 'FP']), description='Decomposition strategy', doc='MINLP Decomposition strategy to be applied to the method. ' 'Currently available Outer Approximation (OA), Extended Cutting ' 'Plane (ECP), Global Outer Approximation (GOA) and Feasibility Pump (FP).' )) CONFIG.declare( 'add_regularization', ConfigValue( default=None, domain=In([ 'level_L1', 'level_L2', 'level_L_infinity', 'grad_lag', 'hess_lag', 'hess_only_lag', 'sqp_lag' ]), description='add regularization', doc= 'Solving a regularization problem before solve the fixed subproblem' 'the objective function of the regularization problem.')) CONFIG.declare( 'init_strategy', ConfigValue( default=None, domain=In(['rNLP', 'initial_binary', 'max_binary', 'FP']), description='Initialization strategy', doc='Initialization strategy used by any method. Currently the ' 'continuous relaxation of the MINLP (rNLP), solve a maximal ' 'covering problem (max_binary), and fix the initial value for ' 'the integer variables (initial_binary).')) CONFIG.declare( 'max_slack', ConfigValue( default=1000.0, domain=PositiveFloat, description='Maximum slack variable', doc= 'Maximum slack variable value allowed for the Outer Approximation ' 'cuts.')) CONFIG.declare( 'OA_penalty_factor', ConfigValue( default=1000.0, domain=PositiveFloat, description='Outer Approximation slack penalty factor', doc= 'In the objective function of the Outer Approximation method, the ' 'slack variables corresponding to all the constraints get ' 'multiplied by this number and added to the objective.')) CONFIG.declare( 'call_after_main_solve', ConfigValue( default=_DoNothing(), domain=None, description='Function to be executed after every main problem', doc='Callback hook after a solution of the main problem.')) CONFIG.declare( 'call_after_subproblem_solve', ConfigValue( default=_DoNothing(), domain=None, description='Function to be executed after every subproblem', doc='Callback hook after a solution of the nonlinear subproblem.')) CONFIG.declare( 'call_after_subproblem_feasible', ConfigValue(default=_DoNothing(), domain=None, description= 'Function to be executed after every feasible subproblem', doc='Callback hook after a feasible solution' ' of the nonlinear subproblem.')) CONFIG.declare( 'tee', ConfigValue(default=False, description='Stream output to terminal.', domain=bool)) CONFIG.declare( 'logger', ConfigValue( default='pyomo.contrib.mindtpy', description='The logger object or name to use for reporting.', domain=a_logger)) CONFIG.declare( 'logging_level', ConfigValue( default=logging.INFO, domain=NonNegativeInt, description='The logging level for MindtPy.' 'CRITICAL = 50, ERROR = 40, WARNING = 30, INFO = 20, DEBUG = 10, NOTSET = 0', )) CONFIG.declare( 'integer_to_binary', ConfigValue( default=False, description= 'Convert integer variables to binaries (for no-good cuts).', domain=bool)) CONFIG.declare( 'add_no_good_cuts', ConfigValue( default=False, description= 'Add no-good cuts (no-good cuts) to binary variables to disallow same integer solution again.' 'Note that integer_to_binary flag needs to be used to apply it to actual integers and not just binaries.', domain=bool)) CONFIG.declare( 'use_tabu_list', ConfigValue( default=False, description= 'Use tabu list and incumbent callback to disallow same integer solution again.', domain=bool)) CONFIG.declare( 'add_affine_cuts', ConfigValue(default=False, description='Add affine cuts drive from MC++.', domain=bool)) CONFIG.declare( 'single_tree', ConfigValue( default=False, description= 'Use single tree implementation in solving the MIP main problem.', domain=bool)) CONFIG.declare( 'solution_pool', ConfigValue( default=False, description='Use solution pool in solving the MIP main problem.', domain=bool)) CONFIG.declare( 'num_solution_iteration', ConfigValue( default=5, description= 'The number of MIP solutions (from the solution pool) used to generate the fixed NLP subproblem in each iteration.', domain=PositiveInt)) CONFIG.declare( 'add_slack', ConfigValue( default=False, description='Whether add slack variable here.' 'slack variables here are used to deal with nonconvex MINLP.', domain=bool)) CONFIG.declare( 'cycling_check', ConfigValue( default=True, description= 'Check if OA algorithm is stalled in a cycle and terminate.', domain=bool)) CONFIG.declare( 'feasibility_norm', ConfigValue( default='L_infinity', domain=In(['L1', 'L2', 'L_infinity']), description= 'Different forms of objective function in feasibility subproblem.') ) CONFIG.declare( 'differentiate_mode', ConfigValue(default='reverse_symbolic', domain=In(['reverse_symbolic', 'sympy']), description='Differentiate mode to calculate jacobian.')) CONFIG.declare( 'linearize_inactive', ConfigValue(default=False, description='Add OA cuts for inactive constraints.', domain=bool)) CONFIG.declare( 'use_mcpp', ConfigValue( default=False, description= "Use package MC++ to set a bound for variable 'objective_value', which is introduced when the original problem's objective function is nonlinear.", domain=bool)) CONFIG.declare( 'equality_relaxation', ConfigValue( default=False, description= 'Use dual solution from the NLP solver to add OA cuts for equality constraints.', domain=bool)) CONFIG.declare( 'calculate_dual', ConfigValue(default=False, description='Calculate duals of the NLP subproblem.', domain=bool)) CONFIG.declare( 'use_fbbt', ConfigValue(default=False, description= 'Use fbbt to tighten the feasible region of the problem.', domain=bool)) CONFIG.declare( 'use_dual_bound', ConfigValue( default=True, description= 'Add dual bound constraint to enforce the objective satisfies best-found dual bound.', domain=bool)) CONFIG.declare( 'heuristic_nonconvex', ConfigValue( default=False, description= 'Use dual solution from the NLP solver and slack variables to add OA cuts for equality constraints (Equality relaxation)' 'and minimize the sum of the slack variables (Augmented Penalty).', domain=bool)) CONFIG.declare( 'partition_obj_nonlinear_terms', ConfigValue( default=True, description= 'Partition objective with the sum of nonlinear terms using epigraph reformulation.', domain=bool)) _add_subsolver_configs(CONFIG) _add_tolerance_configs(CONFIG) _add_fp_configs(CONFIG) _add_bound_configs(CONFIG) _add_loa_configs(CONFIG) return CONFIG
class Iapws95ParameterBlockData(PhysicalParameterBlock): CONFIG = PhysicalParameterBlock.CONFIG() CONFIG.declare( "phase_presentation", ConfigValue( default=PhaseType.MIX, domain=In(PhaseType), description="Set the way phases are presented to models", doc="""Set the way phases are presented to models. The MIX option appears to the framework to be a mixed phase containing liquid and/or vapor. The mixed option can simplify calculations at the unit model level since it can be treated as a single phase, but unit models such as flash vessels will not be able to treate the phases indepedently. The LG option presents as two sperate phases to the framework. The L or G options can be used if it is known for sure that only one phase is present. **default** - PhaseType.MIX **Valid values:** { **PhaseType.MIX** - Present a mixed phase with liquid and/or vapor, **PhaseType.LG** - Present a liquid and vapor phase, **PhaseType.L** - Assume only liquid can be present, **PhaseType.G** - Assume only vapor can be present}""")) CONFIG.declare( "state_vars", ConfigValue( default=StateVars.PH, domain=In(StateVars), description="State variable set", doc= """The set of state variables to use. Depending on the use, one state variable set or another may be better computationally. Usually pressure and enthalpy are the best choice because they are well behaved during a phase change. **default** - StateVars.PH **Valid values:** { **StateVars.PH** - Pressure-Enthalpy, **StateVars.TPX** - Temperature-Pressure-Quality}""")) def build(self): super(Iapws95ParameterBlockData, self).build() self.state_block_class = Iapws95StateBlock # Location of the *.so or *.dll file for external functions self.plib = _so self.available = os.path.isfile(self.plib) # Phase list self.private_phase_list = Set(initialize=["Vap", "Liq"]) if self.config.phase_presentation == PhaseType.MIX: self.phase_list = Set(initialize=["Mix"]) elif self.config.phase_presentation == PhaseType.LG: self.phase_list = Set(initialize=["Vap", "Liq"]) elif self.config.phase_presentation == PhaseType.L: self.phase_list = Set(initialize=["Liq"]) elif self.config.phase_presentation == PhaseType.G: self.phase_list = Set(initialize=["Vap"]) # State var set self.state_vars = self.config.state_vars # Component list - a list of component identifiers self.component_list = Set(initialize=['H2O']) # List of phase equilibrium self.phase_equilibrium_idx = Set(initialize=[1]) self.phase_equilibrium_list = {1: ["H2O", ("Vap", "Liq")]} # Parameters, these should match what's in the C code self.temperature_crit = Param(initialize=647.096, doc='Critical temperature [K]') self.pressure_crit = Param(initialize=2.2064e7, doc='Critical pressure [Pa]') self.dens_mass_crit = Param(initialize=322, doc='Critical density [kg/m3]') self.gas_const = Param(initialize=8.3144598, doc='Gas Constant [J/mol/K]') self.mw = Param(initialize=0.01801528, doc='Molecular weight [kg/mol]') #Thermal conductivity parameters. # "Release on the IAPWS Formulation 2011 for the Thermal Conductivity of # Ordinary Water Substance" self.tc_L0 = Param(RangeSet(0, 5), initialize={ 0: 2.443221e-3, 1: 1.323095e-2, 2: 6.770357e-3, 3: -3.454586e-3, 4: 4.096266e-4 }, doc="0th order themalcondutivity paramters") self.tc_L1 = Param(RangeSet(0, 5), RangeSet(0, 6), initialize={ (0, 0): 1.60397357, (1, 0): 2.33771842, (2, 0): 2.19650529, (3, 0): -1.21051378, (4, 0): -2.7203370, (0, 1): -0.646013523, (1, 1): -2.78843778, (2, 1): -4.54580785, (3, 1): 1.60812989, (4, 1): 4.57586331, (0, 2): 0.111443906, (1, 2): 1.53616167, (2, 2): 3.55777244, (3, 2): -0.621178141, (4, 2): -3.18369245, (0, 3): 0.102997357, (1, 3): -0.463045512, (2, 3): -1.40944978, (3, 3): 0.0716373224, (4, 3): 1.1168348, (0, 4): -0.0504123634, (1, 4): 0.0832827019, (2, 4): 0.275418278, (3, 4): 0.0, (4, 4): -0.19268305, (0, 5): 0.00609859258, (1, 5): -0.00719201245, (2, 5): -0.0205938816, (3, 5): 0.0, (4, 5): 0.012913842 }, doc="1st order themalcondutivity paramters") #Viscosity paramters #"Release on the IAPWS Formulation 2008 for the Viscosity of # Ordinary Water Substance " self.visc_H0 = Param(RangeSet(0, 4), initialize={ 0: 1.67752, 1: 2.20462, 2: 0.6366564, 3: -0.241605 }, doc="0th order viscosity parameters") self.visc_H1 = Param(RangeSet(0, 6), RangeSet(0, 7), initialize={ (0, 0): 5.20094e-1, (1, 0): 8.50895e-2, (2, 0): -1.08374, (3, 0): -2.89555e-1, (4, 0): 0.0, (5, 0): 0.0, (0, 1): 2.22531e-1, (1, 1): 9.99115e-1, (2, 1): 1.88797, (3, 1): 1.26613, (4, 1): 0.0, (5, 1): 1.20573e-1, (0, 2): -2.81378e-1, (1, 2): -9.06851e-1, (2, 2): -7.72479e-1, (3, 2): -4.89837e-1, (4, 2): -2.57040e-1, (5, 2): 0.0, (0, 3): 1.61913e-1, (1, 3): 2.57399e-1, (2, 3): 0.0, (3, 3): 0.0, (4, 3): 0.0, (5, 3): 0.0, (0, 4): -3.25372e-2, (1, 4): 0.0, (2, 4): 0.0, (3, 4): 6.98452e-2, (4, 4): 0.0, (5, 4): 0.0, (0, 5): 0.0, (1, 5): 0.0, (2, 5): 0.0, (3, 5): 0.0, (4, 5): 8.72102e-3, (5, 5): 0.0, (0, 6): 0.0, (1, 6): 0.0, (2, 6): 0.0, (3, 6): -4.35673e-3, (4, 6): 0.0, (5, 6): -5.93264e-4 }, doc="1st order viscosity parameters") self.smoothing_pressure_over = Param( mutable=True, initialize=1e-4, doc='Smooth max parameter (pressure over)') self.smoothing_pressure_under = Param( mutable=True, initialize=1e-4, doc='Smooth max parameter (pressure under)') @classmethod def define_metadata(cls, obj): obj.add_properties({ 'temperature_crit': { 'method': None, 'units': 'K' }, 'pressure_crit': { 'method': None, 'units': 'Pa' }, 'dens_mass_crit': { 'method': None, 'units': 'kg/m^3' }, 'gas_const': { 'method': None, 'units': 'J/mol.K' }, 'mw': { 'method': None, 'units': 'kg/mol' }, 'temperature_sat': { 'method': 'None', 'units': 'K' }, 'flow_mol': { 'method': None, 'units': 'mol/s' }, 'flow_mass': { 'method': None, 'units': 'kg/s' }, 'temperature': { 'method': None, 'units': 'K' }, 'pressure': { 'method': None, 'units': 'Pa' }, 'vapor_frac': { 'method': None, 'units': None }, 'dens_mass_phase': { 'method': None, 'units': 'kg/m^3' }, 'temperature_red': { 'method': None, 'units': None }, 'pressure_sat': { 'method': None, 'units': 'kPa' }, 'energy_internal_mol_phase': { 'method': None, 'units': 'J/mol' }, 'enth_mol_phase': { 'method': None, 'units': 'J/mol' }, 'entr_mol_phase': { 'method': None, 'units': 'J/mol.K' }, 'cp_mol_phase': { 'method': None, 'units': 'J/mol.K' }, 'cv_mol_phase': { 'method': None, 'units': 'J/mol.K' }, 'speed_sound_phase': { 'method': None, 'units': 'm/s' }, 'dens_mol_phase': { 'method': None, 'units': 'mol/m^3' }, 'therm_cond_phase': { 'method': None, 'units': 'W/m.K' }, 'visc_d_phase': { 'method': None, 'units': 'Pa.s' }, 'visc_k_phase': { 'method': None, 'units': 'm^2/s' }, 'phase_frac': { 'method': None, 'units': None }, 'flow_mol_comp': { 'method': None, 'units': 'mol/s' }, 'energy_internal_mol': { 'method': None, 'units': 'J/mol' }, 'enth_mol': { 'method': None, 'units': 'J/mol' }, 'entr_mol': { 'method': None, 'units': 'J/mol.K' }, 'cp_mol': { 'method': None, 'units': 'J/mol.K' }, 'cv_mol': { 'method': None, 'units': 'J/mol.K' }, 'heat_capacity_ratio': { 'method': None, 'units': None }, 'dens_mass': { 'method': None, 'units': 'kg/m^3' }, 'dens_mol': { 'method': None, 'units': 'mol/m^3' }, 'dh_vap_mol': { 'method': None, 'units': 'J/mol' } }) obj.add_default_units({ 'time': 's', 'length': 'm', 'mass': 'kg', 'amount': 'mol', 'temperature': 'K', 'energy': 'J', 'holdup': 'mol' })