def phase_equil(b, phase_pair): # This method is called via StateBlock.build, thus does not need clean-up # try/except statements suffix = "_" + phase_pair[0] + "_" + phase_pair[1] # Smooth VLE assumes a liquid and a vapor phase, so validate this (l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps) = _valid_VL_component_list(b, phase_pair) if l_phase is None or v_phase is None: raise ConfigurationError( "{} Generic Property Package phase pair {}-{} was set to use " "Smooth VLE formulation, however this is not a vapor-liquid pair.". format(b.params.name, phase_pair[0], phase_pair[1])) # Definition of equilibrium temperature for smooth VLE t_units = b.params.get_metadata().default_units["temperature"] if v_only_comps == []: b.add_component( "_t1" + suffix, Var(initialize=b.temperature.value, doc='Intermediate temperature for calculating Teq', units=t_units)) _t1 = getattr(b, "_t1" + suffix) b.add_component( "eps_1" + suffix, Param(default=0.01, mutable=True, doc='Smoothing parameter for Teq', units=t_units)) eps_1 = getattr(b, "eps_1" + suffix) # PSE paper Eqn 13 def rule_t1(b): return _t1 == smooth_max(b.temperature, b.temperature_bubble[phase_pair], eps_1) b.add_component("_t1_constraint" + suffix, Constraint(rule=rule_t1)) else: _t1 = b.temperature if l_only_comps == []: b.add_component( "eps_2" + suffix, Param(default=0.0005, mutable=True, doc='Smoothing parameter for Teq', units=t_units)) eps_2 = getattr(b, "eps_2" + suffix) # PSE paper Eqn 14 # TODO : Add option for supercritical extension def rule_teq(b): return b._teq[phase_pair] == smooth_min( _t1, b.temperature_dew[phase_pair], eps_2) elif v_only_comps == []: def rule_teq(b): return b._teq[phase_pair] == _t1 else: def rule_teq(b): return b._teq[phase_pair] == b.temperature b.add_component("_teq_constraint" + suffix, Constraint(rule=rule_teq))
def initialize(blk, state_args_PA=None, state_args_SA=None, outlvl=idaeslog.NOTSET, solver=None, optarg={}): ''' Initialization routine. 1.- initialize state blocks, using an initial guess for inlet primary air and secondary air. 2.- Use PA and SA values to guess flue gas component molar flowrates, Temperature, and Pressure. Initialize flue gas state block. 3.- Then, solve complete model. Keyword Arguments: state_args_PA : a dict of arguments to be passed to the property package(s) for the primary air state block to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_SA : a dict of arguments to be passed to the property package(s) for secondary air state block to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine optarg : solver options dictionary object (default={}) solver : str indicating whcih solver to use during initialization (default = None, use default solver) Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize inlet property blocks blk.primary_air.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_PA) blk.primary_air_moist.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_PA) blk.secondary_air.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_SA) init_log.info_high("Initialization Step 1 Complete.") state_args = { "flow_mol_comp": { "H2O": (blk.primary_air_inlet.flow_mol_comp[0, "H2O"].value + blk.secondary_air_inlet.flow_mol_comp[0, "H2O"].value), "CO2": (blk.primary_air_inlet.flow_mol_comp[0, "CO2"].value + blk.secondary_air_inlet.flow_mol_comp[0, "CO2"].value), "N2": (blk.primary_air_inlet.flow_mol_comp[0, "N2"].value + blk.secondary_air_inlet.flow_mol_comp[0, "N2"].value), "O2": (blk.primary_air_inlet.flow_mol_comp[0, "O2"].value + blk.secondary_air_inlet.flow_mol_comp[0, "O2"].value), "SO2": (blk.primary_air_inlet.flow_mol_comp[0, "SO2"].value + blk.secondary_air_inlet.flow_mol_comp[0, "SO2"].value), "NO": (blk.primary_air_inlet.flow_mol_comp[0, "NO"].value + blk.secondary_air_inlet.flow_mol_comp[0, "NO"].value) }, "temperature": 1350.00, "pressure": blk.primary_air_inlet.pressure[0].value } # initialize flue gas outlet blk.flue_gas.initialize(state_args=state_args, outlvl=outlvl, solver=solver) init_log.info_high("Initialization Step 2 Complete.") if blk.config.calculate_PA_SA_flows is False: # Option 1: given PA and SA component flow rates - fixed inlets # fix inlet component molar flow rates # unfix ratio_PA2coal, SR, and fluegas_o2_pct_dry blk.primary_air_inlet.flow_mol_comp[...].fix() blk.secondary_air_inlet.flow_mol_comp[...].fix() blk.ratio_PA2coal.unfix() blk.SR.unfix() blk.fluegas_o2_pct_dry.unfix() dof = degrees_of_freedom(blk) else: # Option 2: SR, ratioPA2_coal to estimate TCA, PA, SA # unfix component molar flow rates, but keep T and P fixed. blk.primary_air_inlet.flow_mol_comp[:, :].unfix() blk.secondary_air_inlet.flow_mol_comp[:, :].unfix() dof = degrees_of_freedom(blk) if not dof == 0: raise ConfigurationError('User needs to check ' 'degrees of freedom') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) init_log.info("Initialization Complete.")
def build(self): """Add contents to the block.""" super().build() self._state_block_class = FlueGasStateBlock _valid_comps = ["N2", "O2", "NO", "CO2", "H2O", "SO2"] for j in self.config.components: if j not in _valid_comps: raise ConfigurationError(f"Component '{j}' is not supported") self.add_component(j, Component()) # Create Phase object self.Vap = VaporPhase() # Molecular weight self.mw_comp = Param( self.component_list, initialize={ k: v for k, v in { "O2": 0.0319988, "N2": 0.0280134, "NO": 0.0300061, "CO2": 0.0440095, "H2O": 0.0180153, "SO2": 0.064064, }.items() if k in self.component_list }, doc="Molecular Weight [kg/mol]", units=pyunits.kg / pyunits.mol, ) # Thermodynamic reference state self.pressure_ref = Param( within=PositiveReals, default=1.01325e5, doc="Reference pressure [Pa]", units=pyunits.Pa, ) self.temperature_ref = Param( within=PositiveReals, default=298.15, doc="Reference temperature [K]", units=pyunits.K, ) # Critical Properties self.pressure_crit = Param( self.component_list, within=PositiveReals, initialize={ k: v for k, v in { "O2": 50.45985e5, "N2": 33.943875e5, "NO": 64.85e5, "CO2": 73.8e5, "H2O": 220.64e5, "SO2": 7.883e6, }.items() if k in self.component_list }, doc="Critical pressure [Pa]", units=pyunits.Pa, ) self.temperature_crit = Param( self.component_list, within=PositiveReals, initialize={ k: v for k, v in { "O2": 154.58, "N2": 126.19, "NO": 180.0, "CO2": 304.18, "H2O": 647, "SO2": 430.8, }.items() if k in self.component_list }, doc="Critical temperature [K]", units=pyunits.K, ) # Constants for specific heat capacity, enthalpy, and entropy # calculations for ideal gas (from NIST 01/08/2020 # https://webbook.nist.gov/cgi/cbook.cgi?ID=C7727379&Units=SI&Mask=1#Thermo-Gas) cp_mol_ig_comp_coeff_parameter_A = { k: v for k, v in { "N2": 19.50583, "O2": 30.03235, "CO2": 24.99735, "H2O": 30.092, "NO": 23.83491, "SO2": 21.43049, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_B = { k: v for k, v in { "N2": 19.88705, "O2": 8.772972, "CO2": 55.18696, "H2O": 6.832514, "NO": 12.58878, "SO2": 74.35094, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_C = { k: v for k, v in { "N2": -8.598535, "O2": -3.98813, "CO2": -33.69137, "H2O": 6.793435, "NO": -1.139011, "SO2": -57.75217, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_D = { k: v for k, v in { "N2": 1.369784, "O2": 0.788313, "CO2": 7.948387, "H2O": -2.53448, "NO": -1.497459, "SO2": 16.35534, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_E = { k: v for k, v in { "N2": 0.527601, "O2": -0.7416, "CO2": -0.136638, "H2O": 0.082139, "NO": 0.214194, "SO2": 0.086731, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_F = { k: v for k, v in { "N2": -4.935202, "O2": -11.3247, "CO2": -403.6075, "H2O": -250.881, "NO": 83.35783, "SO2": -305.7688, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_G = { k: v for k, v in { "N2": 212.39, "O2": 236.1663, "CO2": 228.2431, "H2O": 223.3967, "NO": 237.1219, "SO2": 254.8872, }.items() if k in self.component_list } cp_mol_ig_comp_coeff_parameter_H = { k: v for k, v in { "N2": 0, "O2": 0, "CO2": -393.5224, "H2O": -241.8264, "NO": 90.29114, "SO2": -296.8422, }.items() if k in self.component_list } self.cp_mol_ig_comp_coeff_A = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_A, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K, ) self.cp_mol_ig_comp_coeff_B = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_B, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK, ) self.cp_mol_ig_comp_coeff_C = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_C, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**2, ) self.cp_mol_ig_comp_coeff_D = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_D, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**3, ) self.cp_mol_ig_comp_coeff_E = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_E, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K * pyunits.kK**2, ) self.cp_mol_ig_comp_coeff_F = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_F, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.kJ / pyunits.mol, ) self.cp_mol_ig_comp_coeff_G = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_G, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.J / pyunits.mol / pyunits.K, ) self.cp_mol_ig_comp_coeff_H = Param( self.component_list, initialize=cp_mol_ig_comp_coeff_parameter_H, doc="Constants for spec. heat capacity for ideal gas", units=pyunits.kJ / pyunits.mol, ) # Viscosity and thermal conductivity parameters self.ce_param = Param( initialize=2.6693e-5, units=(pyunits.g**0.5 * pyunits.mol**0.5 * pyunits.angstrom**2 * pyunits.K**-0.5 * pyunits.cm**-1 * pyunits.s**-1), doc="Parameter for the Chapman-Enskog viscosity correlation", ) self.sigma = Param( self.component_list, initialize={ k: v for k, v in { "O2": 3.458, "N2": 3.621, "NO": 3.47, "CO2": 3.763, "H2O": 2.605, "SO2": 4.29, }.items() if k in self.component_list }, doc="collision diameter", units=pyunits.angstrom, ) self.ep_Kappa = Param( self.component_list, initialize={ k: v for k, v in { "O2": 107.4, "N2": 97.53, "NO": 119.0, "CO2": 244.0, "H2O": 572.4, "SO2": 252.0, }.items() if k in self.component_list }, doc= "Boltzmann constant divided by characteristic Lennard-Jones energy", units=pyunits.K, ) self.set_default_scaling("flow_mol", 1e-4) self.set_default_scaling("flow_mass", 1e-3) self.set_default_scaling("flow_vol", 1e-3) # anything not explicitly listed self.set_default_scaling("mole_frac_comp", 1) self.set_default_scaling("mole_frac_comp", 1e3, index="NO") self.set_default_scaling("mole_frac_comp", 1e3, index="SO2") self.set_default_scaling("mole_frac_comp", 1e2, index="H2O") self.set_default_scaling("mole_frac_comp", 1e2, index="CO2") self.set_default_scaling("flow_vol", 1) # For flow_mol_comp, will calculate from flow_mol and mole_frac_comp # user should set a scale for both, and for each compoent of # mole_frac_comp self.set_default_scaling("pressure", 1e-5) self.set_default_scaling("temperature", 1e-1) self.set_default_scaling("pressure_red", 1e-3) self.set_default_scaling("temperature_red", 1) self.set_default_scaling("enth_mol_phase", 1e-3) self.set_default_scaling("enth_mol", 1e-3) self.set_default_scaling("entr_mol", 1e-2) self.set_default_scaling("entr_mol_phase", 1e-2) self.set_default_scaling("cp_mol", 0.1) self.set_default_scaling("cp_mol_phase", 0.1) self.set_default_scaling("compress_fact", 1) self.set_default_scaling("dens_mol_phase", 1) self.set_default_scaling("pressure_sat", 1e-4) self.set_default_scaling("visc_d_comp", 1e4) self.set_default_scaling("therm_cond_comp", 1e2) self.set_default_scaling("visc_d", 1e4) self.set_default_scaling("therm_cond", 1e2) self.set_default_scaling("mw", 1) self.set_default_scaling("mw_comp", 1)
def initialize(self, state_args_feed=None, state_args_liq=None, state_args_vap=None, hold_state_liq=False, hold_state_vap=False, solver=None, optarg=None, outlvl=idaeslog.NOTSET): # TODO: # 1. Initialization for dynamic mode. Currently not supported. # 2. Handle unfixed side split fraction vars # 3. Better logic to handle and fix state vars. init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") init_log.info("Begin initialization.") if solver is None: init_log.warning("Solver not provided. Default solver(ipopt) " " being used for initialization.") solver = get_default_solver() if self.config.has_liquid_side_draw: if not self.liq_side_sf.fixed: raise ConfigurationError( "Liquid side draw split fraction not fixed but " "has_liquid_side_draw set to True.") if self.config.has_vapor_side_draw: if not self.vap_side_sf.fixed: raise ConfigurationError( "Vapor side draw split fraction not fixed but " "has_vapor_side_draw set to True.") # Create initial guess if not provided by using current values if self.config.is_feed_tray and state_args_feed is None: state_args_feed = {} state_args_liq = {} state_args_vap = {} state_dict = ( self.properties_in_feed[ self.flowsheet().config.time.first()] .define_port_members()) for k in state_dict.keys(): if "flow" in k: if state_dict[k].is_indexed(): state_args_feed[k] = {} state_args_liq[k] = {} state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_feed[k][m] = \ value(state_dict[k][m]) state_args_liq[k][m] = \ value(0.1 * state_dict[k][m]) state_args_vap[k][m] = \ value(0.1 * state_dict[k][m]) else: state_args_feed[k] = value(state_dict[k]) state_args_liq[k] = 0.1 * value(state_dict[k]) state_args_vap[k] = 0.1 * value(state_dict[k]) else: if state_dict[k].is_indexed(): state_args_feed[k] = {} state_args_liq[k] = {} state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_feed[k][m] = \ value(state_dict[k][m]) state_args_liq[k][m] = \ value(state_dict[k][m]) state_args_vap[k][m] = \ value(state_dict[k][m]) else: state_args_feed[k] = value(state_dict[k]) state_args_liq[k] = value(state_dict[k]) state_args_vap[k] = value(state_dict[k]) # Create initial guess if not provided by using current values if not self.config.is_feed_tray and state_args_liq is None: state_args_liq = {} state_dict = ( self.properties_in_liq[ self.flowsheet().config.time.first()] .define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args_liq[k] = {} for m in state_dict[k].keys(): state_args_liq[k][m] = \ value(state_dict[k][m]) else: state_args_liq[k] = value(state_dict[k]) # Create initial guess if not provided by using current values if not self.config.is_feed_tray and state_args_vap is None: state_args_vap = {} state_dict = ( self.properties_in_vap[ self.flowsheet().config.time.first()] .define_port_members()) for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args_vap[k] = {} for m in state_dict[k].keys(): state_args_vap[k][m] = \ value(state_dict[k][m]) else: state_args_vap[k] = value(state_dict[k]) if self.config.is_feed_tray: feed_flags = self.properties_in_feed.initialize( outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_feed, state_vars_fixed=False) liq_in_flags = self.properties_in_liq. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_liq, state_vars_fixed=False) vap_in_flags = self.properties_in_vap. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=True, state_args=state_args_vap, state_vars_fixed=False) # state args to initialize the mixed outlet state block state_args_mixed = {} if self.config.is_feed_tray: # if feed tray, initialize the mixed state block at # the same condition. state_args_mixed = state_args_feed else: # if not feed tray, initialize mixed state block at average of # vap/liq inlets except pressure. While this is crude, it # will work for most combination of state vars. state_dict = ( self.properties_in_liq[ self.flowsheet().config.time.first()] .define_port_members()) for k in state_dict.keys(): if k == "pressure": # Take the lowest pressure and this is the liq inlet state_args_mixed[k] = value(self.properties_in_liq[0]. component(state_dict[k]. local_name)) elif state_dict[k].is_indexed(): state_args_mixed[k] = {} for m in state_dict[k].keys(): if "flow" in k: state_args_mixed[k][m] = \ value(self.properties_in_liq[0]. component(state_dict[k].local_name)[m]) \ + value(self.properties_in_vap[0]. component(state_dict[k].local_name)[m]) else: state_args_mixed[k][m] = \ 0.5 * (value(self.properties_in_liq[0]. component(state_dict[k]. local_name)[m]) + value(self.properties_in_vap[0]. component(state_dict[k].local_name)[m])) else: if "flow" in k: state_args_mixed[k] = \ value(self.properties_in_liq[0]. component(state_dict[k].local_name)) +\ value(self.properties_in_vap[0]. component(state_dict[k].local_name)) else: state_args_mixed[k] = \ 0.5 * (value(self.properties_in_liq[0]. component(state_dict[k].local_name)) + value(self.properties_in_vap[0]. component(state_dict[k].local_name))) # Initialize the mixed outlet state block self.properties_out. \ initialize(outlvl=outlvl, solver=solver, optarg=optarg, hold_state=False, state_args=state_args_mixed, state_vars_fixed=False) # Deactivate energy balance self.enthalpy_mixing_equations.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].temperature.\ fix(state_args_mixed["temperature"]) except AttributeError: init_log.warning("Trying to fix outlet temperature " "during initialization but temperature attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") # Deactivate pressure balance self.pressure_drop_equation.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].pressure.\ fix(state_args_mixed["pressure"]) except AttributeError: init_log.warning("Trying to fix outlet pressure " "during initialization but pressure attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info( "Mass balance solve {}.".format(idaeslog.condition(res)) ) # Activate energy balance self.enthalpy_mixing_equations.activate() try: self.properties_out[:].temperature.unfix() except AttributeError: pass with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info( "Mass and energy balance solve {}.".format(idaeslog.condition(res)) ) # Activate pressure balance self.pressure_drop_equation.activate() try: self.properties_out[:].pressure.unfix() except AttributeError: pass if degrees_of_freedom(self) == 0: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info( "Mass, energy and pressure balance solve {}.". format(idaeslog.condition(res))) else: raise Exception("State vars fixed but degrees of freedom " "for tray block is not zero during " "initialization.") init_log.info( "Initialization complete, status {}.". format(idaeslog.condition(res))) if not self.config.is_feed_tray: if not hold_state_vap: self.properties_in_vap.release_state(flags=vap_in_flags, outlvl=outlvl) if not hold_state_liq: self.properties_in_liq.release_state(flags=liq_in_flags, outlvl=outlvl) if hold_state_liq and hold_state_vap: return liq_in_flags, vap_in_flags elif hold_state_vap: return vap_in_flags elif hold_state_liq: return liq_in_flags else: self.properties_in_liq.release_state(flags=liq_in_flags, outlvl=outlvl) self.properties_in_vap.release_state(flags=vap_in_flags, outlvl=outlvl) return feed_flags
def _setup_dynamics(self): """ This method automates the setting of the dynamic flag and time domain for unit models. Performs the following: 1) Determines if this is a top level flowsheet 2) Gets dynamic flag from parent if not top level, or checks validity of argument provided 3) Checks has_holdup flag if present and dynamic = True Args: None Returns: None """ # Get parent object if hasattr(self.parent_block(), "config"): # Parent block has a config block, so use this parent = self.parent_block() else: # Use parent flowsheet try: parent = self.flowsheet() except ConfigurationError: raise DynamicError('{} has no parent flowsheet from which to ' 'get dynamic argument. Please provide a ' 'value for this argument when constructing ' 'the unit.' .format(self.name)) # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == useDefault: # Get flag from parent flowsheet try: self.config.dynamic = parent.config.dynamic except AttributeError: # No flowsheet, raise exception raise DynamicError('{} parent flowsheet has no dynamic ' 'argument. Please provide a ' 'value for this argument when constructing ' 'the unit.' .format(self.name)) # Check for case when dynamic=True, but parent dynamic=False if (self.config.dynamic and not parent.config.dynamic): 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)) # Set and validate has_holdup argument if self.config.has_holdup == useDefault: # Default to same value as dynamic flag self.config.has_holdup = self.config.dynamic elif self.config.has_holdup is False: if self.config.dynamic is True: # Dynamic model must have has_holdup = True raise ConfigurationError( "{} invalid arguments for dynamic and has_holdup. " "If dynamic = True, has_holdup must also be True " "(was False)".format(self.name))
def build_parameters(b): param_block = b.parent_block() if not (b.is_vapor_phase() or b.is_liquid_phase()): raise PropertyNotSupportedError( "{} received unrecognized phase " "name {}. Cubic equation of state supports only Vap and Liq " "phases.".format(param_block.name, b)) if b.config.equation_of_state_options["type"] not in set( item for item in CubicType): raise ConfigurationError( "{} Unrecognized option for equation of " "state type: {}. Must be an instance of CubicType " "Enum.".format(b.name, b.config.equation_of_state_options["type"])) ctype = b.config.equation_of_state_options["type"] b._cubic_type = ctype cname = ctype.name # Check to see if ConfigBlock was created by previous phase if hasattr(param_block, cname + "_eos_options"): ConfigBlock = getattr(param_block, cname + "_eos_options") for key, value in b.config.equation_of_state_options.items(): if ConfigBlock[key] != value: raise ConfigurationError( "In {}, different {} equation of " "state options for {} are set in different phases, which is " "not supported.".format(b.name, cname, key)) # Once the options have been validated, we don't have anything # left to do mixing_rule_a = ConfigBlock["mixing_rule_a"] mixing_rule_b = ConfigBlock["mixing_rule_b"] b._mixing_rule_a = mixing_rule_a b._mixing_rule_b = mixing_rule_b return setattr(param_block, cname + "_eos_options", deepcopy(CubicConfig)) ConfigBlock = getattr(param_block, cname + "_eos_options") ConfigBlock.set_value(b.config.equation_of_state_options) mixing_rule_a = ConfigBlock["mixing_rule_a"] mixing_rule_b = ConfigBlock["mixing_rule_b"] b._mixing_rule_a = mixing_rule_a b._mixing_rule_b = mixing_rule_b kappa_data = param_block.config.parameter_data[cname + "_kappa"] param_block.add_component( cname + '_kappa', Var(param_block.component_list, param_block.component_list, within=Reals, initialize=kappa_data, doc=cname + ' binary interaction parameters', units=None)) if b._cubic_type == CubicType.PR: func_fw = func_fw_PR elif b._cubic_type == CubicType.SRK: func_fw = func_fw_SRK else: raise BurntToast( "{} received unrecognized cubic type. This should " "never happen, so please contact the IDAES developers " "with this bug.".format(b.name)) setattr(param_block, cname + "_func_fw", func_fw) setattr(param_block, cname + "_func_alpha", func_alpha_soave) setattr(param_block, cname + "_func_dalpha_dT", func_dalpha_dT_soave) setattr(param_block, cname + "_func_d2alpha_dT2", func_d2alpha_dT2_soave)
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 is None and fs is not None: # We will get units from parent pass elif self.config.time_units is None and self.config.dynamic: raise ConfigurationError( f"{self.name} - no units were specified for the time domain. " f"Units must be be specified for dynamic models.") elif self.config.time_units is None and not self.config.dynamic: _log.debug("No units specified for stady-state time domain.") 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)) add_object_reference(self, "_time", self.config.time) 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.time add_object_reference(self, "_time", fs.time) self._time_units = fs._time_units
def initialize(self, state_args_feed=None, state_args_liq=None, state_args_vap=None, solver=None, outlvl=idaeslog.NOTSET): # TODO: # 1. Check initialization for dynamic mode. Currently not supported. # 2. Handle unfixed side split fraction vars # 3. Better logic to handle and fix state vars. init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") init_log.info("Begin initialization.") if solver is None: init_log.warning("Solver not provided. Default solver(ipopt) " " being used for initialization.") solver = get_default_solver() if self.config.has_liquid_side_draw: if not self.liq_side_sf.fixed: raise ConfigurationError( "Liquid side draw split fraction not fixed but " "has_liquid_side_draw set to True.") if self.config.has_vapor_side_draw: if not self.vap_side_sf.fixed: raise ConfigurationError( "Vapor side draw split fraction not fixed but " "has_vapor_side_draw set to True.") # Initialize the inlet state blocks if self.config.is_feed_tray: self.properties_in_feed.initialize(state_args=state_args_feed, solver=solver, outlvl=outlvl) self.properties_in_liq.initialize(state_args=state_args_liq, solver=solver, outlvl=outlvl) self.properties_in_vap.initialize(state_args=state_args_vap, solver=solver, outlvl=outlvl) # state args to initialize the mixed outlet state block state_args_mixed = {} if self.config.is_feed_tray and state_args_feed is not None: # if initial guess provided for the feed stream, initialize the # mixed state block at the same condition. state_args_mixed = state_args_feed else: state_dict = \ self.properties_out[self.flowsheet().config.time.first()].\ define_state_vars() if self.config.is_feed_tray: for k in state_dict.keys(): if state_dict[k].is_indexed(): state_args_mixed[k] = {} for m in state_dict[k].keys(): state_args_mixed[k][m] = \ self.properties_in_feed[self.flowsheet(). config.time.first()].\ component(state_dict[k].local_name)[m].value else: state_args_mixed[k] = \ self.properties_in_feed[self.flowsheet(). config.time.first()].\ component(state_dict[k].local_name).value else: # if not feed tray, initialize mixed state block at average of # vap/liq inlets except pressure. While this is crude, it # will work for most combination of state vars. for k in state_dict.keys(): if k == "pressure": # Take the lowest pressure and this is the liq inlet state_args_mixed[k] = self.properties_in_liq[0].\ component(state_dict[k].local_name).value elif state_dict[k].is_indexed(): state_args_mixed[k] = {} for m in state_dict[k].keys(): state_args_mixed[k][m] = \ 0.5 * (self.properties_in_liq[0]. component(state_dict[k].local_name)[m]. value + self.properties_in_vap[0]. component(state_dict[k].local_name)[m]. value) else: state_args_mixed[k] = \ 0.5 * (self.properties_in_liq[0]. component(state_dict[k].local_name).value + self.properties_in_vap[0]. component(state_dict[k].local_name).value) # Initialize the mixed outlet state block self.properties_out.initialize(state_args=state_args_mixed, solver=solver, outlvl=outlvl) # Deactivate energy balance self.enthalpy_mixing_equations.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].temperature.\ fix(state_args_mixed["temperature"]) except AttributeError: init_log.warning("Trying to fix outlet temperature " "during initialization but temperature attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") # Deactivate pressure balance self.pressure_drop_equation.deactivate() # Try fixing the outlet temperature if else pass # NOTE: if passed then there would probably be a degree of freedom try: self.properties_out[:].pressure.\ fix(state_args_mixed["pressure"]) except AttributeError: init_log.warning("Trying to fix outlet pressure " "during initialization but pressure attribute " "unavailable in the state block. Initialization " "proceeding with a potential degree of freedom.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info_high( "Mass balance solve {}.".format(idaeslog.condition(res)) ) # Activate energy balance self.enthalpy_mixing_equations.activate() try: self.properties_out[:].temperature.unfix() except AttributeError: pass with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info_high( "Mass and energy balance solve {}.".format(idaeslog.condition(res)) ) # Activate pressure balance self.pressure_drop_equation.activate() try: self.properties_out[:].pressure.unfix() except AttributeError: pass with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(self, tee=slc.tee) init_log.info_high( "Mass, energy and pressure balance solve {}.". format(idaeslog.condition(res))) init_log.info( "Initialization complete, status {}.". format(idaeslog.condition(res)))
def build(self): """ General build method for MixerData. This method calls a number of sub-methods which automate the construction of expected attributes of unit models. Inheriting models should call `super().build`. Args: None Returns: None """ # Call super.build() super(MixerData, self).build() # Call setup methods from ControlVolumeBlockData self._get_property_package() self._get_indexing_sets() # Create list of inlet names inlet_list = self.create_inlet_list() # Build StateBlocks inlet_blocks = self.add_inlet_state_blocks(inlet_list) if self.config.mixed_state_block is None: mixed_block = self.add_mixed_state_block() else: mixed_block = self.get_mixed_state_block() if self.config.material_balance_type != MaterialBalanceType.none: self.add_material_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) else: raise BurntToast("{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format( self.name)) if self.config.energy_mixing_type == MixingType.extensive: self.add_energy_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif self.config.energy_mixing_type == MixingType.none: pass else: raise ConfigurationError( "{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) if self.config.momentum_mixing_type == MomentumMixingType.minimize: self.add_pressure_minimization_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif self.config.momentum_mixing_type == MomentumMixingType.equality: self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif self.config.momentum_mixing_type == \ MomentumMixingType.minimize_and_equality: self.add_pressure_minimization_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.pressure_equality_constraints.deactivate() elif self.config.momentum_mixing_type == MomentumMixingType.none: pass else: raise ConfigurationError( "{} recieved unrecognised value for " "momentum_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) self.add_port_objects(inlet_list, inlet_blocks, mixed_block)
def build(self): ''' Callable method for Block construction. ''' # Call super.build() to initialize Block # In this case we are replicating the super.build to get around a # chicken-and-egg problem # The super.build tries to validate units, but they have not been set # and cannot be set until the config block is created by super.build super(ReactionParameterBlock, self).build() self.default_scaling_factor = {} # Validate and set base units of measurement self.get_metadata().add_default_units(self.config.base_units) units_meta = self.get_metadata().default_units for key, unit in self.config.base_units.items(): if key in [ 'time', 'length', 'mass', 'amount', 'temperature', "current", "luminous intensity" ]: if not isinstance(unit, _PyomoUnit): raise ConfigurationError( "{} recieved unexpected units for quantity {}: {}. " "Units must be instances of a Pyomo unit object.". format(self.name, key, unit)) else: raise ConfigurationError( "{} defined units for an unexpected quantity {}. " "Generic reaction packages only support units for the 7 " "base SI quantities.".format(self.name, key)) # Check that main 5 base units are assigned for k in ['time', 'length', 'mass', 'amount', 'temperature']: if not isinstance(units_meta[k], _PyomoUnit): raise ConfigurationError( "{} units for quantity {} were not assigned. " "Please make sure to provide units for all base units " "when configuring the reaction package.".format( self.name, k)) # TODO: Need way to tie reaction package to a specfic property package self._validate_property_parameter_units() self._validate_property_parameter_properties() # Call configure method to set construction arguments self.configure() # Build core components self._reaction_block_class = GenericReactionBlock # Alias associated property package to keep line length down ppack = self.config.property_package if not hasattr(ppack, "_electrolyte") or not ppack._electrolyte: pc_set = ppack._phase_component_set elif ppack.config.state_components.name == "true": pc_set = ppack.true_phase_component_set elif ppack.config.state_components.name == "apparent": pc_set = ppack.apparent_phase_component_set else: raise BurntToast() # Construct rate reaction attributes if required if len(self.config.rate_reactions) > 0: # Construct rate reaction index self.rate_reaction_idx = Set( initialize=self.config.rate_reactions.keys()) # Construct rate reaction stoichiometry dict self.rate_reaction_stoichiometry = {} for r, rxn in self.config.rate_reactions.items(): for p, j in pc_set: self.rate_reaction_stoichiometry[(r, p, j)] = 0 if rxn.stoichiometry is None: raise ConfigurationError( "{} rate reaction {} was not provided with a " "stoichiometry configuration argument.".format( self.name, r)) else: for k, v in rxn.stoichiometry.items(): if k[0] not in ppack.phase_list: raise ConfigurationError( "{} stoichiometry for rate reaction {} " "included unrecognised phase {}.".format( self.name, r, k[0])) if k[1] not in ppack.component_list: raise ConfigurationError( "{} stoichiometry for rate reaction {} " "included unrecognised component {}.".format( self.name, r, k[1])) self.rate_reaction_stoichiometry[(r, k[0], k[1])] = v # Check that a method was provided for the rate form if rxn.rate_form is None: raise ConfigurationError( "{} rate reaction {} was not provided with a " "rate_form configuration argument.".format( self.name, r)) # Construct equilibrium reaction attributes if required if len(self.config.equilibrium_reactions) > 0: # Construct equilibrium reaction index self.equilibrium_reaction_idx = Set( initialize=self.config.equilibrium_reactions.keys()) # Construct equilibrium reaction stoichiometry dict self.equilibrium_reaction_stoichiometry = {} for r, rxn in self.config.equilibrium_reactions.items(): for p, j in pc_set: self.equilibrium_reaction_stoichiometry[(r, p, j)] = 0 if rxn.stoichiometry is None: raise ConfigurationError( "{} equilibrium reaction {} was not provided with a " "stoichiometry configuration argument.".format( self.name, r)) else: for k, v in rxn.stoichiometry.items(): if k[0] not in ppack.phase_list: raise ConfigurationError( "{} stoichiometry for equilibrium reaction {} " "included unrecognised phase {}.".format( self.name, r, k[0])) if k[1] not in ppack.component_list: raise ConfigurationError( "{} stoichiometry for equilibrium reaction {} " "included unrecognised component {}.".format( self.name, r, k[1])) self.equilibrium_reaction_stoichiometry[(r, k[0], k[1])] = v # Check that a method was provided for the equilibrium form if rxn.equilibrium_form is None: raise ConfigurationError( "{} equilibrium reaction {} was not provided with a " "equilibrium_form configuration argument.".format( self.name, r)) # Add a master reaction index which includes both types of reactions if (len(self.config.rate_reactions) > 0 and len(self.config.equilibrium_reactions) > 0): self.reaction_idx = Set( initialize=(self.rate_reaction_idx | self.equilibrium_reaction_idx)) elif len(self.config.rate_reactions) > 0: self.reaction_idx = Set(initialize=self.rate_reaction_idx) elif len(self.config.equilibrium_reactions) > 0: self.reaction_idx = Set(initialize=self.equilibrium_reaction_idx) else: raise BurntToast("{} Generic property package failed to construct " "master reaction Set. This should not happen. " "Please contact the IDAES developers with this " "bug".format(self.name)) # Construct blocks to contain parameters for each reaction for r in self.reaction_idx: self.add_component("reaction_" + str(r), Block()) # Build parameters if len(self.config.rate_reactions) > 0: for r in self.rate_reaction_idx: rblock = getattr(self, "reaction_" + r) r_config = self.config.rate_reactions[r] order_init = {} for p, j in pc_set: if "reaction_order" in r_config.parameter_data: try: order_init[p, j] = r_config.parameter_data[ "reaction_order"][p, j] except KeyError: order_init[p, j] = 0 else: # Assume elementary reaction and use stoichiometry try: if r_config.stoichiometry[p, j] < 0: # These are reactants, but order is -ve stoic order_init[p, j] = -r_config.stoichiometry[p, j] else: # Anything else is a product, not be included order_init[p, j] = 0 except KeyError: order_init[p, j] = 0 rblock.reaction_order = Var(pc_set, initialize=order_init, doc="Reaction order", units=None) for val in self.config.rate_reactions[r].values(): try: val.build_parameters(rblock, self.config.rate_reactions[r]) except AttributeError: pass if len(self.config.equilibrium_reactions) > 0: for r in self.equilibrium_reaction_idx: rblock = getattr(self, "reaction_" + r) r_config = self.config.equilibrium_reactions[r] order_init = {} for p, j in pc_set: if "reaction_order" in r_config.parameter_data: try: order_init[p, j] = r_config.parameter_data[ "reaction_order"][p, j] except KeyError: order_init[p, j] = 0 else: # Assume elementary reaction and use stoichiometry try: # Here we use the stoic. coeff. directly # However, solids should be excluded as they # normally do not appear in the equilibrium # relationship pobj = ppack.get_phase(p) if not pobj.is_solid_phase(): order_init[p, j] = r_config.stoichiometry[p, j] else: order_init[p, j] = 0 except KeyError: order_init[p, j] = 0 rblock.reaction_order = Var(pc_set, initialize=order_init, doc="Reaction order", units=None) for val in self.config.equilibrium_reactions[r].values(): try: val.build_parameters( rblock, self.config.equilibrium_reactions[r]) except AttributeError: pass except KeyError as err: # This likely arises from mismatched true and apparent # species sets. Reaction packages must use the same # basis as the associated thermo properties # Raise an exception to inform the user raise PropertyPackageError( "{} KeyError encountered whilst constructing " "reaction parameters. This may be due to " "mismatched state_components between the " "Reaction Package and the associated Physical " "Property Package - Reaction Packages must use the" "same basis (true or apparent species) as the " "Physical Property Package.".format(self.name), err) # As a safety check, make sure all Vars in reaction blocks are fixed for v in self.component_objects(Var, descend_into=True): for i in v: if v[i].value is None: raise ConfigurationError( "{} parameter {} was not assigned" " a value. Please check your configuration " "arguments.".format(self.name, v.local_name)) v[i].fix() # Set default scaling factors if self.config.default_scaling_factors is not None: self.default_scaling_factor.update( self.config.default_scaling_factors) # Finally, call populate_default_scaling_factors method to fill blanks iscale.populate_default_scaling_factors(self)
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(GibbsReactorData, self).build() # Validate list of inert species for i in self.config.inert_species: if i not in self.config.property_package.component_list: raise ConfigurationError( "{} invalid component in inert_species argument. {} is " "not in the property package component list." .format(self.name, i)) # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_total_element_balances() self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add Ports self.add_inlet_port() self.add_outlet_port() # Add performance equations # Add Lagrangian multiplier variables e_units = self.config.property_package.get_metadata( ).get_derived_units("energy_mole") self.lagrange_mult = Var(self.flowsheet().config.time, self.config.property_package.element_list, domain=Reals, initialize=100, doc="Lagrangian multipliers", units=e_units) # TODO : Remove this once sacling is properly implemented self.gibbs_scaling = Param(default=1, mutable=True) # Use Lagrangian multiple method to derive equations for Out_Fi # Use RT*lagrange as the Lagrangian multiple such that lagrange is in # a similar order of magnitude as log(Yi) @self.Constraint(self.flowsheet().config.time, self.config.property_package._phase_component_set, doc="Gibbs energy minimisation constraint") def gibbs_minimization(b, t, p, j): # Use natural log of species mole flow to avoid Pyomo solver # warnings of reaching infeasible point if j in self.config.inert_species: return Constraint.Skip return 0 == b.gibbs_scaling * ( b.control_volume.properties_out[t].gibbs_mol_phase_comp[p, j] + sum(b.lagrange_mult[t, e] * b.control_volume.properties_out[t]. config.parameters.element_comp[j][e] for e in b.config.property_package.element_list)) if len(self.config.inert_species) > 0: @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.inert_species, doc="Inert species balances") def inert_species_balance(b, t, p, j): # Add species balances for inert components cv = b.control_volume e_comp = cv.properties_out[t].config.parameters.element_comp # Check for linear dependence with element balances # If an inert species is the only source of element e, # the inert species balance would be linearly dependent on the # element balance for e. dependent = True if len(self.config.property_package.phase_list) > 1: # Multiple phases avoid linear dependency dependent = False else: for e in self.config.property_package.element_list: if e_comp[j][e] == 0: # Element e not in component j, no effect continue else: for i in self.config.property_package.component_list: if i == j: continue else: # If comp j shares element e with comp i # cannot be linearly dependent if e_comp[i][e] != 0: dependent = False if (not dependent and (p, j) in self.config.property_package._phase_component_set): return 0 == ( cv.properties_in[t].get_material_flow_terms(p, j) - cv.properties_out[t].get_material_flow_terms(p, j)) else: return Constraint.Skip # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = Reference(self.control_volume.heat[:]) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.control_volume.deltaP[:])
def build(self): super().build() if self.config.property_package_feed is None: raise ConfigurationError( "Users must provide a feed property package to the evaporator unit model" ) if self.config.property_package_vapor is None: raise ConfigurationError( "Users must provide a vapor property package to the evaporator unit model" ) # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta_feed = ( self.config.property_package_feed.get_metadata().get_derived_units) # Add shared unit model variables self.U = Var( initialize=1e3, bounds=(10, 1e4), units=pyunits.J * pyunits.s**-1 * pyunits.m**-2 * pyunits.K**-1, ) self.area = Var(initialize=1e2, bounds=(1e-1, 1e4), units=pyunits.m**2) self.delta_temperature_in = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) self.delta_temperature_out = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) self.lmtd = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K) # Add feed_side block self.feed_side = Block() # Add unit variables to feed self.feed_side.heat_transfer = Var(initialize=1e4, bounds=(1, 1e10), units=pyunits.J * pyunits.s**-1) # Add feed_side state blocks # Feed state block tmp_dict = dict(**self.config.property_package_args_feed) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package_feed tmp_dict["defined_state"] = True # feed inlet defined self.feed_side.properties_feed = ( self.config.property_package_feed.state_block_class( self.flowsheet().config.time, doc="Material properties of feed inlet", default=tmp_dict, )) # Brine state block tmp_dict["defined_state"] = False # brine outlet not yet defined self.feed_side.properties_brine = ( self.config.property_package_feed.state_block_class( self.flowsheet().config.time, doc="Material properties of brine outlet", default=tmp_dict, )) # Vapor state block tmp_dict = dict(**self.config.property_package_args_vapor) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package_vapor tmp_dict["defined_state"] = False # vapor outlet not yet defined self.feed_side.properties_vapor = ( self.config.property_package_vapor.state_block_class( self.flowsheet().config.time, doc="Material properties of vapor outlet", default=tmp_dict, )) # Add condenser self.condenser = Condenser( default={"property_package": self.config.property_package_vapor}) # Add ports - oftentimes users interact with these rather than the state blocks self.add_port(name="inlet_feed", block=self.feed_side.properties_feed) self.add_port(name="outlet_brine", block=self.feed_side.properties_brine) self.add_port(name="outlet_vapor", block=self.feed_side.properties_vapor) self.add_port(name="inlet_condenser", block=self.condenser.control_volume.properties_in) self.add_port(name="outlet_condenser", block=self.condenser.control_volume.properties_out) ### FEED SIDE CONSTRAINTS ### # Mass balance @self.feed_side.Constraint( self.flowsheet().time, self.config.property_package_feed.component_list, doc="Mass balance", ) def eq_mass_balance(b, t, j): lb = b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].lb b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].fix(lb) if j == "H2O": return ( b.properties_feed[t].flow_mass_phase_comp["Liq", "H2O"] == b.properties_brine[t].flow_mass_phase_comp["Liq", "H2O"] + b.properties_vapor[t].flow_mass_phase_comp["Vap", "H2O"]) else: return (b.properties_feed[t].flow_mass_phase_comp["Liq", j] == b.properties_brine[t].flow_mass_phase_comp["Liq", j]) # Energy balance @self.feed_side.Constraint(self.flowsheet().time, doc="Energy balance") def eq_energy_balance(b, t): return (b.heat_transfer + b.properties_feed[t].enth_flow == b.properties_brine[t].enth_flow + b.properties_vapor[t].enth_flow_phase["Vap"]) # Brine pressure @self.feed_side.Constraint(self.flowsheet().time, doc="Brine pressure") def eq_brine_pressure(b, t): return b.properties_brine[t].pressure == b.properties_brine[ t].pressure_sat # Vapor pressure @self.feed_side.Constraint(self.flowsheet().time, doc="Vapor pressure") def eq_vapor_pressure(b, t): return b.properties_vapor[t].pressure == b.properties_brine[ t].pressure # Vapor temperature @self.feed_side.Constraint(self.flowsheet().time, doc="Vapor temperature") def eq_vapor_temperature(b, t): return (b.properties_vapor[t].temperature == b.properties_brine[t].temperature) # return b.properties_vapor[t].temperature == 0.5*(b.properties_out[t].temperature + b.properties_in[t].temperature) ### EVAPORATOR CONSTRAINTS ### # Temperature difference in @self.Constraint(self.flowsheet().time, doc="Temperature difference in") def eq_delta_temperature_in(b, t): return (b.delta_temperature_in == b.condenser.control_volume.properties_in[t].temperature - b.feed_side.properties_brine[t].temperature) # Temperature difference out @self.Constraint(self.flowsheet().time, doc="Temperature difference out") def eq_delta_temperature_out(b, t): return (b.delta_temperature_out == b.condenser.control_volume.properties_out[t].temperature - b.feed_side.properties_brine[t].temperature) # log mean temperature @self.Constraint(self.flowsheet().time, doc="Log mean temperature difference") def eq_lmtd(b, t): dT_in = b.delta_temperature_in dT_out = b.delta_temperature_out temp_units = pyunits.get_units(dT_in) dT_avg = (dT_in + dT_out) / 2 # external function that ruturns the real root, for the cuberoot of negitive # numbers, so it will return without error for positive and negitive dT. b.cbrt = ExternalFunction(library=functions_lib(), function="cbrt", arg_units=[temp_units**3]) return b.lmtd == b.cbrt((dT_in * dT_out * dT_avg)) * temp_units # Heat transfer between feed side and condenser @self.Constraint(self.flowsheet().time, doc="Heat transfer balance") def eq_heat_balance(b, t): return b.feed_side.heat_transfer == -b.condenser.control_volume.heat[ t] # Evaporator heat transfer @self.Constraint(self.flowsheet().time, doc="Evaporator heat transfer") def eq_evaporator_heat(b, t): return b.feed_side.heat_transfer == b.U * b.area * b.lmtd
def add_material_balances(self, balance_type=MaterialBalanceType.useDefault, **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 """ # Check if balance_type is useDefault, and get default if necessary if balance_type == MaterialBalanceType.useDefault: try: blk = self._get_representative_property_block() balance_type = blk.default_material_balance_type() except NotImplementedError: raise ConfigurationError( "{} property package has not implemented a " "default_material_balance_type, thus cannot use " "MaterialBalanceType.useDefault when constructing " "material balances. Please contact the developer of " "your property package to implement the necessary " "default attributes.".format(self.name)) self._constructed_material_balance_type = balance_type 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 build(self): """Build the model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Check phase lists match assumptions if self.config.vapor_property_package.phase_list != ["Vap"]: raise ConfigurationError( f"{self.name} SolventReboiler model requires that the vapor " f"phase property package have a single phase named 'Vap'") if self.config.liquid_property_package.phase_list != ["Liq"]: raise ConfigurationError( f"{self.name} SolventReboiler model requires that the liquid " f"phase property package have a single phase named 'Liq'") # Check for at least one common component in component lists if not any( j in self.config.vapor_property_package.component_list for j in self.config.liquid_property_package.component_list): raise ConfigurationError( f"{self.name} SolventReboiler model requires that the liquid " f"and vapor phase property packages have at least one " f"common component.") # --------------------------------------------------------------------- # Add Control Volume for the Liquid Phase self.liquid_phase = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.liquid_property_package, "property_package_args": self.config.liquid_property_package_args }) self.liquid_phase.add_state_blocks(has_phase_equilibrium=True) # Separate liquid and vapor phases means that phase equilibrium will # be handled at the unit model level, thus has_phase_equilibrium is # False, but has_mass_transfer is True. self.liquid_phase.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True, has_phase_equilibrium=False) # Need to include enthalpy transfer term for the mass transfer self.liquid_phase.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True, has_enthalpy_transfer=True) self.liquid_phase.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # --------------------------------------------------------------------- # Add single state block for vapor phase tmp_dict = dict(**self.config.vapor_property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["defined_state"] = False self.vapor_phase = \ self.config.vapor_property_package.build_state_block( self.flowsheet().time, doc="Vapor phase properties", default=tmp_dict) # --------------------------------------------------------------------- # Check flow basis is compatable # TODO : Could add code to convert flow bases, but not now t_init = self.flowsheet().time.first() if (self.vapor_phase[t_init].get_material_flow_basis() != self. liquid_phase.properties_out[t_init].get_material_flow_basis()): raise ConfigurationError( f"{self.name} vapor and liquid property packages must use the " f"same material flow basis.") # --------------------------------------------------------------------- # Add Ports for the reboiler self.add_inlet_port(name="inlet", block=self.liquid_phase, doc="Liquid feed") self.add_outlet_port(name="bottoms", block=self.liquid_phase, doc="Bottoms stream") self.add_outlet_port(name="vapor_reboil", block=self.vapor_phase, doc="Vapor stream from reboiler") # --------------------------------------------------------------------- # Add unit level constraints # First, need the union and intersection of component lists all_comps = (self.vapor_phase.component_list | self.liquid_phase.properties_out.component_list) common_comps = (self.vapor_phase.component_list & self.liquid_phase.properties_out.component_list) # Get units for unit conversion vunits = self.config.vapor_property_package.get_metadata( ).get_derived_units lunits = self.config.liquid_property_package.get_metadata( ).get_derived_units flow_basis = self.vapor_phase[t_init].get_material_flow_basis() if flow_basis == MaterialFlowBasis.molar: fb = "flow_mole" elif flow_basis == MaterialFlowBasis.molar: fb = "flow_mass" else: raise ConfigurationError( f"{self.name} SolventReboiler only supports mass or molar " f"basis for MaterialFlowBasis.") if any(j not in common_comps for j in self.vapor_phase.component_list): # We have non-condensable components present, need zero-flow param self.zero_flow_param = Param(mutable=True, default=1e-8, units=vunits("flow_mole")) # Material balances def rule_material_balance(blk, t, j): if j in common_comps: # Component is in equilibrium # Mass transfer equals vapor flowrate return (-blk.liquid_phase.mass_transfer_term[t, "Liq", j] == pyunits.convert( blk.vapor_phase[t].get_material_flow_terms( "Vap", j), to_units=lunits(fb))) elif j in self.vapor_phase.component_list: # Non-condensable component # No mass transfer term # Set vapor flowrate to an arbitary small value return (blk.vapor_phase[t].get_material_flow_terms( "Vap", j) == blk.zero_flow_param) else: # Non-vaporisable comonent # Mass transfer term is zero, no vapor flowrate return (blk.liquid_phase.mass_transfer_term[t, "Liq", j] == 0 * lunits(fb)) self.unit_material_balance = Constraint( self.flowsheet().time, all_comps, rule=rule_material_balance, doc="Unit level material balances") # Phase equilibrium constraints # For all common components, equate fugacity in vapor and liquid def rule_phase_equilibrium(blk, t, j): return (blk.liquid_phase.properties_out[t].fug_phase_comp["Liq", j] == pyunits.convert(blk.vapor_phase[t].fug_phase_comp["Vap", j], to_units=lunits("pressure"))) self.unit_phase_equilibrium = Constraint( self.flowsheet().time, common_comps, rule=rule_phase_equilibrium, doc="Unit level phase equilibrium constraints") # Temperature equality constraint def rule_temperature_balance(blk, t): return (blk.liquid_phase.properties_out[t].temperature == pyunits.convert(blk.vapor_phase[t].temperature, to_units=lunits("temperature"))) self.unit_temperature_equality = Constraint( self.flowsheet().time, rule=rule_temperature_balance, doc="Unit level temperature equality") # Unit level energy balance # Energy leaving in vapor phase must be equal and opposite to enthalpy # transfer from liquid phase def rule_energy_balance(blk, t): return (-blk.liquid_phase.enthalpy_transfer[t] == pyunits.convert( blk.vapor_phase[t].get_enthalpy_flow_terms("Vap"), to_units=lunits("energy") / lunits("time"))) self.unit_enthalpy_balance = Constraint( self.flowsheet().time, rule=rule_energy_balance, doc="Unit level enthalpy_balance") # Pressure balance constraint def rule_pressure_balance(blk, t): return ( blk.liquid_phase.properties_out[t].pressure == pyunits.convert( blk.vapor_phase[t].pressure, to_units=lunits("pressure"))) self.unit_pressure_balance = Constraint( self.flowsheet().time, rule=rule_pressure_balance, doc="Unit level pressure balance") # Set references to balance terms at unit level self.heat_duty = Reference(self.liquid_phase.heat[:]) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != MomentumBalanceType.none): self.deltaP = Reference(self.liquid_phase.deltaP[:])
def add_material_mixing_equations(self, inlet_blocks, mixed_block, mb_type): """ Add material mixing equations. """ pp = self.config.property_package # Get phase component list(s) pc_set = mixed_block.phase_component_set # Get units metadata units = pp.get_metadata() flow_basis = mixed_block[ self.flowsheet().config.time.first()].get_material_flow_basis() if flow_basis == MaterialFlowBasis.molar: flow_units = units.get_derived_units("flow_mole") elif flow_basis == MaterialFlowBasis.mass: flow_units = units.get_derived_units("flow_mass") else: # Let this pass for now with no units flow_units = None if mb_type == MaterialBalanceType.componentPhase: # Create equilibrium generation term and constraints if required if self.config.has_phase_equilibrium is True: try: self.phase_equilibrium_generation = Var( self.flowsheet().config.time, pp.phase_equilibrium_idx, domain=Reals, doc="Amount of generation in unit by phase equilibria", units=flow_units) except AttributeError: raise PropertyNotSupportedError( "{} Property package does not contain a list of phase " "equilibrium reactions (phase_equilibrium_idx), " "thus does not support phase equilibrium.".format( self.name)) # Define terms to use in mixing equation def phase_equilibrium_term(b, t, p, j): if self.config.has_phase_equilibrium: sd = {} for r in pp.phase_equilibrium_idx: if pp.phase_equilibrium_list[r][0] == j: if (pp.phase_equilibrium_list[r][1][0] == p): sd[r] = 1 elif (pp.phase_equilibrium_list[r][1][1] == p): sd[r] = -1 else: sd[r] = 0 else: sd[r] = 0 return sum(b.phase_equilibrium_generation[t, r] * sd[r] for r in pp.phase_equilibrium_idx) else: return 0 # Write phase-component balances @self.Constraint( self.flowsheet().config.time, pc_set, doc="Material mixing equations", ) def material_mixing_equations(b, t, p, j): if (p, j) in pc_set: return 0 == ( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) + phase_equilibrium_term(b, t, p, j)) else: return Constraint.Skip elif mb_type == MaterialBalanceType.componentTotal: # Write phase-component balances @self.Constraint( self.flowsheet().config.time, mixed_block.component_list, doc="Material mixing equations", ) def material_mixing_equations(b, t, j): return 0 == sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for p in mixed_block.phase_list if (p, j) in pc_set) elif mb_type == MaterialBalanceType.total: # Write phase-component balances @self.Constraint(self.flowsheet().config.time, doc="Material mixing equations") def material_mixing_equations(b, t): return 0 == sum( sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for j in mixed_block.component_list if (p, j) in pc_set) for p in mixed_block.phase_list) elif mb_type == MaterialBalanceType.elementTotal: raise ConfigurationError("{} Mixers do not support elemental " "material balances.".format(self.name)) elif mb_type == MaterialBalanceType.none: pass else: raise BurntToast( "{} Mixer received unrecognised value for " "material_balance_type. This should not happen, " "please report this bug to the IDAES developers.".format( self.name))
def build(self): """Build the model. Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(CondenserData, self).build() # Check config arguments if self.config.temperature_spec is None: raise ConfigurationError("temperature_spec config argument " "has not been specified. Please select " "a valid option.") if (self.config.condenser_type == CondenserType.partialCondenser) and \ (self.config.temperature_spec == TemperatureSpec.atBubblePoint): raise ConfigurationError("condenser_type set to partial but " "temperature_spec set to atBubblePoint. " "Select customTemperature and specify " "outlet temperature.") # Add Control Volume for the condenser self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) self._make_ports() if self.config.condenser_type == CondenserType.totalCondenser: self._make_splits_total_condenser() if (self.config.temperature_spec == TemperatureSpec.atBubblePoint): # Option 1: if true, condition for total condenser # (T_cond = T_bubble) # Option 2: if this is false, then user has selected # custom temperature spec and needs to fix an outlet # temperature. def rule_total_cond(self, t): return self.control_volume.properties_out[t].\ temperature == self.control_volume.properties_out[t].\ temperature_bubble self.eq_total_cond_spec = Constraint(self.flowsheet().time, rule=rule_total_cond) else: self._make_splits_partial_condenser() # Add object reference to variables of the control volume # Reference to the heat duty add_object_reference(self, "heat_duty", self.control_volume.heat) # Reference to the pressure drop (if set to True) if self.config.has_pressure_change: add_object_reference(self, "deltaP", self.control_volume.deltaP)
def common(b, pobj): # TODO: determine if Henry's Law applies to Cubic EoS systems # For now, raise an exception if found # Follow on questions: # If Henry's law is used for a component, how does that effect # calculating A, B and phi? for j in b.component_list: cobj = b.params.get_component(j) if (cobj.config.henry_component is not None and pobj.local_name in cobj.config.henry_component): raise PropertyNotSupportedError( "{} Cubic equations of state do not support Henry's " "components [{}, {}].".format(b.name, pobj.local_name, j)) ctype = pobj._cubic_type cname = pobj.config.equation_of_state_options["type"].name mixing_rule_a = pobj._mixing_rule_a mixing_rule_b = pobj._mixing_rule_b if hasattr(b, cname + "_fw"): # Common components already constructed by previous phase return # Create expressions for coefficients def rule_fw(m, j): func_fw = getattr(m.params, cname + "_func_fw") cobj = m.params.get_component(j) return func_fw(cobj) b.add_component( cname + '_fw', Expression(b.component_list, rule=rule_fw, doc='EoS S factor')) def rule_a_crit(m, j): cobj = m.params.get_component(j) return (EoS_param[ctype]['omegaA'] * ((Cubic.gas_constant(b) * cobj.temperature_crit)**2 / cobj.pressure_crit)) b.add_component( cname + '_a_crit', Expression(b.component_list, rule=rule_a_crit, doc='Component a coefficient at T_crit')) def rule_a(m, j): cobj = m.params.get_component(j) fw = getattr(m, cname + "_fw")[j] ac = getattr(m, cname + '_a_crit')[j] func_alpha = getattr(m.params, cname + "_func_alpha") return ac * func_alpha(m.temperature, fw, cobj) b.add_component( cname + '_a', Expression(b.component_list, rule=rule_a, doc='Component a coefficient')) def rule_da_dT(m, j): cobj = m.params.get_component(j) fw = getattr(m, cname + "_fw")[j] ac = getattr(m, cname + '_a_crit')[j] func_dalpha_dT = getattr(m.params, cname + "_func_dalpha_dT") return ac * func_dalpha_dT(m.temperature, fw, cobj) b.add_component( cname + '_da_dT', Expression(b.component_list, rule=rule_da_dT, doc='Temperature derivative of component a')) def rule_d2a_dT2(m, j): cobj = m.params.get_component(j) fw = getattr(m, cname + "_fw")[j] ac = getattr(m, cname + '_a_crit')[j] func_d2alpha_dT2 = getattr(m.params, cname + "_func_d2alpha_dT2") return ac * func_d2alpha_dT2(m.temperature, fw, cobj) b.add_component( cname + '_d2a_dT2', Expression(b.component_list, rule=rule_d2a_dT2, doc='Second temperature derivative' 'of component a')) def func_b(m, j): cobj = m.params.get_component(j) return (EoS_param[ctype]['coeff_b'] * Cubic.gas_constant(b) * cobj.temperature_crit / cobj.pressure_crit) b.add_component( cname + '_b', Expression(b.component_list, rule=func_b, doc='Component b coefficient')) if mixing_rule_a == MixingRuleA.default: def rule_am(m, p): a = getattr(m, cname + "_a") return rule_am_default(m, cname, a, p) b.add_component(cname + '_am', Expression(b.phase_list, rule=rule_am)) def rule_daij_dT(m, i, j): a = getattr(m, cname + "_a") da_dT = getattr(m, cname + "_da_dT") k = getattr(m.params, cname + "_kappa") # Include temperature derivative of k for future extension dk_ij_dT = 0 return sqrt( a[i] * a[j]) * (-dk_ij_dT + (1 - k[i, j]) / 2 * (da_dT[i] / a[i] + da_dT[j] / a[j])) b.add_component( cname + '_daij_dT', Expression(b.component_list, b.component_list, rule=rule_daij_dT)) def rule_dam_dT(m, p): daij_dT = getattr(m, cname + "_daij_dT") return sum( sum(m.mole_frac_phase_comp[p, i] * m.mole_frac_phase_comp[p, j] * daij_dT[i, j] for j in m.components_in_phase(p)) for i in m.components_in_phase(p)) b.add_component(cname + "_dam_dT", Expression(b.phase_list, rule=rule_dam_dT)) def rule_d2am_dT2(m, p): k = getattr(m.params, cname + "_kappa") a = getattr(m, cname + "_a") da_dT = getattr(m, cname + "_da_dT") d2a_dT2 = getattr(m, cname + "_d2a_dT2") # Placeholders for if temperature dependent k is needed dk_dT = 0 d2k_dT2 = 0 # Initialize loop variable d2am_dT2 = 0 for i in m.components_in_phase(p): for j in m.components_in_phase(p): d2aij_dT2 = ( sqrt(a[i] * a[j]) * (-d2k_dT2 - dk_dT * (da_dT[i] / a[i] + da_dT[j] / a[j]) + (1 - k[i, j]) / 2 * (d2a_dT2[i] / a[i] + d2a_dT2[j] / a[j] - 1 / 2 * (da_dT[i] / a[i] - da_dT[j] / a[j])**2))) d2am_dT2 += (m.mole_frac_phase_comp[p, i] * m.mole_frac_phase_comp[p, j] * d2aij_dT2) return d2am_dT2 b.add_component(cname + "_d2am_dT2", Expression(b.phase_list, rule=rule_d2am_dT2)) def rule_delta(m, p, i): # See pg. 145 in Properties of Gases and Liquids a = getattr(m, cname + "_a") am = getattr(m, cname + "_am") kappa = getattr(m.params, cname + "_kappa") return (2 * sqrt(a[i]) / am[p] * sum(m.mole_frac_phase_comp[p, j] * sqrt(a[j]) * (1 - kappa[i, j]) for j in b.components_in_phase(p))) b.add_component(cname + "_delta", Expression(b.phase_component_set, rule=rule_delta)) else: raise ConfigurationError( "{} Unrecognized option for Equation of State " "mixing_rule_a: {}. Must be an instance of MixingRuleA " "Enum.".format(b.name, mixing_rule_a)) if mixing_rule_b == MixingRuleB.default: def rule_bm(m, p): b = getattr(m, cname + "_b") return rule_bm_default(m, b, p) b.add_component(cname + '_bm', Expression(b.phase_list, rule=rule_bm)) else: raise ConfigurationError( "{} Unrecognized option for Equation of State " "mixing_rule_a: {}. Must be an instance of MixingRuleB " "Enum.".format(b.name, mixing_rule_b)) def rule_A(m, p): am = getattr(m, cname + "_am") return (am[p] * m.pressure / (Cubic.gas_constant(b) * m.temperature)**2) b.add_component(cname + '_A', Expression(b.phase_list, rule=rule_A)) def rule_B(m, p): bm = getattr(m, cname + "_bm") return (bm[p] * m.pressure / (Cubic.gas_constant(b) * m.temperature)) b.add_component(cname + '_B', Expression(b.phase_list, rule=rule_B)) # Add components at equilibrium state if required if (b.params.config.phases_in_equilibrium is not None and (not b.config.defined_state or b.always_flash)): def func_a_eq(m, p1, p2, j): cobj = m.params.get_component(j) fw = getattr(m, cname + "_fw")[j] ac = getattr(m, cname + '_a_crit')[j] func_alpha = getattr(m.params, cname + "_func_alpha") return ac * func_alpha(m._teq[p1, p2], fw, cobj) b.add_component( '_' + cname + '_a_eq', Expression(b.params._pe_pairs, b.component_list, rule=func_a_eq, doc='Component a coefficient at Teq')) def rule_am_eq(m, p1, p2, p3): try: rule = m.params.get_phase( p3).config.equation_of_state_options["mixing_rule_a"] except (KeyError, TypeError): rule = MixingRuleA.default a = getattr(m, "_" + cname + "_a_eq") if rule == MixingRuleA.default: return rule_am_default(m, cname, a, p3, (p1, p2)) else: raise ConfigurationError( "{} Unrecognized option for Equation of State " "mixing_rule_a: {}. Must be an instance of MixingRuleA " "Enum.".format(m.name, rule)) b.add_component( '_' + cname + '_am_eq', Expression(b.params._pe_pairs, b.phase_list, rule=rule_am_eq)) def rule_A_eq(m, p1, p2, p3): am_eq = getattr(m, "_" + cname + "_am_eq") return (am_eq[p1, p2, p3] * m.pressure / (Cubic.gas_constant(b) * m._teq[p1, p2])**2) b.add_component( '_' + cname + '_A_eq', Expression(b.params._pe_pairs, b.phase_list, rule=rule_A_eq)) def rule_B_eq(m, p1, p2, p3): bm = getattr(m, cname + "_bm") return (bm[p3] * m.pressure / (Cubic.gas_constant(b) * m._teq[p1, p2])) b.add_component( '_' + cname + '_B_eq', Expression(b.params._pe_pairs, b.phase_list, rule=rule_B_eq)) def rule_delta_eq(m, p1, p2, p3, i): # See pg. 145 in Properties of Gases and Liquids a = getattr(m, "_" + cname + "_a_eq") am = getattr(m, "_" + cname + "_am_eq") kappa = getattr(m.params, cname + "_kappa") return ( 2 * sqrt(a[p1, p2, i]) / am[p1, p2, p3] * sum(m.mole_frac_phase_comp[p3, j] * sqrt(a[p1, p2, j]) * (1 - kappa[i, j]) for j in m.components_in_phase(p3))) b.add_component( "_" + cname + "_delta_eq", Expression(b.params._pe_pairs, b.phase_component_set, rule=rule_delta_eq)) # Set up external function calls b.add_component("_" + cname + "_ext_func_param", Param(default=ctype.value)) b.add_component("_" + cname + "_proc_Z_liq", ExternalFunction(library=_so, function="ceos_z_liq")) b.add_component("_" + cname + "_proc_Z_vap", ExternalFunction(library=_so, function="ceos_z_vap"))
def build(self): super().build() self._tech_type = "ozonation" build_siso(self) if 'toc' not in self.config.property_package.config.solute_list: raise ConfigurationError( "TOC must be in solute list for Ozonation or Ozone/AOP") self.contact_time = Var(self.flowsheet().time, units=pyunits.minute, doc="Ozone contact time") self.concentration_time = Var(self.flowsheet().time, units=(pyunits.mg * pyunits.minute) / pyunits.liter, doc="CT value for ozone contactor") self.mass_transfer_efficiency = Var( self.flowsheet().time, units=pyunits.dimensionless, doc="Ozone mass transfer efficiency") self.specific_energy_coeff = Var( self.flowsheet().time, units=pyunits.kWh / pyunits.lb, bounds=(0, None), doc="Specific energy consumption for ozone generation") self._fixed_perf_vars.append(self.contact_time) self._fixed_perf_vars.append(self.concentration_time) self._fixed_perf_vars.append(self.mass_transfer_efficiency) self._fixed_perf_vars.append(self.specific_energy_coeff) self.ozone_flow_mass = Var(self.flowsheet().time, units=pyunits.lb / pyunits.hr, bounds=(0, None), doc="Mass flow rate of ozone") self.ozone_consumption = Var(self.flowsheet().time, units=pyunits.mg / pyunits.liter, doc="Ozone consumption") self.electricity = Var(self.flowsheet().time, units=pyunits.kW, bounds=(0, None), doc="Ozone generation power demand") @self.Constraint(self.flowsheet().time, doc="Ozone consumption constraint") def ozone_consumption_constraint(b, t): return (b.ozone_consumption[t] == ( (pyunits.convert(b.properties_in[t].conc_mass_comp['toc'], to_units=pyunits.mg / pyunits.liter) + self.concentration_time[t] / self.contact_time[t])) / self.mass_transfer_efficiency[t]) @self.Constraint(self.flowsheet().time, doc='Ozone mass flow constraint') def ozone_flow_mass_constraint(b, t): return b.ozone_flow_mass[t] == pyunits.convert( b.properties_in[t].flow_vol * b.ozone_consumption[t], to_units=pyunits.lb / pyunits.hr) @self.Constraint(self.flowsheet().time, doc='Ozone power constraint') def electricity_constraint(b, t): return b.electricity[t] == (b.specific_energy_coeff[t] * b.ozone_flow_mass[t]) self._perf_var_dict["Ozone Contact Time (min)"] = self.contact_time self._perf_var_dict["Ozone CT Value ((mg*min)/L)"] = \ self.concentration_time self._perf_var_dict["Ozone Mass Transfer Efficiency"] = \ self.mass_transfer_efficiency self._perf_var_dict["Ozone Mass Flow (lb/hr)"] = self.ozone_flow_mass self._perf_var_dict["Ozone Unit Power Demand (kW)"] = \ self.electricity
def build(self): # Call UnitModel.build to setup dynamics super().build() self.scaling_factor = Suffix(direction=Suffix.EXPORT) if (len(self.config.property_package.phase_list) > 1 or 'Liq' not in [p for p in self.config.property_package.phase_list]): raise ConfigurationError( "NF model only supports one liquid phase ['Liq']," "the property package has specified the following phases {}" .format([p for p in self.config.property_package.phase_list])) units_meta = self.config.property_package.get_metadata().get_derived_units # TODO: update IDAES such that solvent and solute lists are automatically created on the parameter block self.solvent_list = Set() self.solute_list = Set() for c in self.config.property_package.component_list: comp = self.config.property_package.get_component(c) try: if comp.is_solvent(): self.solvent_list.add(c) if comp.is_solute(): self.solute_list.add(c) except TypeError: raise ConfigurationError("NF model only supports one solvent and one or more solutes," "the provided property package has specified a component '{}' " "that is not a solvent or solute".format(c)) if len(self.solvent_list) > 1: raise ConfigurationError("NF model only supports one solvent component," "the provided property package has specified {} solvent components" .format(len(self.solvent_list))) # Add unit parameters self.A_comp = Var( self.flowsheet().config.time, self.solvent_list, initialize=1e-12, bounds=(1e-18, 1e-6), domain=NonNegativeReals, units=units_meta('length')*units_meta('pressure')**-1*units_meta('time')**-1, doc='Solvent permeability coeff.') self.B_comp = Var( self.flowsheet().config.time, self.solute_list, initialize=1e-8, bounds=(1e-11, 1e-5), domain=NonNegativeReals, units=units_meta('length')*units_meta('time')**-1, doc='Solute permeability coeff.') self.sigma = Var( self.flowsheet().config.time, initialize=0.5, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Reflection coefficient') self.dens_solvent = Param( initialize=1000, units=units_meta('mass')*units_meta('length')**-3, doc='Pure water density') # Add unit variables self.flux_mass_phase_comp_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, initialize=1e-3, bounds=(1e-12, 1e6), units=units_meta('mass')*units_meta('length')**-2*units_meta('time')**-1, doc='Flux at feed inlet') self.flux_mass_phase_comp_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, initialize=1e-3, bounds=(1e-12, 1e6), units=units_meta('mass')*units_meta('length')**-2*units_meta('time')**-1, doc='Flux at feed outlet') self.avg_conc_mass_phase_comp_in = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.solute_list, initialize=1e-3, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=units_meta('mass')*units_meta('length')**-3, doc='Average solute concentration at feed inlet') self.avg_conc_mass_phase_comp_out = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.solute_list, initialize=1e-3, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=units_meta('mass')*units_meta('length')**-3, doc='Average solute concentration at feed outlet') self.area = Var( initialize=1, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=units_meta('length') ** 2, doc='Membrane area') # Build control volume for feed side self.feed_side = ControlVolume0DBlock(default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.feed_side.add_state_blocks( has_phase_equilibrium=False) self.feed_side.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) self.feed_side.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=True) self.feed_side.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=self.config.has_pressure_change) # Add permeate block tmp_dict = dict(**self.config.property_package_args) tmp_dict["has_phase_equilibrium"] = False tmp_dict["parameters"] = self.config.property_package tmp_dict["defined_state"] = False # permeate block is not an inlet self.properties_permeate = self.config.property_package.state_block_class( self.flowsheet().config.time, doc="Material properties of permeate", default=tmp_dict) # Add Ports self.add_inlet_port(name='inlet', block=self.feed_side) self.add_outlet_port(name='retentate', block=self.feed_side) self.add_port(name='permeate', block=self.properties_permeate) # References for control volume # pressure change if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = Reference(self.feed_side.deltaP) # mass transfer self.mass_transfer_phase_comp = Var( self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, initialize=1, bounds=(1e-8, 1e6), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass transfer to permeate') @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_transfer_term(self, t, p, j): return self.mass_transfer_phase_comp[t, p, j] == -self.feed_side.mass_transfer_term[t, p, j] # NF performance equations @self.Expression(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Average flux expression") def flux_mass_phase_comp_avg(b, t, p, j): return 0.5 * (b.flux_mass_phase_comp_in[t, p, j] + b.flux_mass_phase_comp_out[t, p, j]) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Permeate production") def eq_permeate_production(b, t, p, j): return (b.properties_permeate[t].get_material_flow_terms(p, j) == b.area * b.flux_mass_phase_comp_avg[t, p, j]) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Inlet water and salt flux") def eq_flux_in(b, t, p, j): prop_feed = b.feed_side.properties_in[t] prop_perm = b.properties_permeate[t] comp = self.config.property_package.get_component(j) if comp.is_solvent(): return (b.flux_mass_phase_comp_in[t, p, j] == b.A_comp[t, j] * b.dens_solvent * ((prop_feed.pressure - prop_perm.pressure) - b.sigma[t] * (prop_feed.pressure_osm - prop_perm.pressure_osm))) elif comp.is_solute(): return (b.flux_mass_phase_comp_in[t, p, j] == b.B_comp[t, j] * (prop_feed.conc_mass_phase_comp[p, j] - prop_perm.conc_mass_phase_comp[p, j]) + ((1 - b.sigma[t]) * b.flux_mass_phase_comp_in[t, p, j] * 1 / b.dens_solvent * b.avg_conc_mass_phase_comp_in[t, p, j]) ) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Outlet water and salt flux") def eq_flux_out(b, t, p, j): prop_feed = b.feed_side.properties_out[t] prop_perm = b.properties_permeate[t] comp = self.config.property_package.get_component(j) if comp.is_solvent(): return (b.flux_mass_phase_comp_out[t, p, j] == b.A_comp[t, j] * b.dens_solvent * ((prop_feed.pressure - prop_perm.pressure) - b.sigma[t] * (prop_feed.pressure_osm - prop_perm.pressure_osm))) elif comp.is_solute(): return (b.flux_mass_phase_comp_out[t, p, j] == b.B_comp[t, j] * (prop_feed.conc_mass_phase_comp[p, j] - prop_perm.conc_mass_phase_comp[p, j]) + ((1 - b.sigma[t]) * b.flux_mass_phase_comp_out[t, p, j] * 1 / b.dens_solvent * b.avg_conc_mass_phase_comp_out[t, p, j]) ) # Average concentration # COMMENT: Chen approximation of logarithmic average implemented @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.solute_list, doc="Average inlet concentration") def eq_avg_conc_in(b, t, p, j): prop_feed = b.feed_side.properties_in[t] prop_perm = b.properties_permeate[t] return (b.avg_conc_mass_phase_comp_in[t, p, j] == (prop_feed.conc_mass_phase_comp[p, j] * prop_perm.conc_mass_phase_comp[p, j] * (prop_feed.conc_mass_phase_comp[p, j] + prop_perm.conc_mass_phase_comp[p, j])/2)**(1/3)) @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.solute_list, doc="Average inlet concentration") def eq_avg_conc_out(b, t, p, j): prop_feed = b.feed_side.properties_out[t] prop_perm = b.properties_permeate[t] return (b.avg_conc_mass_phase_comp_out[t, p, j] == (prop_feed.conc_mass_phase_comp[p, j] * prop_perm.conc_mass_phase_comp[p, j] * (prop_feed.conc_mass_phase_comp[p, j] + prop_perm.conc_mass_phase_comp[p, j])/2)**(1/3)) # Feed and permeate-side connection @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer from feed to permeate") def eq_connect_mass_transfer(b, t, p, j): return (b.properties_permeate[t].get_material_flow_terms(p, j) == -b.feed_side.mass_transfer_term[t, p, j]) @self.Constraint(self.flowsheet().config.time, doc="Enthalpy transfer from feed to permeate") def eq_connect_enthalpy_transfer(b, t): return (b.properties_permeate[t].get_enthalpy_flow_terms('Liq') == -b.feed_side.enthalpy_transfer[t]) @self.Constraint(self.flowsheet().config.time, doc="Isothermal assumption for permeate") def eq_permeate_isothermal(b, t): return b.feed_side.properties_out[t].temperature == \ b.properties_permeate[t].temperature
def add_material_mixing_equations(self, inlet_blocks, mixed_block, mb_type): """ Add material mixing equations. """ # Get phase component list(s) phase_component_list = self._get_phase_comp_list() if mb_type == MaterialBalanceType.componentPhase: # Create equilibrium generation term and constraints if required if self.config.has_phase_equilibrium is True: # Get units from property package units = {} for u in ['holdup', 'time']: try: units[u] = (self.config.property_package.get_metadata( ).default_units[u]) except KeyError: units[u] = '-' try: self.phase_equilibrium_generation = Var( self.flowsheet().config.time, self.config.property_package.phase_equilibrium_idx, domain=Reals, doc="Amount of generation in unit by phase " "equilibria [{}/{}]".format(units['holdup'], units['time'])) except AttributeError: raise PropertyNotSupportedError( "{} Property package does not contain a list of phase " "equilibrium reactions (phase_equilibrium_idx), " "thus does not support phase equilibrium.".format( self.name)) # Define terms to use in mixing equation def phase_equilibrium_term(b, t, p, j): if self.config.has_phase_equilibrium: sd = {} for r in b.config.property_package.phase_equilibrium_idx: if b.config.property_package.\ phase_equilibrium_list[r][0] == j: if b.config.property_package.\ phase_equilibrium_list[r][1][0] == p: sd[r] = 1 elif b.config.property_package.\ phase_equilibrium_list[r][1][1] == p: sd[r] = -1 else: sd[r] = 0 else: sd[r] = 0 return sum(b.phase_equilibrium_generation[t, r] * sd[r] for r in b.config.property_package.phase_equilibrium_idx) else: return 0 # Write phase-component balances @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Material mixing equations") def material_mixing_equations(b, t, p, j): if j in phase_component_list[p]: return 0 == ( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) + phase_equilibrium_term(b, t, p, j)) else: return Constraint.Skip elif mb_type == MaterialBalanceType.componentTotal: # Write phase-component balances @self.Constraint(self.flowsheet().config.time, self.config.property_package.component_list, doc="Material mixing equations") def material_mixing_equations(b, t, j): return 0 == sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for p in b.config.property_package.phase_list) elif mb_type == MaterialBalanceType.total: # Write phase-component balances @self.Constraint(self.flowsheet().config.time, doc="Material mixing equations") def material_mixing_equations(b, t): return 0 == sum( sum( sum(inlet_blocks[i][t].get_material_flow_terms(p, j) for i in range(len(inlet_blocks))) - mixed_block[t].get_material_flow_terms(p, j) for j in b.config.property_package.component_list) for p in b.config.property_package.phase_list) elif mb_type == MaterialBalanceType.elementTotal: raise ConfigurationError("{} Mixers do not support elemental " "material balances.".format(self.name)) elif mb_type == MaterialBalanceType.none: pass else: raise BurntToast( "{} Mixer received unrecognised value for " "material_balance_type. This should not happen, " "please report this bug to the IDAES developers.".format( self.name))
def build(self): """Build the model. Args: None Returns: None """ # Setup model build logger model_log = idaeslog.getModelLogger(self.name, tag="unit") # Call UnitModel.build to setup dynamics super(CondenserData, self).build() # Check config arguments if self.config.temperature_spec is None: raise ConfigurationError("temperature_spec config argument " "has not been specified. Please select " "a valid option.") if (self.config.condenser_type == CondenserType.partialCondenser) and \ (self.config.temperature_spec == TemperatureSpec.atBubblePoint): raise ConfigurationError("condenser_type set to partial but " "temperature_spec set to atBubblePoint. " "Select customTemperature and specify " "outlet temperature.") # Add Control Volume for the condenser self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) self.control_volume.add_state_blocks(has_phase_equilibrium=True) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_phase_equilibrium=True) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=True) # Note: No momentum balance added for the condenser as the condenser # outlet pressure is a spec set by the user. # Get liquid and vapor phase objects from the property package # to be used below. Avoids repition. _liquid_list = [] _vapor_list = [] for p in self.config.property_package.phase_list: pobj = self.config.property_package.get_phase(p) if pobj.is_vapor_phase(): _vapor_list.append(p) elif pobj.is_liquid_phase(): _liquid_list.append(p) else: _liquid_list.append(p) model_log.warning( "A non-liquid/non-vapor phase was detected but will " "be treated as a liquid.") # Create a pyomo set for indexing purposes. This set is appended to # model otherwise results in an abstract set. self._liquid_set = Set(initialize=_liquid_list) self._vapor_set = Set(initialize=_vapor_list) self._make_ports() if self.config.condenser_type == CondenserType.totalCondenser: self._make_splits_total_condenser() if (self.config.temperature_spec == TemperatureSpec.atBubblePoint): # Option 1: if true, condition for total condenser # (T_cond = T_bubble) # Option 2: if this is false, then user has selected # custom temperature spec and needs to fix an outlet # temperature. # Get index for bubble point temperature and and assume it # will have only a single phase equilibrium pair. This is to # support the generic property framework where the T_bubble # is indexed by the phases_in_equilibrium. In distillation, # the assumption is that there will only be a single pair # i.e. vap-liq. idx = next( iter(self.control_volume.properties_out[self.flowsheet( ).config.time.first()].temperature_bubble)) def rule_total_cond(self, t): return self.control_volume.properties_out[t].\ temperature == self.control_volume.properties_out[t].\ temperature_bubble[idx] self.eq_total_cond_spec = Constraint(self.flowsheet().time, rule=rule_total_cond) else: self._make_splits_partial_condenser() # Add object reference to variables of the control volume # Reference to the heat duty self.heat_duty = Reference(self.control_volume.heat[:]) self.condenser_pressure = Reference( self.control_volume.properties_out[:].pressure)
def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg={}): ''' Downcomer initialization routine. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) for the control_volume of the model to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine optarg : solver options dictionary object (default={}) solver : str indicating whcih solver to use during initialization (default = None, use default solver) Returns: None ''' init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) init_log.info_low("Starting initialization...") flags = blk.control_volume.initialize( outlvl=outlvl + 1, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") # make sure 0 DoF if degrees_of_freedom(blk) != 0: raise ConfigurationError( "Incorrect degrees of freedom when initializing {}: dof = {}". format(blk.name, degrees_of_freedom(blk))) # Fix outlet pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].pressure.fix( value(blk.control_volume.properties_in[t].pressure)) blk.pressure_change_total_eqn.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) # Unfix outlet enthalpy and pressure for t in blk.flowsheet().config.time: blk.control_volume.properties_out[t].pressure.unfix() blk.pressure_change_total_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(res))) blk.control_volume.release_state(flags, outlvl + 1) init_log.info("Initialization Complete.")
def calculate_state( self, var_args=None, hold_state=False, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ Solves state blocks given a set of variables and their values. These variables can be state variables or properties. This method is typically used before initialization to solve for state variables because non-state variables (i.e. properties) cannot be fixed in initialization routines. Keyword Arguments: var_args : dictionary with variables and their values, they can be state variables or properties {(VAR_NAME, INDEX): VALUE} hold_state : flag indicating whether all of the state variables should be fixed after calculate state. True - State variables will be fixed. False - State variables will remain unfixed, unless already fixed. outlvl : idaes logger object that sets output level of solve call (default=idaeslog.NOTSET) solver : solver name string if None is provided the default solver for IDAES will be used (default = None) optarg : solver options dictionary object (default={}) Returns: results object from state block solve """ # Get logger solve_log = idaeslog.getSolveLogger(self.name, level=outlvl, tag="properties") # Initialize at current state values (not user provided) self.initialize(solver=solver, optarg=optarg, outlvl=outlvl) # Set solver and options opt = get_solver(solver, optarg) # Fix variables and check degrees of freedom flags = ( {} ) # dictionary noting which variables were fixed and their previous state for k in self.keys(): sb = self[k] for (v_name, ind), val in var_args.items(): var = getattr(sb, v_name) if iscale.get_scaling_factor(var[ind]) is None: _log.warning( "While using the calculate_state method on {sb_name}, variable {v_name} " "was provided as an argument in var_args, but it does not have a scaling " "factor. This suggests that the calculate_scaling_factor method has not been " "used or the variable was created on demand after the scaling factors were " "calculated. It is recommended to touch all relevant variables (i.e. call " "them or set an initial value) before using the calculate_scaling_factor " "method.".format(v_name=v_name, sb_name=sb.name)) if var[ind].is_fixed(): flags[(k, v_name, ind)] = True if value(var[ind]) != val: raise ConfigurationError( "While using the calculate_state method on {sb_name}, {v_name} was " "fixed to a value {val}, but it was already fixed to value {val_2}. " "Unfix the variable before calling the calculate_state " "method or update var_args." "".format( sb_name=sb.name, v_name=var.name, val=val, val_2=value(var[ind]), )) else: flags[(k, v_name, ind)] = False var[ind].fix(val) if degrees_of_freedom(sb) != 0: raise RuntimeError( "While using the calculate_state method on {sb_name}, the degrees " "of freedom were {dof}, but 0 is required. Check var_args and ensure " "the correct fixed variables are provided." "".format(sb_name=sb.name, dof=degrees_of_freedom(sb))) # Solve with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = solve_indexed_blocks(opt, [self], tee=slc.tee) solve_log.info_high("Calculate state: {}.".format( idaeslog.condition(results))) if not check_optimal_termination(results): _log.warning( "While using the calculate_state method on {sb_name}, the solver failed " "to converge to an optimal solution. This suggests that the user provided " "infeasible inputs, or that the model is poorly scaled, poorly initialized, " "or degenerate.") # unfix all variables fixed with var_args for (k, v_name, ind), previously_fixed in flags.items(): if not previously_fixed: var = getattr(self[k], v_name) var[ind].unfix() # fix state variables if hold_state if hold_state: fix_state_vars(self) return results
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super(HeatExchanger1DData, self).build() # Set flow directions for the control volume blocks and specify # dicretisation if not specified. if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: set_direction_shell = FlowDirection.forward set_direction_tube = FlowDirection.forward if (self.config.shell_side.transformation_method != self.config.tube_side.transformation_method) or ( self.config.shell_side.transformation_scheme != self.config.tube_side.transformation_scheme): raise ConfigurationError( "HeatExchanger1D only supports similar transformation " "schemes on the shell side and tube side domains for " "both cocurrent and countercurrent flow patterns.") if self.config.shell_side.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the shell side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the shell side.") self.config.shell_side.transformation_method = "dae.finite_difference" if self.config.tube_side.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the tube side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the tube side.") self.config.tube_side.transformation_method = "dae.finite_difference" if self.config.shell_side.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the shell side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the shell side.") self.config.shell_side.transformation_scheme = "BACKWARD" if self.config.tube_side.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the tube side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the tube side.") self.config.tube_side.transformation_scheme = "BACKWARD" elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent: set_direction_shell = FlowDirection.forward set_direction_tube = FlowDirection.backward if self.config.shell_side.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the shell side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the shell side.") self.config.shell_side.transformation_method = "dae.finite_difference" if self.config.tube_side.transformation_method is useDefault: _log.warning("Discretization method was " "not specified for the tube side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the tube side.") self.config.tube_side.transformation_method = "dae.finite_difference" if self.config.shell_side.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the shell side of the " "counter-current heat exchanger. " "Defaulting to backward finite " "difference on the shell side.") self.config.shell_side.transformation_scheme = "BACKWARD" if self.config.tube_side.transformation_scheme is useDefault: _log.warning("Discretization scheme was " "not specified for the tube side of the " "counter-current heat exchanger. " "Defaulting to forward finite " "difference on the tube side.") self.config.tube_side.transformation_scheme = "BACKWARD" else: raise ConfigurationError( "{} HeatExchanger1D only supports cocurrent and " "countercurrent flow patterns, but flow_type configuration" " argument was set to {}.".format(self.name, self.config.flow_type)) # Control volume 1D for shell self.shell = ControlVolume1DBlock( default={ "dynamic": self.config.shell_side.dynamic, "has_holdup": self.config.shell_side.has_holdup, "property_package": self.config.shell_side.property_package, "property_package_args": self.config.shell_side.property_package_args, "transformation_method": self.config.shell_side.transformation_method, "transformation_scheme": self.config.shell_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, }) self.tube = ControlVolume1DBlock( default={ "dynamic": self.config.tube_side.dynamic, "has_holdup": self.config.tube_side.has_holdup, "property_package": self.config.tube_side.property_package, "property_package_args": self.config.tube_side.property_package_args, "transformation_method": self.config.tube_side.transformation_method, "transformation_scheme": self.config.tube_side.transformation_scheme, "finite_elements": self.config.finite_elements, "collocation_points": self.config.collocation_points, }) self.shell.add_geometry(flow_direction=set_direction_shell) self.tube.add_geometry(flow_direction=set_direction_tube) self.shell.add_state_blocks( information_flow=set_direction_shell, has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium, ) self.tube.add_state_blocks( information_flow=set_direction_tube, has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium, ) # Populate shell self.shell.add_material_balances( balance_type=self.config.shell_side.material_balance_type, has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium, ) self.shell.add_energy_balances( balance_type=self.config.shell_side.energy_balance_type, has_heat_transfer=True, ) self.shell.add_momentum_balances( balance_type=self.config.shell_side.momentum_balance_type, has_pressure_change=self.config.shell_side.has_pressure_change, ) self.shell.apply_transformation() # Populate tube self.tube.add_material_balances( balance_type=self.config.tube_side.material_balance_type, has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium, ) self.tube.add_energy_balances( balance_type=self.config.tube_side.energy_balance_type, has_heat_transfer=True, ) self.tube.add_momentum_balances( balance_type=self.config.tube_side.momentum_balance_type, has_pressure_change=self.config.tube_side.has_pressure_change, ) self.tube.apply_transformation() # Add Ports for shell side self.add_inlet_port(name="shell_inlet", block=self.shell) self.add_outlet_port(name="shell_outlet", block=self.shell) # Add Ports for tube side self.add_inlet_port(name="tube_inlet", block=self.tube) self.add_outlet_port(name="tube_outlet", block=self.tube) # Add reference to control volume geometry add_object_reference(self, "shell_area", self.shell.area) add_object_reference(self, "shell_length", self.shell.length) add_object_reference(self, "tube_area", self.tube.area) add_object_reference(self, "tube_length", self.tube.length) self._make_performance()
def initialize(self, state_args=None, solver=None, optarg=None, outlvl=idaeslog.NOTSET): init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") solverobj = get_solver(solver, optarg) # Initialize the inlet and outlet state blocks. Calling the state # blocks initialize methods directly so that custom set of state args # can be passed to the inlet and outlet state blocks as control_volume # initialize method initializes the state blocks with the same # state conditions. flags = self.control_volume.properties_in. \ initialize(state_args=state_args, solver=solver, optarg=optarg, outlvl=outlvl, hold_state=True) # Initialize outlet state block at same conditions of inlet except # the temperature. Set the temperature to a temperature guess based # on the desired boilup_ratio. # Get index for bubble point temperature and and assume it # will have only a single phase equilibrium pair. This is to # support the generic property framework where the T_bubble # is indexed by the phases_in_equilibrium. In distillation, # the assumption is that there will only be a single pair # i.e. vap-liq. idx = next( iter(self.control_volume.properties_in[0].temperature_bubble)) temp_guess = 0.5 * ( value(self.control_volume.properties_in[0].temperature_dew[idx]) - value(self.control_volume.properties_in[0]. temperature_bubble[idx])) + \ value(self.control_volume.properties_in[0].temperature_bubble[idx]) state_args_outlet = {} state_dict_outlet = (self.control_volume.properties_in[ self.flowsheet().time.first()].define_port_members()) for k in state_dict_outlet.keys(): if state_dict_outlet[k].is_indexed(): state_args_outlet[k] = {} for m in state_dict_outlet[k].keys(): state_args_outlet[k][m] = value(state_dict_outlet[k][m]) else: if k != "temperature": state_args_outlet[k] = value(state_dict_outlet[k]) else: state_args_outlet[k] = temp_guess self.control_volume.properties_out.initialize( state_args=state_args_outlet, solver=solver, optarg=optarg, outlvl=outlvl, hold_state=False) if degrees_of_freedom(self) == 0: with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solverobj.solve(self, tee=slc.tee) init_log.info("Initialization Complete, {}.".format( idaeslog.condition(res))) else: raise ConfigurationError( "State vars fixed but degrees of freedom " "for reboiler is not zero during " "initialization. Please ensure that the boilup_ratio " "or the outlet temperature is fixed.") if not check_optimal_termination(res): raise InitializationError( f"{self.name} failed to initialize successfully. Please check " f"the output logs for more information.") self.control_volume.properties_in.\ release_state(flags=flags, outlvl=outlvl)
def define_state(b): # FpcTP contains full information on the phase equilibrium, so flash # calculations re not always needed b.always_flash = False # Check that only necessary state_bounds are defined expected_keys = ["flow_mol_phase_comp", "enth_mol", "temperature", "pressure"] if (b.params.config.state_bounds is not None and any(b.params.config.state_bounds.keys()) not in expected_keys): for k in b.params.config.state_bounds.keys(): if k not in expected_keys: raise ConfigurationError( "{} - found unexpected state_bounds key {}. Please ensure " "bounds are provided only for expected state variables " "and that you have typed the variable names correctly." .format(b.name, k)) units = b.params.get_metadata().derived_units # Get bounds and initial values from config args f_bounds, f_init = get_bounds_from_config( b, "flow_mol_phase_comp", units["flow_mole"]) t_bounds, t_init = get_bounds_from_config( b, "temperature", units["temperature"]) p_bounds, p_init = get_bounds_from_config( b, "pressure", units["pressure"]) # Add state variables b.flow_mol_phase_comp = Var(b.phase_component_set, initialize=f_init, domain=NonNegativeReals, bounds=f_bounds, doc='Phase-component molar flowrate', units=units["flow_mole"]) b.pressure = Var(initialize=p_init, domain=NonNegativeReals, bounds=p_bounds, doc='State pressure', units=units["pressure"]) b.temperature = Var(initialize=t_init, domain=NonNegativeReals, bounds=t_bounds, doc='State temperature', units=units["temperature"]) # Add supporting variables b.flow_mol = Expression( expr=sum(b.flow_mol_phase_comp[i] for i in b.phase_component_set), doc="Total molar flowrate") def flow_mol_phase(b, p): return sum(b.flow_mol_phase_comp[p, j] for j in b.component_list if (p, j) in b.phase_component_set) b.flow_mol_phase = Expression(b.phase_list, rule=flow_mol_phase, doc='Phase molar flow rates') def rule_flow_mol_comp(b, j): return sum(b.flow_mol_phase_comp[p, j] for p in b.phase_list if (p, j) in b.phase_component_set) b.flow_mol_comp = Expression(b.component_list, rule=rule_flow_mol_comp, doc='Component molar flow rates') def mole_frac_comp(b, j): return (sum(b.flow_mol_phase_comp[p, j] for p in b.phase_list if (p, j) in b.phase_component_set) / b.flow_mol) b.mole_frac_comp = Expression(b.component_list, rule=mole_frac_comp, doc='Mixture mole fractions') b.mole_frac_phase_comp = Var( b.phase_component_set, bounds=(1e-20, 1.001), initialize=1/len(b.component_list), doc='Phase mole fractions', units=None) def rule_mole_frac_phase_comp(b, p, j): # Calcualting mole frac phase comp is degenerate if there is only one # component in phase. # Count components comp_count = 0 for p1, j1 in b.phase_component_set: if p1 == p: comp_count += 1 if comp_count > 1: return b.mole_frac_phase_comp[p, j] * b.flow_mol_phase[p] == \ b.flow_mol_phase_comp[p, j] else: return b.mole_frac_phase_comp[p, j] == 1 b.mole_frac_phase_comp_eq = Constraint( b.phase_component_set, rule=rule_mole_frac_phase_comp) def rule_phase_frac(b, p): if len(b.phase_list) == 1: return 1 else: return b.flow_mol_phase[p] / b.flow_mol b.phase_frac = Expression( b.phase_list, rule=rule_phase_frac, doc='Phase fractions') # Add electrolye state vars if required if b.params._electrolyte: define_electrolyte_state(b) # ------------------------------------------------------------------------- # General Methods def get_material_flow_terms_FpcTP(p, j): """Create material flow terms for control volume.""" return b.flow_mol_phase_comp[p, j] b.get_material_flow_terms = get_material_flow_terms_FpcTP def get_enthalpy_flow_terms_FpcTP(p): """Create enthalpy flow terms.""" # enth_mol_phase probably does not exist when this is created # Use try/except to build flow term if not present try: eflow = b._enthalpy_flow_term except AttributeError: def rule_eflow(b, p): return b.flow_mol_phase[p] * b.enth_mol_phase[p] eflow = b._enthalpy_flow_term = Expression( b.phase_list, rule=rule_eflow) return eflow[p] b.get_enthalpy_flow_terms = get_enthalpy_flow_terms_FpcTP def get_material_density_terms_FpcTP(p, j): """Create material density terms.""" # dens_mol_phase probably does not exist when this is created # Use try/except to build term if not present try: mdens = b._material_density_term except AttributeError: def rule_mdens(b, p, j): return b.dens_mol_phase[p] * b.mole_frac_phase_comp[p, j] mdens = b._material_density_term = Expression( b.phase_component_set, rule=rule_mdens) return mdens[p, j] b.get_material_density_terms = get_material_density_terms_FpcTP def get_energy_density_terms_FpcTP(p): """Create energy density terms.""" # Density and energy terms probably do not exist when this is created # Use try/except to build term if not present try: edens = b._energy_density_term except AttributeError: def rule_edens(b, p): return b.dens_mol_phase[p] * b.energy_internal_mol_phase[p] edens = b._energy_density_term = Expression( b.phase_list, rule=rule_edens) return edens[p] b.get_energy_density_terms = get_energy_density_terms_FpcTP def default_material_balance_type_FpcTP(): return MaterialBalanceType.componentTotal b.default_material_balance_type = default_material_balance_type_FpcTP def default_energy_balance_type_FpcTP(): return EnergyBalanceType.enthalpyTotal b.default_energy_balance_type = default_energy_balance_type_FpcTP def get_material_flow_basis_FpcTP(): return MaterialFlowBasis.molar b.get_material_flow_basis = get_material_flow_basis_FpcTP def define_state_vars_FpcTP(): """Define state vars.""" return {"flow_mol_phase_comp": b.flow_mol_phase_comp, "temperature": b.temperature, "pressure": b.pressure} b.define_state_vars = define_state_vars_FpcTP def define_display_vars_FpcTP(): """Define display vars.""" return {"Molar Flowrate": b.flow_mol_phase_comp, "Temperature": b.temperature, "Pressure": b.pressure} b.define_display_vars = define_display_vars_FpcTP
def homotopy(model, variables, targets, max_solver_iterations=50, max_solver_time=10, step_init=0.1, step_cut=0.5, iter_target=4, step_accel=0.5, max_step=1, min_step=0.05, max_eval=200): """ Homotopy meta-solver routine using Ipopt as the non-linear solver. This routine takes a model along with a list of fixed variables in that model and a list of target values for those variables. The routine then tries to iteratively move the values of the fixed variables to their target values using an adaptive step size. Args: model : model to be solved variables : list of Pyomo Var objects to be varied using homotopy. Variables must be fixed. targets : list of target values for each variable max_solver_iterations : maximum number of solver iterations per homotopy step (default=50) max_solver_time : maximum cpu time for the solver per homotopy step (default=10) step_init : initial homotopy step size (default=0.1) step_cut : factor by which to reduce step size on failed step (default=0.5) step_accel : acceleration factor for adjusting step size on successful step (default=0.5) iter_target : target number of solver iterations per homotopy step (default=4) max_step : maximum homotopy step size (default=1) min_step : minimum homotopy step size (default=0.05) max_eval : maximum number of homotopy evaluations (both successful and unsuccessful) (default=200) Returns: Termination Condition : A Pyomo TerminationCondition Enum indicating how the meta-solver terminated (see documentation) Solver Progress : a fraction indication how far the solver progressed from the initial values to the target values Number of Iterations : number of homotopy evaluations before solver terminated """ eps = 1e-3 # Tolerance for homotopy step convergence to 1 # Get model logger _log = logging.getLogger(__name__) # Validate model is an instance of Block if not isinstance(model, Block): raise TypeError("Model provided was not a valid Pyomo model object " "(instance of Block). Please provide a valid model.") if degrees_of_freedom(model) != 0: raise ConfigurationError( "Degrees of freedom in model are not equal to zero. Homotopy " "should not be used on probelms which are not well-defined.") # Validate variables and targets if len(variables) != len(targets): raise ConfigurationError( "Number of variables and targets do not match.") for i in range(len(variables)): v = variables[i] t = targets[i] if not isinstance(v, _VarData): raise TypeError("Variable provided ({}) was not a valid Pyomo Var " "component.".format(v)) # Check that v is part of model parent = v.parent_block() while parent != model: if parent is None: raise ConfigurationError( "Variable {} is not part of model".format(v)) parent = parent.parent_block() # Check that v is fixed if not v.fixed: raise ConfigurationError( "Homotopy metasolver provided with unfixed variable {}." "All variables must be fixed.".format(v.name)) # Check bounds on v (they don't really matter, but check for sanity) if v.ub is not None: if v.value > v.ub: raise ConfigurationError( "Current value for variable {} is greater than the " "upper bound for that variable. Please correct this " "before continuing.".format(v.name)) if t > v.ub: raise ConfigurationError( "Target value for variable {} is greater than the " "upper bound for that variable. Please correct this " "before continuing.".format(v.name)) if v.lb is not None: if v.value < v.lb: raise ConfigurationError( "Current value for variable {} is less than the " "lower bound for that variable. Please correct this " "before continuing.".format(v.name)) if t < v.lb: raise ConfigurationError( "Target value for variable {} is less than the " "lower bound for that variable. Please correct this " "before continuing.".format(v.name)) # TODO : Should we be more restrictive on these values to avoid users # TODO : picking numbers that are less likely to solve (but still valid)? # Validate homotopy parameter selections if not 0.05 <= step_init <= 0.8: raise ConfigurationError("Invalid value for step_init ({}). Must lie " "between 0.05 and 0.8.".format(step_init)) if not 0.1 <= step_cut <= 0.9: raise ConfigurationError("Invalid value for step_cut ({}). Must lie " "between 0.1 and 0.9.".format(step_cut)) if step_accel < 0: raise ConfigurationError( "Invalid value for step_accel ({}). Must be " "greater than or equal to 0.".format(step_accel)) if iter_target < 1: raise ConfigurationError( "Invalid value for iter_target ({}). Must be " "greater than or equal to 1.".format(iter_target)) if not isinstance(iter_target, int): raise ConfigurationError("Invalid value for iter_target ({}). Must be " "an an integer.".format(iter_target)) if not 0.05 <= max_step <= 1: raise ConfigurationError("Invalid value for max_step ({}). Must lie " "between 0.05 and 1.".format(max_step)) if not 0.01 <= min_step <= 0.1: raise ConfigurationError("Invalid value for min_step ({}). Must lie " "between 0.01 and 0.1.".format(min_step)) if not min_step <= max_step: raise ConfigurationError("Invalid argumnets: step_min must be less " "or equal to step_max.") if not min_step <= step_init <= max_step: raise ConfigurationError("Invalid arguments: step_init must lie " "between min_step and max_step.") if max_eval < 1: raise ConfigurationError( "Invalid value for max_eval ({}). Must be " "greater than or equal to 1.".format(step_accel)) if not isinstance(max_eval, int): raise ConfigurationError("Invalid value for max_eval ({}). Must be " "an an integer.".format(iter_target)) # Create solver object solver_obj = SolverFactory('ipopt') # Perform initial solve of model to confirm feasible initial solution results, solved, sol_iter, sol_time, sol_reg = ipopt_solve_with_stats( model, solver_obj, max_solver_iterations, max_solver_time) if not solved: _log.exception("Homotopy Failed - initial solution infeasible.") return TerminationCondition.infeasible, 0, 0 elif sol_reg != "-": _log.warning( "Homotopy - initial solution converged with regularization.") return TerminationCondition.other, 0, 0 else: _log.info("Homotopy - initial point converged") # Set up homotopy variables # Get initial values and deltas for all variables v_init = [] for i in range(len(variables)): v_init.append(variables[i].value) n_0 = 0.0 # Homotopy progress variable s = step_init # Set step size to step_init iter_count = 0 # Counter for homotopy iterations # Save model state to dict # TODO : for very large models, it may be necessary to dump this to a file current_state = to_json(model, return_dict=True) while n_0 < 1.0: iter_count += 1 # Increase iter_count regardless of success or failure # Calculate next n value given current step size if n_0 + s >= 1.0 - eps: n_1 = 1.0 else: n_1 = n_0 + s _log.info("Homotopy Iteration {}. Next Step: {} (Current: {})".format( iter_count, n_1, n_0)) # Update values for all variables using n_1 for i in range(len(variables)): variables[i].fix(targets[i] * n_1 + v_init[i] * (1 - n_1)) # Solve model at new state results, solved, sol_iter, sol_time, sol_reg = ipopt_solve_with_stats( model, solver_obj, max_solver_iterations, max_solver_time) # Check solver output for convergence if solved: # Step succeeded - accept current state current_state = to_json(model, return_dict=True) # Update n_0 to accept current step n_0 = n_1 # Check solver iterations and calculate next step size s_proposed = s * (1 + step_accel * (iter_target / sol_iter - 1)) if s_proposed > max_step: s = max_step elif s_proposed < min_step: s = min_step else: s = s_proposed else: # Step failed - reload old state from_json(model, current_state) # Try to cut back step size if s > min_step: # Step size can be cut s = max(min_step, s * step_cut) else: # Step is already at minimum size, terminate homotopy _log.exception( "Homotopy failed - could not converge at minimum step " "size. Current progress is {}".format(n_0)) return TerminationCondition.minStepLength, n_0, iter_count if iter_count >= max_eval: # Use greater than or equal to to be safe _log.exception("Homotopy failed - maximum homotopy iterations " "exceeded. Current progress is {}".format(n_0)) return TerminationCondition.maxEvaluations, n_0, iter_count if sol_reg == "-": _log.info("Homotopy successful - converged at target values in {} " "iterations.".format(iter_count)) return TerminationCondition.optimal, n_0, iter_count else: _log.exception("Homotopy failed - converged at target values with " "regularization in {} iterations.".format(iter_count)) return TerminationCondition.other, n_0, iter_count
def build(self): """ General build method for MixerData. This method calls a number of sub-methods which automate the construction of expected attributes of unit models. Inheriting models should call `super().build`. Args: None Returns: None """ # Call super.build() super(MixerData, self).build() # Call setup methods from ControlVolumeBlockData self._get_property_package() self._get_indexing_sets() # Create list of inlet names inlet_list = self.create_inlet_list() # Build StateBlocks inlet_blocks = self.add_inlet_state_blocks(inlet_list) if self.config.mixed_state_block is None: mixed_block = self.add_mixed_state_block() else: mixed_block = self.get_mixed_state_block() mb_type = self.config.material_balance_type if mb_type == MaterialBalanceType.useDefault: t_ref = self.flowsheet().config.time.first() mb_type = mixed_block[t_ref].default_material_balance_type() if mb_type != MaterialBalanceType.none: self.add_material_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block, mb_type=mb_type) else: raise BurntToast("{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format( self.name)) if self.config.energy_mixing_type == MixingType.extensive: self.add_energy_mixing_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif self.config.energy_mixing_type == MixingType.none: pass else: raise ConfigurationError( "{} received unrecognised value for " "material_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) # Add to try/expect to catch cases where pressure is not supported # by properties. try: if self.config.momentum_mixing_type == MomentumMixingType.minimize: self.add_pressure_minimization_equations( inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif (self.config.momentum_mixing_type == MomentumMixingType.equality): self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) elif (self.config.momentum_mixing_type == MomentumMixingType.minimize_and_equality): self.add_pressure_minimization_equations( inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.add_pressure_equality_equations(inlet_blocks=inlet_blocks, mixed_block=mixed_block) self.pressure_equality_constraints.deactivate() elif self.config.momentum_mixing_type == MomentumMixingType.none: pass else: raise ConfigurationError( "{} recieved unrecognised value for " "momentum_mixing_type argument. This " "should not occur, so please contact " "the IDAES developers with this bug.".format(self.name)) except PropertyNotSupportedError: raise PropertyNotSupportedError( "{} The property package supplied for this unit does not " "appear to support pressure, which is required for momentum " "mixing. Please set momentum_mixing_type to " "MomentumMixingType.none or provide a property package which " "supports pressure.".format(self.name)) self.add_port_objects(inlet_list, inlet_blocks, mixed_block)
def define_state(b): # FTPx formulation always requires a flash, so set flag to True # TODO: should have some checking to make sure developers implement this properly b.always_flash = True # Check that only necessary state_bounds are defined expected_keys = ["flow_mol", "enth_mol", "temperature", "pressure"] if (b.params.config.state_bounds is not None and any(b.params.config.state_bounds.keys()) not in expected_keys): for k in b.params.config.state_bounds.keys(): if "mole_frac" in k: _log.warning("{} - found state_bounds argument for {}." " Mole fraction bounds are set automatically and " "this argument will be ignored.".format( b.name, k)) elif k not in expected_keys: raise ConfigurationError( "{} - found unexpected state_bounds key {}. Please ensure " "bounds are provided only for expected state variables " "and that you have typed the variable names correctly.". format(b.name, k)) units = b.params.get_metadata().derived_units # Get bounds and initial values from config args f_bounds, f_init = get_bounds_from_config(b, "flow_mol", units["flow_mole"]) h_bounds, h_init = get_bounds_from_config(b, "enth_mol", units["energy_mole"]) p_bounds, p_init = get_bounds_from_config(b, "pressure", units["pressure"]) t_bounds, t_init = get_bounds_from_config(b, "temperature", units["temperature"]) # Add state variables b.flow_mol = Var(initialize=f_init, domain=NonNegativeReals, bounds=f_bounds, doc=' Total molar flowrate', units=units["flow_mole"]) b.mole_frac_comp = Var(b.component_list, bounds=(0, None), initialize=1 / len(b.component_list), doc='Mixture mole fractions', units=None) b.pressure = Var(initialize=p_init, domain=NonNegativeReals, bounds=p_bounds, doc='State pressure', units=units["pressure"]) b.enth_mol = Var(initialize=h_init, bounds=h_bounds, doc='State molar enthalpy', units=units["energy_mole"]) # Add supporting variables if f_init is None: fp_init = None else: fp_init = f_init / len(b.phase_list) b.flow_mol_phase = Var(b.phase_list, initialize=fp_init, domain=NonNegativeReals, bounds=f_bounds, doc='Phase molar flow rates', units=units["flow_mole"]) b.mole_frac_phase_comp = Var(b.phase_component_set, initialize=1 / len(b.component_list), bounds=(0, None), doc='Phase mole fractions', units=None) b.temperature = Var(initialize=t_init, domain=NonNegativeReals, bounds=t_bounds, doc='Temperature', units=units["temperature"]) b.phase_frac = Var(b.phase_list, initialize=1 / len(b.phase_list), bounds=(0, None), doc='Phase fractions', units=None) # Add supporting constraints if b.config.defined_state is False: # applied at outlet only b.sum_mole_frac_out = Constraint(expr=1e3 == 1e3 * sum(b.mole_frac_comp[i] for i in b.component_list)) def rule_enth_mol(b): return b.enth_mol == sum(b.enth_mol_phase[p] * b.phase_frac[p] for p in b.phase_list) b.enth_mol_eq = Constraint(rule=rule_enth_mol, doc="Total molar enthalpy mixing rule") if len(b.phase_list) == 1: def rule_total_mass_balance(b): return b.flow_mol_phase[b.phase_list[1]] == b.flow_mol b.total_flow_balance = Constraint(rule=rule_total_mass_balance) def rule_comp_mass_balance(b, i): return 1e3*b.mole_frac_comp[i] == \ 1e3*b.mole_frac_phase_comp[b.phase_list[1], i] b.component_flow_balances = Constraint(b.component_list, rule=rule_comp_mass_balance) def rule_phase_frac(b, p): return b.phase_frac[p] == 1 b.phase_fraction_constraint = Constraint(b.phase_list, rule=rule_phase_frac) elif len(b.phase_list) == 2: # For two phase, use Rachford-Rice formulation def rule_total_mass_balance(b): return sum(b.flow_mol_phase[p] for p in b.phase_list) == \ b.flow_mol b.total_flow_balance = Constraint(rule=rule_total_mass_balance) def rule_comp_mass_balance(b, i): return b.flow_mol * b.mole_frac_comp[i] == sum( b.flow_mol_phase[p] * b.mole_frac_phase_comp[p, i] for p in b.phase_list if (p, i) in b.phase_component_set) b.component_flow_balances = Constraint(b.component_list, rule=rule_comp_mass_balance) def rule_mole_frac(b): return 1e3*sum(b.mole_frac_phase_comp[b.phase_list[1], i] for i in b.component_list if (b.phase_list[1], i) in b.phase_component_set) -\ 1e3*sum(b.mole_frac_phase_comp[b.phase_list[2], i] for i in b.component_list if (b.phase_list[2], i) in b.phase_component_set) == 0 b.sum_mole_frac = Constraint(rule=rule_mole_frac) def rule_phase_frac(b, p): return b.phase_frac[p] * b.flow_mol == b.flow_mol_phase[p] b.phase_fraction_constraint = Constraint(b.phase_list, rule=rule_phase_frac) else: # Otherwise use a general formulation def rule_comp_mass_balance(b, i): return b.flow_mol * b.mole_frac_comp[i] == sum( b.flow_mol_phase[p] * b.mole_frac_phase_comp[p, i] for p in b.phase_list if (p, i) in b.phase_component_set) b.component_flow_balances = Constraint(b.component_list, rule=rule_comp_mass_balance) def rule_mole_frac(b, p): return 1e3 * sum(b.mole_frac_phase_comp[p, i] for i in b.component_list if (p, i) in b.phase_component_set) == 1e3 b.sum_mole_frac = Constraint(b.phase_list, rule=rule_mole_frac) def rule_phase_frac(b, p): return b.phase_frac[p] * b.flow_mol == b.flow_mol_phase[p] b.phase_fraction_constraint = Constraint(b.phase_list, rule=rule_phase_frac) # ------------------------------------------------------------------------- # General Methods def get_material_flow_terms_FTPx(p, j): """Create material flow terms for control volume.""" if j in b.component_list: return b.flow_mol_phase[p] * b.mole_frac_phase_comp[p, j] else: return 0 b.get_material_flow_terms = get_material_flow_terms_FTPx def get_enthalpy_flow_terms_FTPx(p): """Create enthalpy flow terms.""" return b.flow_mol_phase[p] * b.enth_mol_phase[p] b.get_enthalpy_flow_terms = get_enthalpy_flow_terms_FTPx def get_material_density_terms_FTPx(p, j): """Create material density terms.""" if j in b.component_list: return b.dens_mol_phase[p] * b.mole_frac_phase_comp[p, j] else: return 0 b.get_material_density_terms = get_material_density_terms_FTPx def get_energy_density_terms_FTPx(p): """Create energy density terms.""" return b.dens_mol_phase[p] * b.enth_mol_phase[p] b.get_energy_density_terms = get_energy_density_terms_FTPx def default_material_balance_type_FTPx(): return MaterialBalanceType.componentTotal b.default_material_balance_type = default_material_balance_type_FTPx def default_energy_balance_type_FTPx(): return EnergyBalanceType.enthalpyTotal b.default_energy_balance_type = default_energy_balance_type_FTPx def get_material_flow_basis_FTPx(): return MaterialFlowBasis.molar b.get_material_flow_basis = get_material_flow_basis_FTPx def define_state_vars_FPhx(): """Define state vars.""" return { "flow_mol": b.flow_mol, "mole_frac_comp": b.mole_frac_comp, "enth_mol": b.enth_mol, "pressure": b.pressure } b.define_state_vars = define_state_vars_FPhx def define_display_vars_FPhx(): """Define display vars.""" return { "Total Molar Flowrate": b.flow_mol, "Total Mole Fraction": b.mole_frac_comp, "Molar Enthalpy": b.enth_mol, "Pressure": b.pressure } b.define_display_vars = define_display_vars_FPhx
def initialize(blk, liquid_state_args=None, vapor_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Initialization routine for solvent condenser unit model. Keyword Arguments: liquid_state_args : a dict of arguments to be passed to the liquid property package to provide an initial state for initialization (see documentation of the specific property package) (default = none). vapor_state_args : a dict of arguments to be passed to the vapor property package to provide an initial state for initialization (see documentation of the specific property package) (default = none). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default IDAES solver) Returns: None ''' if optarg is None: optarg = {} # Check DOF if degrees_of_freedom(blk) != 0: raise ConfigurationError( f"{blk.name} degrees of freedom were not 0 at the beginning " f"of initialization. DoF = {degrees_of_freedom(blk)}") # Set solver options init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") solverobj = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize liquid phase control volume block flags = blk.vapor_phase.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=vapor_state_args, hold_state=True) init_log.info_high('Initialization Step 1 Complete.') # --------------------------------------------------------------------- # Initialize liquid phase state block if liquid_state_args is None: t_init = blk.flowsheet().time.first() liquid_state_args = {} liq_state_vars = blk.liquid_phase[t_init].define_state_vars() vap_state = blk.vapor_phase.properties_out[t_init] # Check for unindexed state variables for sv in liq_state_vars: if "flow" in sv: # Flow varaible, assume 10% condensation if "phase_comp" in sv: # Flow is indexed by phase and component liquid_state_args[sv] = {} for p, j in liq_state_vars[sv]: if j in vap_state.component_list: liquid_state_args[sv][p, j] = 0.1 * value( getattr(vap_state, sv)[p, j]) else: liquid_state_args[sv][p, j] = 1e-8 elif "comp" in sv: # Flow is indexed by component liquid_state_args[sv] = {} for j in liq_state_vars[sv]: if j in vap_state.component_list: liquid_state_args[sv][j] = 0.1 * value( getattr(vap_state, sv)[j]) else: liquid_state_args[sv][j] = 1e-8 elif "phase" in sv: # Flow is indexed by phase liquid_state_args[sv] = {} for p in liq_state_vars[sv]: liquid_state_args[sv][p] = 0.1 * value( getattr(vap_state, sv)["Vap"]) else: liquid_state_args[sv] = 0.1 * value( getattr(vap_state, sv)) elif "mole_frac" in sv: liquid_state_args[sv] = {} if "phase" in sv: # Variable is indexed by phase and component for p, j in liq_state_vars[sv].keys(): if j in vap_state.component_list: liquid_state_args[sv][p, j] = value( vap_state.fug_phase_comp["Vap", j] / vap_state.pressure) else: liquid_state_args[sv][p, j] = 1e-8 else: for j in liq_state_vars[sv].keys(): if j in vap_state.component_list: liquid_state_args[sv][j] = value( vap_state.fug_phase_comp["Vap", j] / vap_state.pressure) else: liquid_state_args[sv][j] = 1e-8 else: liquid_state_args[sv] = value(getattr(vap_state, sv)) blk.liquid_phase.initialize(outlvl=outlvl, optarg=optarg, solver=solver, state_args=liquid_state_args, hold_state=False) init_log.info_high('Initialization Step 2 Complete.') # --------------------------------------------------------------------- # Solve unit model with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: results = solverobj.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format( idaeslog.condition(results))) # --------------------------------------------------------------------- # Release Inlet state blk.vapor_phase.release_state(flags, outlvl) init_log.info('Initialization Complete: {}'.format( idaeslog.condition(results)))