class IsentropicPerformanceCurveData(ProcessBlockData): """Block that holds performance curves. Typically these are in the form of constraints that relate head, efficiency, or pressure ratio to volumetric or mass flow. Additional varaibles can be included if needed, such as speed. For convenience an option is provided to add head expressions to the block. performance curves, and any additional variables, constraints, or expressions can be added to this block either via callback provided to the configuration, or after the model is constructued.""" CONFIG = ProcessBlockData.CONFIG( doc="Configuration dictionary for the performance curve block.") CONFIG.declare("build_callback", ConfigValue( default=None, doc="Optional callback to add performance curve constraints")) CONFIG.declare("build_head_expressions", ConfigValue( default=True, domain=bool, doc="If true add expressions for 'head' and 'head_isentropic'." " These expressions can be used in performance curve constraints.")) def has_constraints(self): for o in self.component_data_objects(Constraint): return True return False def build(self): super().build() if self.config.build_head_expressions: try: @self.Expression(self.flowsheet().config.time) def head_isentropic(b, t): # units are energy/mass b = b.parent_block() if hasattr(b.control_volume.properties_in[t], "flow_mass"): return (b.work_isentropic[t] / b.control_volume.properties_in[t].flow_mass) else: return (b.work_isentropic[t] / b.control_volume.properties_in[t].flow_mol / b.control_volume.properties_in[t].mw) @self.Expression(self.flowsheet().config.time) def head(b, t): # units are energy/mass b = b.parent_block() if hasattr(b.control_volume.properties_in[t], "flow_mass"): return (b.work_mechanical[t] / b.control_volume.properties_in[t].flow_mass) else: return (b.work_mechanical[t] / b.control_volume.properties_in[t].flow_mol / b.control_volume.properties_in[t].mw) except PropertyNotSupportedError: _log.exception( "flow_mass or flow_mol and mw are not supported by the" " property package but are required for isentropic pressure" " changer head calculation") raise if self.config.build_callback is not None: self.config.build_callback(self)
class PhysicalParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for thermophysical parameter blocks. These are blocks that contain a set of parameters associated with a specific thermophysical property package, and are linked to by all instances of that property package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "default_arguments", ConfigBlock( implicit=True, description="Default arguments to use with Property Package")) def build(self): """ General build method for PropertyParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(PhysicalParameterBlock, self).build()
class PhysicalParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for thermophysical parameter blocks. These are blocks that contain a set of parameters associated with a specific thermophysical property package, and are linked to by all instances of that property package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "default_arguments", ConfigBlock( implicit=True, description="Default arguments to use with Property Package")) def build(self): """ General build method for PropertyParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(PhysicalParameterBlock, self).build() def get_phase_component_set(self): """ Method to get phase-component set for property package. If a phase- component set has not been constructed yet, this method will construct one. Args: None Returns: Phase-component Set object """ try: return self._phase_component_set except AttributeError: # Phase-component set does not exist, so create one. pc_set = [] if hasattr(self.config, "phase_component_list"): for p in self.config.phase_component_list: for j in self.config.phase_component_list[p]: pc_set.append((p, j)) else: # Otherwise assume all components in all phases for p in self.phase_list: for j in self.component_list: pc_set.append((p, j)) self._phase_component_set = Set(initialize=pc_set, ordered=True) return self._phase_component_set
class MyBlockData(ProcessBlockData): CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("xinit", ConfigValue(default=1001, domain=float)) CONFIG.declare("yinit", ConfigValue(default=1002, domain=float)) def build(self): super(MyBlockData, self).build() self.x = Var(initialize=self.config.xinit) self.y = Var(initialize=self.config.yinit)
class FlowsheetBlockData(ProcessBlockData): """ The FlowsheetBlockData Class forms the base class for all IDAES process flowsheet models. The main purpose of this class is to automate the tasks common to all flowsheet models and ensure that the necessary attributes of a flowsheet model are present. The most signfiicant role of the FlowsheetBlockData class is to automatically create the time domain for the flowsheet. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "dynamic", ConfigValue(default=useDefault, domain=In([useDefault, True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent or False, **True** - set as a dynamic model, **False** - set as a steady-state model.}""")) CONFIG.declare( "time", ConfigValue( default=None, domain=is_time_domain, description="Flowsheet time domain", doc= """Pointer to the time domain for the flowsheet. Users may provide an existing time domain from another flowsheet, otherwise the flowsheet will search for a parent with a time domain or create a new time domain and reference it here.""")) CONFIG.declare( "time_set", ConfigValue( default=[0], domain=list_of_floats, description="Set of points for initializing time domain", doc="""Set of points for initializing time domain. This should be a list of floating point numbers, **default** - [0].""")) CONFIG.declare( "default_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Default property package to use in flowsheet", doc="""Indicates the default property package to be used by models within this flowsheet if not otherwise specified, **default** - None. **Valid values:** { **None** - no default property package, **a ParameterBlock object**.}""")) def build(self): """ General build method for FlowsheetBlockData. This method calls a number of sub-methods which automate the construction of expected attributes of flowsheets. Inheriting models should call `super().build`. Args: None Returns: None """ super(FlowsheetBlockData, self).build() # Set up dynamic flag and time domain self._setup_dynamics() def is_flowsheet(self): """ Method which returns True to indicate that this component is a flowsheet. Args: None Returns: True """ return True # TODO [Qi]: this should be implemented as a transformation def model_check(self): """ This method runs model checks on all unit models in a flowsheet. This method searches for objects which inherit from UnitModelBlockData and executes the model_check method if it exists. Args: None Returns: None """ _log.info("Executing model checks.") for o in self.component_objects(descend_into=False): if isinstance(o, UnitModelBlockData): try: o.model_check() except AttributeError: _log.warning('{} Model/block has no model check. To ' 'correct this, add a model_check method to ' 'the associated unit model class'.format( o.name)) def stream_table(self, true_state=False, time_point=0, orient='columns'): """ Method to generate a stream table by iterating over all Arcs in the flowsheet. Args: true_state : whether the state variables (True) or display variables (False, default) from the StateBlocks should be used in the stream table. time_point : point in the time domain at which to create stream table (default = 0) orient : whether stream should be shown by columns ("columns") or rows ("index") Returns: A pandas dataframe containing stream table information """ dict_arcs = {} for a in self.component_objects(ctype=Arc, descend_into=False): dict_arcs[a.local_name] = a return create_stream_table_dataframe(dict_arcs, time_point=time_point, orient=orient, true_state=true_state) def serialize(self, file_base_name, overwrite=False): """ Serializes the flowsheet and saves it to a file that can be read by the idaes-model-vis jupyter lab extension. :param file_base_name: The file prefix to the .idaes.vis file produced. The file is created/saved in the directory that you ran from Jupyter Lab. :param overwrite: Boolean to overwrite an existing file_base_name.idaes.vis. If True, the existing file with the same file_base_name will be overwritten. This will cause you to lose any saved layout. If False and there is an existing file with that file_base_name, you will get an error message stating that you cannot save a file to the file_base_name (and therefore overwriting the saved layout). If there is not an existing file with that file_base_name then it saves as normal. Defaults to False. :return: None """ serializer = FlowsheetSerializer() serializer.serialize(self, file_base_name, overwrite) def get_costing(self, module=costing, year=None): self.costing = pe.Block() module.global_costing_parameters(self.costing, year) def _get_stream_table_contents(self, time_point=0): """ Calls stream_table method and returns result """ return self.stream_table(time_point) def _setup_dynamics(self): # Look for parent flowsheet fs = self.flowsheet() # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == useDefault: if fs is None: # No parent, so default to steady-state and warn user _log.warning('{} is a top level flowsheet, but dynamic flag ' 'set to useDefault. Dynamic ' 'flag set to False by default'.format(self.name)) self.config.dynamic = False else: # Get dynamic flag from parent flowsheet self.config.dynamic = fs.config.dynamic # Check for case when dynamic=True, but parent dynamic=False elif self.config.dynamic is True: if fs is not None and fs.config.dynamic is False: raise DynamicError( '{} trying to declare a dynamic model within ' 'a steady-state flowsheet. This is not ' 'supported by the IDAES framework. Try ' 'creating a dynamic flowsheet instead, and ' 'declaring some models as steady-state.'.format(self.name)) if self.config.time is not None: # Validate user provided time domain if (self.config.dynamic is True and not isinstance(self.config.time, ContinuousSet)): raise DynamicError( '{} was set as a dynamic flowsheet, but time domain ' 'provided was not a ContinuousSet.'.format(self.name)) else: # If no parent flowsheet, set up time domain if fs is None: # Create time domain if self.config.dynamic: # Check if time_set has at least two points if len(self.config.time_set) < 2: # Check if time_set is at default value if self.config.time_set == [0.0]: # If default, set default end point to be 1.0 self.config.time_set = [0.0, 1.0] else: # Invalid user input, raise Excpetion raise DynamicError( "Flowsheet provided with invalid " "time_set attribute - must have at " "least two values (start and end).") # For dynamics, need a ContinuousSet self.time = ContinuousSet(initialize=self.config.time_set) else: # For steady-state, use an ordered Set self.time = pe.Set(initialize=self.config.time_set, ordered=True) # Set time config argument as reference to time domain self.config.time = self.time else: # Set time config argument to parent time self.config.time = fs.config.time
class PhysicalParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for thermophysical parameter blocks. These are blocks that contain a set of parameters associated with a specific thermophysical property package, and are linked to by all instances of that property package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "default_arguments", ConfigBlock( implicit=True, description="Default arguments to use with Property Package")) def build(self): """ General build method for PropertyParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(PhysicalParameterBlock, self).build() # Need this to work with the Helmholtz EoS package if not hasattr(self, "_state_block_class"): self._state_block_class = None # By default, property packages do not include inherent reactions self._has_inherent_reactions = False # This is a dict to store default property scaling factors. They are # defined in the parameter block to provide a universal default for # quantities in a particular kind of state block. For example, you can # set flow scaling once instead of for every state block. Some of these # may be left for the user to set and some may be defined in a property # module where reasonable defaults can be defined a priori. See # set_default_scaling, get_default_scaling, and unset_default_scaling self.default_scaling_factor = {} def set_default_scaling(self, attrbute, value, index=None): """Set a default scaling factor for a property. Args: attribute: property attribute name value: default scaling factor index: for indexed properties, if this is not provied the scaling factor default applies to all indexed elements where specific indexes are no specifcally specified. Returns: None """ self.default_scaling_factor[(attrbute, index)] = value def unset_default_scaling(self, attrbute, index=None): """Remove a previously set default value Args: attribute: property attribute name index: optional index for indexed properties Returns: None """ try: del self.default_scaling_factor[(attrbute, index)] except KeyError: pass def get_default_scaling(self, attrbute, index=None): """ Returns a default scale factor for a property Args: attribute: property attribute name index: optional index for indexed properties Returns: None """ try: # If a specific component data index exists return self.default_scaling_factor[(attrbute, index)] except KeyError: try: # indexed, but no specifc index? return self.default_scaling_factor[(attrbute, None)] except KeyError: # Can't find a default scale factor for what you asked for return None @property def state_block_class(self): if self._state_block_class is not None: return self._state_block_class else: raise AttributeError( "{} has not assigned a StateBlock class to be associated " "with this property package. Please contact the developer of " "the property package.".format(self.name)) @property def has_inherent_reactions(self): return self._has_inherent_reactions def build_state_block(self, *args, **kwargs): """ Methods to construct a StateBlock assoicated with this PhysicalParameterBlock. This will automatically set the parameters construction argument for the StateBlock. Returns: StateBlock """ default = kwargs.pop("default", {}) initialize = kwargs.pop("initialize", {}) if initialize == {}: default["parameters"] = self else: for i in initialize.keys(): initialize[i]["parameters"] = self return self.state_block_class( # pylint: disable=not-callable *args, **kwargs, default=default, initialize=initialize) def get_phase_component_set(self): """ Method to get phase-component set for property package. If a phase- component set has not been constructed yet, this method will construct one. Args: None Returns: Phase-Component Set object """ try: return self._phase_component_set except AttributeError: # Phase-component set does not exist, so create one. pc_set = [] for p in self.phase_list: p_obj = self.get_phase(p) if p_obj.config.component_list is not None: c_list = p_obj.config.component_list else: c_list = self.component_list for j in c_list: pc_set.append((p, j)) self._phase_component_set = Set(initialize=pc_set, ordered=True) return self._phase_component_set def get_component(self, comp): """ Method to retrieve a Component object based on a name from the component_list. Args: comp: name of Component object to retrieve Returns: Component object """ obj = getattr(self, comp) if not isinstance(obj, ComponentData): raise PropertyPackageError( "{} get_component found an attribute {}, but it does not " "appear to be an instance of a Component object.".format( self.name, comp)) return obj def get_phase(self, phase): """ Method to retrieve a Phase object based on a name from the phase_list. Args: phase: name of Phase object to retrieve Returns: Phase object """ obj = getattr(self, phase) if not isinstance(obj, PhaseData): raise PropertyPackageError( "{} get_phase found an attribute {}, but it does not " "appear to be an instance of a Phase object.".format( self.name, phase)) return obj def _validate_parameter_block(self): """ Tries to catch some possible mistakes and provide the user with useful error messages. """ try: # Check names in component list have matching Component objects for c in self.component_list: obj = getattr(self, str(c)) if not isinstance(obj, ComponentData): raise TypeError( "Property package {} has an object {} whose " "name appears in component_list but is not an " "instance of Component".format(self.name, c)) except AttributeError: # No component list raise PropertyPackageError( f"Property package {self.name} has not defined any " f"Components.") try: # Valdiate that names in phase list have matching Phase objects for p in self.phase_list: obj = getattr(self, str(p)) if not isinstance(obj, PhaseData): raise TypeError( "Property package {} has an object {} whose " "name appears in phase_list but is not an " "instance of Phase".format(self.name, p)) except AttributeError: # No phase list raise PropertyPackageError( f"Property package {self.name} has not defined any Phases.") # Also check that the phase-component set has been created. self.get_phase_component_set()
class StateBlockData(ProcessBlockData): """ This is the base class for state block data objects. These are blocks that contain the Pyomo components associated with calculating a set of thermophysical and transport properties for a given material. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "parameters", ConfigValue( domain=is_physical_parameter_block, description="""A reference to an instance of the Property Parameter Block associated with this property package.""")) CONFIG.declare( "defined_state", ConfigValue( default=False, domain=Bool, description="Flag indicating if incoming state is fully defined", doc="""Flag indicating whether the state should be considered fully defined, and thus whether constraints such as sum of mass/mole fractions should be included, **default** - False. **Valid values:** { **True** - state variables will be fully defined, **False** - state variables will not be fully defined.}""")) CONFIG.declare( "has_phase_equilibrium", ConfigValue( default=True, domain=Bool, description="Phase equilibrium constraint flag", doc="""Flag indicating whether phase equilibrium constraints should be constructed in this state block, **default** - True. **Valid values:** { **True** - StateBlock should calculate phase equilibrium, **False** - StateBlock should not calculate phase equilibrium.}""")) def __init__(self, *args, **kwargs): self._lock_attribute_creation = False super().__init__(*args, **kwargs) def lock_attribute_creation_context(self): """Returns a context manager that does not allow attributes to be created while in the context and allows attributes to be created normally outside the context. """ return _lock_attribute_creation_context(self) def is_property_constructed(self, attr): """Returns True if the attribute ``attr`` already exists, or false if it would be added in ``__getattr__``, or does not exist. Args: attr (str): Attribute name to check Return: True if the attribute is already constructed, False otherwise """ with self.lock_attribute_creation_context(): return hasattr(self, attr) @property def component_list(self): return self.parent_component()._return_component_list() @property def phase_list(self): return self.parent_component()._return_phase_list() @property def phase_component_set(self): return self.parent_component()._return_phase_component_set() @property def has_inherent_reactions(self): return self.parent_component()._has_inherent_reactions() @property def include_inherent_reactions(self): return self.parent_component()._include_inherent_reactions() def build(self): """ General build method for StateBlockDatas. Args: None Returns: None """ super(StateBlockData, self).build() add_object_reference(self, "_params", self.config.parameters) # TODO: Deprecate this at some point # Backwards compatability check for old-style property packages self._params._validate_parameter_block() @property def params(self): return self._params def define_state_vars(self): """ Method that returns a dictionary of state variables used in property package. Implement a placeholder method which returns an Exception to force users to overload this. """ raise NotImplementedError('{} property package has not implemented the' ' define_state_vars method. Please contact ' 'the property package developer.') def define_port_members(self): """ Method used to specify components to populate Ports with. Defaults to define_state_vars, and developers should overload as required. """ return self.define_state_vars() def define_display_vars(self): """ Method used to specify components to use to generate stream tables and other outputs. Defaults to define_state_vars, and developers should overload as required. """ return self.define_state_vars() def get_material_flow_terms(self, *args, **kwargs): """ Method which returns a valid expression for material flow to use in the material balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_material_flow_terms method. Please ' 'contact the property package developer.') def get_material_density_terms(self, *args, **kwargs): """ Method which returns a valid expression for material density to use in the material balances . """ raise NotImplementedError('{} property package has not implemented the' ' get_material_density_terms method. Please ' 'contact the property package developer.') def get_material_diffusion_terms(self, *args, **kwargs): """ Method which returns a valid expression for material diffusion to use in the material balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_material_diffusion_terms method. ' 'Please contact the property package ' 'developer.') def get_enthalpy_flow_terms(self, *args, **kwargs): """ Method which returns a valid expression for enthalpy flow to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_enthalpy_flow_terms method. Please ' 'contact the property package developer.') def get_energy_density_terms(self, *args, **kwargs): """ Method which returns a valid expression for enthalpy density to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_energy_density_terms method. Please ' 'contact the property package developer.') def get_energy_diffusion_terms(self, *args, **kwargs): """ Method which returns a valid expression for energy diffusion to use in the energy balances. """ raise NotImplementedError('{} property package has not implemented the' ' get_energy_diffusion_terms method. ' 'Please contact the property package ' 'developer.') def get_material_flow_basis(self, *args, **kwargs): """ Method which returns an Enum indicating the basis of the material flow term. """ return MaterialFlowBasis.other def calculate_bubble_point_temperature(self, *args, **kwargs): """ Method which computes the bubble point temperature for a multi- component mixture given a pressure and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_bubble_point_temperature method.' ' Please contact the property package ' 'developer.') def calculate_dew_point_temperature(self, *args, **kwargs): """ Method which computes the dew point temperature for a multi- component mixture given a pressure and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_dew_point_temperature method.' ' Please contact the property package ' 'developer.') def calculate_bubble_point_pressure(self, *args, **kwargs): """ Method which computes the bubble point pressure for a multi- component mixture given a temperature and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_bubble_point_pressure method.' ' Please contact the property package ' 'developer.') def calculate_dew_point_pressure(self, *args, **kwargs): """ Method which computes the dew point pressure for a multi- component mixture given a temperature and mole fraction. """ raise NotImplementedError('{} property package has not implemented the' ' calculate_dew_point_pressure method.' ' Please contact the property package ' 'developer.') def __getattr__(self, attr): """ This method is used to avoid generating unnecessary property calculations in state blocks. __getattr__ is called whenever a property is called for, and if a propery does not exist, it looks for a method to create the required property, and any associated components. Create a property calculation if needed. Return an attrbute error if attr == 'domain' or starts with a _ . The error for _ prevents a recursion error if trying to get a function to create a property and that function doesn't exist. Pyomo also ocasionally looks for things that start with _ and may not exist. Pyomo also looks for the domain attribute, and it may not exist. This works by creating a property calculation by calling the "_"+attr function. A list of __getattr__ calls is maintained in self.__getattrcalls to check for recursive loops which maybe useful for debugging. This list is cleared after __getattr__ completes successfully. Args: attr: an attribute to create and return. Should be a property component. """ if self._lock_attribute_creation: raise AttributeError( f"{attr} does not exist, and attribute creation is locked.") def clear_call_list(self, attr): """Local method for cleaning up call list when a call is handled. Args: attr: attribute currently being handled """ if self.__getattrcalls[-1] == attr: if len(self.__getattrcalls) <= 1: del self.__getattrcalls else: del self.__getattrcalls[-1] else: raise PropertyPackageError( "{} Trying to remove call {} from __getattr__" " call list, however this is not the most " "recent call in the list ({}). This indicates" " a bug in the __getattr__ calls. Please " "contact the IDAES developers with this bug.".format( self.name, attr, self.__getattrcalls[-1])) # Check that attr is not something we shouldn't touch if attr == "domain" or attr.startswith("_"): # Don't interfere with anything by getting attributes that are # none of my business raise PropertyPackageError( '{} {} does not exist, but is a protected ' 'attribute. Check the naming of your ' 'components to avoid any reserved names'.format( self.name, attr)) if attr == "config": try: self._get_config_args() return self.config except: raise BurntToast("{} getattr method was triggered by a call " "to the config block, but _get_config_args " "failed. This should never happen.") # Check for recursive calls try: # Check if __getattrcalls is initialized self.__getattrcalls except AttributeError: # Initialize it self.__getattrcalls = [attr] else: # Check to see if attr already appears in call list if attr in self.__getattrcalls: # If it does, indicates a recursive loop. if attr == self.__getattrcalls[-1]: # attr method is calling itself self.__getattrcalls.append(attr) raise PropertyPackageError( '{} _{} made a recursive call to ' 'itself, indicating a potential ' 'recursive loop. This is generally ' 'caused by the {} method failing to ' 'create the {} component.'.format( self.name, attr, attr, attr)) else: self.__getattrcalls.append(attr) raise PropertyPackageError( '{} a potential recursive loop has been ' 'detected whilst trying to construct {}. ' 'A method was called, but resulted in a ' 'subsequent call to itself, indicating a ' 'recursive loop. This may be caused by a ' 'method trying to access a component out ' 'of order for some reason (e.g. it is ' 'declared later in the same method). See ' 'the __getattrcalls object for a list of ' 'components called in the __getattr__ ' 'sequence.'.format(self.name, attr)) # If not, add call to list self.__getattrcalls.append(attr) # Get property information from properties metadata try: m = self.config.parameters.get_metadata().properties if m is None: raise PropertyPackageError( '{} property package get_metadata()' ' method returned None when trying to create ' '{}. Please contact the developer of the ' 'property package'.format(self.name, attr)) except KeyError: # If attr not in metadata, assume package does not # support property clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by property package (property is ' 'not listed in package metadata properties).'.format( self.name, attr)) # Get method name from resulting properties try: if m[attr]['method'] is None: # If method is none, property should be constructed # by property package, so raise PropertyPackageError clear_call_list(self, attr) raise PropertyPackageError( '{} {} should be constructed automatically ' 'by property package, but is not present. ' 'This can be caused by methods being called ' 'out of order.'.format(self.name, attr)) elif m[attr]['method'] is False: # If method is False, package does not support property # Raise NotImplementedError clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by property package ' '(property method is listed as False in ' 'package property metadata).'.format(self.name, attr)) elif isinstance(m[attr]['method'], str): # Try to get method name in from PropertyBlock object try: f = getattr(self, m[attr]['method']) except AttributeError: # If fails, method does not exist clear_call_list(self, attr) raise PropertyPackageError( '{} {} package property metadata method ' 'returned a name that does not correspond' ' to any method in the property package. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) else: # Otherwise method name is invalid clear_call_list(self, attr) raise PropertyPackageError( '{} {} package property metadata method ' 'returned invalid value for method name. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) except KeyError: # No method key - raise Exception # Need to use an AttributeError so Pyomo.DAE will handle this clear_call_list(self, attr) raise PropertyNotSupportedError( '{} package property metadata method ' 'does not contain a method for {}. ' 'Please select a package which supports ' 'the necessary properties for your process.'.format( self.name, attr)) # Call attribute if it is callable # If this fails, it should return a meaningful error. if callable(f): try: f() except Exception: # Clear call list and reraise error clear_call_list(self, attr) raise else: # If f is not callable, inform the user and clear call list clear_call_list(self, attr) raise PropertyPackageError( '{} tried calling attribute {} in order to create ' 'component {}. However the method is not callable.'.format( self.name, f, attr)) # Clear call list, and return comp = getattr(self, attr) clear_call_list(self, attr) return comp def calculate_scaling_factors(self): super().calculate_scaling_factors() # Get scaling factor defaults, if no scaling factor set for v in self.component_data_objects((Constraint, Var, Expression), descend_into=False): if iscale.get_scaling_factor(v) is None: # don't replace if set name = v.getname().split("[")[0] index = v.index() sf = self.config.parameters.get_default_scaling(name, index) if sf is not None: iscale.set_scaling_factor(v, sf)
class PIDBlockData(ProcessBlockData): CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "pv", ConfigValue( default=None, description="Measured process variable", doc="A Pyomo Var, Expression, or Reference for the measured" " process variable. Should be indexed by time.")) CONFIG.declare( "output", ConfigValue( default=None, description="Controlled process variable", doc="A Pyomo Var, Expression, or Reference for the controlled" " process variable. Should be indexed by time.")) CONFIG.declare( "upper", ConfigValue( default=1.0, domain=float, description="Output upper limit", doc="The upper limit for the controller output, default=1")) CONFIG.declare( "lower", ConfigValue( default=0.0, domain=float, description="Output lower limit", doc="The lower limit for the controller output, default=0")) CONFIG.declare( "calculate_initial_integral", ConfigValue( default=True, domain=bool, description="Calculate the initial integral term value if true, " " otherwise provide a variable err_i0, which can be fixed", doc="Calculate the initial integral term value if true, otherwise" " provide a variable err_i0, which can be fixed, default=True")) CONFIG.declare( "pid_form", ConfigValue(default=PIDForm.velocity, domain=In(PIDForm), description="Velocity or standard form", doc="Velocity or standard form")) # TODO<jce> options for P, PI, and PD, you can currently do PI by setting # the derivative time to 0, this class should handle PI and PID # controllers. Proportional, only controllers are sufficiently # different that another class should be implemented. # TODO<jce> Anti-windup the integral term can keep accumulating error when # the controller output is at a bound. This can cause trouble, # and ways to deal with it should be implemented # TODO<jce> Implement way to better deal with the integral term for setpoint # changes (see bumpless). I need to look into the more, but this # would basically use the calculation like the one already used # for the first time point to calculate integral error to keep the # controller output from suddenly jumping in response to a set # point change or transition from manual to automatic control. def _build_standard(self, time_set, t0): # Want to fix the output variable at the first time step to make # solving easier. This calculates the initial integral error to line up # with the initial output value, keeps the controller from initially # jumping. if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # integral error @self.Expression(time_set, doc="Integral error") def err_i(b, t_end): return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) * (t - time_set.prev(t)) / 2.0 for t in time_set if t <= t_end and t > t0) # Calculate the unconstrained controller output @self.Expression(time_set, doc="Unconstrained controller output") def unconstrained_output(b, t): return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] + b.time_d[t] * b.err_d[t]) @self.Expression(doc="Initial integral error at the end") def err_i_end(b): return b.err_i[time_set.last()] def _build_velocity(self, time_set, t0): if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # Calculate the unconstrained controller output @self.Expression(time_set, doc="Unconstrained controller output") def unconstrained_output(b, t): if t == t0: # do the standard first step so I have a previous time # for the rest of the velocity form return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i0 + b.time_d[t] * b.err_d[t]) tb = time_set.prev(t) # time back a step return self.output[tb] + self.gain[t] * ( b.pterm[t] - b.pterm[tb] + (t - tb) / b.time_i[t] * (b.err[t] + b.err[tb]) / 2 + b.time_d[t] * (b.err_d[t] - b.err_d[tb])) @self.Expression(doc="Initial integral error at the end") def err_i_end(b): tl = time_set.last() return b.time_i[tl]*(b.output[tl] - b.gain[tl]*b.pterm[tl]\ - b.gain[tl]*b.time_d[tl]*b.err_d[tl])/b.gain[tl] def build(self): """ Build the PID block """ if isinstance(self.flowsheet().time, ContinuousSet): # time may not be a continuous set if you have a steady state model # in the steady state model case obviously the controller should # not be active, but you can still add it. if 'scheme' not in self.flowsheet().time.get_discretization_info(): # if you have a dynamic model, must do time discretization # before adding the PID model raise RunTimeError( "PIDBlock must be added after time discretization") super().build() # do the ProcessBlockData voodoo for config # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration requires 'pv'") if self.config.output is None: raise ConfigurationError( "Controller configuration requires 'output'") # Shorter pointers to time set information time_set = self.flowsheet().time t0 = time_set.first() # Variable for basic controller settings may change with time. self.setpoint = pyo.Var(time_set, doc="Setpoint") self.gain = pyo.Var(time_set, doc="Controller gain") self.time_i = pyo.Var(time_set, doc="Integral time") self.time_d = pyo.Var(time_set, doc="Derivative time") # Make the initial derivative term a variable so you can set it. This # should let you carry on from the end of another time period self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0) self.err_d0.fix() if not self.config.calculate_initial_integral: self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0) self.err_i0.fix() # Make references to the output and measured variables self.pv = pyo.Reference(self.config.pv) # No duplicate self.output = pyo.Reference(self.config.output) # No duplicate # Create an expression for error from setpoint @self.Expression(time_set, doc="Setpoint error") def err(b, t): return self.setpoint[t] - self.pv[t] # Use expressions to allow the some future configuration @self.Expression(time_set) def pterm(b, t): return -self.pv[t] @self.Expression(time_set) def dterm(b, t): return -self.pv[t] @self.Expression(time_set) def iterm(b, t): return self.err[t] # Output limits parameter self.limits = pyo.Param(["l", "h"], mutable=True, doc="controller output limits", initialize={ "l": self.config.lower, "h": self.config.upper }) # Smooth min and max are used to limit output, smoothing parameter here self.smooth_eps = pyo.Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") # This is ugly, but want integral and derivative error as expressions, # nice implementation with variables is harder to initialize and solve @self.Expression(time_set, doc="Derivative error.") def err_d(b, t): if t == t0: return self.err_d0 else: return (b.dterm[t] - b.dterm[time_set.prev(t)])\ /(t - time_set.prev(t)) if self.config.pid_form == PIDForm.standard: self._build_standard(time_set, t0) else: self._build_velocity(time_set, t0) # Add the controller output constraint and limit it with smooth min/max e = self.smooth_eps h = self.limits["h"] l = self.limits["l"] @self.Constraint(time_set, doc="Controller output constraint") def output_constraint(b, t): if t == t0: return pyo.Constraint.Skip else: return self.output[t] ==\ smooth_min( smooth_max(self.unconstrained_output[t], l, e), h, e)
class ReactionBlockDataBase(ProcessBlockData): """ This is the base class for reaction block data objects. These are blocks that contain the Pyomo components associated with calculating a set of reacion properties for a given material. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "parameters", ConfigValue( domain=is_reaction_parameter_block, description="""A reference to an instance of the Reaction Parameter Block associated with this property package.""")) CONFIG.declare( "state_block", ConfigValue( domain=is_state_block, description="""A reference to an instance of a StateBlock with which this reaction block should be associated.""")) CONFIG.declare( "has_equilibrium", ConfigValue(default=True, domain=In([True, False]), description="Equilibrium constraint flag", doc="""Flag indicating whether equilibrium constraints should be constructed in this reaction block, **default** - True. **Valid values:** { **True** - ReactionBlock should enforce equilibrium constraints, **False** - ReactionBlock should not enforce equilibrium constraints.}""")) def __init__(self, *args, **kwargs): self._lock_attribute_creation = False super().__init__(*args, **kwargs) def lock_attribute_creation_context(self): """Returns a context manager that does not allow attributes to be created while in the context and allows attributes to be created normally outside the context. """ return _lock_attribute_creation_context(self) def is_property_constructed(self, attr): """Returns True if the attribute ``attr`` already exists, or false if it would be added in ``__getattr__``, or does not exist. Args: attr (str): Attribute name to check Return: True if the attribute is already constructed, False otherwise """ with self.lock_attribute_creation_context(): return hasattr(self, attr) def build(self): """ General build method for PropertyBlockDatas. Inheriting models should call super().build. Args: None Returns: None """ super(ReactionBlockDataBase, self).build() add_object_reference(self, "_params", self.config.parameters) self._validate_state_block() @property def params(self): return self._params def _validate_state_block(self): """ Method to validate that the associated state block matches with the PropertyParameterBlock assoicated with the ReactionParameterBlock. """ # Add a reference to the corresponding state block data for later use add_object_reference(self, "state_ref", self.config.state_block[self.index()]) # Validate that property package of state matches that of reaction pack if (self.config.parameters.config.property_package != self.state_ref.config.parameters): raise PropertyPackageError( '{} the StateBlock associated with this ' 'ReactionBlock does not match with the ' 'PropertyParamterBlock associated with the ' 'ReactionParameterBlock. The modelling framework ' 'does not support mixed associations of property ' 'and reaction packages.'.format(self.name)) def get_reaction_rate_basis(self): """ Method which returns an Enum indicating the basis of the reaction rate term. """ return MaterialFlowBasis.other def __getattr__(self, attr): """ This method is used to avoid generating unnecessary property calculations in reaction blocks. __getattr__ is called whenever a property is called for, and if a propery does not exist, it looks for a method to create the required property, and any associated components. Create a property calculation if needed. Return an attrbute error if attr == 'domain' or starts with a _ . The error for _ prevents a recursion error if trying to get a function to create a property and that function doesn't exist. Pyomo also ocasionally looks for things that start with _ and may not exist. Pyomo also looks for the domain attribute, and it may not exist. This works by creating a property calculation by calling the "_"+attr function. A list of __getattr__ calls is maintained in self.__getattrcalls to check for recursive loops which maybe useful for debugging. This list is cleared after __getattr__ completes successfully. Args: attr: an attribute to create and return. Should be a property component. """ if self._lock_attribute_creation: raise AttributeError( f"{attr} does not exist, and attribute creation is locked.") def clear_call_list(self, attr): """Local method for cleaning up call list when a call is handled. Args: attr: attribute currently being handled """ if self.__getattrcalls[-1] == attr: if len(self.__getattrcalls) <= 1: del self.__getattrcalls else: del self.__getattrcalls[-1] else: raise PropertyPackageError( "{} Trying to remove call {} from __getattr__" " call list, however this is not the most " "recent call in the list ({}). This indicates" " a bug in the __getattr__ calls. Please " "contact the IDAES developers with this bug.".format( self.name, attr, self.__getattrcalls[-1])) # Check that attr is not something we shouldn't touch if attr == "domain" or attr.startswith("_"): # Don't interfere with anything by getting attributes that are # none of my business raise PropertyPackageError( '{} {} does not exist, but is a protected ' 'attribute. Check the naming of your ' 'components to avoid any reserved names'.format( self.name, attr)) if attr == "config": try: self._get_config_args() return self.config except: raise BurntToast("{} getattr method was triggered by a call " "to the config block, but _get_config_args " "failed. This should never happen.") # Check for recursive calls try: # Check if __getattrcalls is initialized self.__getattrcalls except AttributeError: # Initialize it self.__getattrcalls = [attr] else: # Check to see if attr already appears in call list if attr in self.__getattrcalls: # If it does, indicates a recursive loop. if attr == self.__getattrcalls[-1]: # attr method is calling itself self.__getattrcalls.append(attr) raise PropertyPackageError( '{} _{} made a recursive call to ' 'itself, indicating a potential ' 'recursive loop. This is generally ' 'caused by the {} method failing to ' 'create the {} component.'.format( self.name, attr, attr, attr)) else: self.__getattrcalls.append(attr) raise PropertyPackageError( '{} a potential recursive loop has been ' 'detected whilst trying to construct {}. ' 'A method was called, but resulted in a ' 'subsequent call to itself, indicating a ' 'recursive loop. This may be caused by a ' 'method trying to access a component out ' 'of order for some reason (e.g. it is ' 'declared later in the same method). See ' 'the __getattrcalls object for a list of ' 'components called in the __getattr__ ' 'sequence.'.format(self.name, attr)) # If not, add call to list self.__getattrcalls.append(attr) # Get property information from get_supported_properties try: m = self.config.parameters.get_metadata().properties if m is None: raise PropertyPackageError( '{} reaction package get_supported_properties' ' method returned None when trying to create ' '{}. Please contact the developer of the ' 'property package'.format(self.name, attr)) except KeyError: # If attr not in get_supported_properties, assume package does not # support property clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by reaction package (property is ' 'not listed in get_supported_properties).'.format( self.name, attr, attr)) # Get method name from get_supported_properties try: if m[attr]['method'] is None: # If method is none, property should be constructed # by property package, so raise PropertyPackageError clear_call_list(self, attr) raise PropertyPackageError( '{} {} should be constructed automatically ' 'by reaction package, but is not present. ' 'This can be caused by methods being called ' 'out of order.'.format(self.name, attr)) elif m[attr]['method'] is False: # If method is False, package does not support property # Raise NotImplementedError clear_call_list(self, attr) raise PropertyNotSupportedError( '{} {} is not supported by reaction package ' '(property method is listed as False in ' 'get_supported_properties).'.format(self.name, attr)) elif isinstance(m[attr]['method'], str): # Try to get method name in from PropertyBlock object try: f = getattr(self, m[attr]['method']) except AttributeError: # If fails, method does not exist clear_call_list(self, attr) raise PropertyPackageError( '{} {} get_supported_properties method ' 'returned a name that does not correspond' ' to any method in the reaction package. ' 'Please contact the developer of the ' 'reaction package.'.format(self.name, attr)) else: # Otherwise method name is invalid clear_call_list(self, attr) raise PropertyPackageError( '{} {} get_supported_properties method ' 'returned invalid value for method name. ' 'Please contact the developer of the ' 'reaction package.'.format(self.name, attr)) except KeyError: # No method key - raise Exception # Need to use an AttributeError so Pyomo.DAE will handle this clear_call_list(self, attr) raise PropertyNotSupportedError( '{} get_supported_properties method ' 'does not contain a method for {}. ' 'Please select a package which supports ' 'the necessary properties for your process.'.format( self.name, attr)) # Call attribute if it is callable # If this fails, it should return a meaningful error. if callable(f): try: f() except Exception: # Clear call list and reraise error clear_call_list(self, attr) raise else: # If f is not callable, inform the user and clear call list clear_call_list(self, attr) raise PropertyPackageError( '{} tried calling attribute {} in order to create ' 'component {}. However the method is not callable.'.format( self.name, f, attr)) # Clear call list, and return comp = getattr(self, attr) clear_call_list(self, attr) return comp
class FlowsheetBlockData(ProcessBlockData): """ The FlowsheetBlockData Class forms the base class for all IDAES process flowsheet models. The main purpose of this class is to automate the tasks common to all flowsheet models and ensure that the necessary attributes of a flowsheet model are present. The most signfiicant role of the FlowsheetBlockData class is to automatically create the time domain for the flowsheet. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("dynamic", ConfigValue( default=useDefault, domain=In([useDefault, True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent or False, **True** - set as a dynamic model, **False** - set as a steady-state model.}""")) CONFIG.declare("time", ConfigValue( default=None, domain=is_time_domain, description="Flowsheet time domain", doc="""Pointer to the time domain for the flowsheet. Users may provide an existing time domain from another flowsheet, otherwise the flowsheet will search for a parent with a time domain or create a new time domain and reference it here.""")) CONFIG.declare("time_set", ConfigValue( default=[0], domain=list_of_floats, description="Set of points for initializing time domain", doc="""Set of points for initializing time domain. This should be a list of floating point numbers, **default** - [0].""")) CONFIG.declare("default_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Default property package to use in flowsheet", doc="""Indicates the default property package to be used by models within this flowsheet if not otherwise specified, **default** - None. **Valid values:** { **None** - no default property package, **a ParameterBlock object**.}""")) def build(self): """ General build method for FlowsheetBlockData. This method calls a number of sub-methods which automate the construction of expected attributes of flowsheets. Inheriting models should call `super().build`. Args: None Returns: None """ super(FlowsheetBlockData, self).build() # Set up dynamic flag and time domain self._setup_dynamics() def is_flowsheet(self): """ Method which returns True to indicate that this component is a flowsheet. Args: None Returns: True """ return True # TODO [Qi]: this should be implemented as a transformation def model_check(self): """ This method runs model checks on all unit models in a flowsheet. This method searches for objects which inherit from UnitModelBlockData and executes the model_check method if it exists. Args: None Returns: None """ _log.info("Executing model checks.") for o in self.component_objects(descend_into=False): if isinstance(o, UnitModelBlockData): try: o.model_check() except AttributeError: _log.warning('{} Model/block has no model check. To ' 'correct this, add a model_check method to ' 'the associated unit model class' .format(o.name)) def _setup_dynamics(self): # Look for parent flowsheet fs = self.flowsheet() # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == useDefault: if fs is None: # No parent, so default to steady-state and warn user _log.warning('{} is a top level flowhseet, but dynamic flag ' 'set to useDefault. Dynamic ' 'flag set to False by default' .format(self.name)) self.config.dynamic = False else: # Get dynamic flag from parent flowsheet self.config.dynamic = fs.config.dynamic # Check for case when dynamic=True, but parent dynamic=False elif self.config.dynamic is True: if fs is not None and fs.config.dynamic is False: raise DynamicError( '{} trying to declare a dynamic model within ' 'a steady-state flowsheet. This is not ' 'supported by the IDAES framework. Try ' 'creating a dynamic flowsheet instead, and ' 'declaring some models as steady-state.' .format(self.name)) if self.config.time is not None: # Validate user provided time domain if (self.config.dynamic is True and not isinstance(self.config.time, ContinuousSet)): raise DynamicError( '{} was set as a dynamic flowsheet, but time domain ' 'provided was not a ContinuousSet.'.format(self.name)) else: # If no parent flowsheet, set up time domain if fs is None: # Create time domain if self.config.dynamic: # Check if time_set has at least two points if len(self.config.time_set) < 2: # Check if time_set is at default value if self.config.time_set == [0.0]: # If default, set default end point to be 1.0 self.config.time_set = [0.0, 1.0] else: # Invalid user input, raise Excpetion raise DynamicError( "Flowsheet provided with invalid " "time_set attribute - must have at " "least two values (start and end).") # For dynamics, need a ContinuousSet self.time = ContinuousSet(initialize=self.config.time_set) else: # For steady-state, use an ordered Set self.time = pe.Set(initialize=self.config.time_set, ordered=True) # Set time config argument as reference to time domain self.config.time = self.time else: # Set time config argument to parent time self.config.time = fs.config.time
class PIDBlockData(ProcessBlockData): CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "pv", ConfigValue( default=None, description="Measured process variable", doc="A Pyomo Var, Expression, or Reference for the measured" " process variable. Should be indexed by time.")) CONFIG.declare( "output", ConfigValue( default=None, description="Controlled process variable", doc="A Pyomo Var, Expression, or Reference for the controlled" " process variable. Should be indexed by time.")) CONFIG.declare( "upper", ConfigValue( default=1.0, domain=float, description="Output upper limit", doc="The upper limit for the controller output, default=1")) CONFIG.declare( "lower", ConfigValue( default=0.0, domain=float, description="Output lower limit", doc="The lower limit for the controller output, default=0")) CONFIG.declare( "calculate_initial_integral", ConfigValue( default=True, domain=bool, description="Calculate the initial integral term value if true, " " otherwise provide a variable err_i0, which can be fixed", doc="Calculate the initial integral term value if true, otherwise" " provide a variable err_i0, which can be fixed, default=True")) # other options can be added, but this covers the bare minimum # # TODO<jce> options for P, PI, and PD, you can currently do PI by setting # the derivative time to 0, but it would be better not to # add the derivative term at all if not needed. PD and P are less # common, but they exist and should be supported # TODO<jce> Anti-windup the integral term can keep accumulating error when # the controller output is at a bound. This can cause trouble, # and ways to deal with it should be implemented # TODO<jce> Implement way to better deal with the integral term for setpoint # changes (see bumpless). I need to look into the more, but this # would basically use the calculation like the one already used # for the first time point to calculate integral error to keep the # controller output from suddenly jumping in response to a set # point change or transition from manual to automatic control. # TODO<jce> Implement the integral term using variables. The integral # expressions are nice because initialization is not required and # they reduce the total number of variables, but they there is an # integral expression and each time and the later ones contain a # very large number of terms. def build(self): """ Build the PID block """ super().build() # do the ProcessBlockData voodoo for config # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration requires 'pv'") if self.config.output is None: raise ConfigurationError( "Controller configuration requires 'output'") # Shorter pointers to time set information time_set = self.flowsheet().time t0 = time_set.first() # Variable for basic controller settings may change with time. self.setpoint = pyo.Var(time_set, doc="Setpoint") self.gain = pyo.Var(time_set, doc="Controller gain") self.time_i = pyo.Var(time_set, doc="Integral time") self.time_d = pyo.Var(time_set, doc="Derivative time") # Make the initial derivative term a variable so you can set it. This # should let you carry on from the end of another time period self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0) self.err_d0.fix() if not self.config.calculate_initial_integral: self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0) self.err_i0.fix() # Make references to the output and measured variables self.pv = pyo.Reference(self.config.pv) # No duplicate self.output = pyo.Reference(self.config.output) # No duplicate # Create an expression for error from setpoint @self.Expression(time_set, doc="Setpoint error") def err(b, t): return self.setpoint[t] - self.pv[t] # Use expressions to allow the some future configuration @self.Expression(time_set) def pterm(b, t): return -self.pv[t] @self.Expression(time_set) def dterm(b, t): return -self.pv[t] @self.Expression(time_set) def iterm(b, t): return self.err[t] # Output limits parameter self.limits = pyo.Param(["l", "h"], mutable=True, doc="controller output limits", initialize={ "l": self.config.lower, "h": self.config.upper }) # Smooth min and max are used to limit output, smoothing parameter here self.smooth_eps = pyo.Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") # This is ugly, but want integral and derivative error as expressions, # nice implementation with variables is harder to initialize and solve @self.Expression(time_set, doc="Derivative error.") def err_d(b, t): if t == t0: return self.err_d0 else: return (b.dterm[t] - b.dterm[time_set.prev(t)])\ /(t - time_set.prev(t)) # Want to fix the output varaible at the first time step to make # solving easier. This calculates the initial integral error to line up # with the initial output value, keeps the controller from initially # jumping. if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # integral error @self.Expression(time_set, doc="Integral error") def err_i(b, t_end): return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) * (t - time_set.prev(t)) / 2.0 for t in time_set if t <= t_end and t > t0) # Calculate the unconstrainted controller output @self.Expression(time_set, doc="Unconstrained contorler output") def unconstrained_output(b, t): return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] + b.time_d[t] * b.err_d[t]) # Add the controller output constraint and limit it with smooth min/max e = self.smooth_eps h = self.limits["h"] l = self.limits["l"] @self.Constraint(time_set, doc="Controller output constraint") def output_constraint(b, t): if t == t0: return pyo.Constraint.Skip else: return self.output[t] ==\ smooth_min( smooth_max(self.unconstrained_output[t], l, e), h, e)
class FlowsheetBlockData(ProcessBlockData): """ The FlowsheetBlockData Class forms the base class for all IDAES process flowsheet models. The main purpose of this class is to automate the tasks common to all flowsheet models and ensure that the necessary attributes of a flowsheet model are present. The most signfiicant role of the FlowsheetBlockData class is to automatically create the time domain for the flowsheet. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "dynamic", ConfigValue(default=useDefault, domain=In([useDefault, True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent or False, **True** - set as a dynamic model, **False** - set as a steady-state model.}""")) CONFIG.declare( "time", ConfigValue( default=None, domain=is_time_domain, description="Flowsheet time domain", doc= """Pointer to the time domain for the flowsheet. Users may provide an existing time domain from another flowsheet, otherwise the flowsheet will search for a parent with a time domain or create a new time domain and reference it here.""")) CONFIG.declare( "time_set", ConfigValue( default=[0], domain=list_of_floats, description="Set of points for initializing time domain", doc="""Set of points for initializing time domain. This should be a list of floating point numbers, **default** - [0].""")) CONFIG.declare( "time_units", ConfigValue( description="Units for time domain", doc="""Pyomo Units object describing the units of the time domain. This must be defined for dynamic simulations, default = None.""")) CONFIG.declare( "default_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Default property package to use in flowsheet", doc="""Indicates the default property package to be used by models within this flowsheet if not otherwise specified, **default** - None. **Valid values:** { **None** - no default property package, **a ParameterBlock object**.}""")) def build(self): """ General build method for FlowsheetBlockData. This method calls a number of sub-methods which automate the construction of expected attributes of flowsheets. Inheriting models should call `super().build`. Args: None Returns: None """ super(FlowsheetBlockData, self).build() self._time_units = None # Set up dynamic flag and time domain self._setup_dynamics() @property def time_units(self): return self._time_units def is_flowsheet(self): """ Method which returns True to indicate that this component is a flowsheet. Args: None Returns: True """ return True def model_check(self): """ This method runs model checks on all unit models in a flowsheet. This method searches for objects which inherit from UnitModelBlockData and executes the model_check method if it exists. Args: None Returns: None """ _log.info("Executing model checks.") for o in self.component_objects(descend_into=False): if isinstance(o, UnitModelBlockData): try: o.model_check() except AttributeError: # This should never happen, but just in case _log.warning( '{} Model/block has no model_check method.'.format( o.name)) def stream_table(self, true_state=False, time_point=0, orient='columns'): """ Method to generate a stream table by iterating over all Arcs in the flowsheet. Args: true_state : whether the state variables (True) or display variables (False, default) from the StateBlocks should be used in the stream table. time_point : point in the time domain at which to create stream table (default = 0) orient : whether stream should be shown by columns ("columns") or rows ("index") Returns: A pandas dataframe containing stream table information """ dict_arcs = {} for a in self.component_objects(ctype=Arc, descend_into=False): dict_arcs[a.local_name] = a return create_stream_table_dataframe(dict_arcs, time_point=time_point, orient=orient, true_state=true_state) def visualize(self, model_name, **kwargs): """ Starts up a flask server that serializes the model and pops up a webpage with the visualization Args: model_name : The name of the model that flask will use as an argument for the webpage Keyword Args: **kwargs: Additional keywords for :func:`idaes.ui.fsvis.visualize()` Returns: None """ visualize(self, model_name, **kwargs) def get_costing(self, module=costing, year=None): self.costing = pe.Block() module.global_costing_parameters(self.costing, year) def _get_stream_table_contents(self, time_point=0): """ Calls stream_table method and returns result """ return self.stream_table(time_point) def _setup_dynamics(self): # Look for parent flowsheet fs = self.flowsheet() # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == useDefault: if fs is None: # No parent, so default to steady-state and warn user _log.warning('{} is a top level flowsheet, but dynamic flag ' 'set to useDefault. Dynamic ' 'flag set to False by default'.format(self.name)) self.config.dynamic = False else: # Get dynamic flag from parent flowsheet self.config.dynamic = fs.config.dynamic # Check for case when dynamic=True, but parent dynamic=False elif self.config.dynamic is True: if fs is not None and fs.config.dynamic is False: raise DynamicError( '{} trying to declare a dynamic model within ' 'a steady-state flowsheet. This is not ' 'supported by the IDAES framework. Try ' 'creating a dynamic flowsheet instead, and ' 'declaring some models as steady-state.'.format(self.name)) # Validate units for time domain if self.config.time_units is None and self.config.dynamic: _log.warning("DEPRECATED: No units were specified for the time " "domain. Users should provide units via the " "time_units configuration argument.") elif self.config.time_units is None and not self.config.dynamic: _log.info_high("DEPRECATED: No units were specified for the time " "domain. Users should provide units via the " "time_units configuration argument.") elif not isinstance(self.config.time_units, _PyomoUnit): raise ConfigurationError( "{} unrecognised value for time_units argument. This must be " "a Pyomo Unit object (not a compound unit).".format(self.name)) if self.config.time is not None: # Validate user provided time domain if (self.config.dynamic is True and not isinstance(self.config.time, ContinuousSet)): raise DynamicError( '{} was set as a dynamic flowsheet, but time domain ' 'provided was not a ContinuousSet.'.format(self.name)) self._time_units = self.config.time_units else: # If no parent flowsheet, set up time domain if fs is None: # Create time domain if self.config.dynamic: # Check if time_set has at least two points if len(self.config.time_set) < 2: # Check if time_set is at default value if self.config.time_set == [0.0]: # If default, set default end point to be 1.0 self.config.time_set = [0.0, 1.0] else: # Invalid user input, raise Excpetion raise DynamicError( "Flowsheet provided with invalid " "time_set attribute - must have at " "least two values (start and end).") # For dynamics, need a ContinuousSet self.time = ContinuousSet(initialize=self.config.time_set) else: # For steady-state, use an ordered Set self.time = pe.Set(initialize=self.config.time_set, ordered=True) self._time_units = self.config.time_units # Set time config argument as reference to time domain self.config.time = self.time else: # Set time config argument to parent time self.config.time = fs.config.time self._time_units = fs._time_units
class ReactionParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for reaction parameter blocks. These are blocks that contain a set of parameters associated with a specific reaction package, and are linked to by all instances of that reaction package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def build(self): """ General build method for ReactionParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(ReactionParameterBlock, self).build() # TODO: Need way to tie reaction package to a specfic property package self._validate_property_parameter_units() self._validate_property_parameter_properties() def _validate_property_parameter_units(self): """ Checks that the property parameter block associated with the reaction block uses the same set of default units. """ r_units = self.get_metadata().default_units prop_units = self.config.property_package.get_metadata().default_units for u in r_units: try: if prop_units[u] != r_units[u]: raise KeyError() except KeyError: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not use the same set of ' 'units of measurement ({}). Please choose a ' 'property package which uses the same units.'.format( self.name, u)) def _validate_property_parameter_properties(self): """ Checks that the property parameter block associated with the reaction block supports the necessary properties with correct units. """ req_props = self.get_metadata().required_properties supp_props = self.config.property_package.get_metadata().properties for p in req_props: if p not in supp_props: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) elif supp_props[p]['method'] is False: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) # Check property units if req_props[p]['units'] != supp_props[p]['units']: raise PropertyPackageError( '{} the units associated with property {} in this ' 'reaction package ({}) do not match with the units ' 'used in the assoicated property package ({}). Please ' 'choose a property package which used the same ' 'units for all properties.'.format(self.name, p, req_props[p]['units'], supp_props[p]['units']))
class ReactionParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for reaction parameter blocks. These are blocks that contain a set of parameters associated with a specific reaction package, and are linked to by all instances of that reaction package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def __init__(self, *args, **kwargs): self.__reaction_block_class = None super().__init__(*args, **kwargs) def build(self): """ General build method for ReactionParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(ReactionParameterBlock, self).build() if not hasattr(self, "_reaction_block_class"): self._reaction_block_class = None # TODO: Need way to tie reaction package to a specfic property package self._validate_property_parameter_units() self._validate_property_parameter_properties() # This is a dict to store default property scaling factors. They are # defined in the parameter block to provide a universal default for # quantities in a particular kind of state block. For example, you can # set flow scaling once instead of for every state block. Some of these # may be left for the user to set and some may be defined in a property # module where reasonable defaults can be defined a priori. See # set_default_scaling, get_default_scaling, and unset_default_scaling self.default_scaling_factor = {} def set_default_scaling(self, attrbute, value, index=None): """Set a default scaling factor for a property. Args: attribute: property attribute name value: default scaling factor index: for indexed properties, if this is not provied the scaling factor default applies to all indexed elements where specific indexes are no specifcally specified. Returns: None """ self.default_scaling_factor[(attrbute, index)] = value def unset_default_scaling(self, attrbute, index=None): """Remove a previously set default value Args: attribute: property attribute name index: optional index for indexed properties Returns: None """ try: del self.default_scaling_factor[(attrbute, index)] except KeyError: pass def get_default_scaling(self, attrbute, index=None): """ Returns a default scale factor for a property Args: attribute: property attribute name index: optional index for indexed properties Returns: None """ try: # If a specific component data index exists return self.default_scaling_factor[(attrbute, index)] except KeyError: try: # indexed, but no specifc index? return self.default_scaling_factor[(attrbute, None)] except KeyError: # Can't find a default scale factor for what you asked for return None @property def reaction_block_class(self): if self._reaction_block_class is not None: return self._reaction_block_class else: raise AttributeError( "{} has not assigned a ReactionBlock class to be associated " "with this reaction package. Please contact the developer of " "the reaction package.".format(self.name)) @reaction_block_class.setter def reaction_block_class(self, val): _log.warning("DEPRECATED: reaction_block_class should not be set " "directly. Property package developers should set the " "_reaction_block_class attribute instead.") self._reaction_block_class = val def build_reaction_block(self, *args, **kwargs): """ Methods to construct a ReactionBlock assoicated with this ReactionParameterBlock. This will automatically set the parameters construction argument for the ReactionBlock. Returns: ReactionBlock """ default = kwargs.pop("default", {}) initialize = kwargs.pop("initialize", {}) if initialize == {}: default["parameters"] = self else: for i in initialize.keys(): initialize[i]["parameters"] = self return self.reaction_block_class(*args, **kwargs, default=default, initialize=initialize) def _validate_property_parameter_units(self): """ Checks that the property parameter block associated with the reaction block uses the same set of default units. """ r_units = self.get_metadata().default_units prop_units = self.config.property_package.get_metadata().default_units for u in r_units: try: # TODO: This check is for backwards compatability with # pre-units property packages. It can be removed once these are # fully deprecated. if isinstance(prop_units[u], str) and (prop_units[u] != r_units[u]): raise KeyError() elif prop_units[u] is not r_units[u]: raise KeyError() except KeyError: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not use the same set of ' 'units of measurement ({}). Please choose a ' 'property package which uses the same units.'.format( self.name, u)) def _validate_property_parameter_properties(self): """ Checks that the property parameter block associated with the reaction block supports the necessary properties with correct units. """ req_props = self.get_metadata().required_properties supp_props = self.config.property_package.get_metadata().properties for p in req_props: if p not in supp_props: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) elif supp_props[p]['method'] is False: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) # Check property units if req_props[p]['units'] != supp_props[p]['units']: raise PropertyPackageError( '{} the units associated with property {} in this ' 'reaction package ({}) do not match with the units ' 'used in the assoicated property package ({}). Please ' 'choose a property package which used the same ' 'units for all properties.'.format(self.name, p, req_props[p]['units'], supp_props[p]['units']))
class ControlVolumeBlockData(ProcessBlockData): """ The ControlVolumeBlockData Class forms the base class for all IDAES ControlVolume models. The purpose of this class is to automate the tasks common to all control volume blockss and ensure that the necessary attributes of a control volume block are present. The most signfiicant role of the ControlVolumeBlockData class is to set up the construction arguments for the control volume block, automatically link to the time domain of the parent block, and to get the information about the property and reaction packages. """ CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "dynamic", ConfigValue(domain=In([useDefault, True, False]), default=useDefault, description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent, **True** - set as a dynamic model, **False** - set as a steady-state model}""")) CONFIG.declare( "has_holdup", ConfigValue( default=useDefault, domain=In([useDefault, True, 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.}""")) 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( "auto_construct", ConfigValue( default=False, domain=In([True, False]), description="Argument indicating whether ControlVolume should " "automatically construct balance equations", doc="""If set to True, this argument will trigger the auto_construct method which will attempt to construct a set of material, energy and momentum balance equations based on the parent unit's config block. The parent unit must have a config block which derives from CONFIG_Base, **default** - False. **Valid values:** { **True** - use automatic construction, **False** - do not use automatic construciton.}""")) def build(self): """ General build method for Control Volumes blocks. This method calls a number of sub-methods which automate the construction of expected attributes of all ControlVolume blocks. Inheriting models should call `super().build`. Args: None Returns: None """ super(ControlVolumeBlockData, self).build() # Setup dynamics flag and time domain self._setup_dynamics() # Get property package details self._get_property_package() # Get indexing sets self._get_indexing_sets() # Get reaction package details (as necessary) self._get_reaction_package() if self.config.auto_construct is True: self._auto_construct() def add_geometry(self, *args, **kwargs): """ Method for defining the geometry of the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_geometry. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_material_balances(self, balance_type=MaterialBalanceType.componentPhase, **kwargs): """ General method for adding material balances to a control volume. This method makes calls to specialised sub-methods for each type of material balance. Args: balance_type - MaterialBalanceType Enum indicating which type of material balance should be constructed. has_rate_reactions - whether default generation terms for rate reactions should be included in material balances has_equilibrium_reactions - whether generation terms should for chemical equilibrium reactions should be included in material balances has_phase_equilibrium - whether generation terms should for phase equilibrium behaviour should be included in material balances has_mass_transfer - whether generic mass transfer terms should be included in material balances custom_molar_term - a Pyomo Expression representing custom terms to be included in material balances on a molar basis. custom_mass_term - a Pyomo Expression representing custom terms to be included in material balances on a mass basis. Returns: Constraint objects constructed by sub-method """ if balance_type == MaterialBalanceType.none: mb = None elif balance_type == MaterialBalanceType.componentPhase: mb = self.add_phase_component_balances(**kwargs) elif balance_type == MaterialBalanceType.componentTotal: mb = self.add_total_component_balances(**kwargs) elif balance_type == MaterialBalanceType.elementTotal: mb = self.add_total_element_balances(**kwargs) elif balance_type == MaterialBalanceType.total: mb = self.add_total_material_balances(**kwargs) else: raise ConfigurationError( "{} invalid balance_type for add_material_balances." "Please contact the unit model developer with this bug.". format(self.name)) return mb def add_energy_balances(self, balance_type=EnergyBalanceType.enthalpyPhase, **kwargs): """ General method for adding energy balances to a control volume. This method makes calls to specialised sub-methods for each type of energy balance. Args: balance_type (EnergyBalanceType): Enum indicating which type of energy balance should be constructed. has_heat_of_reaction (bool): whether terms for heat of reaction should be included in energy balance has_heat_transfer (bool): whether generic heat transfer terms should be included in energy balances has_work_transfer (bool): whether generic mass transfer terms should be included in energy balances custom_term (Expression): a Pyomo Expression representing custom terms to be included in energy balances Returns: Constraint objects constructed by sub-method """ if balance_type == EnergyBalanceType.none: eb = None elif balance_type == EnergyBalanceType.enthalpyTotal: eb = self.add_total_enthalpy_balances(**kwargs) elif balance_type == EnergyBalanceType.enthalpyPhase: eb = self.add_phase_enthalpy_balances(**kwargs) elif balance_type == EnergyBalanceType.energyTotal: eb = self.add_total_energy_balances(**kwargs) elif balance_type == EnergyBalanceType.energyPhase: eb = self.add_phase_energy_balances(**kwargs) else: raise ConfigurationError( "{} invalid balance_type for add_energy_balances." "Please contact the unit model developer with this bug.". format(self.name)) return eb def add_momentum_balances(self, balance_type=MomentumBalanceType.pressureTotal, **kwargs): """ General method for adding momentum balances to a control volume. This method makes calls to specialised sub-methods for each type of momentum balance. Args: balance_type (MomentumBalanceType): Enum indicating which type of momentum balance should be constructed. has_pressure_change (bool): whether default generation terms for pressure change should be included in momentum balances custom_term (Expression): a Pyomo Expression representing custom terms to be included in momentum balances Returns: Constraint objects constructed by sub-method """ if balance_type == MomentumBalanceType.none: mb = None elif balance_type == MomentumBalanceType.pressureTotal: mb = self.add_total_pressure_balances(**kwargs) elif balance_type == MomentumBalanceType.pressurePhase: mb = self.add_phase_pressure_balances(**kwargs) elif balance_type == MomentumBalanceType.momentumTotal: mb = self.add_total_momentum_balances(**kwargs) elif balance_type == MomentumBalanceType.momentumPhase: mb = self.add_phase_momentum_balances(**kwargs) else: raise ConfigurationError( "{} invalid balance_type for add_momentum_balances." "Please contact the unit model developer with this bug.". format(self.name)) return mb def _auto_construct(self): """ Placeholder _auto_construct method to ensure a useful exception is returned if auto_build is set to True but something breaks in the process. Derived ControlVolume classes should overload this. Args: None Returns: None """ parent = self.parent_block() self.add_geometry() self.add_state_blocks() self.add_reaction_blocks() self.add_material_balances( material_balance_type=parent.config.material_balance_type, has_rate_reactions=parent.config.has_rate_reactions, has_equilibrium_reactions=parent.config.has_equilibrium_reactions, has_phase_equilibrium=parent.config.has_phase_equilibrium, has_mass_transfer=parent.config.has_mass_transfer) self.add_energy_balances( energy_balance_type=parent.config.energy_balance_type, has_heat_of_reaction=parent.config.has_heat_of_reaction, has_heat_transfer=parent.config.has_heat_transfer, has_work_transfer=parent.config.has_work_transfer) self.add_momentum_balances( has_pressure_change=parent.config.has_pressure_change) try: self.apply_transformation() except AttributeError: pass # Add placeholder methods for adding property and reaction packages def add_state_blocks(self, *args, **kwargs): """ Method for adding StateBlocks to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_state_blocks. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_reaction_blocks(self, *args, **kwargs): """ Method for adding ReactionBlocks to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_reaction_blocks. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) # Add placeholder methods for all types of material, energy and momentum # balance equations which return NotImplementedErrors def add_phase_component_balances(self, *args, **kwargs): """ Method for adding material balances indexed by phase and component to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_phase_component_material_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_component_balances(self, *args, **kwargs): """ Method for adding material balances indexed by component to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_component_material_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_element_balances(self, *args, **kwargs): """ Method for adding total elemental material balances indexed to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_element_material_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_material_balances(self, *args, **kwargs): """ Method for adding a total material balance to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_material_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_phase_enthalpy_balances(self, *args, **kwargs): """ Method for adding enthalpy balances indexed by phase to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_phase_enthalpy_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_enthalpy_balances(self, *args, **kwargs): """ Method for adding a total enthalpy balance to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_enthalpy_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_phase_energy_balances(self, *args, **kwargs): """ Method for adding energy balances (including kinetic energy) indexed by phase to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_phase_energy_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_energy_balances(self, *args, **kwargs): """ Method for adding a total energy balance (including kinetic energy) to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_energy_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_phase_pressure_balances(self, *args, **kwargs): """ Method for adding pressure balances indexed by phase to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_phase_pressure_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_pressure_balances(self, *args, **kwargs): """ Method for adding a total pressure balance to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_pressure_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_phase_momentum_balances(self, *args, **kwargs): """ Method for adding momentum balances indexed by phase to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_phase_momentum_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def add_total_momentum_balances(self, *args, **kwargs): """ Method for adding a total momentum balance to the control volume. See specific control volume documentation for details. """ raise NotImplementedError( "{} control volume class has not implemented a method for " "add_total_momentum_balances. Please contact the " "developer of the ControlVolume class you are using.".format( self.name)) def _rxn_rate_conv(b, t, x, j, has_rate_reactions): """ Method to determine conversion term for reaction rate terms in material balance equations. This method gets the basis of the material flow and reaction rate terms and determines the correct conversion factor. """ # If rate reactions are not required, skip the rest and return 1 if not has_rate_reactions: return 1 if x is None: # 0D control volume flow_basis = b.properties_out[t].get_material_flow_basis() prop = b.properties_out[t] rxn_basis = b.reactions[t].get_reaction_rate_basis() else: # 1D control volume flow_basis = b.properties[t, x].get_material_flow_basis() prop = b.properties[t, x] rxn_basis = b.reactions[t, x].get_reaction_rate_basis() # Check for undefined basis if flow_basis == MaterialFlowBasis.other: raise ConfigurationError( "{} contains reaction terms, but the property package " "used an undefined basis (MaterialFlowBasis.other). " "Rate based reaction terms require the property " "package to define the basis of the material flow " "terms.".format(b.name)) if rxn_basis == MaterialFlowBasis.other: raise ConfigurationError( "{} contains reaction terms, but the reaction package " "used an undefined basis (MaterialFlowBasis.other). " "Rate based reaction terms require the reaction " "package to define the basis of the reaction rate " "terms.".format(b.name)) try: if flow_basis == rxn_basis: return 1 elif (flow_basis == MaterialFlowBasis.mass and rxn_basis == MaterialFlowBasis.molar): return prop.mw[j] elif (flow_basis == MaterialFlowBasis.molar and rxn_basis == MaterialFlowBasis.mass): return 1 / prop.mw[j] else: raise BurntToast( "{} encountered unrecognsied combination of bases " "for reaction rate terms. Please contact the IDAES" " developers with this bug.".format(b.name)) except AttributeError: raise PropertyNotSupportedError( "{} property package does not support " "molecular weight (mw), which is required for " "using property and reaction packages with " "different bases.".format(b.name))
class MomentumBalanceType(Enum): none = 0 pressureTotal = 1 pressurePhase = 2 momentumTotal = 3 momentumPhase = 4 # Enumerate options for flow direction class FlowDirection(Enum): forward = 1 backward = 2 # Set up example ConfigBlock that will work with ControlVolume autobuild method CONFIG_Template = ProcessBlockData.CONFIG() CONFIG_Template.declare( "dynamic", ConfigValue(default=useDefault, domain=In([useDefault, True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic, **default** - useDefault. **Valid values:** { **useDefault** - get flag from parent, **True** - set as a dynamic model, **False** - set as a steady-state model}""")) CONFIG_Template.declare( "has_holdup", ConfigValue( default=False,
class ReactionParameterBlock(ProcessBlockData, property_meta.HasPropertyClassMetadata): """ This is the base class for reaction parameter blocks. These are blocks that contain a set of parameters associated with a specific reaction package, and are linked to by all instances of that reaction package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare( "property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def __init__(self, *args, **kwargs): self.__reaction_block_class = None super().__init__(*args, **kwargs) def build(self): """ General build method for ReactionParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ super(ReactionParameterBlock, self).build() if not hasattr(self, "_reaction_block_class"): self._reaction_block_class = None # TODO: Need way to tie reaction package to a specfic property package self._validate_property_parameter_units() self._validate_property_parameter_properties() @property def reaction_block_class(self): if self._reaction_block_class is not None: return self._reaction_block_class else: raise AttributeError( "{} has not assigned a ReactionBlock class to be associated " "with this reaction package. Please contact the developer of " "the reaction package.".format(self.name)) @reaction_block_class.setter def reaction_block_class(self, val): _log.warning("DEPRECATED: reaction_block_class should not be set " "directly. Property package developers should set the " "_reaction_block_class attribute instead.") self._reaction_block_class = val def build_reaction_block(self, *args, **kwargs): """ Methods to construct a ReactionBlock assoicated with this ReactionParameterBlock. This will automatically set the parameters construction argument for the ReactionBlock. Returns: ReactionBlock """ default = kwargs.pop("default", {}) initialize = kwargs.pop("initialize", {}) if initialize == {}: default["parameters"] = self else: for i in initialize.keys(): initialize[i]["parameters"] = self return self.reaction_block_class(*args, **kwargs, default=default, initialize=initialize) def _validate_property_parameter_units(self): """ Checks that the property parameter block associated with the reaction block uses the same set of default units. """ r_units = self.get_metadata().default_units prop_units = self.config.property_package.get_metadata().default_units for u in r_units: try: # TODO: This check is for backwards compatability with # pre-units property packages. It can be removed once these are # fully deprecated. if isinstance(prop_units[u], str) and (prop_units[u] != r_units[u]): raise KeyError() elif prop_units[u] is not r_units[u]: raise KeyError() except KeyError: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not use the same set of ' 'units of measurement ({}). Please choose a ' 'property package which uses the same units.'.format( self.name, u)) def _validate_property_parameter_properties(self): """ Checks that the property parameter block associated with the reaction block supports the necessary properties with correct units. """ req_props = self.get_metadata().required_properties supp_props = self.config.property_package.get_metadata().properties for p in req_props: if p not in supp_props: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) elif supp_props[p]['method'] is False: raise PropertyPackageError( '{} the property package associated with this ' 'reaction package does not support the necessary ' 'property, {}. Please choose a property package ' 'which supports all required properties.'.format( self.name, p)) # Check property units if req_props[p]['units'] != supp_props[p]['units']: raise PropertyPackageError( '{} the units associated with property {} in this ' 'reaction package ({}) do not match with the units ' 'used in the assoicated property package ({}). Please ' 'choose a property package which used the same ' 'units for all properties.'.format(self.name, p, req_props[p]['units'], supp_props[p]['units']))