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())
class ReactionBlockData(ReactionBlockDataBase): CONFIG = ConfigBlock(implicit=True) def build(self): super(ReactionBlockData, self).build() self.reaction_rate = Var(["r1", "r2"], units=units.mol/units.m**3/units.s) self.dh_rxn = {"r1": 10*units.J/units.mol, "r2": 20*units.J/units.mol, "e1": 30*units.J/units.mol, "e2": 40*units.J/units.mol} def model_check(self): self.check = True def get_reaction_rate_basis(b): if b.config.parameters.basis_switch == 1: return MaterialFlowBasis.molar elif b.config.parameters.basis_switch == 2: return MaterialFlowBasis.mass else: return MaterialFlowBasis.other
class NoPressureStateBlockData(StateBlockData): CONFIG = ConfigBlock(implicit=True) def build(self): super(NoPressureStateBlockData, self).build() self.flow_vol = Var(initialize=20) self.flow_mol_phase_comp = Var(self._params.phase_list, self._params.component_list, initialize=2) self.temperature = Var(initialize=300) self.test_var = Var(initialize=1) def get_material_flow_terms(b, p, j): return b.test_var def get_enthalpy_flow_terms(b, p): return b.test_var def default_material_balance_type(self): return MaterialBalanceType.componentPhase def default_energy_balance_type(self): return EnergyBalanceType.enthalpyTotal
class StateTestBlockData(StateBlockData): CONFIG = ConfigBlock(implicit=True) def build(self): super(StateTestBlockData, self).build() self.phase_list = Set(initialize=["p1", "p2"]) self.component_list = Set(initialize=["c1", "c2"]) self.phase_equilibrium_idx = Set(initialize=["e1", "e2"]) self.phase_equilibrium_list = \ {"e1": ["c1", ("p1", "p2")], "e2": ["c2", ("p1", "p2")]} self.pressure = Var(initialize=1e5) self.flow_mol_phase_comp = Var(self.phase_list, self.component_list, initialize=1) self.enth_mol_phase = Var(self.phase_list, initialize=2) self.enth_mol = Var(initialize=2) # total molar enthalpy (both phases) self.temperature = Var(initialize=5) def get_material_flow_terms(b, p, j): return b.flow_mol_phase_comp[p, j] def get_enthalpy_flow_terms(b, p): return b.enth_mol_phase[p] def define_state_vars(self): return { "component_flow": self.flow_mol_phase_comp, "enthalpy": self.enth_mol_phase, "pressure": self.pressure } def model_check(self): self.check = True
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 GenericReactionPackageError(PropertyPackageError): # Error message for when a property is called for but no option provided def __init__(self, block, prop): self.prop = prop self.block = block def __str__(self): return f"Generic Reaction Package instance {self.block} called for " \ f"{self.prop}, but was not provided with a method " \ f"for this property. Please add a method for this property " \ f"in the reaction parameter configuration." rxn_config = ConfigBlock() 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,
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 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 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 BigM_Transformation(Transformation): """Relax disjunctive model using big-M terms. Relaxes a disjunctive model into an algebraic model by adding Big-M terms to all disjunctive constraints. This transformation accepts the following keyword arguments: bigM: A user-specified value (or dict) of M values to use (see below) targets: the targets to transform [default: the instance] M values are determined as follows: 1) if the constraint appears in the bigM argument dict 2) if the constraint parent_component appears in the bigM argument dict 3) if any block which is an ancestor to the constraint appears in the bigM argument dict 3) if 'None' is in the bigM argument dict 4) if the constraint or the constraint parent_component appear in a BigM Suffix attached to any parent_block() beginning with the constraint's parent_block and moving up to the root model. 5) if None appears in a BigM Suffix attached to any parent_block() between the constraint and the root model. 6) if the constraint is linear, estimate M using the variable bounds M values may be a single value or a 2-tuple specifying the M for the lower bound and the upper bound of the constraint body. Specifying "bigM=N" is automatically mapped to "bigM={None: N}". The transformation will create a new Block with a unique name beginning "_pyomo_gdp_bigm_relaxation". That Block will contain an indexed Block named "relaxedDisjuncts", which will hold the relaxed disjuncts. This block is indexed by an integer indicating the order in which the disjuncts were relaxed. Each block has a dictionary "_constraintMap": 'srcConstraints': ComponentMap(<transformed constraint>: <src constraint>) 'transformedConstraints': ComponentMap(<src constraint>: <transformed constraint>) All transformed Disjuncts will have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding OR or XOR constraint. """ CONFIG = ConfigBlock("gdp.bigm") CONFIG.declare('targets', ConfigValue( default=None, domain=target_list, description="target or list of targets that will be relaxed", doc=""" This specifies the list of components to relax. If None (default), the entire model is transformed. Note that if the transformation is done out of place, the list of targets should be attached to the model before it is cloned, and the list will specify the targets on the cloned instance.""" )) CONFIG.declare('bigM', ConfigValue( default=None, domain=_to_dict, description="Big-M value used for constraint relaxation", doc=""" A user-specified value, dict, or ComponentMap of M values that override M-values found through model Suffixes or that would otherwise be calculated using variable domains.""" )) def __init__(self): """Initialize transformation object.""" super(BigM_Transformation, self).__init__() self.handlers = { Constraint: self._transform_constraint, Var: False, # Note that if a Var appears on a Disjunct, we # still treat its bounds as global. If the # intent is for its bounds to be on the # disjunct, it should be declared with no bounds # and the bounds should be set in constraints on # the Disjunct. Connector: False, Expression: False, Suffix: False, Param: False, Set: False, RangeSet: False, Disjunction: self._warn_for_active_disjunction, Disjunct: self._warn_for_active_disjunct, Block: self._transform_block_on_disjunct, } def _get_bigm_suffix_list(self, block): # Note that you can only specify suffixes on BlockData objects or # SimpleBlocks. Though it is possible at this point to stick them # on whatever components you want, we won't pick them up. suffix_list = [] while block is not None: bigm = block.component('BigM') if type(bigm) is Suffix: suffix_list.append(bigm) block = block.parent_block() return suffix_list def _get_bigm_arg_list(self, bigm_args, block): # Gather what we know about blocks from args exactly once. We'll still # check for constraints in the moment, but if that fails, we've # preprocessed the time-consuming part of traversing up the tree. arg_list = [] if bigm_args is None: return arg_list while block is not None: if block in bigm_args: arg_list.append({block: bigm_args[block]}) block = block.parent_block() return arg_list def _apply_to(self, instance, **kwds): assert not NAME_BUFFER self.used_args = ComponentMap() # If everything was sure to go well, # this could be a dictionary. But if # someone messes up and gives us a Var # as a key in bigMargs, I need the error # not to be when I try to put it into # this map! try: self._apply_to_impl(instance, **kwds) finally: # Clear the global name buffer now that we are done NAME_BUFFER.clear() # same for our bookkeeping about what we used from bigM arg dict self.used_args.clear() def _apply_to_impl(self, instance, **kwds): config = self.CONFIG(kwds.pop('options', {})) # We will let args override suffixes and estimate as a last # resort. More specific args/suffixes override ones anywhere in # the tree. Suffixes lower down in the tree override ones higher # up. if 'default_bigM' in kwds: logger.warn("DEPRECATED: the 'default_bigM=' argument has been " "replaced by 'bigM='") config.bigM = kwds.pop('default_bigM') config.set_value(kwds) bigM = config.bigM targets = config.targets if targets is None: targets = (instance, ) _HACK_transform_whole_instance = True else: _HACK_transform_whole_instance = False # We need to check that all the targets are in fact on instance. As we # do this, we will use the set below to cache components we know to be # in the tree rooted at instance. knownBlocks = {} for t in targets: # check that t is in fact a child of instance if not is_child_of(parent=instance, child=t, knownBlocks=knownBlocks): raise GDP_Error("Target %s is not a component on instance %s!" % (t.name, instance.name)) elif t.ctype is Disjunction: if t.parent_component() is t: self._transform_disjunction(t, bigM) else: self._transform_disjunctionData( t, bigM, t.index()) elif t.ctype in (Block, Disjunct): if t.parent_component() is t: self._transform_block(t, bigM) else: self._transform_blockData(t, bigM) else: raise GDP_Error( "Target %s was not a Block, Disjunct, or Disjunction. " "It was of type %s and can't be transformed." % (t.name, type(t))) # issue warnings about anything that was in the bigM args dict that we # didn't use if bigM is not None: unused_args = ComponentSet(bigM.keys()) - \ ComponentSet(self.used_args.keys()) if len(unused_args) > 0: warning_msg = ("Unused arguments in the bigM map! " "These arguments were not used by the " "transformation:\n") for component in unused_args: if hasattr(component, 'name'): warning_msg += "\t%s\n" % component.name else: warning_msg += "\t%s\n" % component logger.warn(warning_msg) # HACK for backwards compatibility with the older GDP transformations # # Until the writers are updated to find variables on things # other than active blocks, we need to reclassify the Disjuncts # as Blocks after transformation so that the writer will pick up # all the variables that it needs (in this case, indicator_vars). if _HACK_transform_whole_instance: HACK_GDP_Disjunct_Reclassifier().apply_to(instance) def _add_transformation_block(self, instance): # make a transformation block on instance to put transformed disjuncts # on transBlockName = unique_component_name( instance, '_pyomo_gdp_bigm_relaxation') transBlock = Block() instance.add_component(transBlockName, transBlock) transBlock.relaxedDisjuncts = Block(Any) transBlock.lbub = Set(initialize=['lb', 'ub']) return transBlock def _transform_block(self, obj, bigM): for i in sorted(iterkeys(obj)): self._transform_blockData(obj[i], bigM) def _transform_blockData(self, obj, bigM): # Transform every (active) disjunction in the block for disjunction in obj.component_objects( Disjunction, active=True, sort=SortComponents.deterministic, descend_into=(Block, Disjunct), descent_order=TraversalStrategy.PostfixDFS): self._transform_disjunction(disjunction, bigM) def _add_xor_constraint(self, disjunction, transBlock): # Put the disjunction constraint on the transformation block and # determine whether it is an OR or XOR constraint. # We never do this for just a DisjunctionData because we need to know # about the index set of its parent component (so that we can make the # index of this constraint match). So if we called this on a # DisjunctionData, we did something wrong. assert isinstance(disjunction, Disjunction) # first check if the constraint already exists if not disjunction._algebraic_constraint is None: return disjunction._algebraic_constraint() # add the XOR (or OR) constraints to parent block (with unique name) # It's indexed if this is an IndexedDisjunction, not otherwise orC = Constraint(disjunction.index_set()) if \ disjunction.is_indexed() else Constraint() # The name used to indicate if there were OR or XOR disjunctions, # however now that Disjunctions are allowed to mix the state we # can no longer make that distinction in the name. # nm = '_xor' if xor else '_or' nm = '_xor' orCname = unique_component_name( transBlock, disjunction.getname( fully_qualified=True, name_buffer=NAME_BUFFER) + nm) transBlock.add_component(orCname, orC) disjunction._algebraic_constraint = weakref_ref(orC) return orC def _transform_disjunction(self, obj, bigM): if not obj.active: return # if this is an IndexedDisjunction we have seen in a prior call to the # transformation, we already have a transformation block for it. We'll # use that. if obj._algebraic_constraint is not None: transBlock = obj._algebraic_constraint().parent_block() else: transBlock = self._add_transformation_block(obj.parent_block()) # If this is an IndexedDisjunction, we have to create the XOR constraint # here because we want its index to match the disjunction. In any case, # we might as well. xorConstraint = self._add_xor_constraint(obj, transBlock) # relax each of the disjunctionDatas for i in sorted(iterkeys(obj)): self._transform_disjunctionData(obj[i], bigM, i, xorConstraint, transBlock) # deactivate so the writers don't scream obj.deactivate() def _transform_disjunctionData(self, obj, bigM, index, xorConstraint=None, transBlock=None): if not obj.active: return # Do not process a deactivated disjunction # We won't have these arguments if this got called straight from # targets. But else, we created them earlier, and have just been passing # them through. if transBlock is None: # It's possible that we have already created a transformation block # for another disjunctionData from this same container. If that's # the case, let's use the same transformation block. (Else it will # be really confusing that the XOR constraint goes to that old block # but we create a new one here.) if not obj.parent_component()._algebraic_constraint is None: transBlock = obj.parent_component()._algebraic_constraint().\ parent_block() else: transBlock = self._add_transformation_block(obj.parent_block()) if xorConstraint is None: xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) xor = obj.xor or_expr = 0 # Just because it's unlikely this is what someone meant to do... if len(obj.disjuncts) == 0: raise GDP_Error("Disjunction %s is empty. This is " "likely indicative of a modeling error." % obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)) for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var # make suffix list. (We don't need it until we are # transforming constraints, but it gets created at the # disjunct level, so more efficient to make it here and # pass it down.) suffix_list = self._get_bigm_suffix_list(disjunct) arg_list = self._get_bigm_arg_list(bigM, disjunct) # relax the disjunct self._transform_disjunct(disjunct, transBlock, bigM, arg_list, suffix_list) # add or (or xor) constraint if xor: xorConstraint[index] = or_expr == 1 else: xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) # and deactivate for the writers obj.deactivate() def _transform_disjunct(self, obj, transBlock, bigM, arg_list, suffix_list): # deactivated -> either we've already transformed or user deactivated if not obj.active: if obj.indicator_var.is_fixed(): if value(obj.indicator_var) == 0: # The user cleanly deactivated the disjunct: there # is nothing for us to do here. return else: raise GDP_Error( "The disjunct %s is deactivated, but the " "indicator_var is fixed to %s. This makes no sense." % ( obj.name, value(obj.indicator_var) )) if obj._transformation_block is None: raise GDP_Error( "The disjunct %s is deactivated, but the " "indicator_var is not fixed and the disjunct does not " "appear to have been relaxed. This makes no sense. " "(If the intent is to deactivate the disjunct, fix its " "indicator_var to 0.)" % ( obj.name, )) if not obj._transformation_block is None: # we've transformed it, which means this is the second time it's # appearing in a Disjunction raise GDP_Error( "The disjunct %s has been transformed, but a disjunction " "it appears in has not. Putting the same disjunct in " "multiple disjunctions is not supported." % obj.name) # add reference to original disjunct on transformation block relaxedDisjuncts = transBlock.relaxedDisjuncts relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] # we will keep a map of constraints (hashable, ha!) to a tuple to # indicate where their m value came from, either (arg dict, key) if it # came from args, (Suffix, key) if it came from Suffixes, or (M_lower, # M_upper) if we calcualted it ourselves. I am keeping it here because I # want it to move with the disjunct transformation blocks in the case of # nested constraints, to make it easier to query. relaxationBlock.bigm_src = {} obj._transformation_block = weakref_ref(relaxationBlock) relaxationBlock._srcDisjunct = weakref_ref(obj) # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for CHull, as it uses the component_objects / # component_data_objects generators. For BigM, that is OK, # because we never use those generators with active=True. I am # only noting it here for the future when someone (me?) is # comparing the two relaxations. # # Transform each component within this disjunct self._transform_block_components(obj, obj, bigM, arg_list, suffix_list) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_block_components(self, block, disjunct, bigM, arg_list, suffix_list): # We first need to find any transformed disjunctions that might be here # because we need to move their transformation blocks up onto the parent # block before we transform anything else on this block destinationBlock = disjunct._transformation_block().parent_block() for obj in block.component_data_objects( Disjunction, sort=SortComponents.deterministic, descend_into=(Block)): if obj.algebraic_constraint is None: # This could be bad if it's active since that means its # untransformed, but we'll wait to yell until the next loop continue # get this disjunction's relaxation block. transBlock = obj.algebraic_constraint().parent_block() # move transBlock up to parent component self._transfer_transBlock_data(transBlock, destinationBlock) # we leave the transformation block because it still has the XOR # constraints, which we want to be on the parent disjunct. # Now look through the component map of block and transform everything # we have a handler for. Yell if we don't know how to handle it. (Note # that because we only iterate through active components, this means # non-ActiveComponent types cannot have handlers.) for obj in block.component_objects(active=True, descend_into=False): handler = self.handlers.get(obj.ctype, None) if not handler: if handler is None: raise GDP_Error( "No BigM transformation handler registered " "for modeling components of type %s. If your " "disjuncts contain non-GDP Pyomo components that " "require transformation, please transform them first." % obj.ctype) continue # obj is what we are transforming, we pass disjunct # through so that we will have access to the indicator # variables down the line. handler(obj, disjunct, bigM, arg_list, suffix_list) def _transfer_transBlock_data(self, fromBlock, toBlock): # We know that we have a list of transformed disjuncts on both. We need # to move those over. We know the XOR constraints are on the block, and # we need to leave those on the disjunct. disjunctList = toBlock.relaxedDisjuncts for idx, disjunctBlock in iteritems(fromBlock.relaxedDisjuncts): newblock = disjunctList[len(disjunctList)] newblock.transfer_attributes_from(disjunctBlock) # update the mappings original = disjunctBlock._srcDisjunct() original._transformation_block = weakref_ref(newblock) newblock._srcDisjunct = weakref_ref(original) # we delete this container because we just moved everything out del fromBlock.relaxedDisjuncts # Note that we could handle other components here if we ever needed # to, but we control what is on the transformation block and # currently everything is on the blocks that we just moved... def _warn_for_active_disjunction(self, disjunction, disjunct, bigMargs, arg_list, suffix_list): # this should only have gotten called if the disjunction is active assert disjunction.active problemdisj = disjunction if disjunction.is_indexed(): for i in sorted(iterkeys(disjunction)): if disjunction[i].active: # a _DisjunctionData is active, we will yell about # it specifically. problemdisj = disjunction[i] break parentblock = problemdisj.parent_block() # the disjunction should only have been active if it wasn't transformed assert problemdisj.algebraic_constraint is None _probDisjName = problemdisj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) raise GDP_Error("Found untransformed disjunction %s in disjunct %s! " "The disjunction must be transformed before the " "disjunct. If you are using targets, put the " "disjunction before the disjunct in the list." % (_probDisjName, disjunct.name)) def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs, arg_list, suffix_list): assert innerdisjunct.active problemdisj = innerdisjunct if innerdisjunct.is_indexed(): for i in sorted(iterkeys(innerdisjunct)): if innerdisjunct[i].active: # This is shouldn't be true, we will complain about it. problemdisj = innerdisjunct[i] break raise GDP_Error("Found active disjunct {0} in disjunct {1}! " "Either {0} " "is not in a disjunction or the disjunction it is in " "has not been transformed. " "{0} needs to be deactivated " "or its disjunction transformed before {1} can be " "transformed.".format(problemdisj.name, outerdisjunct.name)) def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list, suffix_list): # We look through everything on the component map of the block # and transform it just as we would if it was on the disjunct # directly. (We are passing the disjunct through so that when # we find constraints, _xform_constraint will have access to # the correct indicator variable.) for i in sorted(iterkeys(block)): self._transform_block_components( block[i], disjunct, bigMargs, arg_list, suffix_list) def _get_constraint_map_dict(self, transBlock): if not hasattr(transBlock, "_constraintMap"): transBlock._constraintMap = { 'srcConstraints': ComponentMap(), 'transformedConstraints': ComponentMap()} return transBlock._constraintMap def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, suffix_list): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src constraintMap = self._get_constraint_map_dict(transBlock) disjunctionRelaxationBlock = transBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER) name = unique_component_name(transBlock, cons_name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), disjunctionRelaxationBlock.lbub) # HACK: We get burned by #191 here... When #1319 is merged we # can revist this and I think stop catching the AttributeError. except (TypeError, AttributeError): # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(disjunctionRelaxationBlock.lbub) transBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint constraintMap['srcConstraints'][newConstraint] = obj constraintMap['transformedConstraints'][obj] = newConstraint for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue # first, we see if an M value was specified in the arguments. # (This returns None if not) M = self._get_M_from_args(c, bigMargs, arg_list, bigm_src) if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "from the BigM argument is %s." % (cons_name, str(M))) # if we didn't get something from args, try suffixes: if M is None: M = self._get_M_from_suffixes(c, suffix_list, bigm_src) if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "after checking suffixes is %s." % (cons_name, str(M))) if not isinstance(M, (tuple, list)): if M is None: M = (None, None) else: try: M = (-M, M) except: logger.error("Error converting scalar M-value %s " "to (-M,M). Is %s not a numeric type?" % (M, type(M))) raise if len(M) != 2: raise GDP_Error("Big-M %s for constraint %s is not of " "length two. " "Expected either a single value or " "tuple or list of length two for M." % (str(M), name)) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) bigm_src[c] = M if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) bigm_src[c] = M if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "after estimating (if needed) is %s." % (cons_name, str(M))) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb',) i_ub = i + ('ub',) elif obj.is_indexed(): i_lb = (i, 'lb',) i_ub = (i, 'ub',) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c. body - M_expr) if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper) # deactivate because we relaxed c.deactivate() def _get_M_from_args(self, constraint, bigMargs, arg_list, bigm_src): # check args: we first look in the keys for constraint and # constraintdata. In the absence of those, we traverse up the blocks, # and as a last resort check for a value for None if bigMargs is None: return None # check for the constraint itself and its container parent = constraint.parent_component() if constraint in bigMargs: m = bigMargs[constraint] self.used_args[constraint] = m bigm_src[constraint] = (bigMargs, constraint) return m elif parent in bigMargs: m = bigMargs[parent] self.used_args[parent] = m bigm_src[constraint] = (bigMargs, parent) return m # use the precomputed traversal up the blocks for arg in arg_list: for block, val in iteritems(arg): self.used_args[block] = val bigm_src[constraint] = (bigMargs, block) return val # last check for value for None! if None in bigMargs: m = bigMargs[None] self.used_args[None] = m bigm_src[constraint] = (bigMargs, None) return m return None def _get_M_from_suffixes(self, constraint, suffix_list, bigm_src): M = None # first we check if the constraint or its parent is a key in any of the # suffix lists for bigm in suffix_list: if constraint in bigm: M = bigm[constraint] bigm_src[constraint] = (bigm, constraint) break # if c is indexed, check for the parent component if constraint.parent_component() in bigm: M = bigm[constraint.parent_component()] bigm_src[constraint] = (bigm, constraint.parent_component()) break # if we didn't get an M that way, traverse upwards through the blocks # and see if None has a value on any of them. if M is None: for bigm in suffix_list: if None in bigm: M = bigm[None] bigm_src[constraint] = (bigm, None) break return M def _estimate_M(self, expr, name): # Calculate a best guess at M repn = generate_standard_repn(expr, quadratic=False) M = [0, 0] if not repn.is_nonlinear(): if repn.constant is not None: for i in (0, 1): if M[i] is not None: M[i] += repn.constant for i, coef in enumerate(repn.linear_coefs or []): var = repn.linear_vars[i] bounds = (value(var.lb), value(var.ub)) for i in (0, 1): # reverse the bounds if the coefficient is negative if coef > 0: j = i else: j = 1 - i if bounds[i] is not None: M[j] += value(bounds[i]) * coef else: raise GDP_Error( "Cannot estimate M for " "expressions with unbounded variables." "\n\t(found unbounded var %s while processing " "constraint %s)" % (var.name, name)) else: # expression is nonlinear. Try using `contrib.fbbt` to estimate. expr_lb, expr_ub = compute_bounds_on_expr(expr) if expr_lb is None or expr_ub is None: raise GDP_Error("Cannot estimate M for unbounded nonlinear " "expressions.\n\t(found while processing " "constraint %s)" % name) else: M = (expr_lb, expr_ub) return tuple(M) # These are all functions to retrieve transformed components from original # ones and vice versa. def get_src_disjunct(self, transBlock): """Return the Disjunct object whose transformed components are on transBlock. Parameters ---------- transBlock: _BlockData which is in the relaxedDisjuncts IndexedBlock on a transformation block. """ try: return transBlock._srcDisjunct() except: raise GDP_Error("Block %s doesn't appear to be a transformation " "block for a disjunct. No source disjunct found." "\n\t(original error: %s)" % (transBlock.name, sys.exc_info()[1])) def get_src_constraint(self, transformedConstraint): """Return the original Constraint whose transformed counterpart is transformedConstraint Parameters ---------- transformedConstraint: Constraint, which must be a component on one of the BlockDatas in the relaxedDisjuncts Block of a transformation block """ transBlock = transformedConstraint.parent_block() # This should be our block, so if it's not, the user messed up and gave # us the wrong thing. If they happen to also have a _constraintMap then # the world is really against us. if not hasattr(transBlock, "_constraintMap"): raise GDP_Error("Constraint %s is not a transformed constraint" % transformedConstraint.name) # if something goes wrong here, it's a bug in the mappings. return transBlock._constraintMap['srcConstraints'][transformedConstraint] def _find_parent_disjunct(self, constraint): # traverse up until we find the disjunct this constraint lives on parent_disjunct = constraint.parent_block() while not isinstance(parent_disjunct, _DisjunctData): if parent_disjunct is None: raise GDP_Error( "Constraint %s is not on a disjunct and so was not " "transformed" % constraint.name) parent_disjunct = parent_disjunct.parent_block() return parent_disjunct def _get_constraint_transBlock(self, constraint): parent_disjunct = self._find_parent_disjunct(constraint) # we know from _find_parent_disjunct that parent_disjunct is a Disjunct, # so the below is OK transBlock = parent_disjunct._transformation_block if transBlock is None: raise GDP_Error("Constraint %s is on a disjunct which has not been " "transformed" % constraint.name) # if it's not None, it's the weakref we wanted. transBlock = transBlock() return transBlock def get_transformed_constraint(self, srcConstraint): """Return the transformed version of srcConstraint Parameters ---------- srcConstraint: Constraint, which must be in the subtree of a transformed Disjunct """ transBlock = self._get_constraint_transBlock(srcConstraint) if hasattr(transBlock, "_constraintMap") and transBlock._constraintMap[ 'transformedConstraints'].get(srcConstraint): return transBlock._constraintMap['transformedConstraints'][ srcConstraint] raise GDP_Error("Constraint %s has not been transformed." % srcConstraint.name) def get_src_disjunction(self, xor_constraint): """Return the Disjunction corresponding to xor_constraint Parameters ---------- xor_constraint: Constraint, which must be the logical constraint (located on the transformation block) of some Disjunction """ # NOTE: This is indeed a linear search through the Disjunctions on the # model. I am leaving it this way on the assumption that asking XOR # constraints for their Disjunction is not going to be a common # question. If we ever need efficiency then we should store a reverse # map from the XOR constraint to the Disjunction on the transformation # block while we do the transformation. And then this method could query # that map. m = xor_constraint.model() for disjunction in m.component_data_objects(Disjunction): if disjunction._algebraic_constraint: if disjunction._algebraic_constraint() is xor_constraint: return disjunction raise GDP_Error("It appears that %s is not an XOR or OR constraint " "resulting from transforming a Disjunction." % xor_constraint.name) def get_m_value_src(self, constraint): """Return a tuple indicating how the M value used to transform constraint was specified. (In particular, this can be used to verify which BigM Suffixes were actually necessary to the transformation.) If the M value came from an arg, returns (bigm_arg_dict, key), where bigm_arg_dict is the dictionary itself and key is the key in that dictionary which gave us the M value. If the M value came from a Suffix, returns (suffix, key) where suffix is the BigM suffix used and key is the key in that Suffix. If the transformation calculated the value, returns (M_lower, M_upper), where M_lower is the float we calculated for the lower bound constraint and M_upper is the value calculated for the upper bound constraint. Parameters ---------- constraint: Constraint, which must be in the subtree of a transformed Disjunct """ transBlock = self._get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) return transBlock.bigm_src[constraint]
def _make_heater_config_block(config): """ Declare configuration options for HeaterData block. """ config.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentPhase, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) config.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) config.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) config.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) config.declare( "has_pressure_change", ConfigValue( default=False, domain=In([True, False]), description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""")) config.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) config.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}"""))
class PressureChangerData(UnitModelBlockData): """ Standard Compressor/Expander Unit Model Class """ CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare("material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) CONFIG.declare("momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare("has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Phase equilibrium construction flag", doc="""Indicates whether terms for phase equilibrium should be constructed, **default** = False. **Valid values:** { **True** - include phase equilibrium terms **False** - exclude phase equilibrium terms.}""")) CONFIG.declare("compressor", ConfigValue( default=True, domain=In([True, False]), description="Compressor flag", doc="""Indicates whether this unit should be considered a compressor (True (default), pressure increase) or an expander (False, pressure decrease).""")) CONFIG.declare("thermodynamic_assumption", ConfigValue( default=ThermodynamicAssumption.isothermal, domain=In(ThermodynamicAssumption), description="Thermodynamic assumption to use", doc="""Flag to set the thermodynamic assumption to use for the unit. - ThermodynamicAssumption.isothermal (default) - ThermodynamicAssumption.isentropic - ThermodynamicAssumption.pump - ThermodynamicAssumption.adiabatic""")) CONFIG.declare("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """ Args: None Returns: None """ # Call UnitModel.build super(PressureChangerData, self).build() # Add a control volume to the unit including setting up dynamics. self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) # Add geomerty variables to control volume if self.config.has_holdup: self.control_volume.add_geometry() # Add inlet and outlet state blocks to control volume self.control_volume.add_state_blocks( has_phase_equilibrium=self.config.has_phase_equilibrium) # Add mass balance # Set has_equilibrium is False for now # TO DO; set has_equilibrium to True self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=self.config.has_phase_equilibrium) # Add energy balance self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_work_transfer=True) # add momentum balance self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # Add Ports self.add_inlet_port() self.add_outlet_port() # Set Unit Geometry and holdup Volume if self.config.has_holdup is True: add_object_reference(self, "volume", self.control_volume.volume) # Construct performance equations # Set references to balance terms at unit level # Add Work transfer variable 'work' as necessary add_object_reference(self, "work_mechanical", self.control_volume.work) # Add Momentum balance variable 'deltaP' as necessary add_object_reference(self, "deltaP", self.control_volume.deltaP) # Set reference to scaling factor for pressure in control volume add_object_reference(self, "sfp", self.control_volume.scaling_factor_pressure) # Set reference to scaling factor for energy in control volume add_object_reference(self, "sfe", self.control_volume.scaling_factor_energy) # Performance Variables self.ratioP = Var(self.flowsheet().config.time, initialize=1.0, doc="Pressure Ratio") # Pressure Ratio @self.Constraint(self.flowsheet().config.time, doc="Pressure ratio constraint") def ratioP_calculation(b, t): return (self.sfp*b.ratioP[t] * b.control_volume.properties_in[t].pressure == self.sfp*b.control_volume.properties_out[t].pressure) # Construct equations for thermodynamic assumption if self.config.thermodynamic_assumption == \ ThermodynamicAssumption.isothermal: self.add_isothermal() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.isentropic: self.add_isentropic() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.pump: self.add_pump() elif self.config.thermodynamic_assumption == \ ThermodynamicAssumption.adiabatic: self.add_adiabatic() def add_pump(self): """ Add constraints for the incompressible fluid assumption Args: None Returns: None """ self.work_fluid = Var( self.flowsheet().config.time, initialize=1.0, doc="Work required to increase the pressure of the liquid") self.efficiency_pump = Var( self.flowsheet().config.time, initialize=1.0, doc="Pump efficiency") @self.Constraint(self.flowsheet().config.time, doc="Pump fluid work constraint") def fluid_work_calculation(b, t): return b.work_fluid[t] == ( (b.control_volume.properties_out[t].pressure - b.control_volume.properties_in[t].pressure) * b.control_volume.properties_out[t].flow_vol) # Actual work @self.Constraint(self.flowsheet().config.time, doc="Actual mechanical work calculation") def actual_work(b, t): if b.config.compressor: return b.sfe*b.work_fluid[t] == b.sfe*( b.work_mechanical[t]*b.efficiency_pump[t]) else: return b.sfe*b.work_mechanical[t] == b.sfe*( b.work_fluid[t]*b.efficiency_pump[t]) def add_isothermal(self): """ Add constraints for isothermal assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint(self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet temperature") def isothermal(b, t): return b.control_volume.properties_in[t].temperature == \ b.control_volume.properties_out[t].temperature def add_adiabatic(self): """ Add constraints for adiabatic assumption. Args: None Returns: None """ # Isothermal constraint @self.Constraint(self.flowsheet().config.time, doc="For isothermal condition: Equate inlet and " "outlet enthalpy") def adiabatic(b, t): return b.control_volume.properties_in[t].enth_mol == \ b.control_volume.properties_out[t].enth_mol def add_isentropic(self): """ Add constraints for isentropic assumption. Args: None Returns: None """ # Get indexing sets from control volume # Add isentropic variables self.efficiency_isentropic = Var(self.flowsheet().config.time, initialize=0.8, doc="Efficiency with respect to an " "isentropic process [-]") self.work_isentropic = Var(self.flowsheet().config.time, initialize=0.0, doc="Work input to unit if isentropic " "process [-]") # Build isentropic state block tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = False self.properties_isentropic = ( self.config.property_package.state_block_class( self.flowsheet().config.time, doc="isentropic properties at outlet", default=tmp_dict)) # Connect isentropic state block properties @self.Constraint(self.flowsheet().config.time, doc="Pressure for isentropic calculations") def isentropic_pressure(b, t): return b.sfp*b.properties_isentropic[t].pressure == \ b.sfp*b.control_volume.properties_out[t].pressure # This assumes isentropic composition is the same as outlet mb_type = self.config.material_balance_type if mb_type == MaterialBalanceType.useDefault: mb_type = \ self.control_volume._get_representative_property_block() \ .default_material_balance_type() if mb_type == \ MaterialBalanceType.componentPhase: @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Material flows for isentropic properties") def isentropic_material(b, t, p, j): return ( b.properties_isentropic[t].get_material_flow_terms(p, j) == b.control_volume.properties_out[t] .get_material_flow_terms(p, j)) elif mb_type == \ MaterialBalanceType.componentTotal: @self.Constraint(self.flowsheet().config.time, self.config.property_package.component_list, doc="Material flows for isentropic properties") def isentropic_material(b, t, j): return (sum( b.properties_isentropic[t].get_material_flow_terms(p, j) for p in self.config.property_package.phase_list) == sum(b.control_volume.properties_out[t] .get_material_flow_terms(p, j) for p in self.config.property_package.phase_list)) elif mb_type == \ MaterialBalanceType.total: @self.Constraint(self.flowsheet().config.time, doc="Material flows for isentropic properties") def isentropic_material(b, t, p, j): return (sum(sum( b.properties_isentropic[t].get_material_flow_terms(p, j) for j in self.config.property_package.component_list) for p in self.config.property_package.phase_list) == sum(sum(b.control_volume.properties_out[t] .get_material_flow_terms(p, j) for j in self.config.property_package.component_list) for p in self.config.property_package.phase_list)) elif mb_type == \ MaterialBalanceType.elementTotal: raise BalanceTypeNotSupportedError( "{} PressureChanger does not support element balances." .format(self.name)) elif mb_type == \ MaterialBalanceType.none: raise BalanceTypeNotSupportedError( "{} PressureChanger does not support material_balance_type" " = none." .format(self.name)) else: raise BurntToast( "{} PressureChanger received an unexpected argument for " "material_balance_type. This should never happen. Please " "contact the IDAES developers with this bug." .format(self.name)) # This assumes isentropic entropy is the same as inlet @self.Constraint(self.flowsheet().config.time, doc="Isentropic assumption") def isentropic(b, t): return b.properties_isentropic[t].entr_mol == \ b.control_volume.properties_in[t].entr_mol # Isentropic work @self.Constraint(self.flowsheet().config.time, doc="Calculate work of isentropic process") def isentropic_energy_balance(b, t): return b.sfe*b.work_isentropic[t] == b.sfe*( sum(b.properties_isentropic[t].get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list) - sum(b.control_volume.properties_in[t] .get_enthalpy_flow_terms(p) for p in b.config.property_package.phase_list)) # Actual work @self.Constraint(self.flowsheet().config.time, doc="Actual mechanical work calculation") def actual_work(b, t): if b.config.compressor: return b.sfe*b.work_isentropic[t] == b.sfe*( b.work_mechanical[t]*b.efficiency_isentropic[t]) else: return b.sfe*b.work_mechanical[t] == b.sfe*( b.work_isentropic[t]*b.efficiency_isentropic[t]) def model_check(blk): """ Check that pressure change matches with compressor argument (i.e. if compressor = True, pressure should increase or work should be positive) Args: None Returns: None """ if blk.config.compressor: # Compressor # Check that pressure does not decrease if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with negative deltaP.' .format(blk.name)) if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with ratioP less than 1.' .format(blk.name)) if any(blk.control_volume.properties_out[t].pressure.fixed and (value(blk.control_volume.properties_in[t].pressure) > value(blk.control_volume.properties_out[t].pressure)) for t in blk.flowsheet().config.time): logger.warning('{} Compressor set with pressure decrease.' .format(blk.name)) # Check that work is not negative if any(blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) < 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Compressor maybe set with negative work.' .format(blk.name)) else: # Expander # Check that pressure does not increase if any(blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with positive deltaP.' .format(blk.name)) if any(blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with ratioP greater ' 'than 1.'.format(blk.name)) if any(blk.control_volume.properties_out[t].pressure.fixed and (value(blk.control_volume.properties_in[t].pressure) < value(blk.control_volume.properties_out[t].pressure)) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine maybe set with pressure ', 'increase.'.format(blk.name)) # Check that work is not positive if any(blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) > 0.0) for t in blk.flowsheet().config.time): logger.warning('{} Expander/turbine set with positive work.' .format(blk.name)) # Run holdup block model checks blk.control_volume.model_check() # Run model checks on isentropic property block try: for t in blk.flowsheet().config.time: blk.properties_in[t].model_check() except AttributeError: pass def initialize(blk, state_args=None, routine=None, outlvl=0, solver='ipopt', optarg={'tol': 1e-6}): ''' General wrapper for pressure changer initialisation routines Keyword Arguments: routine : str stating which initialization routine to execute * None - use routine matching thermodynamic_assumption * 'isentropic' - use isentropic initialization routine * 'isothermal' - use isothermal initialization routine state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' if routine is None: # Use routine for specific type of unit routine = blk.config.thermodynamic_assumption # Call initialisation routine if routine is ThermodynamicAssumption.isentropic: blk.init_isentropic(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) else: # Call the general initialization routine in UnitModelBlockData super(PressureChangerData, blk).initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) def init_isentropic(blk, state_args, outlvl, solver, optarg): ''' Initialisation routine for unit (default solver ipopt) Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialisation routine * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' # Set solver options if outlvl > 3: stee = True else: stee = False opt = SolverFactory(solver) opt.options = optarg # --------------------------------------------------------------------- # Initialize Isentropic block blk.control_volume.properties_in.initialize(outlvl=outlvl-1, optarg=optarg, solver=solver, state_args=state_args) if outlvl > 0: logger.info('{} Initialisation Step 1 Complete.'.format(blk.name)) # --------------------------------------------------------------------- # Initialize holdup block flags = blk.control_volume.initialize(outlvl=outlvl-1, optarg=optarg, solver=solver, state_args=state_args) if outlvl > 0: logger.info('{} Initialisation Step 2 Complete.'.format(blk.name)) # --------------------------------------------------------------------- # Solve for isothermal conditions if isinstance( blk.control_volume.properties_in[ blk.flowsheet().config.time[1]].temperature, Var): for t in blk.flowsheet().config.time: blk.control_volume.properties_in[t].temperature.fix() blk.isentropic.deactivate() results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: logger.info('{} Initialisation Step 3 Complete.' .format(blk.name)) else: logger.warning('{} Initialisation Step 3 Failed.' .format(blk.name)) for t in blk.flowsheet().config.time: blk.control_volume.properties_in[t].temperature.unfix() blk.isentropic.activate() elif outlvl > 0: logger.info('{} Initialisation Step 3 Skipped.'.format(blk.name)) # --------------------------------------------------------------------- # Solve unit results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: logger.info('{} Initialisation Step 4 Complete.' .format(blk.name)) else: logger.warning('{} Initialisation Step 4 Failed.' .format(blk.name)) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl-1) if outlvl > 0: logger.info('{} Initialisation Complete.'.format(blk.name)) def _get_performance_contents(self, time_point=0): var_dict = {} if hasattr(self, "deltaP"): var_dict["Mechanical Work"] = self.work_mechanical[time_point] if hasattr(self, "deltaP"): var_dict["Pressure Change"] = self.deltaP[time_point] if hasattr(self, "ratioP"): var_dict["Pressure Ratio"] = self.deltaP[time_point] if hasattr(self, "efficiency_pump"): var_dict["Efficiency"] = self.deltaP[time_point] if hasattr(self, "efficiency_isentropic"): var_dict["Isentropic Efficiency"] = self.deltaP[time_point] return {"vars": var_dict}
def _define_turbine_multistage_config(config): config.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag", doc= "Only False, in a dynamic flowsheet this is psuedo-steady-state.", ), ) config.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag", doc= "Only False, in a dynamic flowsheet this is psuedo-steady-state.", ), ) config.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PropertyParameterObject** - a PropertyParameterBlock object.}""", ), ) config.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) config.declare( "num_parallel_inlet_stages", ConfigValue( default=4, domain=int, description= "Number of parallel inlet stages to simulate partial arc " "admission. Default=4", ), ) config.declare( "throttle_valve_function", ConfigValue( default=ValveFunctionType.linear, domain=In(ValveFunctionType), description= "Valve function type, if custom provide an expression rule", doc= """The type of valve function, if custom provide an expression rule with the valve_function_rule argument. **default** - ValveFunctionType.linear **Valid values** - { ValveFunctionType.linear, ValveFunctionType.quick_opening, ValveFunctionType.equal_percentage, ValveFunctionType.custom}""", ), ) config.declare( "throttle_valve_function_callback", ConfigValue( default=None, description="A callback to add a custom valve function to the " "throttle valves or None. If a callback is provided, it should " "take the valve block data as an argument and add a " "valve_function expressions to it. Default=None", ), ) config.declare( "num_hp", ConfigValue( default=2, domain=int, description= "Number of high pressure stages not including inlet stage", doc="Number of high pressure stages not including inlet stage", ), ) config.declare( "num_ip", ConfigValue( default=10, domain=int, description="Number of intermediate pressure stages", doc="Number of intermediate pressure stages", ), ) config.declare( "num_lp", ConfigValue( default=5, domain=int, description= "Number of low pressure stages not including outlet stage", doc="Number of low pressure stages not including outlet stage", ), ) config.declare( "hp_split_locations", ConfigList( default=[], domain=int, description="Locations of splitters in HP section", doc="A list of index locations of splitters in the HP section. The " "indexes indicate after which stage to include splitters. 0 is " "between the inlet stage and the first regular HP stage.", ), ) config.declare( "ip_split_locations", ConfigList( default=[], domain=int, description="Locations of splitters in IP section", doc="A list of index locations of splitters in the IP section. The " "indexes indicate after which stage to include splitters.", ), ) config.declare( "lp_split_locations", ConfigList( default=[], domain=int, description="Locations of splitter in LP section", doc="A list of index locations of splitters in the LP section. The " "indexes indicate after which stage to include splitters.", ), ) config.declare( "hp_disconnect", ConfigList( default=[], domain=int, description="HP Turbine stages to not connect to next with an arc.", doc="HP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "ip_disconnect", ConfigList( default=[], domain=int, description="IP Turbine stages to not connect to next with an arc.", doc="IP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "lp_disconnect", ConfigList( default=[], domain=int, description="LP Turbine stages to not connect to next with an arc.", doc="LP Turbine stages to not connect to next with an arc. This is " "usually used to insert additional units between stages on a " "flowsheet, such as a reheater", ), ) config.declare( "hp_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, hp split index: number of splitter outlets, if not 2", ), ) config.declare( "ip_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, ip split index: number of splitter outlets, if not 2", ), ) config.declare( "lp_split_num_outlets", ConfigValue( default={}, domain=dict, description= "Dict, lp split index: number of splitter outlets, if not 2", ), )
class HelmTurbineMultistageData(UnitModelBlockData): CONFIG = ConfigBlock() _define_turbine_multistage_config(CONFIG) def build(self): super().build() config = self.config unit_cfg = { # general unit model config "dynamic": config.dynamic, "has_holdup": config.has_holdup, "property_package": config.property_package, "property_package_args": config.property_package_args, } ni = self.config.num_parallel_inlet_stages inlet_idx = self.inlet_stage_idx = pyo.RangeSet(ni) thrtl_cfg = unit_cfg.copy() thrtl_cfg["valve_function"] = self.config.throttle_valve_function thrtl_cfg["valve_function_callback"] = \ self.config.throttle_valve_function_callback # Adding unit models # ------------------------ # Splitter to inlet that splits main flow into parallel flows for # paritial arc admission to the turbine self.inlet_split = HelmSplitter(default=self._split_cfg(unit_cfg, ni)) self.throttle_valve = SteamValve(inlet_idx, default=thrtl_cfg) self.inlet_stage = HelmTurbineInletStage(inlet_idx, default=unit_cfg) # mixer to combine the parallel flows back together self.inlet_mix = HelmMixer(default=self._mix_cfg(unit_cfg, ni)) # add turbine sections. # inlet stage -> hp stages -> ip stages -> lp stages -> outlet stage self.hp_stages = HelmTurbineStage(pyo.RangeSet(config.num_hp), default=unit_cfg) self.ip_stages = HelmTurbineStage(pyo.RangeSet(config.num_ip), default=unit_cfg) self.lp_stages = HelmTurbineStage(pyo.RangeSet(config.num_lp), default=unit_cfg) self.outlet_stage = HelmTurbineOutletStage(default=unit_cfg) for i in self.hp_stages: self.hp_stages[i].ratioP.fix() self.hp_stages[i].efficiency_isentropic.fix() for i in self.ip_stages: self.ip_stages[i].ratioP.fix() self.ip_stages[i].efficiency_isentropic.fix() for i in self.lp_stages: self.lp_stages[i].ratioP.fix() self.lp_stages[i].efficiency_isentropic.fix() # Then make splitter config. If number of outlets is specified # make a specific config, otherwise use default with 2 outlets s_sfg_default = self._split_cfg(unit_cfg, 2) hp_splt_cfg = {} ip_splt_cfg = {} lp_splt_cfg = {} # Now to finish up if there are more than two outlets, set that for i, v in config.hp_split_num_outlets.items(): hp_splt_cfg[i] = self._split_cfg(unit_cfg, v) for i, v in config.ip_split_num_outlets.items(): ip_splt_cfg[i] = self._split_cfg(unit_cfg, v) for i, v in config.lp_split_num_outlets.items(): lp_splt_cfg[i] = self._split_cfg(unit_cfg, v) # put in splitters for turbine steam extractions if config.hp_split_locations: self.hp_split = HelmSplitter(config.hp_split_locations, default=s_sfg_default, initialize=hp_splt_cfg) else: self.hp_split = {} if config.ip_split_locations: self.ip_split = HelmSplitter(config.ip_split_locations, default=s_sfg_default, initialize=ip_splt_cfg) else: self.ip_split = {} if config.lp_split_locations: self.lp_split = HelmSplitter(config.lp_split_locations, default=s_sfg_default, initialize=lp_splt_cfg) else: self.lp_split = {} # Done with unit models. Adding Arcs (streams). # ------------------------------------------------ # First up add streams in the inlet section def _split_to_rule(b, i): return { "source": getattr(self.inlet_split, "outlet_{}".format(i)), "destination": self.throttle_valve[i].inlet, } def _valve_to_rule(b, i): return { "source": self.throttle_valve[i].outlet, "destination": self.inlet_stage[i].inlet, } def _inlet_to_rule(b, i): return { "source": self.inlet_stage[i].outlet, "destination": getattr(self.inlet_mix, "inlet_{}".format(i)), } self.stream_throttle_inlet = Arc(inlet_idx, rule=_split_to_rule) self.stream_throttle_outlet = Arc(inlet_idx, rule=_valve_to_rule) self.stream_inlet_mix_inlet = Arc(inlet_idx, rule=_inlet_to_rule) # There are three sections HP, IP, and LP which all have the same sort # of internal connctions, so the functions below provide some generic # capcbilities for adding the internal Arcs (streams). def _arc_indexes(nstages, index_set, discon, splits): """ This takes the index set of all possible streams in a turbine section and throws out arc indexes for stages that are disconnected and arc indexes that are not needed because there is no splitter after a stage. Args: nstages (int): Number of stages in section index_set (Set): Index set for arcs in the section discon (list): Disconnected stages in the section splits (list): Spliter locations """ sr = set() # set of things to remove from the Arc index set for i in index_set: if (i[0] in discon or i[0] == nstages) and i[0] in splits: # don't connect stage i to next remove stream after split sr.add((i[0], 2)) elif (i[0] in discon or i[0] == nstages) and i[0] not in splits: # no splitter and disconnect so remove both streams sr.add((i[0], 1)) sr.add((i[0], 2)) elif i[0] not in splits: # no splitter and not disconnected so just second stream sr.add((i[0], 2)) else: # has splitter so need both streams don't remove anything pass for i in sr: # remove the unneeded Arc indexes index_set.remove(i) def _arc_rule(turbines, splitters): """ This creates a rule function for arcs in a turbine section. When this is used, the indexes for nonexistant stream will have already been removed, so any indexes the rule will get should have a stream associated. Args: turbines (TurbineStage): Indexed block with turbine section stages splitters (Separator): Indexed block of splitters """ def _rule(b, i, j): if i in splitters and j == 1: # stage to splitter return { "source": turbines[i].outlet, "destination": splitters[i].inlet, } elif j == 2: # splitter to next stage return { "source": splitters[i].outlet_1, "destination": turbines[i + 1].inlet, } else: # no splitter, stage to next stage return { "source": turbines[i].outlet, "destination": turbines[i + 1].inlet, } return _rule # Create initial arcs index sets with all possible streams self.hp_stream_idx = pyo.Set(initialize=self.hp_stages.index_set() * [1, 2]) self.ip_stream_idx = pyo.Set(initialize=self.ip_stages.index_set() * [1, 2]) self.lp_stream_idx = pyo.Set(initialize=self.lp_stages.index_set() * [1, 2]) # Throw out unneeded streams for disconnected stages or no splitter _arc_indexes( config.num_hp, self.hp_stream_idx, config.hp_disconnect, config.hp_split_locations, ) _arc_indexes( config.num_ip, self.ip_stream_idx, config.ip_disconnect, config.ip_split_locations, ) _arc_indexes( config.num_lp, self.lp_stream_idx, config.lp_disconnect, config.lp_split_locations, ) # Create connections internal to each turbine section (hp, ip, and lp) self.hp_stream = Arc(self.hp_stream_idx, rule=_arc_rule(self.hp_stages, self.hp_split)) self.ip_stream = Arc(self.ip_stream_idx, rule=_arc_rule(self.ip_stages, self.ip_split)) self.lp_stream = Arc(self.lp_stream_idx, rule=_arc_rule(self.lp_stages, self.lp_split)) # Connect hp section to ip section unless its a disconnect location last_hp = config.num_hp if 0 not in config.ip_disconnect and last_hp not in config.hp_disconnect: # Not disconnected stage so add stream, depending on splitter existance if last_hp in config.hp_split_locations: # connect splitter to ip self.hp_to_ip_stream = Arc( source=self.hp_split[last_hp].outlet_1, destination=self.ip_stages[1].inlet, ) else: # connect last hp to ip self.hp_to_ip_stream = Arc( source=self.hp_stages[last_hp].outlet, destination=self.ip_stages[1].inlet, ) # Connect ip section to lp section unless its a disconnect location last_ip = config.num_ip if 0 not in config.lp_disconnect and last_ip not in config.ip_disconnect: if last_ip in config.ip_split_locations: # connect splitter to ip self.ip_to_lp_stream = Arc( source=self.ip_split[last_ip].outlet_1, destination=self.lp_stages[1].inlet, ) else: # connect last hp to ip self.ip_to_lp_stream = Arc( source=self.ip_stages[last_ip].outlet, destination=self.lp_stages[1].inlet, ) # Connect inlet stage to hp section # not allowing disconnection of inlet and first regular hp stage if 0 in config.hp_split_locations: # connect inlet mix to splitter and splitter to hp section self.inlet_to_splitter_stream = Arc( source=self.inlet_mix.outlet, destination=self.hp_split[0].inlet) self.splitter_to_hp_stream = Arc( source=self.hp_split[0].outlet_1, destination=self.hp_stages[1].inlet) else: # connect mixer to first hp turbine stage self.inlet_to_hp_stream = Arc(source=self.inlet_mix.outlet, destination=self.hp_stages[1].inlet) self.power = pyo.Var(self.flowsheet().time, initialize=-1e8, doc="power (W)") @self.Constraint(self.flowsheet().time) def power_eqn(b, t): return (b.power[t] == b.outlet_stage.control_volume.work[t] * b.outlet_stage.efficiency_mech + sum(b.inlet_stage[i].control_volume.work[t] * b.inlet_stage[i].efficiency_mech for i in b.inlet_stage) + sum(b.hp_stages[i].control_volume.work[t] * b.hp_stages[i].efficiency_mech for i in b.hp_stages) + sum(b.ip_stages[i].control_volume.work[t] * b.ip_stages[i].efficiency_mech for i in b.ip_stages) + sum(b.lp_stages[i].control_volume.work[t] * b.lp_stages[i].efficiency_mech for i in b.lp_stages)) # Connect lp section to outlet stage, not allowing outlet stage to be # disconnected last_lp = config.num_lp if last_lp in config.lp_split_locations: # connect splitter to outlet self.lp_to_outlet_stream = Arc( source=self.lp_split[last_lp].outlet_1, destination=self.outlet_stage.inlet, ) else: # connect last lpstage to outlet self.lp_to_outlet_stream = Arc( source=self.lp_stages[last_lp].outlet, destination=self.outlet_stage.inlet, ) pyo.TransformationFactory("network.expand_arcs").apply_to(self) def _split_cfg(self, unit_cfg, no=2): """ This creates a configuration dictionary for a splitter. Args: unit_cfg: The base unit config dict. no: Number of outlets, default=2 """ # Create a dict for splitter config args cfg = copy.copy(unit_cfg) cfg.update(num_outlets=no) return cfg def _mix_cfg(self, unit_cfg, ni=2): """ This creates a configuration dictionary for a mixer. Args: unit_cfg: The base unit config dict. ni: Number of inlets, default=2 """ cfg = copy.copy(unit_cfg) cfg.update( num_inlets=ni, momentum_mixing_type=MomentumMixingType.minimize_and_equality) return cfg def throttle_cv_fix(self, value): """ Fix the thottle valve coefficients. These are generally the same for each of the parallel stages so this provides a convenient way to set them. Args: value: The value to fix the turbine inlet flow coefficients at """ for i in self.throttle_valve: self.throttle_valve[i].Cv.fix(value) def turbine_inlet_cf_fix(self, value): """ Fix the inlet turbine stage flow coefficient. These are generally the same for each of the parallel stages so this provides a convenient way to set them. Args: value: The value to fix the turbine inlet flow coefficients at """ for i in self.inlet_stage: self.inlet_stage[i].flow_coeff.fix(value) def _init_section( self, stages, splits, disconnects, prev_port, outlvl, solver, optarg, copy_disconneted_flow, copy_disconneted_pressure, ): """ Reuse the initializtion for HP, IP and, LP sections. """ if 0 in splits: copy_port(splits[0].inlet, prev_port) splits[0].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = splits[0].outlet_1 for i in stages: if i - 1 not in disconnects: copy_port(stages[i].inlet, prev_port) else: if copy_disconneted_flow: for t in stages[i].inlet.flow_mol: stages[i].inlet.flow_mol[t] = pyo.value( prev_port.flow_mol[t]) if copy_disconneted_pressure: for t in stages[i].inlet.pressure: stages[i].inlet.pressure[t] = pyo.value( prev_port.pressure[t]) stages[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = stages[i].outlet if i in splits: copy_port(splits[i].inlet, prev_port) splits[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = splits[i].outlet_1 return prev_port def turbine_outlet_cf_fix(self, value): """ Fix the inlet turbine stage flow coefficient. These are generally the same for each of the parallel stages so this provides a convenient way to set them. Args: value: The value to fix the turbine inlet flow coefficients at """ self.outlet_stage.flow_coeff.fix(value) def initialize(self, outlvl=idaeslog.NOTSET, solver=None, flow_iterate=2, optarg=None, copy_disconneted_flow=True, copy_disconneted_pressure=True, calculate_outlet_cf=False, calculate_inlet_cf=False): """ Initialize Args: outlvl: logging level default is NOTSET, which inherits from the parent logger solver: the NL solver flow_iterate: If not calculating flow coefficients, this is the number of times to update the flow and repeat initialization (1 to 5 where 1 does not update the flow guess) optarg: solver arguments, default is None copy_disconneted_flow: Copy the flow through the disconnected stages default is True copy_disconneted_pressure: Copy the pressure through the disconnected stages default is True calculate_outlet_cf: Use the flow initial flow guess to calculate the outlet stage flow coefficient, default is False, calculate_inlet_cf: Use the inlet stage ratioP to calculate the flow coefficent for the inlet stage default is False Returns: None """ # Setup loggers init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # Store initial model specs, restored at the end of initializtion, so # the problem is not altered. This can restore fixed/free vars, # active/inactive constraints, and fixed variable values. sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Assume the flow into the turbine is a reasonable guess for # initializtion flow_guess = self.inlet_split.inlet.flow_mol[0].value for it_count in range(flow_iterate): self.inlet_split.initialize(outlvl=outlvl, solver=solver, optarg=optarg) # Initialize valves for i in self.inlet_stage_idx: u = self.throttle_valve[i] copy_port(u.inlet, getattr(self.inlet_split, "outlet_{}".format(i))) u.initialize(outlvl=outlvl, solver=solver, optarg=optarg) # Initialize turbine for i in self.inlet_stage_idx: u = self.inlet_stage[i] copy_port(u.inlet, self.throttle_valve[i].outlet) u.initialize(outlvl=outlvl, solver=solver, optarg=optarg, calculate_cf=calculate_inlet_cf) # Initialize Mixer self.inlet_mix.use_minimum_inlet_pressure_constraint() for i in self.inlet_stage_idx: copy_port( getattr(self.inlet_mix, "inlet_{}".format(i)), self.inlet_stage[i].outlet, ) getattr(self.inlet_mix, "inlet_{}".format(i)).fix() self.inlet_mix.initialize(outlvl=outlvl, solver=solver, optarg=optarg) for i in self.inlet_stage_idx: getattr(self.inlet_mix, "inlet_{}".format(i)).unfix() self.inlet_mix.use_equal_pressure_constraint() prev_port = self.inlet_mix.outlet prev_port = self._init_section( self.hp_stages, self.hp_split, self.config.hp_disconnect, prev_port, outlvl, solver, optarg, copy_disconneted_flow=copy_disconneted_flow, copy_disconneted_pressure=copy_disconneted_pressure, ) if len(self.hp_stages) in self.config.hp_disconnect: self.config.ip_disconnect.append(0) prev_port = self._init_section( self.ip_stages, self.ip_split, self.config.ip_disconnect, prev_port, outlvl, solver, optarg, copy_disconneted_flow=copy_disconneted_flow, copy_disconneted_pressure=copy_disconneted_pressure, ) if len(self.ip_stages) in self.config.ip_disconnect: self.config.lp_disconnect.append(0) prev_port = self._init_section( self.lp_stages, self.lp_split, self.config.lp_disconnect, prev_port, outlvl, solver, optarg, copy_disconneted_flow=copy_disconneted_flow, copy_disconneted_pressure=copy_disconneted_pressure, ) copy_port(self.outlet_stage.inlet, prev_port) self.outlet_stage.initialize(outlvl=outlvl, solver=solver, optarg=optarg, calculate_cf=calculate_outlet_cf) if calculate_outlet_cf: break if it_count < flow_iterate - 1: for t in self.inlet_split.inlet.flow_mol: self.inlet_split.inlet.flow_mol[t].value = \ self.outlet_stage.inlet.flow_mol[t].value for s in self.hp_split.values(): for i, o in enumerate(s.outlet_list): if i == 0: continue o = getattr(s, o) self.inlet_split.inlet.flow_mol[t].value += \ o.flow_mol[t].value for s in self.ip_split.values(): for i, o in enumerate(s.outlet_list): if i == 0: continue o = getattr(s, o) self.inlet_split.inlet.flow_mol[t].value += \ o.flow_mol[t].value for s in self.lp_split.values(): for i, o in enumerate(s.outlet_list): if i == 0: continue o = getattr(s, o) self.inlet_split.inlet.flow_mol[t].value += \ o.flow_mol[t].value if calculate_inlet_cf: # cf was probably fixed, so will have to set the value agian here # if you ask for it to be calculated. icf = {} for i in self.inlet_stage: for t in self.inlet_stage[i].flow_coeff: icf[i, t] = pyo.value(self.inlet_stage[i].flow_coeff[t]) if calculate_outlet_cf: ocf = pyo.value(self.outlet_stage.flow_coeff) from_json(self, sd=istate, wts=sp) if calculate_inlet_cf: # cf was probably fixed, so will have to set the value agian here # if you ask for it to be calculated. for t in self.inlet_stage[i].flow_coeff: for i in self.inlet_stage: self.inlet_stage[i].flow_coeff[t] = icf[i, t] if calculate_outlet_cf: self.outlet_stage.flow_coeff = ocf def calculate_scaling_factors(self): super().calculate_scaling_factors() # Add a default power scale # pretty safe to say power is around 100 to 1000 MW for t in self.power: if iscale.get_scaling_factor(self.power[t]) is None: iscale.set_scaling_factor(self.power[t], 1e-8) for t, c in self.power_eqn.items(): power_scale = iscale.get_scaling_factor(self.power[t], default=1, warning=True) # Set power equation scale factor iscale.constraint_scaling_transform(c, power_scale, overwrite=False)
def _process_kwargs(o, kwargs): kwargs.setdefault("rule", _rule_default) o._block_data_config_default = kwargs.pop("default", None) o._block_data_config_initialize = ConfigBlock(implicit=True) o._block_data_config_initialize.set_value(kwargs.pop("initialize", None)) o._idx_map = kwargs.pop("idx_map", None)
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 Collocation_Discretization_Transformation(Transformation): CONFIG = ConfigBlock("dae.collocation") CONFIG.declare( 'nfe', ConfigValue( default=10, domain=PositiveInt, description="The desired number of finite element points to be " "included in the discretization")) CONFIG.declare( 'ncp', ConfigValue( default=3, domain=PositiveInt, description="The desired number of collocation points over each " "finite element")) CONFIG.declare( 'wrt', ConfigValue( default=None, description="The ContinuousSet to be discretized", doc="Indicates which ContinuousSet the transformation should be " "applied to. If this keyword argument is not specified then the " "same scheme will be applied to all ContinuousSets.")) CONFIG.declare( 'scheme', ConfigValue( default='LAGRANGE-RADAU', domain=In(['LAGRANGE-RADAU', 'LAGRANGE-LEGENDRE']), description="Indicates which collocation scheme to apply", doc="Options are 'LAGRANGE-RADAU' and 'LAGRANGE-LEGENDRE'. " "The default scheme is Lagrange polynomials with Radau roots")) def __init__(self): super(Collocation_Discretization_Transformation, self).__init__() self._ncp = {} self._nfe = {} self._adot = {} self._adotdot = {} self._afinal = {} self._tau = {} self._reduced_cp = {} self.all_schemes = { 'LAGRANGE-RADAU': (_lagrange_radau_transform, _lagrange_radau_transform_order2), 'LAGRANGE-LEGENDRE': (_lagrange_legendre_transform, _lagrange_legendre_transform_order2) } def _get_radau_constants(self, currentds): """ This function sets the radau collocation points and a values depending on how many collocation points have been specified and whether or not the user has numpy """ if not numpy_available: if self._ncp[currentds] > 10: raise ValueError("Numpy was not found so the maximum number " "of collocation points is 10") from pyomo.dae.utilities import (radau_tau_dict, radau_adot_dict, radau_adotdot_dict) self._tau[currentds] = radau_tau_dict[self._ncp[currentds]] self._adot[currentds] = radau_adot_dict[self._ncp[currentds]] self._adotdot[currentds] = radau_adotdot_dict[self._ncp[currentds]] self._afinal[currentds] = None else: alpha = 1 beta = 0 k = self._ncp[currentds] - 1 cp = sorted(list(calc_cp(alpha, beta, k))) cp.insert(0, 0.0) cp.append(1.0) adot = calc_adot(cp, 1) adotdot = calc_adot(cp, 2) self._tau[currentds] = cp self._adot[currentds] = adot self._adotdot[currentds] = adotdot self._afinal[currentds] = None def _get_legendre_constants(self, currentds): """ This function sets the legendre collocation points and a values depending on how many collocation points have been specified and whether or not the user has numpy """ if not numpy_available: if self._ncp[currentds] > 10: raise ValueError("Numpy was not found so the maximum number " "of collocation points is 10") from pyomo.dae.utilities import (legendre_tau_dict, legendre_adot_dict, legendre_adotdot_dict, legendre_afinal_dict) self._tau[currentds] = legendre_tau_dict[self._ncp[currentds]] self._adot[currentds] = legendre_adot_dict[self._ncp[currentds]] self._adotdot[currentds] = \ legendre_adotdot_dict[self._ncp[currentds]] self._afinal[currentds] = \ legendre_afinal_dict[self._ncp[currentds]] else: alpha = 0 beta = 0 k = self._ncp[currentds] cp = sorted(list(calc_cp(alpha, beta, k))) cp.insert(0, 0.0) adot = calc_adot(cp, 1) adotdot = calc_adot(cp, 2) afinal = calc_afinal(cp) self._tau[currentds] = cp self._adot[currentds] = adot self._adotdot[currentds] = adotdot self._afinal[currentds] = afinal def _apply_to(self, instance, **kwds): """ Applies specified collocation transformation to a modeling instance Keyword Arguments: nfe The desired number of finite element points to be included in the discretization. ncp The desired number of collocation points over each finite element. wrt Indicates which ContinuousSet the transformation should be applied to. If this keyword argument is not specified then the same scheme will be applied to all ContinuousSets. scheme Indicates which collocation scheme to apply. Options are 'LAGRANGE-RADAU' and 'LAGRANGE-LEGENDRE'. The default scheme is Lagrange polynomials with Radau roots. """ config = self.CONFIG(kwds) tmpnfe = config.nfe tmpncp = config.ncp tmpds = config.wrt if tmpds is not None: if tmpds.ctype is not ContinuousSet: raise TypeError("The component specified using the 'wrt' " "keyword must be a continuous set") elif 'scheme' in tmpds.get_discretization_info(): raise ValueError( "The discretization scheme '%s' has already " "been applied to the ContinuousSet '%s'" % (tmpds.get_discretization_info()['scheme'], tmpds.name)) if None in self._nfe: raise ValueError( "A general discretization scheme has already been applied to " "to every ContinuousSet in the model. If you would like to " "specify a specific discretization scheme for one of the " "ContinuousSets you must discretize each ContinuousSet " "separately.") if len(self._nfe) == 0 and tmpds is None: # Same discretization on all ContinuousSets self._nfe[None] = tmpnfe self._ncp[None] = tmpncp currentds = None else: self._nfe[tmpds.name] = tmpnfe self._ncp[tmpds.name] = tmpncp currentds = tmpds.name self._scheme_name = config.scheme self._scheme = self.all_schemes.get(self._scheme_name, None) if self._scheme_name == 'LAGRANGE-RADAU': self._get_radau_constants(currentds) elif self._scheme_name == 'LAGRANGE-LEGENDRE': self._get_legendre_constants(currentds) self._transformBlock(instance, currentds) return instance def _transformBlock(self, block, currentds): self._fe = {} for ds in block.component_objects(ContinuousSet, descend_into=True): if currentds is None or currentds == ds.name: if 'scheme' in ds.get_discretization_info(): raise DAE_Error( "Attempting to discretize ContinuousSet " "'%s' after it has already been discretized. " % ds.name) generate_finite_elements(ds, self._nfe[currentds]) if not ds.get_changed(): if len(ds) - 1 > self._nfe[currentds]: logger.warning( "More finite elements were found in " "ContinuousSet '%s' than the number of " "finite elements specified in apply. The " "larger number of finite elements will be " "used." % ds.name) self._nfe[ds.name] = len(ds) - 1 self._fe[ds.name] = list(ds) generate_colloc_points(ds, self._tau[currentds]) # Adding discretization information to the continuousset # object itself so that it can be accessed outside of the # discretization object disc_info = ds.get_discretization_info() disc_info['nfe'] = self._nfe[ds.name] disc_info['ncp'] = self._ncp[currentds] disc_info['tau_points'] = self._tau[currentds] disc_info['adot'] = self._adot[currentds] disc_info['adotdot'] = self._adotdot[currentds] disc_info['afinal'] = self._afinal[currentds] disc_info['scheme'] = self._scheme_name expand_components(block) for d in block.component_objects(DerivativeVar, descend_into=True): dsets = d.get_continuousset_list() for i in ComponentSet(dsets): if currentds is None or i.name == currentds: oldexpr = d.get_derivative_expression() loc = d.get_state_var()._contset[i] count = dsets.count(i) if count >= 3: raise DAE_Error( "Error discretizing '%s' with respect to '%s'. " "Current implementation only allows for taking the" " first or second derivative with respect to a " "particular ContinuousSet" % (d.name, i.name)) scheme = self._scheme[count - 1] newexpr = create_partial_expression( scheme, oldexpr, i, loc) d.set_derivative_expression(newexpr) if self._scheme_name == 'LAGRANGE-LEGENDRE': # Add continuity equations to DerivativeVar's parent # block add_continuity_equations(d.parent_block(), d, i, loc) # Reclassify DerivativeVar if all indexing ContinuousSets have # been discretized. Add discretization equations to the # DerivativeVar's parent block. if d.is_fully_discretized(): add_discretization_equations(d.parent_block(), d) d.parent_block().reclassify_component_type(d, Var) # Keep track of any reclassified DerivativeVar components so # that the Simulator can easily identify them if the model # is simulated after discretization # TODO: Update the discretization transformations to use # a Block to add things to the model and store discretization # information. Using a list for now because the simulator # does not yet support models containing active Blocks reclassified_list = getattr( block, '_pyomo_dae_reclassified_derivativevars', None) if reclassified_list is None: block._pyomo_dae_reclassified_derivativevars = list() reclassified_list = \ block._pyomo_dae_reclassified_derivativevars reclassified_list.append(d) # Reclassify Integrals if all ContinuousSets have been discretized if block_fully_discretized(block): if block.contains_component(Integral): for i in block.component_objects(Integral, descend_into=True): i.parent_block().reclassify_component_type(i, Expression) # TODO: The following reproduces the old behavior of # "reconstruct()". We should come up with an # implementation that does not rely on manipulating # private attributes i.clear() i._constructed = False i.construct() # If a model contains integrals they are most likely to appear # in the objective function which will need to be reconstructed # after the model is discretized. for k in block.component_objects(Objective, descend_into=True): # TODO: check this, reconstruct might not work # TODO: The following reproduces the old behavior of # "reconstruct()". We should come up with an # implementation that does not rely on manipulating # private attributes k.clear() k._constructed = False k.construct() def reduce_collocation_points(self, instance, var=None, ncp=None, contset=None): """ This method will add additional constraints to a model to reduce the number of free collocation points (degrees of freedom) for a particular variable. Parameters ---------- instance : Pyomo model The discretized Pyomo model to add constraints to var : ``pyomo.environ.Var`` The Pyomo variable for which the degrees of freedom will be reduced ncp : int The new number of free collocation points for `var`. Must be less that the number of collocation points used in discretizing the model. contset : ``pyomo.dae.ContinuousSet`` The :py:class:`ContinuousSet<pyomo.dae.ContinuousSet>` that was discretized and for which the `var` will have a reduced number of degrees of freedom """ if contset is None: raise TypeError("A continuous set must be specified using the " "keyword 'contset'") if contset.ctype is not ContinuousSet: raise TypeError("The component specified using the 'contset' " "keyword must be a ContinuousSet") ds = contset if len(self._ncp) == 0: raise RuntimeError("This method should only be called after using " "the apply() method to discretize the model") elif None in self._ncp: tot_ncp = self._ncp[None] elif ds.name in self._ncp: tot_ncp = self._ncp[ds.name] else: raise ValueError("ContinuousSet '%s' has not been discretized, " "please call the apply_to() method with this " "ContinuousSet to discretize it before calling " "this method" % ds.name) if var is None: raise TypeError("A variable must be specified") if var.ctype is not Var: raise TypeError("The component specified using the 'var' keyword " "must be a variable") if ncp is None: raise TypeError( "The number of collocation points must be specified") if ncp <= 0: raise ValueError( "The number of collocation points must be at least 1") if ncp > tot_ncp: raise ValueError("The number of collocation points used to " "interpolate an individual variable must be less " "than the number used to discretize the original " "model") if ncp == tot_ncp: # Nothing to be done return instance # Check to see if the continuousset is an indexing set of the variable if var.dim() == 0: raise IndexError("ContinuousSet '%s' is not an indexing set of" " the variable '%s'" % (ds.name, var.name)) varidx = var.index_set() if not hasattr(varidx, 'set_tuple'): if ds is not varidx: raise IndexError("ContinuousSet '%s' is not an indexing set of" " the variable '%s'" % (ds.name, var.name)) elif ds not in varidx.set_tuple: raise IndexError("ContinuousSet '%s' is not an indexing set of the" " variable '%s'" % (ds.name, var.name)) if var.name in self._reduced_cp: temp = self._reduced_cp[var.name] if ds.name in temp: raise RuntimeError("Variable '%s' has already been constrained" " to a reduced number of collocation points" " over ContinuousSet '%s'.") else: temp[ds.name] = ncp else: self._reduced_cp[var.name] = {ds.name: ncp} # TODO: Use unique_component_name for this list_name = var.local_name + "_interpolation_constraints" instance.add_component(list_name, ConstraintList()) conlist = instance.find_component(list_name) t = list(ds) fe = ds._fe info = get_index_information(var, ds) tmpidx = info['non_ds'] idx = info['index function'] # Iterate over non_ds indices for n in tmpidx: # Iterate over finite elements for i in range(0, len(fe) - 1): # Iterate over collocation points for k in range(1, tot_ncp - ncp + 1): if ncp == 1: # Constant over each finite element conlist.add( var[idx(n, i, k)] == var[idx(n, i, tot_ncp)]) else: tmp = ds.ord(fe[i]) - 1 tmp2 = ds.ord(fe[i + 1]) - 1 ti = t[tmp + k] tfit = t[tmp2 - ncp + 1:tmp2 + 1] coeff = self._interpolation_coeffs(ti, tfit) conlist.add(var[idx(n, i, k)] == sum( var[idx(n, i, j)] * next(coeff) for j in range(tot_ncp - ncp + 1, tot_ncp + 1))) return instance def _interpolation_coeffs(self, ti, tfit): for i in tfit: l = 1 for j in tfit: if i != j: l = l * (ti - j) / (i - j) yield l
class StateTestBlockData(StateBlockData): CONFIG = ConfigBlock(implicit=True) def build(self): super(StateTestBlockData, self).build() self.flow_vol = Var(initialize=20, units=units.m**3 / units.s) self.flow_mol_phase_comp = Var(self.params.phase_list, self.params.component_list, initialize=2, units=units.mol / units.s) self.test_var = Var(initialize=1) self.enthalpy_flow = Var(initialize=1, units=units.J / units.s) self.energy_dens = Var(initialize=1, units=units.J / units.m**3) self.material_flow_mol = Var(initialize=1, units=units.mol / units.s) self.material_dens_mol = Var(initialize=1, units=units.mol / units.m**3) self.material_flow_mass = Var(initialize=1, units=units.kg / units.s) self.material_dens_mass = Var(initialize=1, units=units.kg / units.m**3) self.pressure = Var(initialize=1e5, units=units.Pa) self.temperature = Var(initialize=300, units=units.K) self.enth_mol = Var(initialize=10000, units=units.J / units.mol) self.gibbs_mol_phase_comp = Var(self.params.phase_list, self.params.component_list, initialize=50, units=units.J / units.mol) self.entr_mol = Var(initialize=1000, units=units.J / units.mol / units.K) self.mole_frac_phase_comp = Var(self.params.phase_list, self.params.component_list, initialize=0.5) def get_material_flow_terms(b, p, j): if b.config.parameters.basis_switch == 2: return b.material_flow_mass else: return b.material_flow_mol def get_material_density_terms(b, p, j): if b.config.parameters.basis_switch == 2: return b.material_dens_mass else: return b.material_dens_mol def get_enthalpy_flow_terms(b, p): return b.enthalpy_flow def get_energy_density_terms(b, p): return b.energy_dens def model_check(self): self.check = True def get_material_flow_basis(b): if b.config.parameters.basis_switch == 1: return MaterialFlowBasis.molar elif b.config.parameters.basis_switch == 2: return MaterialFlowBasis.mass else: return MaterialFlowBasis.other def default_material_balance_type(self): if self.params.default_balance_switch == 1: return MaterialBalanceType.componentPhase else: raise NotImplementedError def default_energy_balance_type(self): if self.params.default_balance_switch == 1: return EnergyBalanceType.enthalpyTotal else: raise NotImplementedError def define_state_vars(self): return { "component_flow_phase": self.flow_mol_phase_comp, "temperature": self.temperature, "pressure": self.pressure }
class BigM_Transformation(Transformation): """Relax disjunctive model using big-M terms. Relaxes a disjunctive model into an algebraic model by adding Big-M terms to all disjunctive constraints. This transformation accepts the following keyword arguments: bigM: A user-specified value (or dict) of M values to use (see below) targets: the targets to transform [default: the instance] M values are determined as follows: 1) if the constraint appears in the bigM argument dict 2) if the constraint parent_component appears in the bigM argument dict 3) if any block which is an ancestor to the constraint appears in the bigM argument dict 3) if 'None' is in the bigM argument dict 4) if the constraint or the constraint parent_component appear in a BigM Suffix attached to any parent_block() beginning with the constraint's parent_block and moving up to the root model. 5) if None appears in a BigM Suffix attached to any parent_block() between the constraint and the root model. 6) if the constraint is linear, estimate M using the variable bounds M values may be a single value or a 2-tuple specifying the M for the lower bound and the upper bound of the constraint body. Specifying "bigM=N" is automatically mapped to "bigM={None: N}". The transformation will create a new Block with a unique name beginning "_pyomo_gdp_bigm_reformulation". That Block will contain an indexed Block named "relaxedDisjuncts", which will hold the relaxed disjuncts. This block is indexed by an integer indicating the order in which the disjuncts were relaxed. Each block has a dictionary "_constraintMap": 'srcConstraints': ComponentMap(<transformed constraint>: <src constraint>) 'transformedConstraints': ComponentMap(<src constraint>: <transformed constraint>) All transformed Disjuncts will have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding OR or XOR constraint. """ CONFIG = ConfigBlock("gdp.bigm") CONFIG.declare('targets', ConfigValue( default=None, domain=target_list, description="target or list of targets that will be relaxed", doc=""" This specifies the list of components to relax. If None (default), the entire model is transformed. Note that if the transformation is done out of place, the list of targets should be attached to the model before it is cloned, and the list will specify the targets on the cloned instance.""" )) CONFIG.declare('bigM', ConfigValue( default=None, domain=_to_dict, description="Big-M value used for constraint relaxation", doc=""" A user-specified value, dict, or ComponentMap of M values that override M-values found through model Suffixes or that would otherwise be calculated using variable domains.""" )) CONFIG.declare('assume_fixed_vars_permanent', ConfigValue( default=False, domain=bool, description="Boolean indicating whether or not to transform so that the " "the transformed model will still be valid when fixed Vars are unfixed.", doc=""" This is only relevant when the transformation will be estimating values for M. If True, the transformation will calculate M values assuming that fixed variables will always be fixed to their current values. This means that if a fixed variable is unfixed after transformation, the transformed model is potentially no longer valid. By default, the transformation will assume fixed variables could be unfixed in the future and will use their bounds to calculate the M value rather than their value. Note that this could make for a weaker LP relaxation while the variables remain fixed. """ )) def __init__(self): """Initialize transformation object.""" super(BigM_Transformation, self).__init__() self.handlers = { Constraint: self._transform_constraint, Var: False, # Note that if a Var appears on a Disjunct, we # still treat its bounds as global. If the # intent is for its bounds to be on the # disjunct, it should be declared with no bounds # and the bounds should be set in constraints on # the Disjunct. BooleanVar: False, Connector: False, Expression: False, Suffix: False, Param: False, Set: False, SetOf: False, RangeSet: False, Disjunction: self._warn_for_active_disjunction, Disjunct: self._warn_for_active_disjunct, Block: self._transform_block_on_disjunct, LogicalConstraint: self._warn_for_active_logical_statement, ExternalFunction: False, } self._generate_debug_messages = False def _get_bigm_suffix_list(self, block, stopping_block=None): # Note that you can only specify suffixes on BlockData objects or # SimpleBlocks. Though it is possible at this point to stick them # on whatever components you want, we won't pick them up. suffix_list = [] # go searching above block in the tree, stop when we hit stopping_block # (This is so that we can search on each Disjunct once, but get any # information between a constraint and its Disjunct while transforming # the constraint). while block is not stopping_block: bigm = block.component('BigM') if type(bigm) is Suffix: suffix_list.append(bigm) block = block.parent_block() return suffix_list def _get_bigm_arg_list(self, bigm_args, block): # Gather what we know about blocks from args exactly once. We'll still # check for constraints in the moment, but if that fails, we've # preprocessed the time-consuming part of traversing up the tree. arg_list = [] if bigm_args is None: return arg_list while block is not None: if block in bigm_args: arg_list.append({block: bigm_args[block]}) block = block.parent_block() return arg_list def _apply_to(self, instance, **kwds): assert not NAME_BUFFER self._generate_debug_messages = is_debug_set(logger) self.used_args = ComponentMap() # If everything was sure to go well, # this could be a dictionary. But if # someone messes up and gives us a Var # as a key in bigMargs, I need the error # not to be when I try to put it into # this map! try: self._apply_to_impl(instance, **kwds) finally: # Clear the global name buffer now that we are done NAME_BUFFER.clear() # same for our bookkeeping about what we used from bigM arg dict self.used_args.clear() def _apply_to_impl(self, instance, **kwds): config = self.CONFIG(kwds.pop('options', {})) # We will let args override suffixes and estimate as a last # resort. More specific args/suffixes override ones anywhere in # the tree. Suffixes lower down in the tree override ones higher # up. if 'default_bigM' in kwds: deprecation_warning("the 'default_bigM=' argument has been " "replaced by 'bigM='", version='5.4') config.bigM = kwds.pop('default_bigM') config.set_value(kwds) bigM = config.bigM self.assume_fixed_vars_permanent = config.assume_fixed_vars_permanent targets = config.targets if targets is None: targets = (instance, ) # We need to check that all the targets are in fact on instance. As we # do this, we will use the set below to cache components we know to be # in the tree rooted at instance. knownBlocks = {} for t in targets: # check that t is in fact a child of instance if not is_child_of(parent=instance, child=t, knownBlocks=knownBlocks): raise GDP_Error( "Target '%s' is not a component on instance '%s'!" % (t.name, instance.name)) elif t.ctype is Disjunction: if t.is_indexed(): self._transform_disjunction(t, bigM) else: self._transform_disjunctionData( t, bigM, t.index()) elif t.ctype in (Block, Disjunct): if t.is_indexed(): self._transform_block(t, bigM) else: self._transform_blockData(t, bigM) else: raise GDP_Error( "Target '%s' was not a Block, Disjunct, or Disjunction. " "It was of type %s and can't be transformed." % (t.name, type(t))) # issue warnings about anything that was in the bigM args dict that we # didn't use if bigM is not None: unused_args = ComponentSet(bigM.keys()) - \ ComponentSet(self.used_args.keys()) if len(unused_args) > 0: warning_msg = ("Unused arguments in the bigM map! " "These arguments were not used by the " "transformation:\n") for component in unused_args: if hasattr(component, 'name'): warning_msg += "\t%s\n" % component.name else: warning_msg += "\t%s\n" % component logger.warning(warning_msg) def _add_transformation_block(self, instance): # make a transformation block on instance to put transformed disjuncts # on transBlockName = unique_component_name( instance, '_pyomo_gdp_bigm_reformulation') transBlock = Block() instance.add_component(transBlockName, transBlock) transBlock.relaxedDisjuncts = Block(NonNegativeIntegers) transBlock.lbub = Set(initialize=['lb', 'ub']) return transBlock def _transform_block(self, obj, bigM): for i in sorted(obj.keys()): self._transform_blockData(obj[i], bigM) def _transform_blockData(self, obj, bigM): # Transform every (active) disjunction in the block for disjunction in obj.component_objects( Disjunction, active=True, sort=SortComponents.deterministic, descend_into=(Block, Disjunct), descent_order=TraversalStrategy.PostfixDFS): self._transform_disjunction(disjunction, bigM) def _add_xor_constraint(self, disjunction, transBlock): # Put the disjunction constraint on the transformation block and # determine whether it is an OR or XOR constraint. # We never do this for just a DisjunctionData because we need to know # about the index set of its parent component (so that we can make the # index of this constraint match). So if we called this on a # DisjunctionData, we did something wrong. assert isinstance(disjunction, Disjunction) # first check if the constraint already exists if disjunction._algebraic_constraint is not None: return disjunction._algebraic_constraint() # add the XOR (or OR) constraints to parent block (with unique name) # It's indexed if this is an IndexedDisjunction, not otherwise orC = Constraint(disjunction.index_set()) if \ disjunction.is_indexed() else Constraint() # The name used to indicate if there were OR or XOR disjunctions, # however now that Disjunctions are allowed to mix the state we # can no longer make that distinction in the name. # nm = '_xor' if xor else '_or' nm = '_xor' orCname = unique_component_name( transBlock, disjunction.getname( fully_qualified=True, name_buffer=NAME_BUFFER) + nm) transBlock.add_component(orCname, orC) disjunction._algebraic_constraint = weakref_ref(orC) return orC def _transform_disjunction(self, obj, bigM): if not obj.active: return # if this is an IndexedDisjunction we have seen in a prior call to the # transformation, we already have a transformation block for it. We'll # use that. if obj._algebraic_constraint is not None: transBlock = obj._algebraic_constraint().parent_block() else: transBlock = self._add_transformation_block(obj.parent_block()) # relax each of the disjunctionDatas for i in sorted(obj.keys()): self._transform_disjunctionData(obj[i], bigM, i, transBlock) # deactivate so the writers don't scream obj.deactivate() def _transform_disjunctionData(self, obj, bigM, index, transBlock=None): if not obj.active: return # Do not process a deactivated disjunction # We won't have these arguments if this got called straight from # targets. But else, we created them earlier, and have just been passing # them through. if transBlock is None: # It's possible that we have already created a transformation block # for another disjunctionData from this same container. If that's # the case, let's use the same transformation block. (Else it will # be really confusing that the XOR constraint goes to that old block # but we create a new one here.) if obj.parent_component()._algebraic_constraint is not None: transBlock = obj.parent_component()._algebraic_constraint().\ parent_block() else: transBlock = self._add_transformation_block(obj.parent_block()) # create or fetch the xor constraint xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) xor = obj.xor or_expr = 0 # Just because it's unlikely this is what someone meant to do... if len(obj.disjuncts) == 0: raise GDP_Error("Disjunction '%s' is empty. This is " "likely indicative of a modeling error." % obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)) for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var # make suffix list. (We don't need it until we are # transforming constraints, but it gets created at the # disjunct level, so more efficient to make it here and # pass it down.) suffix_list = self._get_bigm_suffix_list(disjunct) arg_list = self._get_bigm_arg_list(bigM, disjunct) # relax the disjunct self._transform_disjunct(disjunct, transBlock, bigM, arg_list, suffix_list) # add or (or xor) constraint if xor: xorConstraint[index] = or_expr == 1 else: xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) # and deactivate for the writers obj.deactivate() def _transform_disjunct(self, obj, transBlock, bigM, arg_list, suffix_list): # deactivated -> either we've already transformed or user deactivated if not obj.active: if obj.indicator_var.is_fixed(): if value(obj.indicator_var) == 0: # The user cleanly deactivated the disjunct: there # is nothing for us to do here. return else: raise GDP_Error( "The disjunct '%s' is deactivated, but the " "indicator_var is fixed to %s. This makes no sense." % ( obj.name, value(obj.indicator_var) )) if obj._transformation_block is None: raise GDP_Error( "The disjunct '%s' is deactivated, but the " "indicator_var is not fixed and the disjunct does not " "appear to have been relaxed. This makes no sense. " "(If the intent is to deactivate the disjunct, fix its " "indicator_var to 0.)" % ( obj.name, )) if obj._transformation_block is not None: # we've transformed it, which means this is the second time it's # appearing in a Disjunction raise GDP_Error( "The disjunct '%s' has been transformed, but a disjunction " "it appears in has not. Putting the same disjunct in " "multiple disjunctions is not supported." % obj.name) # add reference to original disjunct on transformation block relaxedDisjuncts = transBlock.relaxedDisjuncts relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] # we will keep a map of constraints (hashable, ha!) to a tuple to # indicate what their M value is and where it came from, of the form: # ((lower_value, lower_source, lower_key), (upper_value, upper_source, # upper_key)), where the first tuple is the information for the lower M, # the second tuple is the info for the upper M, source is the Suffix or # argument dictionary and None if the value was calculated, and key is # the key in the Suffix or argument dictionary, and None if it was # calculated. (Note that it is possible the lower or upper is # user-specified and the other is not, hence the need to store # information for both.) relaxationBlock.bigm_src = {} relaxationBlock.localVarReferences = Block() obj._transformation_block = weakref_ref(relaxationBlock) relaxationBlock._srcDisjunct = weakref_ref(obj) # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for Hull, as it uses the component_objects / # component_data_objects generators. For BigM, that is OK, # because we never use those generators with active=True. I am # only noting it here for the future when someone (me?) is # comparing the two relaxations. # # Transform each component within this disjunct self._transform_block_components(obj, obj, bigM, arg_list, suffix_list) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_block_components(self, block, disjunct, bigM, arg_list, suffix_list): # Find all the variables declared here (including the indicator_var) and # add a reference on the transformation block so these will be # accessible when the Disjunct is deactivated. We don't descend into # Disjuncts because we'll just reference the references which are # already on their transformation blocks. disjunctBlock = disjunct._transformation_block() varRefBlock = disjunctBlock.localVarReferences for v in block.component_objects(Var, descend_into=Block, active=None): varRefBlock.add_component(unique_component_name( varRefBlock, v.getname(fully_qualified=True, name_buffer=NAME_BUFFER)), Reference(v)) # Now need to find any transformed disjunctions that might be here # because we need to move their transformation blocks up onto the parent # block before we transform anything else on this block destinationBlock = disjunctBlock.parent_block() for obj in block.component_data_objects( Disjunction, sort=SortComponents.deterministic, descend_into=(Block)): if obj.algebraic_constraint is None: # This could be bad if it's active since that means its # untransformed, but we'll wait to yell until the next loop continue # get this disjunction's relaxation block. transBlock = obj.algebraic_constraint().parent_block() # move transBlock up to parent component self._transfer_transBlock_data(transBlock, destinationBlock) # we leave the transformation block because it still has the XOR # constraints, which we want to be on the parent disjunct. # Now look through the component map of block and transform everything # we have a handler for. Yell if we don't know how to handle it. (Note # that because we only iterate through active components, this means # non-ActiveComponent types cannot have handlers.) for obj in block.component_objects(active=True, descend_into=False): handler = self.handlers.get(obj.ctype, None) if not handler: if handler is None: raise GDP_Error( "No BigM transformation handler registered " "for modeling components of type %s. If your " "disjuncts contain non-GDP Pyomo components that " "require transformation, please transform them first." % obj.ctype) continue # obj is what we are transforming, we pass disjunct # through so that we will have access to the indicator # variables down the line. handler(obj, disjunct, bigM, arg_list, suffix_list) def _transfer_transBlock_data(self, fromBlock, toBlock): # We know that we have a list of transformed disjuncts on both. We need # to move those over. We know the XOR constraints are on the block, and # we need to leave those on the disjunct. disjunctList = toBlock.relaxedDisjuncts to_delete = [] for idx, disjunctBlock in fromBlock.relaxedDisjuncts.items(): newblock = disjunctList[len(disjunctList)] newblock.transfer_attributes_from(disjunctBlock) # update the mappings original = disjunctBlock._srcDisjunct() original._transformation_block = weakref_ref(newblock) newblock._srcDisjunct = weakref_ref(original) # save index of what we just moved so that we can delete it to_delete.append(idx) # delete everything we moved. for idx in to_delete: del fromBlock.relaxedDisjuncts[idx] # Note that we could handle other components here if we ever needed # to, but we control what is on the transformation block and # currently everything is on the blocks that we just moved... def _warn_for_active_disjunction(self, disjunction, disjunct, bigMargs, arg_list, suffix_list): _warn_for_active_disjunction(disjunction, disjunct, NAME_BUFFER) def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs, arg_list, suffix_list): _warn_for_active_disjunct(innerdisjunct, outerdisjunct, NAME_BUFFER) def _warn_for_active_logical_statement( self, logical_statment, disjunct, infodict, bigMargs, suffix_list): _warn_for_active_logical_constraint(logical_statment, disjunct, NAME_BUFFER) def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list, suffix_list): # We look through everything on the component map of the block # and transform it just as we would if it was on the disjunct # directly. (We are passing the disjunct through so that when # we find constraints, _xform_constraint will have access to # the correct indicator variable.) for i in sorted(block.keys()): self._transform_block_components( block[i], disjunct, bigMargs, arg_list, suffix_list) def _get_constraint_map_dict(self, transBlock): if not hasattr(transBlock, "_constraintMap"): transBlock._constraintMap = { 'srcConstraints': ComponentMap(), 'transformedConstraints': ComponentMap()} return transBlock._constraintMap def _convert_M_to_tuple(self, M, constraint_name): if not isinstance(M, (tuple, list)): if M is None: M = (None, None) else: try: M = (-M, M) except: logger.error("Error converting scalar M-value %s " "to (-M,M). Is %s not a numeric type?" % (M, type(M))) raise if len(M) != 2: raise GDP_Error("Big-M %s for constraint %s is not of " "length two. " "Expected either a single value or " "tuple or list of length two for M." % (str(M), constraint_name)) return M def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src constraintMap = self._get_constraint_map_dict(transBlock) disjunctionRelaxationBlock = transBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER) name = unique_component_name(transBlock, cons_name) if obj.is_indexed(): newConstraint = Constraint(obj.index_set(), disjunctionRelaxationBlock.lbub) # we map the container of the original to the container of the # transformed constraint. Don't do this if obj is a SimpleConstraint # because we will treat that like a _ConstraintData and map to a # list of transformed _ConstraintDatas constraintMap['transformedConstraints'][obj] = newConstraint else: newConstraint = Constraint(disjunctionRelaxationBlock.lbub) transBlock.add_component(name, newConstraint) # add mapping of transformed constraint to original constraint constraintMap['srcConstraints'][newConstraint] = obj for i in sorted(obj.keys()): c = obj[i] if not c.active: continue lower = (None, None, None) upper = (None, None, None) # first, we see if an M value was specified in the arguments. # (This returns None if not) lower, upper = self._get_M_from_args(c, bigMargs, arg_list, lower, upper) M = (lower[0], upper[0]) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "from the BigM argument is %s." % (cons_name, str(M))) # if we didn't get something we need from args, try suffixes: if (M[0] is None and c.lower is not None) or \ (M[1] is None and c.upper is not None): # first get anything parent to c but below disjunct suffix_list = self._get_bigm_suffix_list(c.parent_block(), stopping_block=disjunct) # prepend that to what we already collected for the disjunct. suffix_list.extend(disjunct_suffix_list) lower, upper = self._update_M_from_suffixes(c, suffix_list, lower, upper) M = (lower[0], upper[0]) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "after checking suffixes is %s." % (cons_name, str(M))) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) lower = (M[0], None, None) if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) upper = (M[1], None, None) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "after estimating (if needed) is %s." % (cons_name, str(M))) # save the source information bigm_src[c] = (lower, upper) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb',) i_ub = i + ('ub',) elif obj.is_indexed(): i_lb = (i, 'lb',) i_ub = (i, 'ub',) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint '%s' " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c. body - M_expr) constraintMap[ 'transformedConstraints'][c] = [newConstraint[i_lb]] constraintMap['srcConstraints'][newConstraint[i_lb]] = c if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint '%s' " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: constraintMap['transformedConstraints'][ c].append(newConstraint[i_ub]) else: constraintMap[ 'transformedConstraints'][c] = [newConstraint[i_ub]] constraintMap['srcConstraints'][newConstraint[i_ub]] = c # deactivate because we relaxed c.deactivate() def _process_M_value(self, m, lower, upper, need_lower, need_upper, src, key, constraint_name, from_args=False): m = self._convert_M_to_tuple(m, constraint_name) if need_lower and m[0] is not None: if from_args: self.used_args[key] = m lower = (m[0], src, key) need_lower = False if need_upper and m[1] is not None: if from_args: self.used_args[key] = m upper = (m[1], src, key) need_upper = False return lower, upper, need_lower, need_upper def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper): # check args: we first look in the keys for constraint and # constraintdata. In the absence of those, we traverse up the blocks, # and as a last resort check for a value for None if bigMargs is None: return (lower, upper) # since we check for args first, we know lower[0] and upper[0] are both # None need_lower = constraint.lower is not None need_upper = constraint.upper is not None constraint_name = constraint.getname(fully_qualified=True, name_buffer=NAME_BUFFER) # check for the constraint itself and its container parent = constraint.parent_component() if constraint in bigMargs: m = bigMargs[constraint] (lower, upper, need_lower, need_upper) = self._process_M_value(m, lower, upper, need_lower, need_upper, bigMargs, constraint, constraint_name, from_args=True) if not need_lower and not need_upper: return lower, upper elif parent in bigMargs: m = bigMargs[parent] (lower, upper, need_lower, need_upper) = self._process_M_value(m, lower, upper, need_lower, need_upper, bigMargs, parent, constraint_name, from_args=True) if not need_lower and not need_upper: return lower, upper # use the precomputed traversal up the blocks for arg in arg_list: for block, val in arg.items(): (lower, upper, need_lower, need_upper) = self._process_M_value(val, lower, upper, need_lower, need_upper, bigMargs, block, constraint_name, from_args=True) if not need_lower and not need_upper: return lower, upper # last check for value for None! if None in bigMargs: m = bigMargs[None] (lower, upper, need_lower, need_upper) = self._process_M_value(m, lower, upper, need_lower, need_upper, bigMargs, None, constraint_name, from_args=True) if not need_lower and not need_upper: return lower, upper return lower, upper def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper): # It's possible we found half the answer in args, but we are still # looking for half the answer. need_lower = constraint.lower is not None and lower[0] is None need_upper = constraint.upper is not None and upper[0] is None constraint_name = constraint.getname(fully_qualified=True, name_buffer=NAME_BUFFER) M = None # first we check if the constraint or its parent is a key in any of the # suffix lists for bigm in suffix_list: if constraint in bigm: M = bigm[constraint] (lower, upper, need_lower, need_upper) = self._process_M_value(M, lower, upper, need_lower, need_upper, bigm, constraint, constraint_name) if not need_lower and not need_upper: return lower, upper # if c is indexed, check for the parent component if constraint.parent_component() in bigm: parent = constraint.parent_component() M = bigm[parent] (lower, upper, need_lower, need_upper) = self._process_M_value(M, lower, upper, need_lower, need_upper, bigm, parent, constraint_name) if not need_lower and not need_upper: return lower, upper # if we didn't get an M that way, traverse upwards through the blocks # and see if None has a value on any of them. if M is None: for bigm in suffix_list: if None in bigm: M = bigm[None] (lower, upper, need_lower, need_upper) = self._process_M_value(M, lower, upper, need_lower, need_upper, bigm, None, constraint_name) if not need_lower and not need_upper: return lower, upper return lower, upper def _estimate_M(self, expr, name): # If there are fixed variables here, unfix them for this calculation, # and we'll restore them at the end. fixed_vars = ComponentMap() if not self.assume_fixed_vars_permanent: for v in EXPR.identify_variables(expr, include_fixed=True): if v.fixed: fixed_vars[v] = value(v) v.fixed = False # Calculate a best guess at M repn = generate_standard_repn(expr, quadratic=False) M = [0, 0] if not repn.is_nonlinear(): if repn.constant is not None: for i in (0, 1): if M[i] is not None: M[i] += repn.constant for i, coef in enumerate(repn.linear_coefs or []): var = repn.linear_vars[i] bounds = (value(var.lb), value(var.ub)) for i in (0, 1): # reverse the bounds if the coefficient is negative if coef > 0: j = i else: j = 1 - i if bounds[i] is not None: M[j] += value(bounds[i]) * coef else: raise GDP_Error( "Cannot estimate M for " "expressions with unbounded variables." "\n\t(found unbounded var '%s' while processing " "constraint '%s')" % (var.name, name)) else: # expression is nonlinear. Try using `contrib.fbbt` to estimate. expr_lb, expr_ub = compute_bounds_on_expr(expr) if expr_lb is None or expr_ub is None: raise GDP_Error("Cannot estimate M for unbounded nonlinear " "expressions.\n\t(found while processing " "constraint '%s')" % name) else: M = (expr_lb, expr_ub) # clean up if we unfixed things (fixed_vars is empty if we were assuming # fixed vars are fixed for life) for v, val in fixed_vars.items(): v.fix(val) return tuple(M) # These are all functions to retrieve transformed components from # original ones and vice versa. @wraps(get_src_disjunct) def get_src_disjunct(self, transBlock): return get_src_disjunct(transBlock) @wraps(get_src_disjunction) def get_src_disjunction(self, xor_constraint): return get_src_disjunction(xor_constraint) @wraps(get_src_constraint) def get_src_constraint(self, transformedConstraint): return get_src_constraint(transformedConstraint) @wraps(get_transformed_constraints) def get_transformed_constraints(self, srcConstraint): return get_transformed_constraints(srcConstraint) @deprecated("The get_m_value_src function is deprecated. Use " "the get_M_value_src function is you need source " "information or the get_M_value function if you " "only need values.", version='5.7.1') def get_m_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) ((lower_val, lower_source, lower_key), (upper_val, upper_source, upper_key)) = transBlock.bigm_src[constraint] if constraint.lower is not None and constraint.upper is not None and \ (not lower_source is upper_source or not lower_key is upper_key): raise GDP_Error("This is why this method is deprecated: The lower " "and upper M values for constraint %s came from " "different sources, please use the get_M_value_src " "method." % constraint.name) # if source and key are equal for the two, this is representable in the # old format. if constraint.lower is not None and lower_source is not None: return (lower_source, lower_key) if constraint.upper is not None and upper_source is not None: return (upper_source, upper_key) # else it was calculated: return (lower_val, upper_val) def get_M_value_src(self, constraint): """Return a tuple indicating how the M value used to transform constraint was specified. (In particular, this can be used to verify which BigM Suffixes were actually necessary to the transformation.) Return is of the form: ((lower_M_val, lower_M_source, lower_M_key), (upper_M_val, upper_M_source, upper_M_key)) If the constraint does not have a lower bound (or an upper bound), the first (second) element will be (None, None, None). Note that if a constraint is of the form a <= expr <= b or is an equality constraint, it is not necessarily true that the source of lower_M and upper_M are the same. If the M value came from an arg, source is the dictionary itself and key is the key in that dictionary which gave us the M value. If the M value came from a Suffix, source is the BigM suffix used and key is the key in that Suffix. If the transformation calculated the value, both source and key are None. Parameters ---------- constraint: Constraint, which must be in the subtree of a transformed Disjunct """ transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) return transBlock.bigm_src[constraint] def get_M_value(self, constraint): """Returns the M values used to transform constraint. Return is a tuple: (lower_M_value, upper_M_value). Either can be None if constraint does not have a lower or upper bound, respectively. Parameters ---------- constraint: Constraint, which must be in the subtree of a transformed Disjunct """ transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) lower, upper = transBlock.bigm_src[constraint] return (lower[0], upper[0])
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 TranslatorData(UnitModelBlockData): """ Standard Translator Block Class """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Translator blocks are always steady-state.""", ), ) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Translator blocks do not contain holdup.""", ), ) CONFIG.declare( "outlet_state_defined", ConfigValue( default=True, domain=In([True, False]), description="Indicated whether outlet state will be fully defined", doc="""Indicates whether unit model will fully define outlet state. If False, the outlet property package will enforce constraints such as sum of mole fractions and phase equilibrium. **default** - True. **Valid values:** { **True** - outlet state will be fully defined, **False** - outlet property package should enforce sumation and equilibrium constraints.}""", ), ) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=In([True, False]), description="Indicates whether outlet is in phase equilibrium", doc="""Indicates whether outlet property package should enforce phase equilibrium constraints. **default** - False. **Valid values:** { **True** - outlet property package should calculate phase equilibrium, **False** - outlet property package should notcalculate phase equilibrium.} """, ), ) CONFIG.declare( "inlet_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for incoming stream", doc="""Property parameter object used to define property calculations for the incoming stream, **default** - None. **Valid values:** { **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "inlet_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package " "of the incoming stream", doc="""A ConfigBlock with arguments to be passed to the property block associated with the incoming stream, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "outlet_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for outgoing stream", doc="""Property parameter object used to define property calculations for the outgoing stream, **default** - None. **Valid values:** { **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "outlet_property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package " "of the outgoing stream", doc="""A ConfigBlock with arguments to be passed to the property block associated with the outgoing stream, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) def build(self): """ Begin building model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(TranslatorData, self).build() # Check construction argumnet consistency if (self.config.outlet_state_defined and self.config.has_phase_equilibrium): raise ConfigurationError( "{} cannot calcuate phase equilibrium (has_phase_equilibrium " "= True) when outlet state is set to be fully defined (" "outlet_state_defined = True).".format(self.name)) # Add State Blocks self.properties_in = self.config.inlet_property_package.build_state_block( self.flowsheet().config.time, doc="Material properties in incoming stream", default={ "defined_state": True, "has_phase_equilibrium": False, **self.config.inlet_property_package_args, }, ) self.properties_out = self.config.outlet_property_package.build_state_block( self.flowsheet().config.time, doc="Material properties in outgoing stream", default={ "defined_state": self.config.outlet_state_defined, "has_phase_equilibrium": self.config.has_phase_equilibrium, **self.config.outlet_property_package_args, }, ) # Add outlet port self.add_port(name="inlet", block=self.properties_in, doc="Inlet Port") self.add_port(name="outlet", block=self.properties_out, doc="Outlet Port") def initialize( blk, state_args_in=None, state_args_out=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ This method calls the initialization method of the state blocks. Keyword Arguments: state_args_in : a dict of arguments to be passed to the inlet property package (to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_out : a dict of arguments to be passed to the outlet property package (to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize state block flags = blk.properties_in.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_in, hold_state=True, ) blk.properties_out.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_out, ) if degrees_of_freedom(blk) == 0: with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info("Initialization Complete {}.".format( idaeslog.condition(res))) else: init_log.warning("Initialization incomplete. Degrees of freedom " "were not zero. Please provide sufficient number " "of constraints linking the state variables " "between the two state blocks.") blk.properties_in.release_state(flags=flags, outlvl=outlvl)
class 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 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 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 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)
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 GDPbbSolver(object): """ A branch and bound-based solver for Generalized Disjunctive Programming (GDP) problems The GDPbb solver solves subproblems relaxing certain disjunctions, and builds up a tree of potential active disjunctions. By exploring promising branches, it eventually results in an optimal configuration of disjunctions. Keyword arguments below are specified for the ``solve`` function. """ CONFIG = ConfigBlock("gdpbb") CONFIG.declare( "solver", ConfigValue(default="baron", description="Subproblem solver to use, defaults to baron")) CONFIG.declare( "solver_args", ConfigBlock( implicit=True, description="Block of keyword arguments to pass to the solver.")) CONFIG.declare( "tee", ConfigValue(default=False, domain=bool, description="Flag to stream solver output to console.")) CONFIG.declare( "check_sat", ConfigValue( default=False, domain=bool, description= "When True, GDPBB will check satisfiability via the pyomo.contrib.satsolver interface at each node" )) CONFIG.declare( "logger", ConfigValue( default='pyomo.contrib.gdpbb', description="The logger object or name to use for reporting.", domain=a_logger)) CONFIG.declare( "time_limit", ConfigValue( default=600, domain=PositiveInt, description="Time limit (seconds, default=600)", doc="Seconds allowed until terminated. Note that the time limit can" "currently only be enforced between subsolver invocations. You may" "need to set subsolver time limits as well.")) @deprecated("GDPbb has been merged into GDPopt. " "You can use the algorithm using GDPopt with strategy='LBB'.", logger="pyomo.solvers", version='5.6.9') def __init__(self, *args, **kwargs): super(GDPbbSolver, self).__init__(*args, **kwargs) def available(self, exception_flag=True): """Check if solver is available. TODO: For now, it is always available. However, sub-solvers may not always be available, and so this should reflect that possibility. """ return True def version(self): return __version__ def solve(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) return SolverFactory('gdpopt').solve( model, strategy='LBB', minlp_solver=config.solver, minlp_solver_args=config.solver_args, tee=config.tee, check_sat=config.check_sat, logger=config.logger, time_limit=config.time_limit) # Validate model to be used with gdpbb self.validate_model(model) # Set solver as an MINLP solve_data = GDPbbSolveData() solve_data.timing = Container() solve_data.original_model = model solve_data.results = SolverResults() old_logger_level = config.logger.getEffectiveLevel() with time_code(solve_data.timing, 'total', is_main_timer=True), \ restore_logger_level(config.logger), \ create_utility_block(model, 'GDPbb_utils', solve_data): if config.tee and old_logger_level > logging.INFO: # If the logger does not already include INFO, include it. config.logger.setLevel(logging.INFO) config.logger.info( "Starting GDPbb version %s using %s as subsolver" % (".".join(map(str, self.version())), config.solver)) # Setup results solve_data.results.solver.name = 'GDPbb - %s' % (str( config.solver)) setup_results_object(solve_data, config) # clone original model for root node of branch and bound root = solve_data.working_model = solve_data.original_model.clone() # get objective sense process_objective(solve_data, config) objectives = solve_data.original_model.component_data_objects( Objective, active=True) obj = next(objectives, None) solve_data.results.problem.sense = obj.sense # set up lists to keep track of which disjunctions have been covered. # this list keeps track of the relaxed disjunctions root.GDPbb_utils.unenforced_disjunctions = list( disjunction for disjunction in root.GDPbb_utils.disjunction_list if disjunction.active) root.GDPbb_utils.deactivated_constraints = ComponentSet([ constr for disjunction in root.GDPbb_utils.unenforced_disjunctions for disjunct in disjunction.disjuncts for constr in disjunct.component_data_objects(ctype=Constraint, active=True) if constr.body.polynomial_degree() not in (1, 0) ]) # Deactivate nonlinear constraints in unenforced disjunctions for constr in root.GDPbb_utils.deactivated_constraints: constr.deactivate() # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation. if not hasattr(root, 'BigM'): root.BigM = Suffix() # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed # to True/False values. # TODO this should also be done within the loop, but we aren't handling it right now. # Should affect efficiency, but not correctness. root.GDPbb_utils.disjuncts_fixed_True = ComponentSet() # Only find top-level (non-nested) disjunctions for disjunction in root.component_data_objects(Disjunction, active=True): fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 1 ] fixed_false_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct.indicator_var.fixed and disjunct.indicator_var.value == 0 ] for disjunct in fixed_false_disjuncts: disjunct.deactivate() if len(fixed_false_disjuncts) == len( disjunction.disjuncts) - 1: # all but one disjunct in the disjunction is fixed to False. Remaining one must be true. if not fixed_true_disjuncts: fixed_true_disjuncts = [ disjunct for disjunct in disjunction.disjuncts if disjunct not in fixed_false_disjuncts ] # Reactivate the fixed-true disjuncts for disjunct in fixed_true_disjuncts: newly_activated = ComponentSet() for constr in disjunct.component_data_objects(Constraint): if constr in root.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint root.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. root.GDPbb_utils.deactivated_constraints -= newly_activated root.GDPbb_utils.disjuncts_fixed_True.add(disjunct) if fixed_true_disjuncts: assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (disjunction.name, ) root.GDPbb_utils.unenforced_disjunctions.remove( disjunction) # Check satisfiability if config.check_sat and satisfiable(root, config.logger) is False: # Problem is not satisfiable. Problem is infeasible. obj_value = obj_sign * float('inf') else: # solve the root node config.logger.info("Solving the root node.") obj_value, result, var_values = self.subproblem_solve( root, config) if obj_sign * obj_value == float('inf'): config.logger.info( "Model was found to be infeasible at the root node. Elapsed %.2f seconds." % get_main_elapsed_time(solve_data.timing)) if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = float('inf') solve_data.results.problem.upper_bound = None else: solve_data.results.problem.lower_bound = None solve_data.results.problem.upper_bound = float('-inf') solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = 0 solve_data.results.solver.termination_condition = tc.infeasible return solve_data.results # initialize minheap for Branch and Bound algorithm # Heap structure: (ordering tuple, model) # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter) # - select solutions with lower objective value, # then fewer disjunctions left to explore (depth first), # then more recently encountered (tiebreaker) heap = [] total_nodes_counter = 0 disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions) heapq.heappush(heap, ((obj_sign * obj_value, disjunctions_left, -total_nodes_counter), root, result, var_values)) # loop to branch through the tree while len(heap) > 0: # pop best model off of heap sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop( heap) incumbent_obj_value, disjunctions_left, _ = sort_tuple config.logger.info( "Exploring node with LB %.10g and %s inactive disjunctions." % (incumbent_obj_value, disjunctions_left)) # if all the originally active disjunctions are active, solve and # return solution if disjunctions_left == 0: config.logger.info("Model solved.") # Model is solved. Copy over solution values. original_model = solve_data.original_model for orig_var, val in zip( original_model.GDPbb_utils.variable_list, incumbent_var_values): orig_var.value = val solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition return solve_data.results # Pick the next disjunction to branch on next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[ 0] config.logger.info("Branching on disjunction %s" % next_disjunction.name) assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \ "%s violates this assumption." % (next_disjunction.name, ) new_nodes_counter = 0 for i, disjunct in enumerate(next_disjunction.disjuncts): # Create one branch for each of the disjuncts on the disjunction if any(disj.indicator_var.fixed and disj.indicator_var.value == 1 for disj in next_disjunction.disjuncts if disj is not disjunct): # If any other disjunct is fixed to 1 and an xor relationship applies, # then this disjunct cannot be activated. continue # Check time limit if get_main_elapsed_time( solve_data.timing) >= config.time_limit: if solve_data.results.problem.sense == minimize: solve_data.results.problem.lower_bound = incumbent_obj_value solve_data.results.problem.upper_bound = float( 'inf') else: solve_data.results.problem.lower_bound = float( '-inf') solve_data.results.problem.upper_bound = incumbent_obj_value config.logger.info('GDPopt unable to converge bounds ' 'before time limit of {} seconds. ' 'Elapsed: {} seconds'.format( config.time_limit, get_main_elapsed_time( solve_data.timing))) config.logger.info( 'Final bound values: LB: {} UB: {}'.format( solve_data.results.problem.lower_bound, solve_data.results.problem.upper_bound)) solve_data.results.solver.timing = solve_data.timing solve_data.results.solver.iterations = total_nodes_counter solve_data.results.solver.termination_condition = tc.maxTimeLimit return solve_data.results # Branch on the disjunct child = incumbent_model.clone() # TODO I am leaving the old branching system in place, but there should be # something better, ideally that deals with nested disjunctions as well. disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop( 0) child_disjunct = disjunction_to_branch.disjuncts[i] child_disjunct.indicator_var.fix(1) # Deactivate (and fix to 0) other disjuncts on the disjunction for disj in disjunction_to_branch.disjuncts: if disj is not child_disjunct: disj.deactivate() # Activate nonlinear constraints on the newly fixed child disjunct newly_activated = ComponentSet() for constr in child_disjunct.component_data_objects( Constraint): if constr in child.GDPbb_utils.deactivated_constraints: newly_activated.add(constr) constr.activate() # Set the big M value for the constraint child.BigM[constr] = 1 # Note: we use a default big M value of 1 # because all non-selected disjuncts should be deactivated. # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed. # The default M value should therefore be irrelevant. child.GDPbb_utils.deactivated_constraints -= newly_activated child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct) if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True: # If the disjunct was already branched to True from a parent disjunct branching, just pass # through the incumbent value without resolving. The solution should be the same as the parent. total_nodes_counter += 1 ordering_tuple = (obj_sign * incumbent_obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, incumbent_var_values)) new_nodes_counter += 1 continue if config.check_sat and satisfiable( child, config.logger) is False: # Problem is not satisfiable. Skip this disjunct. continue obj_value, result, var_values = self.subproblem_solve( child, config) total_nodes_counter += 1 ordering_tuple = (obj_sign * obj_value, disjunctions_left - 1, -total_nodes_counter) heapq.heappush(heap, (ordering_tuple, child, result, var_values)) new_nodes_counter += 1 config.logger.info( "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s." % (new_nodes_counter, disjunctions_left - 1, len(heap))) @staticmethod def validate_model(model): # Validates that model has only exclusive disjunctions for d in model.component_data_objects(ctype=Disjunction, active=True): if not d.xor: raise ValueError('GDPbb solver unable to handle ' 'non-exclusive disjunctions') objectives = model.component_data_objects(Objective, active=True) obj = next(objectives, None) if next(objectives, None) is not None: raise RuntimeError( "GDPbb solver is unable to handle model with multiple active objectives." ) if obj is None: raise RuntimeError( "GDPbb solver is unable to handle model with no active objective." ) @staticmethod def subproblem_solve(gdp, config): subproblem = gdp.clone() TransformationFactory('gdp.bigm').apply_to(subproblem) main_obj = next( subproblem.component_data_objects(Objective, active=True)) obj_sign = 1 if main_obj.sense == minimize else -1 try: result = SolverFactory(config.solver).solve( subproblem, **config.solver_args) except RuntimeError as e: config.logger.warning( "Solver encountered RuntimeError. Treating as infeasible. " "Msg: %s\n%s" % (str(e), traceback.format_exc())) var_values = [ v.value for v in subproblem.GDPbb_utils.variable_list ] return obj_sign * float('inf'), SolverResults(), var_values var_values = [v.value for v in subproblem.GDPbb_utils.variable_list] term_cond = result.solver.termination_condition if result.solver.status is SolverStatus.ok and any( term_cond == valid_cond for valid_cond in (tc.optimal, tc.locallyOptimal, tc.feasible)): return value(main_obj.expr), result, var_values elif term_cond == tc.unbounded: return obj_sign * float('-inf'), result, var_values elif term_cond == tc.infeasible: return obj_sign * float('inf'), result, var_values else: config.logger.warning("Unknown termination condition of %s" % term_cond) return obj_sign * float('inf'), result, var_values def __enter__(self): return self def __exit__(self, t, v, traceback): pass
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 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)