def parameters_nt_sum(cobj, prop, nlist, tlist): """ Create parameters for expression forms using n-t parameters Args: cobj: Component object that will contain the parameters prop: name of property parameters are associated with nlist: list of values for n-parameter tlist: list of values for t-parameter Returns: None """ if len(nlist) != len(tlist): raise ConfigurationError( f"{cobj.name} mismatched length between n and t parameters " f"for CoolProp exponential form for property {prop}. Please " f"ensure the number of n and t parameters are equal.") # Use multiple Vars, instead of single indexed Var, to have same # structure as cases where each parameter value has different units for i, nval in enumerate(nlist): coeff = Var(doc="Multiplying parameter for CoolProp exponential form", units=pyunits.dimensionless) cobj.add_component(prop+"_coeff_n"+str(i+1), coeff) coeff.fix(nval) for i, tval in enumerate(tlist): coeff = Var(doc="Exponent parameter for CoolProp exponential form", units=pyunits.dimensionless) cobj.add_component(prop+"_coeff_t"+str(i+1), coeff) coeff.fix(tval)
class FWHCondensing0DData(HeatExchangerData): def build(self): super().build() units_meta = self.shell.config.property_package.get_metadata() self.enth_sub = Var(self.flowsheet().time, initialize=0, units=units_meta.get_derived_units("energy_mole")) self.enth_sub.fix() @self.Constraint( self.flowsheet().time, doc="Calculate steam extraction rate such that all steam condenses", ) def extraction_rate_constraint(b, t): return (b.shell.properties_out[t].enth_mol - b.enth_sub[t] == b.shell.properties_out[t].enth_mol_sat_phase["Liq"]) def initialize(self, *args, **kwargs): """ Use the regular heat exchanger initialization, with the extraction rate constraint deactivated; then it activates the constraint and calculates a steam inlet flow rate. """ solver = kwargs.get("solver", None) optarg = kwargs.get("oparg", {}) outlvl = kwargs.get("outlvl", idaeslog.NOTSET) init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) self.extraction_rate_constraint.deactivate() self.area.fix() self.overall_heat_transfer_coefficient.fix() self.inlet_1.fix() self.inlet_2.fix() self.outlet_1.unfix() self.outlet_2.unfix() # Do regular heat exchanger intialization super().initialize(*args, **kwargs) self.extraction_rate_constraint.activate() self.inlet_1.flow_mol.unfix() # Create solver opt = get_solver(solver, optarg) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info( "Initialization Complete (w/ extraction calc): {}".format( idaeslog.condition(res))) from_json(self, sd=istate, wts=sp)
class HelmTurbineStageData(HelmIsentropicTurbineData): CONFIG = HelmIsentropicTurbineData.CONFIG() def build(self): super().build() self.efficiency_mech = Var(initialize=1.0, doc="Turbine mechanical efficiency") self.efficiency_mech.fix() time_set = self.flowsheet().config.time self.shaft_speed = Var(time_set, doc="Shaft speed [1/s]", initialize=60.0) self.shaft_speed.fix() @self.Expression(time_set, doc="Specific speed [dimensionless]") def specific_speed(b, t): s = b.shaft_speed[t] # 1/s v = b.control_volume.properties_out[t].flow_vol # m3/s his_rate = b.work_isentropic[t] # J/s m = b.control_volume.properties_out[t].flow_mass # kg/s return s * v**0.5 * (his_rate / m)**(-0.75) # dimensionless @self.Expression(time_set, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def initialize( self, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, ): """ Initialize the turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: outlvl : sets output level of initialization routine solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ super().initialize(outlvl=outlvl, solver=solver, optarg=optarg) def calculate_scaling_factors(self): super().calculate_scaling_factors()
class FWHCondensing0DData(HeatExchangerData): def build(self): super().build() self.enth_sub = Var(self.flowsheet().config.time, initialize=0) self.enth_sub.fix() @self.Constraint(self.flowsheet().config.time, doc="Calculate steam extraction rate such that all steam condenses") def extraction_rate_constraint(b, t): return b.shell.properties_out[t].enth_mol - b.enth_sub[t] == \ b.shell.properties_out[t].enth_mol_sat_phase["Liq"] def initialize(self, *args, **kwargs): """ Use the regular heat exchanger initilization, with the extraction rate constraint deactivated; then it activates the constraint and calculates a steam inlet flow rate. """ self.extraction_rate_constraint.deactivate() super().initialize(*args, **kwargs) self.extraction_rate_constraint.activate() solver = kwargs.get("solver", "ipopt") optarg = kwargs.get("oparg", {}) outlvl = kwargs.get("outlvl", 0) opt = SolverFactory(solver) opt.options = optarg tee = True if outlvl >= 3 else False sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) self.area.fix() self.overall_heat_transfer_coefficient.fix() self.inlet_1.fix() self.inlet_2.fix() self.outlet_1.unfix() self.outlet_2.unfix() self.inlet_1.flow_mol.unfix() results = opt.solve(self, tee=tee) if results.solver.termination_condition == TerminationCondition.optimal: if outlvl >= 2: _log.info('{} Initialization Failed.'.format(self.name)) else: _log.warning('{} Initialization Failed.'.format(self.name)) from_json(self, sd=istate, wts=sp)
def build_parameters(cobj): cname = cobj.local_name cdict = CoolPropWrapper._get_component_data(cname) # 29-Dec-21: CoolProp only uses rational_polynomial. Alist, Blist = CoolPropWrapper._get_param_dicts( cname, cdict, "enth_mol_liq_comp", "hL", ["rational_polynomial"]) href = cdict["EOS"][0]["STATES"]["hs_anchor"]["hmolar"] cforms.parameters_polynomial(cobj, "enth_mol_liq_comp", pyunits.J / pyunits.mol, Alist, Blist) href_var = Var(doc="Reference heat of formation", units=pyunits.J / pyunits.mol) cobj.add_component("enth_mol_liq_comp_anchor", href_var) href_var.fix(href)
def parameters_polynomial(cobj, prop, prop_units, alist, blist): """ Create parameters for expression forms using A-B parameters (rational polynomial forms) Args: cobj: Component object that will contain the parameters prop: name of property parameters are associated with prop_units: units of measurement for property Alist: list of values for A-parameter Blist: list of values for B-parameter Returns: None """ for i, aval in enumerate(alist): if i == 0: param_units = prop_units else: param_units = prop_units/pyunits.K**i coeff = Var(doc="A parameter for CoolProp polynomial form", units=param_units) cobj.add_component(prop+"_coeff_A"+str(i), coeff) coeff.fix(aval) for i, bval in enumerate(blist): if i == 0: param_units = pyunits.dimensionless else: param_units = pyunits.K**-i coeff = Var(doc="B parameter for CoolProp exponential form", units=param_units) cobj.add_component(prop+"_coeff_B"+str(i), coeff) coeff.fix(bval)
class TurbineOutletStageData(HelmIsentropicTurbineData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = HelmIsentropicTurbineData.CONFIG() def build(self): super().build() self.flow_coeff = Var(initialize=0.0333, doc="Turbine flow coefficient [kg*C^0.5/s/Pa]") self.eff_dry = Var(initialize=0.87, doc="Turbine dry isentropic efficiency") self.design_exhaust_flow_vol = Var( initialize=6000.0, doc="Design exit volumetirc flowrate [m^3/s]") self.efficiency_mech = Var(initialize=1.0, doc="Turbine mechanical efficiency") self.efficiency_isentropic.unfix() self.eff_dry.fix() self.design_exhaust_flow_vol.fix() self.flow_coeff.fix() self.efficiency_mech.fix() @self.Expression(self.flowsheet().config.time, doc="Eff. fact. correlation") def tel(b, t): f = b.control_volume.properties_out[ t].flow_vol / b.design_exhaust_flow_vol return 1e6 * (-0.0035 * f**5 + 0.022 * f**4 - 0.0542 * f**3 + 0.0638 * f**2 - 0.0328 * f + 0.0064) @self.Constraint(self.flowsheet().config.time, doc="Stodola eq. choked flow") def stodola_equation(b, t): flow = b.control_volume.properties_in[t].flow_mol mw = b.control_volume.properties_in[t].mw Tin = b.control_volume.properties_in[t].temperature Pin = b.control_volume.properties_in[t].pressure Pr = b.ratioP[t] cf = b.flow_coeff return flow**2 * mw**2 * (Tin) == (cf**2 * Pin**2 * (1 - Pr**2)) @self.Constraint(self.flowsheet().config.time, doc="Efficiency correlation") def efficiency_correlation(b, t): x = b.control_volume.properties_out[t].vapor_frac eff = b.efficiency_isentropic[t] dh_isen = b.delta_enth_isentropic[t] tel = b.tel[t] return eff == b.eff_dry * x * (1 - 0.65 * (1 - x)) * (1 + tel / dh_isen) @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def initialize( self, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, calculate_cf=True, ): """ Initialize the outlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl : sets output level of initialization routine solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars for t in self.flowsheet().config.time: if self.outlet.pressure[t].fixed: self.ratioP[t] = value(self.outlet.pressure[t] / self.inlet.pressure[t]) self.deltaP[t] = value(self.outlet.pressure[t] - self.inlet.pressure[t]) # Deactivate special constraints self.stodola_equation.deactivate() self.efficiency_correlation.deactivate() self.efficiency_isentropic.fix() self.deltaP.unfix() self.ratioP.unfix() self.inlet.fix() self.outlet.unfix() super().initialize(outlvl=outlvl, solver=solver, optarg=optarg) for t in self.flowsheet().config.time: mw = self.control_volume.properties_in[t].mw Tin = self.control_volume.properties_in[t].temperature Pin = self.control_volume.properties_in[t].pressure Pr = self.ratioP[t] if not calculate_cf: cf = self.flow_coeff self.inlet.flow_mol[t].fix( value(cf * Pin * sqrt(1 - Pr**2) / mw / sqrt(Tin))) super().initialize(outlvl=outlvl, solver=solver, optarg=optarg) self.control_volume.properties_out[:].pressure.fix() # Free eff_isen and activate sepcial constarints self.efficiency_isentropic.unfix() self.outlet.pressure.fix() if calculate_cf: self.flow_coeff.unfix() self.inlet.flow_mol.unfix() self.inlet.flow_mol[0].fix() flow = self.control_volume.properties_in[0].flow_mol mw = self.control_volume.properties_in[0].mw Tin = self.control_volume.properties_in[0].temperature Pin = self.control_volume.properties_in[0].pressure Pr = self.ratioP[0] self.flow_coeff.value = value(flow * mw * sqrt(Tin / (1 - Pr**2)) / Pin) else: self.inlet.flow_mol.unfix() self.stodola_equation.activate() self.efficiency_correlation.activate() slvr = SolverFactory(solver) slvr.options = optarg self.display() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = slvr.solve(self, tee=slc.tee) init_log.info("Initialization Complete (Outlet Stage): {}".format( idaeslog.condition(res))) # reload original spec if calculate_cf: cf = value(self.flow_coeff) from_json(self, sd=istate, wts=sp) if calculate_cf: # cf was probably fixed, so will have to set the value agian here # if you ask for it to be calculated. self.flow_coeff = cf def calculate_scaling_factors(self): super().calculate_scaling_factors() for t, c in self.stodola_equation.items(): s = iscale.get_scaling_factor( self.control_volume.properties_in[t].flow_mol, default=1, warning=True)**2 iscale.constraint_scaling_transform(c, s)
class PhysicalParameterData(PhysicalParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for methane CLC. """ def build(self): ''' Callable method for Block construction. ''' super(PhysicalParameterData, self).build() self._state_block_class = SolidPhaseThermoStateBlock # Create Phase object self.Sol = SolidPhase() # Create Component objects self.Fe2O3 = Component() self.Fe3O4 = Component() self.Al2O3 = Component() # ------------------------------------------------------------------------- """ Pure solid component properties""" # Mol. weights of solid components - units = kg/mol. ref: NIST webbook mw_comp_dict = {'Fe2O3': 0.15969, 'Fe3O4': 0.231533, 'Al2O3': 0.10196} self.mw_comp = Param( self.component_list, mutable=False, initialize=mw_comp_dict, doc="Molecular weights of solid components [kg/mol]") # Skeletal density of solid components - units = kg/m3. ref: NIST dens_mass_comp_skeletal_dict = { 'Fe2O3': 5250, 'Fe3O4': 5000, 'Al2O3': 3987 } self.dens_mass_comp_skeletal = Param( self.component_list, mutable=False, initialize=dens_mass_comp_skeletal_dict, doc='Skeletal density of solid components' '[kg/m3]') # Ideal gas spec. heat capacity parameters(Shomate) of # components - ref: NIST webbook. Shomate equations from NIST. # Parameters A-E are used for cp calcs while A-H are used for enthalpy # calc. # 1e3*cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) # where T = Temperature (K)/1000, and cp_comp = (kJ/mol.K) # H_comp = H - H(298.15) = A*T + B*T^2/2 + C*T^3/3 + # D*T^4/4 - E/T + F - H where T = Temp (K)/1000 and H_comp = (kJ/mol) cp_param_dict = { ('Al2O3', 1): 102.4290, ('Al2O3', 2): 38.74980, ('Al2O3', 3): -15.91090, ('Al2O3', 4): 2.628181, ('Al2O3', 5): -3.007551, ('Al2O3', 6): -1717.930, ('Al2O3', 7): 146.9970, ('Al2O3', 8): -1675.690, ('Fe3O4', 1): 200.8320000, ('Fe3O4', 2): 1.586435e-7, ('Fe3O4', 3): -6.661682e-8, ('Fe3O4', 4): 9.452452e-9, ('Fe3O4', 5): 3.18602e-8, ('Fe3O4', 6): -1174.1350000, ('Fe3O4', 7): 388.0790000, ('Fe3O4', 8): -1120.8940000, ('Fe2O3', 1): 110.9362000, ('Fe2O3', 2): 32.0471400, ('Fe2O3', 3): -9.1923330, ('Fe2O3', 4): 0.9015060, ('Fe2O3', 5): 5.4336770, ('Fe2O3', 6): -843.1471000, ('Fe2O3', 7): 228.3548000, ('Fe2O3', 8): -825.5032000 } self.cp_param = Param(self.component_list, range(1, 10), mutable=False, initialize=cp_param_dict, doc="Shomate equation heat capacity parameters") # Std. heat of formation of comp. - units = kJ/(mol comp) - ref: NIST enth_mol_form_comp_dict = { 'Fe2O3': -825.5032, 'Fe3O4': -1120.894, 'Al2O3': -1675.690 } self.enth_mol_form_comp = Param( self.component_list, mutable=False, initialize=enth_mol_form_comp_dict, doc="Component molar heats of formation [kJ/mol]") # ------------------------------------------------------------------------- """ Mixed solid properties""" # These are setup as fixed vars to allow for parameter estimation # Particle size self.particle_dia = Var(domain=Reals, initialize=1.5e-3, doc='Diameter of solid particles [m]') self.particle_dia.fix() # TODO -provide reference # Minimum fluidization velocity - EPAT value used for Davidson model self.velocity_mf = Var(domain=Reals, initialize=0.039624, doc='Velocity at minimum fluidization [m/s]') self.velocity_mf.fix() # Minimum fluidization voidage - educated guess as rough # estimate from ergun equation results (0.4) are suspicious self.voidage_mf = Var(domain=Reals, initialize=0.45, doc='Voidage at minimum fluidization [-]') self.voidage_mf.fix() # Particle thermal conductivity self.therm_cond_sol = Var(domain=Reals, initialize=12.3e-3, doc='Thermal conductivity of solid' 'particles [kJ/m.K.s]') self.therm_cond_sol.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'flow_mass': { 'method': None, 'units': 'kg/s' }, 'particle_porosity': { 'method': None, 'units': None }, 'temperature': { 'method': None, 'units': 'K' }, 'mass_frac_comp': { 'method': None, 'units': None }, 'dens_mass_skeletal': { 'method': '_dens_mass_skeletal', 'units': 'kg/m3' }, 'dens_mass_particle': { 'method': '_dens_mass_particle', 'units': 'kg/m3' }, 'cp_mol_comp': { 'method': '_cp_mol_comp', 'units': 'kJ/mol.K' }, 'cp_mass': { 'method': '_cp_mass', 'units': 'kJ/kg.K' }, 'enth_mass': { 'method': '_enth_mass', 'units': 'kJ/kg' }, 'enth_mol_comp': { 'method': '_enth_mol_comp', 'units': 'kJ/mol' } }) obj.add_default_units({ 'time': 's', 'length': 'm', 'mass': 'kg', 'amount': 'mol', 'temperature': 'K', 'energy': 'kJ', 'holdup': 'kg' })
class ReactionParameterData(ReactionParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for superheated steam. """ # Create Class ConfigBlock CONFIG = ConfigBlock() CONFIG.declare( "gas_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the gas phase.", domain=is_physical_parameter_block)) CONFIG.declare( "solid_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the solid phase.", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def build(self): ''' Callable method for Block construction. ''' super(ReactionParameterBlock, self).build() self._reaction_block_class = ReactionBlock # Reaction Index self.rate_reaction_idx = Set(initialize=["R1"]) # Reaction Stoichiometry self.rate_reaction_stoichiometry = { ("R1", "Vap", "O2"): -1, ("R1", "Vap", "N2"): 0, ("R1", "Vap", "CO2"): 0, ("R1", "Vap", "H2O"): 0, ("R1", "Sol", "Fe2O3"): 6, ("R1", "Sol", "Fe3O4"): -4, ("R1", "Sol", "Al2O3"): 0 } # Standard Heat of Reaction - kJ/mol_rxn dh_rxn_dict = {"R1": -469.4432} self.dh_rxn = Param(self.rate_reaction_idx, initialize=dh_rxn_dict, doc="Heat of reaction [kJ/mol]", units=pyunits.kJ / pyunits.mol) # Smoothing factor self.eps = Param(mutable=True, default=1e-8, doc='Smoothing Factor') # Reaction rate scale factor self._scale_factor_rxn = Param(mutable=True, default=1, doc='Scale Factor for reaction eqn.' 'Used to help initialization routine') # ------------------------------------------------------------------------- """ Reaction properties that can be estimated""" # Particle grain radius within OC particle self.grain_radius = Var(domain=Reals, initialize=2.6e-7, doc='Representative particle grain' 'radius within OC particle [m]', units=pyunits.m) self.grain_radius.fix() # Molar density OC particle self.dens_mol_sol = Var(domain=Reals, initialize=22472, doc='Molar density of OC particle [mol/m^3]', units=pyunits.mol / pyunits.m**3) self.dens_mol_sol.fix() # Available volume for reaction - from EPAT report (1-ep)' self.a_vol = Var(domain=Reals, initialize=0.28, doc='Available reaction vol. per vol. of OC', units=pyunits.m**3 / pyunits.m**3) self.a_vol.fix() # Activation Energy self.energy_activation = Var(self.rate_reaction_idx, domain=Reals, initialize=1.4e1, doc='Activation energy [kJ/mol]', units=pyunits.kJ / pyunits.mol) self.energy_activation.fix() # Reaction order self.rxn_order = Var(self.rate_reaction_idx, domain=Reals, initialize=1.0, doc='Reaction order in gas species [-]') self.rxn_order.fix() # Pre-exponential factor self.k0_rxn = Var(self.rate_reaction_idx, domain=Reals, initialize=3.1e-4, doc='Pre-exponential factor' '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') self.k0_rxn.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'k_rxn': { 'method': '_k_rxn', 'units': 'mol^(1-N_reaction)m^(3*N_reaction -2)/s]' }, 'OC_conv': { 'method': "_OC_conv", 'units': None }, 'OC_conv_temp': { 'method': "_OC_conv_temp", 'units': None }, 'reaction_rate': { 'method': "_reaction_rate", 'units': 'mol_rxn/m3.s' } }) obj.add_default_units({ 'time': pyunits.s, 'length': pyunits.m, 'mass': pyunits.kg, 'amount': pyunits.mol, 'temperature': pyunits.K })
class TurbineOutletStageData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = False CONFIG.get('compressor')._default = False CONFIG.get('compressor')._domain = In([False]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get('thermodynamic_assumption')._default = \ ThermodynamicAssumption.isentropic CONFIG.get('thermodynamic_assumption')._domain = \ In([ThermodynamicAssumption.isentropic]) def build(self): super(TurbineOutletStageData, self).build() self.flow_coeff = Var(initialize=0.0333, doc="Turbine flow coefficient [kg*C^0.5/s/Pa]") self.delta_enth_isentropic = Var( self.flowsheet().config.time, initialize=-100, doc="Specific enthalpy change of isentropic process [J/mol]") self.eff_dry = Var(initialize=0.87, doc="Turbine dry isentropic efficiency") self.design_exhaust_flow_vol = Var( initialize=6000.0, doc="Design exit volumetirc flowrate [m^3/s]") self.efficiency_mech = Var(initialize=0.98, doc="Turbine mechanical efficiency") self.flow_scale = Param( mutable=True, default=1e3, doc= "Scaling factor for pressure flow relation should be approximatly" " the same order of magnitude as the expected flow.") self.eff_dry.fix() self.design_exhaust_flow_vol.fix() self.flow_coeff.fix() self.efficiency_mech.fix() self.ratioP[:] = 1 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize @self.Expression(self.flowsheet().config.time, doc="Efficiency factor correlation") def tel(b, t): f = b.control_volume.properties_out[ t].flow_vol / b.design_exhaust_flow_vol return 1e6 * (-0.0035 * f**5 + 0.022 * f**4 - 0.0542 * f**3 + 0.0638 * f**2 - 0.0328 * f + 0.0064) @self.Constraint(self.flowsheet().config.time, doc="Equation: Stodola, for choked flow") def stodola_equation(b, t): flow = b.control_volume.properties_in[t].flow_mol mw = b.control_volume.properties_in[t].mw Tin = b.control_volume.properties_in[t].temperature Pin = b.control_volume.properties_in[t].pressure Pr = b.ratioP[t] cf = b.flow_coeff return (1/b.flow_scale**2)*flow**2*mw**2*(Tin - 273.15) == \ (1/b.flow_scale**2)*cf**2*Pin**2*(1 - Pr**2) @self.Constraint(self.flowsheet().config.time, doc="Equation: isentropic specific enthalpy change") def isentropic_enthalpy(b, t): flow = b.control_volume.properties_in[t].flow_mol dh_isen = b.delta_enth_isentropic[t] work_isen = b.work_isentropic[t] return work_isen == dh_isen * flow @self.Constraint(self.flowsheet().config.time, doc="Equation: Efficiency correlation") def efficiency_correlation(b, t): x = b.control_volume.properties_out[t].vapor_frac eff = b.efficiency_isentropic[t] dh_isen = b.delta_enth_isentropic[t] tel = b.tel[t] return eff == b.eff_dry * x * (1 - 0.65 * (1 - x)) * (1 + tel / dh_isen) @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def initialize(self, state_args={}, outlvl=0, solver='ipopt', optarg={ 'tol': 1e-6, 'max_iter': 30 }): """ Initialize the outlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ stee = True if outlvl >= 3 else False # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Deactivate special constraints self.stodola_equation.deactivate() self.isentropic_enthalpy.deactivate() self.efficiency_correlation.deactivate() self.deltaP.unfix() self.ratioP.unfix() # Fix turbine parameters + eff_isen self.eff_dry.fix() self.design_exhaust_flow_vol.fix() self.flow_coeff.fix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # If there isn't a good guess for efficiency or outlet pressure # provide something reasonable. eff = self.efficiency_isentropic[t] eff.fix( eff.value if value(eff) > 0.3 and value(eff) < 1.0 else 0.8) # for outlet pressure try outlet pressure, pressure ratio, delta P, # then if none of those look reasonable use a pressure ratio of 0.8 # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] prdp = value((self.deltaP[t] - Pin) / Pin) if value(Pout / Pin) > 0.9 or value(Pout / Pin) < 0.01: if value(self.ratioP[t]) < 0.9 and value( self.ratioP[t]) > 0.01: Pout.fix(value(Pin * self.ratioP)) elif prdp < 0.9 and prdp > 0.01: Pout.fix(value(prdp * Pin)) else: Pout.fix(value(Pin * 0.3)) else: Pout.fix() self.deltaP[:] = value(Pout - Pin) self.ratioP[:] = value(Pout / Pin) for t in self.flowsheet().config.time: self.properties_isentropic[t].pressure.value = \ value(self.outlet.pressure[t]) self.properties_isentropic[t].flow_mol.value = \ value(self.inlet.flow_mol[t]) self.properties_isentropic[t].enth_mol.value = \ value(self.inlet.enth_mol[t]*0.95) self.outlet.flow_mol[t].value = \ value(self.inlet.flow_mol[t]) self.outlet.enth_mol[t].value = \ value(self.inlet.enth_mol[t]*0.95) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert (dof == 0) except: _log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super(TurbineOutletStageData, self).initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # Free eff_isen and activate sepcial constarints self.efficiency_isentropic.unfix() self.outlet.pressure.unfix() self.stodola_equation.activate() self.isentropic_enthalpy.activate() self.efficiency_correlation.activate() slvr = SolverFactory(solver) slvr.options = optarg res = slvr.solve(self, tee=stee) if outlvl > 0: if res.solver.termination_condition == TerminationCondition.optimal: _log.info("{} Initialization Complete.".format(self.name)) else: _log.warning( """{} Initialization Failed. The most likely cause of initialization failure for the Turbine inlet stages model is that the flow coefficient is not compatible with flow rate guess.""".format(self.name)) # reload original spec from_json(self, sd=istate, wts=sp)
class SteamValveData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() _define_config(CONFIG) def build(self): super().build() self.valve_opening = Var( self.flowsheet().config.time, initialize=1, doc="Fraction open for valve from 0 to 1", ) umeta = self.config.property_package.get_metadata().get_derived_units if self.config.phase == "Liq": cv_units = umeta("amount") / umeta("time") / umeta("pressure")**0.5 else: cv_units = umeta("amount") / umeta("time") / umeta("pressure") self.Cv = Var(initialize=0.1, doc="Valve flow coefficent", units=cv_units) self.flow_scale = Param( mutable=True, default=1e3, doc="Scaling factor for pressure flow relation should be " "approximatly the same order of magnitude as the expected flow.", ) self.Cv.fix() self.valve_opening.fix() # set up the valve function rule. I'm not sure these matter too much # for us, but the options are easy enough to provide. if self.config.valve_function == ValveFunctionType.linear: rule = _linear_rule elif self.config.valve_function == ValveFunctionType.quick_opening: rule = _quick_open_rule elif self.config.valve_function == ValveFunctionType.equal_percentage: self.alpha = Var(initialize=1, doc="Valve function parameter") self.alpha.fix() rule = _equal_percentage_rule else: rule = self.config.valve_function_rule self.valve_function = Expression(self.flowsheet().config.time, rule=rule, doc="Valve function expression") if self.config.phase == "Liq": rule = _liquid_pressure_flow_rule else: rule = _vapor_pressure_flow_rule self.pressure_flow_equation = Constraint(self.flowsheet().config.time, rule=rule) def initialize( self, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, ): """ Initialize the turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl : sets output level of initialization routine solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but # initializes the values of free vars init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) self.deltaP[:].unfix() self.ratioP[:].unfix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] if self.deltaP[t].value is not None: prdp = value((self.deltaP[t] - Pin) / Pin) else: prdp = -100 # crazy number to say don't use deltaP as guess if value(Pout / Pin) > 1 or value(Pout / Pin) < 0.0: if value(self.ratioP[t]) <= 1 and value(self.ratioP[t]) >= 0: Pout.value = value(Pin * self.ratioP[t]) elif prdp <= 1 and prdp >= 0: Pout.value = value(prdp * Pin) else: Pout.value = value(Pin * 0.95) self.deltaP[t] = value(Pout - Pin) self.ratioP[t] = value(Pout / Pin) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert dof == 0 except: init_log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super().initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # reload original spec from_json(self, sd=istate, wts=sp) def _get_performance_contents(self, time_point=0): pc = super()._get_performance_contents(time_point=time_point) pc["vars"]["Opening"] = self.valve_opening[time_point] pc["vars"]["Valve Coefficient"] = self.Cv if self.config.valve_function == ValveFunctionType.equal_percentage: pc["vars"]["alpha"] = self.alpha pc["params"] = {} pc["params"]["Flow Scaling"] = self.flow_scale return pc
class ReactionParameterData(ReactionParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for superheated steam. """ # Create Class ConfigBlock CONFIG = ConfigBlock() CONFIG.declare( "gas_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the gas phase.", domain=is_physical_parameter_block)) CONFIG.declare( "solid_property_package", ConfigValue( description="Reference to associated PropertyPackageParameter " "object for the solid phase.", domain=is_physical_parameter_block)) CONFIG.declare( "default_arguments", ConfigBlock( description="Default arguments to use with Property Package", implicit=True)) def build(self): ''' Callable method for Block construction. ''' super(ReactionParameterBlock, self).build() self._reaction_block_class = ReactionBlock # Create Phase objects self.Vap = VaporPhase() self.Sol = SolidPhase() # Create Component objects self.CH4 = Component() self.CO2 = Component() self.H2O = Component() self.Fe2O3 = Component() self.Fe3O4 = Component() self.Al2O3 = Component() # Component list subsets self.gas_component_list = Set(initialize=['CO2', 'H2O', 'CH4']) self.sol_component_list = Set(initialize=['Fe2O3', 'Fe3O4', 'Al2O3']) # Reaction Index self.rate_reaction_idx = Set(initialize=["R1"]) # Gas Constant self.gas_const = Param(within=PositiveReals, mutable=False, default=8.314459848e-3, doc='Gas Constant [kJ/mol.K]') # Smoothing factor self.eps = Param(mutable=True, default=1e-8, doc='Smoothing Factor') # Reaction rate scale factor self._scale_factor_rxn = Param(mutable=True, default=1, doc='Scale Factor for reaction eqn.' 'Used to help initialization routine') # Reaction Stoichiometry self.rate_reaction_stoichiometry = { ("R1", "Vap", "CH4"): -1, ("R1", "Vap", "CO2"): 1, ("R1", "Vap", "H2O"): 2, ("R1", "Sol", "Fe2O3"): -12, ("R1", "Sol", "Fe3O4"): 8, ("R1", "Sol", "Al2O3"): 0 } # Reaction stoichiometric coefficient self.rxn_stoich_coeff = Param(self.rate_reaction_idx, default=12, mutable=True, doc='Reaction stoichiometric' 'coefficient [-]') # Standard Heat of Reaction - kJ/mol_rxn dh_rxn_dict = {"R1": 136.5843} self.dh_rxn = Param(self.rate_reaction_idx, initialize=dh_rxn_dict, doc="Heat of reaction [kJ/mol]") # ------------------------------------------------------------------------- """ Reaction properties that can be estimated""" # Particle grain radius within OC particle self.grain_radius = Var(domain=Reals, initialize=2.6e-7, doc='Representative particle grain' 'radius within OC particle [m]') self.grain_radius.fix() # Molar density OC particle self.dens_mol_sol = Var(domain=Reals, initialize=32811, doc='Molar density of OC particle [mol/m^3]') self.dens_mol_sol.fix() # Available volume for reaction - from EPAT report (1-ep)' self.a_vol = Var(domain=Reals, initialize=0.28, doc='Available reaction vol. per vol. of OC') self.a_vol.fix() # Activation Energy self.energy_activation = Var(self.rate_reaction_idx, domain=Reals, initialize=4.9e1, doc='Activation energy [kJ/mol]') self.energy_activation.fix() # Reaction order self.rxn_order = Var(self.rate_reaction_idx, domain=Reals, initialize=1.3, doc='Reaction order in gas species [-]') self.rxn_order.fix() # Pre-exponential factor self.k0_rxn = Var(self.rate_reaction_idx, domain=Reals, initialize=8e-4, doc='Pre-exponential factor' '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]') self.k0_rxn.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'k_rxn': { 'method': '_k_rxn', 'units': 'mol^(1-N_reaction)m^(3*N_reaction -2)/s]' }, 'OC_conv': { 'method': "_OC_conv", 'units': None }, 'OC_conv_temp': { 'method': "_OC_conv_temp", 'units': None }, 'reaction_rate': { 'method': "_reaction_rate", 'units': 'mol_rxn/m3.s' } }) obj.add_default_units({ 'time': 's', 'length': 'm', 'mass': 'kg', 'amount': 'mol', 'temperature': 'K', 'energy': 'kJ' })
class HDAParameterData(PhysicalParameterBlock): CONFIG = PhysicalParameterBlock.CONFIG() def build(self): ''' Callable method for Block construction. ''' super(HDAParameterData, self).build() self._state_block_class = HDAStateBlock self.benzene = Component() self.toluene = Component() self.methane = Component() self.hydrogen = Component() self.diphenyl = Component() self.Vap = VaporPhase() # Thermodynamic reference state self.pressure_ref = Param(mutable=True, default=101325, units=pyunits.Pa, doc='Reference pressure') self.temperature_ref = Param(mutable=True, default=298.15, units=pyunits.K, doc='Reference temperature') # Source: The Properties of Gases and Liquids (1987) # 4th edition, Chemical Engineering Series - Robert C. Reid self.mw_comp = Param(self.component_list, mutable=False, initialize={'benzene': 78.1136E-3, 'toluene': 92.1405E-3, 'hydrogen': 2.016e-3, 'methane': 16.043e-3, 'diphenyl': 154.212e-4}, units=pyunits.kg/pyunits.mol, doc="Molecular weight") # Constants for specific heat capacity, enthalpy # Sources: The Properties of Gases and Liquids (1987) # 4th edition, Chemical Engineering Series - Robert C. Reid self.cp_mol_ig_comp_coeff_A = Var( self.component_list, initialize={"benzene": -3.392E1, "toluene": -2.435E1, "hydrogen": 2.714e1, "methane": 1.925e1, "diphenyl": -9.707e1}, units=pyunits.J/pyunits.mol/pyunits.K, doc="Parameter A for ideal gas molar heat capacity") self.cp_mol_ig_comp_coeff_A.fix() self.cp_mol_ig_comp_coeff_B = Var( self.component_list, initialize={"benzene": 4.739E-1, "toluene": 5.125E-1, "hydrogen": 9.274e-3, "methane": 5.213e-2, "diphenyl": 1.106e0}, units=pyunits.J/pyunits.mol/pyunits.K**2, doc="Parameter B for ideal gas molar heat capacity") self.cp_mol_ig_comp_coeff_B.fix() self.cp_mol_ig_comp_coeff_C = Var( self.component_list, initialize={"benzene": -3.017E-4, "toluene": -2.765E-4, "hydrogen": -1.381e-5, "methane": -8.855e-4, "diphenyl": -8.855e-4}, units=pyunits.J/pyunits.mol/pyunits.K**3, doc="Parameter C for ideal gas molar heat capacity") self.cp_mol_ig_comp_coeff_C.fix() self.cp_mol_ig_comp_coeff_D = Var( self.component_list, initialize={"benzene": 7.130E-8, "toluene": 4.911E-8, "hydrogen": 7.645e-9, "methane": -1.132e-8, "diphenyl": 2.790e-7}, units=pyunits.J/pyunits.mol/pyunits.K**4, doc="Parameter D for ideal gas molar heat capacity") self.cp_mol_ig_comp_coeff_D.fix() # Source: NIST Webook, https://webbook.nist.gov/, retrieved 11/3/2020 self.enth_mol_form_vap_comp_ref = Var( self.component_list, initialize={"benzene": -82.9e3, "toluene": -50.1e3, "hydrogen": 0, "methane": -75e3, "diphenyl": -180e3}, units=pyunits.J/pyunits.mol, doc="Standard heat of formation at reference state") self.enth_mol_form_vap_comp_ref.fix() @classmethod def define_metadata(cls, obj): """Define properties supported and units.""" obj.add_properties( {'flow_mol': {'method': None}, 'mole_frac_comp': {'method': None}, 'temperature': {'method': None}, 'pressure': {'method': None}, 'mw_comp': {'method': None}, 'dens_mol': {'method': None}, 'enth_mol': {'method': '_enth_mol'}}) obj.add_default_units({'time': pyunits.s, 'length': pyunits.m, 'mass': pyunits.kg, 'amount': pyunits.mol, 'temperature': pyunits.K})
class HDAReactionParameterData(ReactionParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for superheated steam. """ def build(self): ''' Callable method for Block construction. ''' super(HDAReactionParameterData, self).build() self._reaction_block_class = HDAReactionBlock # List of valid phases in property package self.phase_list = Set(initialize=['Vap']) # Component list - a list of component identifiers self.component_list = Set( initialize=['benzene', 'toluene', 'hydrogen', 'methane']) # Reaction Index self.rate_reaction_idx = Set(initialize=["R1"]) # Reaction Stoichiometry self.rate_reaction_stoichiometry = { ("R1", "Vap", "benzene"): 1, ("R1", "Vap", "toluene"): -1, ("R1", "Vap", "hydrogen"): -1, ("R1", "Vap", "methane"): 1, ("R1", "Liq", "benzene"): 0, ("R1", "Liq", "toluene"): 0, ("R1", "Liq", "hydrogen"): 0, ("R1", "Liq", "methane"): 0 } # Arrhenius Constant self.arrhenius = Var(initialize=6.3e+10, doc="Arrhenius pre-exponential factor" ) # TODO: Determine correct value and units self.arrhenius.fix() # Activation Energy self.energy_activation = Var(initialize=217.6e3, units=pyunits.J / pyunits.mol / pyunits.K, doc="Activation energy") self.energy_activation.fix() # Heat of Reaction dh_rxn_dict = {"R1": -1.08e5} self.dh_rxn = Param(self.rate_reaction_idx, initialize=dh_rxn_dict, doc="Heat of reaction [J/mol]") # Gas Constant self.gas_const = Param(mutable=False, default=8.314, doc='Gas Constant [J/mol.K]') @classmethod def define_metadata(cls, obj): obj.add_properties({ 'k_rxn': { 'method': None, 'units': 'm^3/mol.s' }, 'reaction_rate': { 'method': None, 'units': 'mol/m^3.s' } }) obj.add_default_units({ 'time': pyunits.s, 'length': pyunits.m, 'mass': pyunits.kg, 'amount': pyunits.mol, 'temperature': pyunits.K })
class ElectricalSplitterData(UnitModelBlockData): """ Unit model to split a electricity from a single inlet into multiple outlets based on split fractions """ CONFIG = ConfigBlock() CONFIG.declare( "dynamic", ConfigValue(domain=In([False]), default=False, description="Dynamic model flag - must be False")) CONFIG.declare( "has_holdup", ConfigValue(default=False, domain=In([False]), description="Holdup construction flag - must be False")) CONFIG.declare( "outlet_list", ConfigValue(domain=list_of_strings, description="List of outlet names", doc="""A list containing names of outlets, **default** - None. **Valid values:** { **None** - use num_outlets argument, **list** - a list of names to use for outlets.}""")) CONFIG.declare( "num_outlets", ConfigValue( domain=int, description="Number of outlets to unit", doc="""Argument indicating number (int) of outlets to construct, not used if outlet_list arg is provided, **default** - None. **Valid values:** { **None** - use outlet_list arg instead, or default to 2 if neither argument provided, **int** - number of outlets to create (will be named with sequential integers from 1 to num_outlets).}""")) def build(self): """ """ super().build() time = self.flowsheet().config.time self.create_outlets() self.electricity = Var(time, domain=NonNegativeReals, initialize=0.0, doc="Electricity into control volume", units=pyunits.kW) self.electricity_in = Port(noruleinit=True, doc="A port for electricity flow") self.electricity_in.add(self.electricity, "electricity") self.split_fraction = Var(self.outlet_list, time, bounds=(0, 1), initialize=1.0 / len(self.outlet_list), doc="Split fractions for outlet streams") @self.Constraint(time, doc="Split constraint") def sum_split(b, t): return 1 == sum(b.split_fraction[o, t] for o in b.outlet_list) @self.Constraint(time, self.outlet_list, doc="Electricity constraint") def electricity_eqn(b, t, o): outlet_obj = getattr(b, o + "_elec") return outlet_obj[t] == b.split_fraction[o, t] * b.electricity[t] def create_outlets(self): """ Create list of outlet stream names based on config arguments. Returns: list of strings """ config = self.config if config.outlet_list is not None and config.num_outlets is not None: # If both arguments provided and not consistent, raise Exception if len(config.outlet_list) != config.num_outlets: raise ConfigurationError( "{} ElectricalSplitter provided with both outlet_list and " "num_outlets arguments, which were not consistent (" "length of outlet_list was not equal to num_outlets). " "Please check your arguments for consistency, and " "note that it is only necessry to provide one of " "these arguments.".format(self.name)) elif (config.outlet_list is None and config.num_outlets is None): # If no arguments provided for outlets, default to num_outlets = 2 config.num_outlets = 2 # Create a list of names for outlet StateBlocks if config.outlet_list is not None: outlet_list = self.config.outlet_list else: outlet_list = [ "outlet_{}".format(n) for n in range(1, config.num_outlets + 1) ] self.outlet_list = outlet_list for p in self.outlet_list: outlet_obj = Var(self.flowsheet().config.time, domain=NonNegativeReals, initialize=0.0, doc="Electricity at outlet {}".format(p), units=pyunits.kW) setattr(self, p + "_elec", outlet_obj) outlet_port = Port(noruleinit=True, doc="Outlet {}".format(p)) outlet_port.add(getattr(self, p + "_elec"), "electricity") setattr(self, p + "_port", outlet_port) def initialize(self, **kwargs): # store original state sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # check for fixed outlet flows and use them to calculate fixed split # fractions for t in self.flowsheet().config.time: for o in self.outlet_list: elec_obj = getattr(self, o + "_elec") if elec_obj[t].fixed: self.split_fraction[o, t].fix( value(elec_obj[t] / self.electricity[t])) # fix or unfix split fractions so n - 1 are fixed for t in self.flowsheet().config.time: # see how many split fractions are fixed n = sum(1 for o in self.outlet_list if self.split_fraction[o, t].fixed) # if number of outlets - 1 we're good if n == len(self.outlet_list) - 1: continue # if too many are fixed, unfix the first, generally assume that is # the main flow, and is the calculated split fraction elif n == len(self.outlet_list): self.split_fraction[self.outlet_list[0], t].unfix() # if not enough fixed, start fixing from the back until there are # are enough else: for o in reversed(self.outlet_list): if not self.split_fraction[o, t].fixed: self.split_fraction[o, t].fix() n += 1 if n == len(self.outlet_list) - 1: break self.electricity.fix() for o in self.outlet_list: getattr(self, o + "_port").unfix() assert degrees_of_freedom(self) == 0 solver = "ipopt" if "solver" in kwargs: solver = kwargs["solver"] opt = SolverFactory(solver) opt.solve(self) from_json(self, sd=istate, wts=sp)
class TurbineOutletStageData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = False CONFIG.get("compressor")._default = False CONFIG.get("compressor")._domain = In([False]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption" )._default = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption")._domain = In( [ThermodynamicAssumption.isentropic]) def build(self): super(TurbineOutletStageData, self).build() self.flow_coeff = Var(initialize=0.0333, doc="Turbine flow coefficient [kg*C^0.5/s/Pa]") self.eff_dry = Var(initialize=0.87, doc="Turbine dry isentropic efficiency") self.design_exhaust_flow_vol = Var( initialize=6000.0, doc="Design exit volumetirc flowrate [m^3/s]") self.efficiency_mech = Var(initialize=0.98, doc="Turbine mechanical efficiency") self.flow_scale = Param( mutable=True, default=1e-4, doc= "Scaling factor for pressure flow relation should be approximatly" " the same order of magnitude as the expected flow.", ) self.eff_dry.fix() self.design_exhaust_flow_vol.fix() self.flow_coeff.fix() self.efficiency_mech.fix() self.ratioP[:] = 1 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize @self.Expression(self.flowsheet().config.time, doc="Efficiency factor correlation") def tel(b, t): f = b.control_volume.properties_out[ t].flow_vol / b.design_exhaust_flow_vol return 1e6 * (-0.0035 * f**5 + 0.022 * f**4 - 0.0542 * f**3 + 0.0638 * f**2 - 0.0328 * f + 0.0064) @self.Constraint(self.flowsheet().config.time, doc="Equation: Stodola, for choked flow") def stodola_equation(b, t): flow = b.control_volume.properties_in[t].flow_mol mw = b.control_volume.properties_in[t].mw Tin = b.control_volume.properties_in[t].temperature Pin = b.control_volume.properties_in[t].pressure Pr = b.ratioP[t] cf = b.flow_coeff return (b.flow_scale**2) * flow**2 * mw**2 * (Tin - 273.15) == ( b.flow_scale**2) * cf**2 * Pin**2 * (1 - Pr**2) @self.Expression( self.flowsheet().config.time, doc="Equation: isentropic specific enthalpy change", ) def delta_enth_isentropic(b, t): flow = b.control_volume.properties_in[t].flow_mol work_isen = b.work_isentropic[t] return work_isen / flow @self.Constraint(self.flowsheet().config.time, doc="Equation: Efficiency correlation") def efficiency_correlation(b, t): x = b.control_volume.properties_out[t].vapor_frac eff = b.efficiency_isentropic[t] dh_isen = b.delta_enth_isentropic[t] tel = b.tel[t] return eff == b.eff_dry * x * (1 - 0.65 * (1 - x)) * (1 + tel / dh_isen) @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def _get_performance_contents(self, time_point=0): pc = super()._get_performance_contents(time_point=time_point) pc["vars"]["Mechanical Efficiency"] = self.efficiency_mech pc["vars"]["Flow Coefficient"] = self.flow_coeff pc["vars"]["Isentropic Efficieincy (Dry)"] = self.eff_dry pc["vars"]["Design Exhaust Flow"] = self.design_exhaust_flow_vol pc["exprs"] = {} pc["exprs"]["Thermodynamic Power"] = self.power_thermo[time_point] pc["exprs"]["Shaft Power"] = self.power_shaft[time_point] pc["params"] = {} pc["params"]["Flow Scaling"] = self.flow_scale return pc def initialize( self, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, ): """ Initialize the outlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl : sets output level of initialization routine solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Deactivate special constraints self.stodola_equation.deactivate() self.efficiency_correlation.deactivate() self.deltaP.unfix() self.ratioP.unfix() # Fix turbine parameters + eff_isen self.eff_dry.fix() self.design_exhaust_flow_vol.fix() self.flow_coeff.fix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # If there isn't a good guess for efficiency or outlet pressure # provide something reasonable. eff = self.efficiency_isentropic[t] eff.fix( eff.value if value(eff) > 0.3 and value(eff) < 1.0 else 0.8) # for outlet pressure try outlet pressure, pressure ratio, delta P, # then if none of those look reasonable use a pressure ratio of 0.8 # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] prdp = value((self.deltaP[t] - Pin) / Pin) if value(Pout / Pin) > 0.95 or value(Pout / Pin) < 0.003: if value(self.ratioP[t]) < 0.9 and value( self.ratioP[t]) > 0.01: Pout.fix(value(Pin * self.ratioP)) elif prdp < 0.9 and prdp > 0.01: Pout.fix(value(prdp * Pin)) else: Pout.fix(value(Pin * 0.3)) else: Pout.fix() self.deltaP[:] = value(Pout - Pin) self.ratioP[:] = value(Pout / Pin) for t in self.flowsheet().config.time: self.properties_isentropic[t].pressure.value = value( self.outlet.pressure[t]) self.properties_isentropic[t].flow_mol.value = value( self.inlet.flow_mol[t]) self.properties_isentropic[t].enth_mol.value = value( self.inlet.enth_mol[t] * 0.95) self.outlet.flow_mol[t].value = value(self.inlet.flow_mol[t]) self.outlet.enth_mol[t].value = value(self.inlet.enth_mol[t] * 0.95) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert dof == 0 except AssertionError: init_log.error("Degrees of freedom not 0, ({})".format(dof)) raise mw = self.control_volume.properties_in[0].mw Tin = self.control_volume.properties_in[0].temperature Pin = self.control_volume.properties_in[0].pressure Pr = self.ratioP[0] cf = self.flow_coeff self.inlet.flow_mol.fix( value(cf * Pin * sqrt(1 - Pr**2) / mw / sqrt(Tin - 273.15))) # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super().initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # Free eff_isen and activate sepcial constarints self.efficiency_isentropic.unfix() self.outlet.pressure.fix() self.inlet.flow_mol.unfix() self.stodola_equation.activate() self.efficiency_correlation.activate() slvr = SolverFactory(solver) slvr.options = optarg with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = slvr.solve(self, tee=slc.tee) init_log.info("Initialization Complete (Outlet Stage): {}".format( idaeslog.condition(res))) # reload original spec from_json(self, sd=istate, wts=sp)
class HydrogenTankData(UnitModelBlockData): """ Simple hydrogen tank model. Unit model to store or supply compressed hydrogen. """ CONFIG = ConfigBlock() # This model is based on steady state material & energy balances. # The accumulation term is computed based on the tank state at # previous time step. Thus, dynamic option is turned off. # However, a dynamic analysis can be performed by creating # an instance of this model for every time step. CONFIG.declare( "dynamic", ConfigValue(domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicats if Hydrogen tank model is dynamic, **default** = False. Equilibrium Reactors do not support dynamic behavior.""")) CONFIG.declare( "has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Indicates whether holdup terms should be constructed or not. **default** - False. Hydrogen tank model uses custom equations for holdup.""")) CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc= """Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc= """A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) def build(self): """Building model Args: None Returns: None """ super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args }) # add inlet and outlet states self.control_volume.add_state_blocks(has_phase_equilibrium=False) # add tank volume self.control_volume.add_geometry() # add phase fractions self.control_volume._add_phase_fractions() # add pressure balance self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) # add a state block 'previous_state' for the storage tank # this state block is needed to compute material and energy holdup # at previous or the starting time step using the property package # for a given P_prev and T_prev # NOTE: there is no flow in the previous state so, # flow_mol state variable is fixed to 0 self.previous_state = (self.config.property_package.build_state_block( self.flowsheet().config.time, doc="Tank state at previous time")) # previous state should not have any flow self.previous_state[:].flow_mol.fix(0) # add local lists for easy use phase_list = self.control_volume.properties_in.phase_list pc_set = self.control_volume.properties_in.phase_component_set component_list = self.control_volume.properties_in.component_list # Get units from property package units = self.config.property_package.\ get_metadata().get_derived_units if (self.control_volume.properties_in[ self.flowsheet().config.time.first()].get_material_flow_basis( ) == MaterialFlowBasis.molar): flow_units = units("flow_mole") material_units = units("amount") elif (self.control_volume.properties_in[ self.flowsheet().config.time.first()].get_material_flow_basis( ) == MaterialFlowBasis.mass): flow_units = units("flow_mass") material_units = units("mass") # Add Inlet and Outlet Ports self.add_inlet_port() self.add_outlet_port() # Define Vars for Tank volume calculations self.tank_diameter = Var(self.flowsheet().config.time, within=NonNegativeReals, initialize=1.0, doc="Diameter of storage tank in m", units=units("length")) self.tank_length = Var(self.flowsheet().config.time, within=NonNegativeReals, initialize=1.0, doc="Length of storage tank in m", units=units("length")) # Tank volume calculation @self.Constraint(self.flowsheet().config.time) def volume_cons(b, t): return (b.control_volume.volume[t] == const.pi * b.tank_length[t] * ((b.tank_diameter[t] / 2)**2)) # define Vars for the model self.dt = Var(self.flowsheet().config.time, domain=NonNegativeReals, initialize=100, doc="Time step for holdup calculation", units=units("time")) self.heat_duty = Var( self.flowsheet().config.time, domain=Reals, initialize=0.0, doc="Heat transferred from surroundings, 0 for adiabatic", units=units("power")) self.material_accumulation = Var( self.flowsheet().config.time, pc_set, within=Reals, initialize=1.0, doc="Accumulation of material in tank", units=flow_units) self.energy_accumulation = Var(self.flowsheet().config.time, phase_list, within=Reals, initialize=1.0, doc="Energy accumulation", units=units("power")) self.material_holdup = Var(self.flowsheet().config.time, pc_set, within=Reals, initialize=1.0, doc="Material holdup in tank", units=material_units) self.energy_holdup = Var(self.flowsheet().config.time, phase_list, within=Reals, initialize=1.0, doc="Energy holdup in tank", units=units("energy")) self.previous_material_holdup = Var( self.flowsheet().config.time, pc_set, within=Reals, initialize=1.0, doc="Tank material holdup at previous time", units=material_units) self.previous_energy_holdup = Var( self.flowsheet().config.time, phase_list, within=Reals, initialize=1.0, doc="Tank energy holdup at previous time", units=units("energy")) # Adiabatic operations are assumed # Fixing the heat_duty to 0 here to avoid any misakes at use # TODO: remove this once the isothermal constraints are added self.heat_duty.fix(0) # Computing material and energy holdup in the tank at previous time # using previous state Pressure and Temperature of the tank @self.Constraint(self.flowsheet().config.time, pc_set, doc="Material holdup at previous time") def previous_material_holdup_rule(b, t, p, j): return ( b.previous_material_holdup[t, p, j] == b.control_volume.volume[t] * b.control_volume.phase_fraction[t, p] * b.previous_state[t].get_material_density_terms(p, j)) @self.Constraint(self.flowsheet().config.time, phase_list, doc="Energy holdup at previous time") def previous_energy_holdup_rule(b, t, p): if (self.control_volume.properties_in[t].get_material_flow_basis() == MaterialFlowBasis.molar): return (b.previous_energy_holdup[t, p] == ( sum(b.previous_material_holdup[t, p, j] for j in component_list) * b.previous_state[t].energy_internal_mol_phase[p])) if (self.control_volume.properties_in[t].get_material_flow_basis() == MaterialFlowBasis.mass): return (b.previous_energy_holdup[t, p] == ( sum(b.previous_material_holdup[t, p, j] for j in component_list) * (b.previous_state[t].energy_internal_mol_phase[p] / b.previous_state[t].mw))) # component material balances @self.Constraint(self.flowsheet().config.time, pc_set, doc="Material balances") def material_balances(b, t, p, j): if (p, j) in pc_set: return ( b.material_accumulation[t, p, j] == (b.control_volume.properties_in[t].\ get_material_flow_terms(p, j) - b.control_volume.properties_out[t].\ get_material_flow_terms(p, j)) ) else: return Constraint.Skip # integration of material accumulation @self.Constraint(self.flowsheet().config.time, pc_set, doc="Material holdup integration") def material_holdup_integration(b, t, p, j): if (p, j) in pc_set: return b.material_holdup[t, p, j] == ( b.dt[t] * b.material_accumulation[t, p, j] + b.previous_material_holdup[t, p, j]) # material holdup calculation @self.Constraint(self.flowsheet().config.time, pc_set, doc="Material holdup calculations") def material_holdup_calculation(b, t, p, j): if (p, j) in pc_set: return ( b.material_holdup[t, p, j] == ( b.control_volume.volume[t] * b.control_volume.phase_fraction[t, p] * b.control_volume.properties_out[t].\ get_material_density_terms(p, j))) # energy accumulation @self.Constraint(self.flowsheet().config.time, doc="Energy accumulation") def energy_accumulation_equation(b, t): return (sum(b.energy_accumulation[t, p] for p in phase_list) * b.dt[t] == sum(b.energy_holdup[t, p] for p in phase_list) - sum(b.previous_energy_holdup[t, p] for p in phase_list)) # energy holdup calculation @self.Constraint(self.flowsheet().config.time, phase_list, doc="Energy holdup calculation") def energy_holdup_calculation(b, t, p): if (self.control_volume.properties_in[t].get_material_flow_basis() == MaterialFlowBasis.molar): return ( b.energy_holdup[t, p] == (sum(b.material_holdup[t, p, j] for j in component_list) * b.control_volume.properties_out[t].\ energy_internal_mol_phase[p]) ) if (self.control_volume.properties_in[t].get_material_flow_basis() == MaterialFlowBasis.mass): return ( b.energy_holdup[t, p] == (sum(b.material_holdup[t, p, j] for j in component_list) * (b.control_volume.properties_out[t].\ energy_internal_mol_phase[p]/ b.control_volume.properties_out[t].mw)) ) # Energy balance based on internal energy, as follows: # n_final * U_final = # n_previous * U_previous + # n_inlet * H_inlet - n_outlet * H_outlet # where, n is number of moles, U is internal energy, H is enthalpy @self.Constraint(self.flowsheet().config.time, doc="Energy balance") def energy_balances(b, t): return (sum( b.energy_holdup[t, p] for p in phase_list) == sum(b.previous_energy_holdup[t, p] for p in phase_list) + b.dt[t] * (sum(b.control_volume.properties_in[t]. get_enthalpy_flow_terms(p) for p in phase_list) - sum(b.control_volume.properties_out[t]. get_enthalpy_flow_terms(p) for p in phase_list))) def initialize(blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Hydrogen tank model 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 * 0 = no output (default) * 1 = return solver state for each step in routine * 2 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' if state_args is None: state_args = dict() init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") opt = get_solver(solver, optarg) init_log.info_low("Starting initialization...") flags = blk.control_volume.initialize(state_args=state_args, outlvl=outlvl, optarg=optarg, solver=solver) flag_previous_state = blk.previous_state.initialize( outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True, state_args=state_args, ) blk.previous_state[0].sum_mole_frac_out.deactivate() init_log.info_high("Initialization Step 1 Complete.") with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format( idaeslog.condition(res))) blk.previous_state[0].sum_mole_frac_out.activate() blk.control_volume.release_state(flags, outlvl) blk.previous_state.release_state(flag_previous_state, outlvl) init_log.info("Initialization Complete.") def calculate_scaling_factors(self): super().calculate_scaling_factors() if hasattr(self, "previous_state"): for t, v in self.previous_state.items(): iscale.set_scaling_factor(v.flow_mol, 1e-3) iscale.set_scaling_factor(v.pressure, 1e-5) iscale.set_scaling_factor(v.temperature, 1e-1) if hasattr(self, "tank_diameter"): for t, v in self.tank_diameter.items(): iscale.set_scaling_factor(v, 1) if hasattr(self, "tank_length"): for t, v in self.tank_length.items(): iscale.set_scaling_factor(v, 1) if hasattr(self, "heat_duty"): for t, v in self.heat_duty.items(): iscale.set_scaling_factor(v, 1e-5) if hasattr(self, "material_accumulation"): for (t, p, j), v in self.material_accumulation.items(): iscale.set_scaling_factor(v, 1e-3) if hasattr(self, "energy_accumulation"): for (t, p), v in self.energy_accumulation.items(): iscale.set_scaling_factor(v, 1e-3) if hasattr(self, "material_holdup"): for (t, p, j), v in self.material_holdup.items(): iscale.set_scaling_factor(v, 1e-5) if hasattr(self, "energy_holdup"): for (t, p), v in self.energy_holdup.items(): iscale.set_scaling_factor(v, 1e-5) if hasattr(self, "previous_material_holdup"): for (t, p, j), v in self.previous_material_holdup.items(): iscale.set_scaling_factor(v, 1e-5) if hasattr(self, "previous_energy_holdup"): for (t, p), v in self.previous_energy_holdup.items(): iscale.set_scaling_factor(v, 1e-5) # Volume constraint if hasattr(self, "volume_cons"): for t, c in self.volume_cons.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.tank_length[t], default=1, warning=True)) # Previous time Material Holdup Rule if hasattr(self, "previous_material_holdup_rule"): for (t, i), c in self.previous_material_holdup_rule.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.material_holdup[t, i, j], default=1, warning=True)) # Previous time Energy Holdup Rule if hasattr(self, "previous_energy_holdup_rule"): for (t, i), c in self.previous_energy_holdup_rule.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.energy_holdup[t, i], default=1, warning=True)) # Material Balances if hasattr(self, "material_balances"): for (t, i, j), c in self.material_balances.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.material_accumulation[t, i, j], default=1, warning=True)) # Material Holdup Integration if hasattr(self, "material_holdup_integration"): for (t, i, j), c in self.material_holdup_integration.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.material_holdup[t, i, j], default=1, warning=True)) # Material Holdup Constraints if hasattr(self, "material_holdup_calculation"): for (t, i, j), c in self.material_holdup_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.material_holdup[t, i, j], default=1, warning=True)) # Enthalpy Balances if hasattr(self, "energy_accumulation_equation"): for t, c in self.energy_accumulation_equation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.energy_accumulation[t, p], default=1, warning=True)) # Energy Holdup Integration if hasattr(self, "energy_holdup_calculation"): for (t, i), c in self.energy_holdup_calculation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.energy_holdup[t, i], default=1, warning=True)) # Energy Balance Equation if hasattr(self, "energy_balances"): for t, c in self.energy_balances.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor(self.energy_holdup[t, i], default=1, warning=True))
class TurbineStageData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = False CONFIG.get("compressor")._default = False CONFIG.get("compressor")._domain = In([False]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption" )._default = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption")._domain = In( [ThermodynamicAssumption.isentropic]) def build(self): super(TurbineStageData, self).build() self.efficiency_mech = Var(initialize=0.98, doc="Turbine mechanical efficiency") self.efficiency_mech.fix() self.ratioP[:] = 0.8 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize time_set = self.flowsheet().config.time self.shaft_speed = Var(time_set, doc="Shaft speed [1/s]", initialize=60.0) @self.Expression(time_set, doc="Specific speed [dimensionless]") def specific_speed(b, t): s = b.shaft_speed[t] # 1/s v = b.control_volume.properties_out[t].flow_vol # m3/s his_rate = b.work_isentropic[t] # J/s m = b.control_volume.properties_out[t].flow_mass # kg/s return s * v**0.5 * (his_rate / m)**(-0.75) # dimensionless @self.Expression(time_set, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def _get_performance_contents(self, time_point=0): pc = super()._get_performance_contents(time_point=time_point) pc["vars"]["Mechanical Efficiency"] = self.efficiency_mech return pc def initialize( self, state_args={}, outlvl=0, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, ): """ Initialize the turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ stee = True if outlvl >= 3 else False # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # If there isn't a good guess for efficiency or outlet pressure # provide something reasonable. eff = self.efficiency_isentropic[t] eff.fix( eff.value if value(eff) > 0.3 and value(eff) < 1.0 else 0.8) # for outlet pressure try outlet pressure, pressure ratio, delta P, # then if none of those look reasonable use a pressure ratio of 0.8 # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] prdp = value((self.deltaP[t] - Pin) / Pin) if self.deltaP[t].fixed: Pout.value = value(Pin - Pout) if self.ratioP[t].fixed: Pout.value = value(self.ratioP[t] * Pin) if value(Pout / Pin) > 0.99 or value(Pout / Pin) < 0.1: if value(self.ratioP[t]) < 0.99 and value( self.ratioP[t]) > 0.1: Pout.fix(value(Pin * self.ratioP[t])) elif prdp < 0.99 and prdp > 0.1: Pout.fix(value(prdp * Pin)) else: Pout.fix(value(Pin * 0.8)) else: Pout.fix() self.deltaP[t] = value(Pout - Pin) self.ratioP[t] = value(Pout / Pin) self.deltaP[:].unfix() self.ratioP[:].unfix() for t in self.flowsheet().config.time: self.properties_isentropic[t].pressure.value = value( self.outlet.pressure[t]) self.properties_isentropic[t].flow_mol.value = value( self.inlet.flow_mol[t]) self.properties_isentropic[t].enth_mol.value = value( self.inlet.enth_mol[t] * 0.95) self.outlet.flow_mol[t].value = value(self.inlet.flow_mol[t]) self.outlet.enth_mol[t].value = value(self.inlet.enth_mol[t] * 0.95) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert dof == 0 except: _log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super(TurbineStageData, self).initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # reload original spec from_json(self, sd=istate, wts=sp)
class TurbineInletStageData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = False CONFIG.get('compressor')._default = False CONFIG.get('compressor')._domain = In([False]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get('thermodynamic_assumption')._default = \ ThermodynamicAssumption.isentropic CONFIG.get('thermodynamic_assumption')._domain = \ In([ThermodynamicAssumption.isentropic]) def build(self): super(TurbineInletStageData, self).build() self.flow_coeff = Var(self.flowsheet().config.time, initialize=1.053/3600.0, doc="Turbine flow coefficient [kg*C^0.5/Pa/s]") self.delta_enth_isentropic = Var(self.flowsheet().config.time, initialize=-1000, doc="Specific enthalpy change of isentropic process [J/mol]") self.blade_reaction = Var(initialize=0.9, doc="Blade reaction parameter") self.blade_velocity = Var(initialize=110.0, doc="Design blade velocity [m/s]") self.eff_nozzle = Var(initialize=0.95, bounds=(0.0, 1.0), doc="Nozzel efficiency (typically 0.90 to 0.95)") self.efficiency_mech = Var(initialize=0.98, doc="Turbine mechanical efficiency") self.flow_scale = Param(mutable=True, default=1e3, doc= "Scaling factor for pressure flow relation should be approximatly" " the same order of magnitude as the expected flow.") self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() self.efficiency_mech.fix() self.ratioP[:] = 1 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize @self.Expression(self.flowsheet().config.time, doc="Entering steam velocity calculation [m/s]") def steam_entering_velocity(b, t): # 1.414 = 44.72/sqrt(1000) for SI if comparing to Liese (2014) # b.delta_enth_isentropic[t] = -(hin - hiesn), the mw converts # enthalpy to a mass basis return 1.414*sqrt(-(1-b.blade_reaction)*b.delta_enth_isentropic[t]/ b.control_volume.properties_in[t].mw*self.eff_nozzle) @self.Constraint(self.flowsheet().config.time, doc="Equation: Turbine inlet flow") def inlet_flow_constraint(b, t): # Some local vars to make the equation more readable g = b.control_volume.properties_in[t].heat_capacity_ratio mw = b.control_volume.properties_in[t].mw flow = b.control_volume.properties_in[t].flow_mol Tin = b.control_volume.properties_in[t].temperature cf = b.flow_coeff[t] Pin = b.control_volume.properties_in[t].pressure Pratio = b.ratioP[t] return ((1/b.flow_scale**2)*flow**2*mw**2*(Tin - 273.15) == (1/b.flow_scale**2)*cf**2*Pin**2* (g/(g - 1)*(Pratio**(2.0/g) - Pratio**((g + 1)/g)))) @self.Constraint(self.flowsheet().config.time, doc="Equation: Isentropic enthalpy change") def isentropic_enthalpy(b, t): return b.work_isentropic[t] == (b.delta_enth_isentropic[t]* b.control_volume.properties_in[t].flow_mol) @self.Constraint(self.flowsheet().config.time, doc="Equation: Efficiency") def efficiency_correlation(b, t): Vr = b.blade_velocity/b.steam_entering_velocity[t] eff = b.efficiency_isentropic[t] R = b.blade_reaction return eff == 2*Vr*((sqrt(1 - R) - Vr) + sqrt((sqrt(1 - R) - Vr)**2 + R)) @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t]*b.efficiency_mech def initialize(self, state_args={}, outlvl=0, solver='ipopt', optarg={'tol': 1e-6, 'max_iter':30}): """ Initialize the inlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ stee = True if outlvl >= 3 else False # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Deactivate special constraints self.inlet_flow_constraint.deactivate() self.isentropic_enthalpy.deactivate() self.efficiency_correlation.deactivate() self.deltaP.unfix() self.ratioP.unfix() # Fix turbine parameters + eff_isen self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # If there isn't a good guess for efficeny or outlet pressure # provide something reasonable. eff = self.efficiency_isentropic[t] eff.fix(eff.value if value(eff) > 0.3 and value(eff) < 1.0 else 0.8) # for outlet pressure try outlet pressure, pressure ratio, delta P, # then if none of those look reasonable use a pressure ratio of 0.8 # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] prdp = value((self.deltaP[t] - Pin)/Pin) if value(Pout/Pin) > 0.98 or value(Pout/Pin) < 0.3: if value(self.ratioP[t]) < 0.98 and value(self.ratioP[t]) > 0.3: Pout.fix(value(Pin*self.ratioP)) elif prdp < 0.98 and prdp > 0.3: Pout.fix(value(prdp*Pin)) else: Pout.fix(value(Pin*0.8)) else: Pout.fix() self.deltaP[:] = value(Pout - Pin) self.ratioP[:] = value(Pout/Pin) for t in self.flowsheet().config.time: self.properties_isentropic[t].pressure.value = \ value(self.outlet.pressure[t]) self.properties_isentropic[t].flow_mol.value = \ value(self.inlet.flow_mol[t]) self.properties_isentropic[t].enth_mol.value = \ value(self.inlet.enth_mol[t]*0.95) self.outlet.flow_mol[t].value = \ value(self.inlet.flow_mol[t]) self.outlet.enth_mol[t].value = \ value(self.inlet.enth_mol[t]*0.95) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert(dof == 0) except: _log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super(TurbineInletStageData, self).initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # Free eff_isen and activate sepcial constarints self.efficiency_isentropic.unfix() self.outlet.pressure.unfix() self.inlet_flow_constraint.activate() self.isentropic_enthalpy.activate() self.efficiency_correlation.activate() slvr = SolverFactory(solver) slvr.options = optarg res = slvr.solve(self, tee=stee) if outlvl > 0: if res.solver.termination_condition == TerminationCondition.optimal: _log.info("{} Initialization Complete.".format(self.name)) else: _log.warning( """{} Initialization Failed. The most likely cause of initialization failure for the Turbine inlet stages model is that the flow coefficient is not compatible with flow rate guess.""".format(self.name)) # reload original spec from_json(self, sd=istate, wts=sp)
class PropertyBlockData(PropertyBlockDataBase): """ Example property package for reactions This package contains the necessary property calculations to demonstrte the basic unit reactor models. System involeves six components (a,b,c,d,e and f) involved in three reactions (labled 1, 2 and 3). Reactions equations are: a + 2b <-> c + d a + 2c <-> 2e a + b <-> f Reactions are assumed to be aqueous and only a liquid phase is considered. Properties supported: - stoichiometric coefficients - rate of reaction - rate coefficients (forward and reverse) - equilibrium coefficients - heats of reaction - specific enthalpy of the fluid mixture """ def build(self): """ Callable method for Block construction """ super(PropertyBlockData, self).build() self._make_params() self._make_vars() self._make_constraints() self._make_balance_terms() def _make_params(self): ''' This section makes references to the necessary parameters contained within the Property Parameter Block provided. ''' # List of valid phases in property package add_object_ref(self, "phase_list", self.config.parameters.phase_list) # Component list - a list of component identifiers add_object_ref(self, "component_list", self.config.parameters.component_list) # Reaction indices - a list of identifiers for each reaction add_object_ref(self, "rate_reaction_idx", self.config.parameters.rate_reaction_idx) # Mixture heat capacity add_object_ref(self, "cp_mol", self.config.parameters.cp_mol) # Stoichiometric coefficients add_object_ref(self, "rate_reaction_stoichiometry", self.config.parameters.rate_reaction_stoichiometry) # Gas constant add_object_ref(self, "gas_const", self.config.parameters.gas_const) # Thermodynamic reference state add_object_ref(self, "temperature_ref", self.config.parameters.temperature_ref) def _make_vars(self): # Create state variables self.flow_mol_comp = Var(self.component_list, domain=Reals, initialize=0.0, bounds=(0, 1e3), doc='Component molar flowrate [mol/s]') self.flow_mol = Var(domain=Reals, initialize=0.0, bounds=(1e-2, 1e3), doc='Total molar flowrate [mol/s]') self.pressure = Var(domain=Reals, initialize=101325.0, doc='State pressure [Pa]') self.temperature = Var(domain=Reals, initialize=303.15, doc='State temperature [K]') self.mole_frac = Var(self.component_list, domain=Reals, initialize=0.0, bounds=(0.0, 1.0), doc='State component mole fractions [-]') self.enth_mol = Var(domain=Reals, initialize=0.0, doc='Mixture specific entahlpy [J/mol]') def _make_constraints(self): # Calcuate total flow self.sum_comp_flows = Constraint(expr=self.flow_mol == sum( self.flow_mol_comp[k] for k in self.component_list)) # Calculate mole fractions def mole_fraction_calculation(b, j): return b.flow_mol_comp[j] == b.mole_frac[j] * b.flow_mol self.mole_fraction_calculation = Constraint( self.component_list, doc="Mole fraction calculation", rule=mole_fraction_calculation) # Mixture enthalpy flow ''' The mixture enthalpy is assumed to be equal to that of pure water in the liquid state, with a constant heat capacity''' self.enth_mol_correlation = Constraint( expr=self.enth_mol == self.cp_mol() * (self.temperature - self.temperature_ref()) - self.mole_frac['d'] * self.dh_rxn_mol[1] - 0.5 * self.mole_frac['e'] * self.dh_rxn_mol[2] - self.mole_frac['f'] * self.dh_rxn_mol[3]) def _dens_mol_phase(self): # Molar density self.dens_mol_phase = Var(self.phase_list, doc="Molar density [mol/m^3]") def dens_mol_phase_correlation(b, p): return b.dens_mol_phase[p] == 55555.0 self.dens_mol_phase_correlation = Constraint( self.phase_list, doc="Molar density correlation", rule=dens_mol_phase_correlation) def _flow_vol(self): # Volumetric flowrate self.flow_vol = Var(doc="Total volumetric flowrate of material " "[m^3/s]") def flow_vol_correlation(b): return b.flow_vol * b.dens_mol_phase['Liq'] == b.flow_mol self.flow_vol_correlation = Constraint( doc="Volumetric flowrate correlation", rule=flow_vol_correlation) def _dh_rxn_mol(self): # Heat of reaction self.dh_rxn_mol = Var(self.rate_reaction_idx, domain=Reals, initialize=0.0, doc='Heats of Reaction [J/mol]') def dh_rxn_mol_constraint(b, i): if i == 1: return b.dh_rxn_mol[i] == 60000 elif i == 2: return b.dh_rxn_mol[i] == 50000 else: return b.dh_rxn_mol[i] == 80000 self.dh_rxn_mol_constraint = Constraint( self.rate_reaction_idx, doc="Heat of reaction constraint", rule=dh_rxn_mol_constraint) def _reaction_rate(self): # Reaction rate self.reaction_rate = Var(self.rate_reaction_idx, domain=Reals, initialize=0.0, doc='Normalised Rate of Reaction [mol/m^3.s]') def rate_expressions(b, j): if j == 1: return b.reaction_rate[j] == ( b.k_rxn_for[j] * (b.mole_frac['a']) * (b.mole_frac['b']**2) - b.k_rxn_back[j] * b.mole_frac['c'] * b.mole_frac['d']) elif j == 2: return b.reaction_rate[j] == ( b.k_rxn_for[j] * (b.mole_frac['a']) * (b.mole_frac['c']**2) - b.k_rxn_back[j] * b.mole_frac['e']) else: return b.reaction_rate[j] == ( b.k_rxn_for[j] * (b.mole_frac['a']) * (b.mole_frac['b']) - b.k_rxn_back[j] * b.mole_frac['f']) try: # Try to build constraint self.rate_expressions = Constraint( self.rate_reaction_idx, doc="Rate of reaction expressions", rule=rate_expressions) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.reaction_rate) self.del_component(self.rate_expressions) raise def _k_rxn_for(self): # Forward rate constants self.k_rxn_for = Var(self.rate_reaction_idx, doc='Rate coefficient for forward reaction') # Arhenius expression for rate coefficients def arrhenius_expression(b, i): if i == 1: return b.k_rxn_for[i] == (17.7 * exp(-12000 / (b.gas_const() * b.temperature))) elif i == 2: return b.k_rxn_for[i] == (1.49 * exp(-7000 / (b.gas_const() * b.temperature))) else: return b.k_rxn_for[i] == (26.5 * exp(-13000 / (b.gas_const() * b.temperature))) try: # Try to build constraint self.arrhenius_expression = Constraint( self.rate_reaction_idx, doc="Arrhenius expression for forward rate constant", rule=arrhenius_expression) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.k_rxn_for) self.del_component(self.arrhenius_expression) raise def _k_rxn_back(self): # Reverse rate constants self.k_rxn_back = Var(self.rate_reaction_idx, doc='Rate coefficient for reverse reaction') # Reverse reaction rates coefficients in terms of forward # coefficients and equilibrium coefficient def rule_rate_const_rev(b, i): return b.k_rxn_for[i] == (b.k_rxn_back[i] * b.k_eq[i]) try: # Try to build constraint self.rate_const_relationship = Constraint( self.rate_reaction_idx, doc="Relationship between forward and reverse rate constants", rule=rule_rate_const_rev) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.k_rxn_back) self.del_component(self.rate_const_relationship) raise def _k_eq(self): # Equilibrium coefficients self.k_eq = Var(self.rate_reaction_idx, initialize=1.0, doc='Equilibrium coefficient') # Equilibrium coefficients as a function of temperature using the # van't Hoff equation''' def vant_hoff(b, i): if i == 1: return log(b.k_eq[i]) - log(20) == ( -(b.dh_rxn_mol[i] / b.gas_const()) * (b.temperature**-1 - 1 / 298.15)) elif i == 2: return log(b.k_eq[i]) - log(5) == ( -(b.dh_rxn_mol[i] / b.gas_const()) * (b.temperature**-1 - 1 / 298.15)) else: return log(b.k_eq[i]) - log(10) == ( -(b.dh_rxn_mol[i] / b.gas_const()) * (b.temperature**-1 - 1 / 298.15)) try: # Try to build constraint self.vant_hoff = Constraint( self.rate_reaction_idx, doc="Van't Hoff equation for equilibrium", rule=vant_hoff) except AttributeError: # If constraint fails, clean up so that DAE can try again later self.del_component(self.k_eq) self.del_component(self.vant_hoff) raise def _diffus(self): # Diffusion tests self.diffus = Var(self.phase_list, self.component_list, domain=Reals, doc="Diffusion coefficient [m^2/s]") self.diffus.fix(1e-2) def _therm_cond(self): self.therm_cond = Var(self.phase_list, domain=Reals, doc="Thermal conductivity [W/m.K]") self.therm_cond.fix(10) def _material_concentration_term(self): self.material_concentration_term = Var( self.phase_list, self.component_list, domain=Reals, doc="Concentration for diffusion") def material_concentration_calc(b, k, j): return b.material_concentration_term[k, j] == (25 * b.mole_frac[j]) self.material_concentration_calc = Constraint( self.phase_list, self.component_list, doc="Molar concentration calculation", rule=material_concentration_calc) def _make_balance_terms(self): def material_balance_term(b, i, j): return b.flow_mol_comp[j] self.material_balance_term = Expression(self.phase_list, self.component_list, rule=material_balance_term) def energy_balance_term(b, i): return b.enth_mol * b.flow_mol self.energy_balance_term = Expression(self.phase_list, rule=energy_balance_term) def material_density_term(b, p, j): return b.dens_mol_phase[p] * b.mole_frac[j] self.material_density_term = Expression(self.phase_list, self.component_list, rule=material_density_term) def energy_density_term(b, p): return b.dens_mol_phase[p] * b.enth_mol self.energy_density_term = Expression(self.phase_list, rule=energy_density_term) def declare_port_members(b): members = { "flow_mol_comp": b.flow_mol_comp, "enth_mol": b.enth_mol, "pressure": b.pressure } return members def model_check(blk): '''Method containing presovle checks for package''' pass
class PHEData(UnitModelBlockData): """Plate Heat Exchanger(PHE) Unit Model.""" CONFIG = UnitModelBlockData.CONFIG() # Configuration template for fluid specific arguments _SideCONFIG = ConfigBlock() CONFIG.declare( "passes", ConfigValue( default=4, domain=int, description="Number of passes", doc="""Number of passes of the fluids through the heat exchanger""" )) CONFIG.declare( "channel_list", ConfigValue( default=[12, 12, 12, 12], domain=list, description="Number of channels for each pass", doc="""Number of channels to be used in each pass where a channel is the space between two plates with a flowing fluid""")) CONFIG.declare( "divider_plate_number", ConfigValue( default=0, domain=int, description="Number of divider plates in heat exchanger", doc= """Divider plates are used to create separate partitions in the unit. Each pass can be separated by a divider plate""")) CONFIG.declare( "port_diameter", ConfigValue( default=0.2045, domain=float, description="Diameter of the ports on the plate [m]", doc="""Diameter of the ports on the plate for fluid entry/exit into a channel""")) CONFIG.declare( "plate_thermal_cond", ConfigValue( default=16.2, domain=float, description="Thermal conductivity [W/m.K]", doc="""Thermal conductivity of the plate material [W/m.K]""")) CONFIG.declare( "total_area", ConfigValue( default=114.3, domain=float, description="Total heat transfer area [m2]", doc="""Total heat transfer area as specifed by the manufacturer""") ) CONFIG.declare( "plate_thickness", ConfigValue(default=0.0006, domain=float, description="Plate thickness [m]", doc="""Plate thickness""")) CONFIG.declare( "plate_vertical_dist", ConfigValue( default=1.897, domain=float, description="Vertical distance between centers of ports [m].", doc= """Vertical distance between centers of ports.(Top and bottom ports) (approximately equals to the plate length)""")) CONFIG.declare( "plate_horizontal_dist", ConfigValue( default=0.409, domain=float, description="Horizontal distance between centers of ports [m].", doc= """Horizontal distance between centers of ports(Left and right ports)""" )) CONFIG.declare( "plate_pact_length", ConfigValue(default=0.381, domain=float, description="Compressed plate pact length [m].", doc="""Compressed plate pact length. Length between the Head and the Follower""")) CONFIG.declare( "surface_enlargement_factor", ConfigValue( default=None, domain=float, description="Surface enlargement factor", doc="""Surface enlargement factor is the ratio of single plate area (obtained from the total area) to the projected plate area""")) CONFIG.declare( "plate_gap", ConfigValue( default=None, domain=float, description="Mean channel spacing or gap bewteen two plates [m]", doc="""The plate gap is the distance between two adjacent plates that forms a flow channel """)) _SideCONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use ", doc="""Property parameter object used to define property calculations **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) _SideCONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package", doc="""A ConfigBlock with arguments to be passed to property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) # Create individual config blocks for hot and cold sides CONFIG.declare("hot_side", _SideCONFIG(doc="Hot fluid config arguments")) CONFIG.declare("cold_side", _SideCONFIG(doc="Cold fluid config arguments")) def build(self): # Call UnitModel.build to setup model super(PHEData, self).build() # Consistency check for number of passes and channels in each pass for i in self.config.channel_list: if not isinstance(i, int): raise ConfigurationError("number of channels ({}) must be" " an integer".format(i)) if (self.config.passes != len(self.config.channel_list)): raise ConfigurationError( "The number of elements in the channel list: {} " " does not match the number of passes ({}) given. " "Please provide as integers, the number of channels of each pass" .format(self.config.channel_list, self.config.passes)) # ====================================================================== # Build hot-side Control Volume (Lean Solvent) self.hot_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.hot_side.property_package, "property_package_args": self.config.hot_side.property_package_args }) self.hot_side.add_state_blocks(has_phase_equilibrium=False) self.hot_side.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_mass_transfer=False, has_phase_equilibrium=False, has_rate_reactions=False) self.hot_side.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # Energy balance is based on the effectiveness Number of Transfer units # (E-NTU method) and inluded as performance equations. Hence the control # volume energy balances are not added. # ====================================================================== # Build cold-side Control Volume(Rich solvent) self.cold_side = ControlVolume0DBlock( default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.cold_side.property_package, "property_package_args": self.config.cold_side.property_package_args }) self.cold_side.add_state_blocks(has_phase_equilibrium=False) self.cold_side.add_material_balances( balance_type=MaterialBalanceType.componentTotal, has_mass_transfer=False, has_phase_equilibrium=False, has_rate_reactions=False) self.cold_side.add_momentum_balances( balance_type=MomentumBalanceType.pressureTotal, has_pressure_change=True) # ====================================================================== # Add Ports to control volumes # hot-side self.add_inlet_port(name="hot_inlet", block=self.hot_side, doc='inlet Port') self.add_outlet_port(name="hot_outlet", block=self.hot_side, doc='outlet Port') # cold-side self.add_inlet_port(name="cold_inlet", block=self.cold_side, doc='inlet Port') self.add_outlet_port(name="cold_outlet", block=self.cold_side, doc='outlet Port') # ====================================================================== # Add performace equation method self._make_params() self._make_performance_method() def _make_params(self): self.P = Param(initialize=self.config.passes, units=None, doc="Total number of passes for hot or cold fluid") self.PH = RangeSet(self.P, doc="Set of hot fluid passes") self.PC = RangeSet(self.P, doc="Set of cold fluid passes(equal to PH)") self.plate_thermal_cond = Param( mutable=True, initialize=self.config.plate_thermal_cond, units=pyunits.W / pyunits.m / pyunits.K, doc="Plate thermal conductivity") self.plate_thick = Param(mutable=True, initialize=self.config.plate_thickness, units=pyunits.m, doc="Plate thickness") self.port_dia = Param(mutable=True, initialize=self.config.port_diameter, units=pyunits.m, doc=" Port diameter of plate ") self.Np = Param(self.PH, units=None, doc="Number of channels in each pass", mutable=True) # Number of channels in each pass for i in self.PH: self.Np[i].value = self.config.channel_list[i - 1] # --------------------------------------------------------------------- # Assign plate specifications # effective plate length & width _effective_plate_length = self.config.plate_vertical_dist - \ self.config.port_diameter _effective_plate_width = self.config.plate_horizontal_dist + \ self.config.port_diameter self.plate_length = Expression(expr=_effective_plate_length) self.plate_width = Expression(expr=_effective_plate_width) # Area of single plate _total_active_plate_number = 2 * sum(self.config.channel_list) - 1 -\ self.config.divider_plate_number self.plate_area = Expression(expr=self.config.total_area / _total_active_plate_number, doc="Heat transfer area of single plate") # Plate gap if self.config.plate_gap is None: _total_plate_number = 2 * sum(self.config.channel_list) + 1 +\ self.config.divider_plate_number _plate_pitch = self.config.plate_pact_length / _total_plate_number _plate_gap = _plate_pitch - self.config.plate_thickness else: _plate_gap = self.config.plate_gap self.plate_gap = Expression(expr=_plate_gap) # Surface enlargement factor if self.config.surface_enlargement_factor is None: _projected_plate_area = _effective_plate_length * _effective_plate_width _surface_enlargement_factor = self.plate_area / _projected_plate_area else: _surface_enlargement_factor = self.config.surface_enlargement_factor self.surface_enlargement_factor = Expression( expr=_surface_enlargement_factor) # Channel equivalent diameter self.channel_dia = Expression(expr=2 * self.plate_gap / _surface_enlargement_factor, doc=" Channel equivalent diameter") # heat transfer parameters self.param_a = Var(initialize=0.3, bounds=(0.2, 0.4), units=None, doc='Nusselt parameter') self.param_b = Var(initialize=0.663, bounds=(0.3, 0.7), units=None, doc='Nusselt parameter') self.param_c = Var(initialize=1 / 3.0, bounds=(1e-5, 2), units=None, doc='Nusselt parameter') self.param_a.fix(0.4) self.param_b.fix(0.663) self.param_c.fix(0.333) def _make_performance_method(self): solvent_list = self.config.hot_side.property_package.component_list_solvent def rule_trh(blk, t): return (blk.hot_side.properties_out[t].temperature / blk.hot_side.properties_in[t].temperature) self.trh = Expression(self.flowsheet().config.time, rule=rule_trh, doc='Ratio of hot outlet temperature to hot' 'inlet temperature') def rule_trc(blk, t): return (blk.cold_side.properties_out[t].temperature / blk.cold_side.properties_in[t].temperature) self.trc = Expression(self.flowsheet().config.time, rule=rule_trc, doc='Ratio of cold outlet temperature to cold' ' inlet temperature') def rule_cp_comp_hot(blk, t, j): return 1e3 * ( blk.hot_side.properties_in[t]._params.cp_param[j, 1] + blk.hot_side.properties_in[t]._params.cp_param[j, 2] / 2 * blk.hot_side.properties_in[t].temperature * (blk.trh[t] + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 3] / 3 * (blk.hot_side.properties_in[t].temperature**2) * (blk.trh[t]**2 + blk.trh[t] + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 4] / 4 * (blk.hot_side.properties_in[t].temperature**3) * (blk.trh[t] + 1) * (blk.trh[t]**2 + 1) + blk.hot_side.properties_in[t]._params.cp_param[j, 5] / 5 * (blk.hot_side.properties_in[t].temperature**4) * (blk.trh[t]**4 + blk.trh[t]**3 + blk.trh[t]**2 + blk.trh[t] + 1)) self.cp_comp_hot = Expression( self.flowsheet().config.time, solvent_list, rule=rule_cp_comp_hot, doc='Component mean specific heat capacity' ' btw inlet and outlet' ' of hot-side temperature') def rule_cp_hot(blk, t): return sum(blk.cp_comp_hot[t, j] * blk.hot_side.properties_in[t].mass_frac_co2_free[j] for j in solvent_list) self.cp_hot = Expression(self.flowsheet().config.time, rule=rule_cp_hot, doc='Hot-side mean specific heat capacity on' 'free CO2 basis') def rule_cp_comp_cold(blk, t, j): return 1e3 * ( blk.cold_side.properties_in[t]._params.cp_param[j, 1] + blk.cold_side.properties_in[t]._params.cp_param[j, 2] / 2 * blk.cold_side.properties_in[t].temperature * (blk.trc[t] + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 3] / 3 * (blk.cold_side.properties_in[t].temperature**2) * (blk.trc[t]**2 + blk.trc[t] + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 4] / 4 * (blk.cold_side.properties_in[t].temperature**3) * (blk.trc[t] + 1) * (blk.trc[t]**2 + 1) + blk.cold_side.properties_in[t]._params.cp_param[j, 5] / 5 * (blk.cold_side.properties_in[t].temperature**4) * (blk.trc[t]**4 + blk.trc[t]**3 + blk.trc[t]**2 + blk.trc[t] + 1)) self.cp_comp_cold = Expression( self.flowsheet().config.time, solvent_list, rule=rule_cp_comp_cold, doc='Component mean specific heat capacity' 'btw inlet and outlet' ' of cold-side temperature') def rule_cp_cold(blk, t): return sum(blk.cp_comp_cold[t, j] * blk.cold_side.properties_in[t].mass_frac_co2_free[j] for j in solvent_list) self.cp_cold = Expression(self.flowsheet().config.time, rule=rule_cp_cold, doc='Cold-side mean specific heat capacity' 'on free CO2 basis') # Model Variables self.Th_in = Var(self.flowsheet().config.time, self.PH, initialize=393, units=pyunits.K, doc="Hot Temperature IN of pass") self.Th_out = Var(self.flowsheet().config.time, self.PH, initialize=325, units=pyunits.K, doc="Hot Temperature OUT of pass") self.Tc_in = Var(self.flowsheet().config.time, self.PH, initialize=320, units=pyunits.K, doc="Cold Temperature IN of pass") self.Tc_out = Var(self.flowsheet().config.time, self.PH, initialize=390, units=pyunits.K, doc="Cold Temperature OUT of pass") # ====================================================================== # PERFORMANCE EQUATIONS # mass flow rate in kg/s def rule_mh_in(blk, t): return blk.hot_side.properties_in[t].flow_mol *\ blk.hot_side.properties_in[t].mw self.mh_in = Expression(self.flowsheet().config.time, rule=rule_mh_in, doc='Hotside mass flow rate [kg/s]') def rule_mc_in(blk, t): return blk.cold_side.properties_in[t].flow_mol *\ blk.cold_side.properties_in[t].mw self.mc_in = Expression(self.flowsheet().config.time, rule=rule_mc_in, doc='Coldside mass flow rate [kg/s]') # ---------------------------------------------------------------------- # port mass velocity[kg/m2.s] def rule_Gph(blk, t): return (4 * blk.mh_in[t] * 7) / (22 * blk.port_dia**2) self.Gph = Expression(self.flowsheet().config.time, rule=rule_Gph, doc='Hotside port mass velocity[kg/m2.s]') def rule_Gpc(blk, t): return (4 * blk.mc_in[t] * 7) / (22 * blk.port_dia**2) self.Gpc = Expression(self.flowsheet().config.time, rule=rule_Gpc, doc='Coldside port mass velocity[kg/m2.s]') # ---------------------------------------------------------------------- # Reynold & Prandtl numbers def rule_Re_h(blk, t, p): return blk.mh_in[t] * blk.channel_dia /\ (blk.Np[p] * blk.plate_width * blk.plate_gap * blk.hot_side.properties_in[t].visc_d) self.Re_h = Expression(self.flowsheet().config.time, self.PH, rule=rule_Re_h, doc='Hotside Reynolds number') def rule_Re_c(blk, t, p): return blk.mc_in[t] * blk.channel_dia /\ (blk.Np[p] * blk.plate_width * blk.plate_gap * blk.cold_side.properties_in[t].visc_d) self.Re_c = Expression(self.flowsheet().config.time, self.PH, rule=rule_Re_c, doc='Coldside Reynolds number') def rule_Pr_h(blk, t): return blk.cp_hot[t] * blk.hot_side.properties_in[t].visc_d /\ blk.hot_side.properties_in[t].thermal_cond self.Pr_h = Expression(self.flowsheet().config.time, rule=rule_Pr_h, doc='Hotside Prandtl number') def rule_Pr_c(blk, t): return blk.cp_cold[t] * blk.cold_side.properties_in[t].visc_d /\ blk.cold_side.properties_in[t].thermal_cond self.Pr_c = Expression(self.flowsheet().config.time, rule=rule_Pr_c, doc='Coldside Prandtl number') # ---------------------------------------------------------------------- # Film heat transfer coefficients def rule_hotside_transfer_coef(blk, t, p): return (blk.hot_side.properties_in[t].thermal_cond / blk.channel_dia * blk.param_a * blk.Re_h[t, p]**blk.param_b * blk.Pr_h[t]**blk.param_c) self.h_hot = Expression(self.flowsheet().config.time, self.PH, rule=rule_hotside_transfer_coef, doc='Hotside heat transfer coefficient') def rule_coldside_transfer_coef(blk, t, p): return (blk.cold_side.properties_in[t].thermal_cond / blk.channel_dia * blk.param_a * blk.Re_c[t, p]**blk.param_b * blk.Pr_c[t]**blk.param_c) self.h_cold = Expression(self.flowsheet().config.time, self.PH, rule=rule_coldside_transfer_coef, doc='Coldside heat transfer coefficient') # ---------------------------------------------------------------------- # Friction factor calculation def rule_fric_h(blk, t): return 18.29 * blk.Re_h[t, 1]**(-0.652) self.fric_h = Expression(self.flowsheet().config.time, rule=rule_fric_h, doc='Hotside friction factor') def rule_fric_c(blk, t): return 1.441 * self.Re_c[t, 1]**(-0.206) self.fric_c = Expression(self.flowsheet().config.time, rule=rule_fric_c, doc='Coldside friction factor') # ---------------------------------------------------------------------- # pressure drop calculation def rule_hotside_dP(blk, t): return (2 * blk.fric_h[t] * (blk.plate_length + blk.port_dia) * blk.P * blk.Gph[t]**2) /\ (blk.hot_side.properties_in[t].dens_mass * blk.channel_dia) + 1.4 * blk.P * blk.Gph[t]**2 * 0.5 /\ blk.hot_side.properties_in[t].dens_mass + \ blk.hot_side.properties_in[t].dens_mass * \ 9.81 * (blk.plate_length + blk.port_dia) self.dP_h = Expression(self.flowsheet().config.time, rule=rule_hotside_dP, doc='Hotside pressure drop [Pa]') def rule_coldside_dP(blk, t): return (2 * blk.fric_c[t] * (blk.plate_length + blk.port_dia) * blk.P * blk.Gpc[t]**2) /\ (blk.cold_side.properties_in[t].dens_mass * blk.channel_dia) +\ 1.4 * (blk.P * blk.Gpc[t]**2 * 0.5 / blk.cold_side.properties_in[t].dens_mass) + \ blk.cold_side.properties_in[t].dens_mass * \ 9.81 * (blk.plate_length + blk.port_dia) self.dP_c = Expression(self.flowsheet().config.time, rule=rule_coldside_dP, doc='Coldside pressure drop [Pa]') def rule_eq_deltaP_hot(blk, t): return blk.hot_side.deltaP[t] == -blk.dP_h[t] self.eq_deltaP_hot = Constraint(self.flowsheet().config.time, rule=rule_eq_deltaP_hot) def rule_eq_deltaP_cold(blk, t): return blk.cold_side.deltaP[t] == -blk.dP_c[t] self.eq_deltaP_cold = Constraint(self.flowsheet().config.time, rule=rule_eq_deltaP_cold) # ---------------------------------------------------------------------- # Overall heat transfer coefficients def rule_U(blk, t, p): return 1.0 /\ (1.0 / blk.h_hot[t, p] + blk.plate_gap / blk.plate_thermal_cond + 1.0 / blk.h_cold[t, p]) self.U = Expression(self.flowsheet().config.time, self.PH, rule=rule_U, doc='Overall heat transfer coefficient') # ---------------------------------------------------------------------- # capacitance of hot and cold fluid def rule_Caph(blk, t, p): return blk.mh_in[t] * blk.cp_hot[t] / blk.Np[p] self.Caph = Expression(self.flowsheet().config.time, self.PH, rule=rule_Caph, doc='Hotfluid capacitance rate') def rule_Capc(blk, t, p): return blk.mc_in[t] * blk.cp_cold[t] / blk.Np[p] self.Capc = Expression(self.flowsheet().config.time, self.PH, rule=rule_Capc, doc='Coldfluid capacitance rate') # ---------------------------------------------------------------------- # min n max capacitance and capacitance ratio def rule_Cmin(blk, t, p): return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] - ( (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5) self.Cmin = Expression(self.flowsheet().config.time, self.PH, rule=rule_Cmin, doc='Minimum capacitance rate') def rule_Cmax(blk, t, p): return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] + ( (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5) self.Cmax = Expression(self.flowsheet().config.time, self.PH, rule=rule_Cmax, doc='Maximum capacitance rate') def rule_CR(blk, t, p): return blk.Cmin[t, p] / blk.Cmax[t, p] self.CR = Expression(self.flowsheet().config.time, self.PH, rule=rule_CR, doc='Capacitance ratio') # ---------------------------------------------------------------------- # Number of Transfer units for sub heat exchanger def rule_NTU(blk, t, p): return blk.U[t, p] * blk.plate_area / blk.Cmin[t, p] self.NTU = Expression(self.flowsheet().config.time, self.PH, rule=rule_NTU, doc='Number of Transfer Units') # ---------------------------------------------------------------------- # effectiveness of sub-heat exchangers def rule_Ecf(blk, t, p): if blk.P.value % 2 == 0: return (1 - exp(-blk.NTU[t, p] * (1 - blk.CR[t, p]))) / \ (1 - blk.CR[t, p] * exp(-blk.NTU[t, p] * (1 - blk.CR[t, p]))) elif blk.P.value % 2 == 1: return (1 - exp(-blk.NTU[t, p] * (1 + blk.CR[t, p]))) / (1 + blk.CR[t, p]) self.Ecf = Expression(self.flowsheet().config.time, self.PH, rule=rule_Ecf, doc='Effectiveness for sub-HX') # ---------------------------------------------------------------------- # Energy balance equations for hot fluid in sub-heat exhanger def rule_Ebh_eq(blk, t, p): return blk.Th_out[t, p] == blk.Th_in[t, p] -\ blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Caph[t, p] * \ (blk.Th_in[t, p] - blk.Tc_in[t, p]) self.Ebh_eq = Constraint( self.flowsheet().config.time, self.PH, rule=rule_Ebh_eq, doc='Hot fluid sub-heat exchanger energy balance') # Hot fluid exit temperature def rule_Tout_hot(blk, t): return blk.Th_out[t, blk.P.value] ==\ blk.hot_side.properties_out[t].temperature self.Tout_hot_eq = Constraint(self.flowsheet().config.time, rule=rule_Tout_hot, doc='Hot fluid exit temperature') # Energy balance equations for cold fluid in sub-heat exhanger def rule_Ebc_eq(blk, t, p): return blk.Tc_out[t, p] == blk.Tc_in[t, p] + \ blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Capc[t, p] * \ (blk.Th_in[t, p] - blk.Tc_in[t, p]) self.Ebc_eq = Constraint( self.flowsheet().config.time, self.PH, rule=rule_Ebc_eq, doc='Cold fluid sub-heat exchanger energy balance') # Cold fluid exit temperature def rule_Tout_cold(blk, t): return blk.Tc_out[t, 1] ==\ blk.cold_side.properties_out[t].temperature self.Tout_cold_eq = Constraint(self.flowsheet().config.time, rule=rule_Tout_cold, doc='Cold fluid exit temperature') # ---------------------------------------------------------------------- # Energy balance boundary conditions def rule_hot_BCIN(blk, t): return blk.Th_in[t, 1] == \ blk.hot_side.properties_in[t].temperature self.hot_BCIN = Constraint(self.flowsheet().config.time, rule=rule_hot_BCIN, doc='Hot fluid inlet boundary conditions') def rule_cold_BCIN(blk, t): return blk.Tc_in[t, blk.P.value] ==\ blk.cold_side.properties_in[t].temperature self.cold_BCIN = Constraint(self.flowsheet().config.time, rule=rule_cold_BCIN, doc='Cold fluid inlet boundary conditions') Pset = [i for i in range(1, self.P.value)] def rule_hot_BC(blk, t, p): return blk.Th_out[t, p] == blk.Th_in[t, p + 1] self.hot_BC = Constraint( self.flowsheet().config.time, Pset, rule=rule_hot_BC, doc='Hot fluid boundary conditions: change of pass') def rule_cold_BC(blk, t, p): return blk.Tc_out[t, p + 1] == blk.Tc_in[t, p] self.cold_BC = Constraint( self.flowsheet().config.time, Pset, rule=rule_cold_BC, doc='Cold fluid boundary conditions: change of pass') # ---------------------------------------------------------------------- # Energy transferred def rule_QH(blk, t): return blk.mh_in[t] * blk.cp_hot[t] *\ (blk.hot_side.properties_in[t].temperature - blk.hot_side.properties_out[t].temperature) self.QH = Expression(self.flowsheet().config.time, rule=rule_QH, doc='Heat lost by hot fluid') def rule_QC(blk, t): return blk.mc_in[t] * blk.cp_cold[t] *\ (blk.cold_side.properties_out[t].temperature - blk.cold_side.properties_in[t].temperature) self.QC = Expression(self.flowsheet().config.time, rule=rule_QH, doc='Heat gain by cold fluid') def initialize(blk, hotside_state_args=None, coldside_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): ''' Initialisation routine for PHE unit (default solver ipopt) Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None) Returns: None ''' # Set solver options 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) hotside_state_args = { 'flow_mol': value(blk.hot_inlet.flow_mol[0]), 'temperature': value(blk.hot_inlet.temperature[0]), 'pressure': value(blk.hot_inlet.pressure[0]), 'mole_frac_comp': { 'H2O': value(blk.hot_inlet.mole_frac_comp[0, 'H2O']), 'CO2': value(blk.hot_inlet.mole_frac_comp[0, 'CO2']), 'MEA': value(blk.hot_inlet.mole_frac_comp[0, 'MEA']) } } coldside_state_args = { 'flow_mol': value(blk.cold_inlet.flow_mol[0]), 'temperature': value(blk.cold_inlet.temperature[0]), 'pressure': value(blk.cold_inlet.pressure[0]), 'mole_frac_comp': { 'H2O': value(blk.cold_inlet.mole_frac_comp[0, 'H2O']), 'CO2': value(blk.cold_inlet.mole_frac_comp[0, 'CO2']), 'MEA': value(blk.cold_inlet.mole_frac_comp[0, 'MEA']) } } # --------------------------------------------------------------------- # Initialize the INLET properties init_log.info('STEP 1: PROPERTY INITIALIZATION') init_log.info_high("INLET Properties initialization") blk.hot_side.properties_in.initialize(state_args=hotside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) blk.cold_side.properties_in.initialize(state_args=coldside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=True) # Initialize the OUTLET properties init_log.info_high("OUTLET Properties initialization") blk.hot_side.properties_out.initialize(state_args=hotside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False) blk.cold_side.properties_out.initialize(state_args=coldside_state_args, outlvl=outlvl, optarg=optarg, solver=solver, hold_state=False) # ---------------------------------------------------------------------- init_log.info('STEP 2: PHE INITIALIZATION') with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("STEP 2 Complete: {}.".format( idaeslog.condition(res))) init_log.info('INITIALIZATION COMPLETED')
class SteamValveData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() _define_config(CONFIG) def build(self): super().build() self.valve_opening = Var(self.flowsheet().config.time, initialize=1, doc="Fraction open for valve from 0 to 1") self.Cv = Var(initialize=0.1, doc="Valve flow coefficent, for vapor " "[mol/s/Pa] for liquid [mol/s/Pa^0.5]") self.flow_scale = Param( mutable=True, default=1e3, doc= "Scaling factor for pressure flow relation should be approximatly" " the same order of magnitude as the expected flow.") self.Cv.fix() self.valve_opening.fix() # set up the valve function rule. I'm not sure these matter too much # for us, but the options are easy enough to provide. if self.config.valve_function == ValveFunctionType.linear: rule = _linear_rule elif self.config.valve_function == ValveFunctionType.quick_opening: rule = _quick_open_rule elif self.config.valve_function == ValveFunctionType.equal_percentage: self.alpha = Var(initialize=1, doc="Valve function parameter") self.alpha.fix() rule = equal_percentage_rule else: rule = self.config.valve_function_rule self.valve_function = Expression(self.flowsheet().config.time, rule=rule, doc="Valve function expression") if self.config.phase == "Liq": rule = _liquid_pressure_flow_rule else: rule = _vapor_pressure_flow_rule self.pressure_flow_equation = Constraint(self.flowsheet().config.time, rule=rule) def initialize(self, state_args={}, outlvl=0, solver='ipopt', optarg={ 'tol': 1e-6, 'max_iter': 30 }): """ Initialize the turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ stee = True if outlvl >= 3 else False # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) self.deltaP[:].unfix() self.ratioP[:].unfix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] if self.deltaP[t].value is not None: prdp = value((self.deltaP[t] - Pin) / Pin) else: prdp = -100 # crazy number to say don't use deltaP as guess if value(Pout / Pin) > 1 or value(Pout / Pin) < 0.0: if value(self.ratioP[t]) <= 1 and value(self.ratioP[t]) >= 0: Pout.value = value(Pin * self.ratioP[t]) elif prdp <= 1 and prdp >= 0: Pout.value = value(prdp * Pin) else: Pout.value = value(Pin * 0.95) self.deltaP[t] = value(Pout - Pin) self.ratioP[t] = value(Pout / Pin) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert (dof == 0) except: _log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super().initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # reload original spec from_json(self, sd=istate, wts=sp)
class HelmholtzStateBlockData(StateBlockData): """ This is a base clase for Helmholtz equations of state using IDAES standard Helmholtz EOS external functions written in C++. """ def initialize(self, *args, **kwargs): # With this particualr property pacakage there is not need for # initialization pass def _external_functions(self): """Create ExternalFunction components. This includes some external functions that are not usually used for testing purposes.""" plib = self.config.parameters.plib self.func_p = EF(library=plib, function="p") self.func_u = EF(library=plib, function="u") self.func_s = EF(library=plib, function="s") self.func_h = EF(library=plib, function="h") self.func_hvpt = EF(library=plib, function="hvpt") self.func_hlpt = EF(library=plib, function="hlpt") self.func_tau = EF(library=plib, function="tau") self.func_vf = EF(library=plib, function="vf") self.func_g = EF(library=plib, function="g") self.func_f = EF(library=plib, function="f") self.func_cv = EF(library=plib, function="cv") self.func_cp = EF(library=plib, function="cp") self.func_w = EF(library=plib, function="w") self.func_delta_liq = EF(library=plib, function="delta_liq") self.func_delta_vap = EF(library=plib, function="delta_vap") self.func_delta_sat_l = EF(library=plib, function="delta_sat_l") self.func_delta_sat_v = EF(library=plib, function="delta_sat_v") self.func_p_sat = EF(library=plib, function="p_sat") self.func_tau_sat = EF(library=plib, function="tau_sat") self.func_phi0 = EF(library=plib, function="phi0") self.func_phi0_delta = EF(library=plib, function="phi0_delta") self.func_phi0_delta2 = EF(library=plib, function="phi0_delta2") self.func_phi0_tau = EF(library=plib, function="phi0_tau") self.func_phi0_tau2 = EF(library=plib, function="phi0_tau2") self.func_phir = EF(library=plib, function="phir") self.func_phir_delta = EF(library=plib, function="phir_delta") self.func_phir_delta2 = EF(library=plib, function="phir_delta2") self.func_phir_tau = EF(library=plib, function="phir_tau") self.func_phir_tau2 = EF(library=plib, function="phir_tau2") self.func_phir_delta_tau = EF(library=plib, function="phir_delta_tau") def _state_vars(self): """ Create the state variables """ self.flow_mol = Var( initialize=1, domain=NonNegativeReals, doc="Total flow [mol/s]" ) self.scaling_factor[self.flow_mol] = 1e-3 if self.state_vars == StateVars.PH: self.pressure = Var( domain=PositiveReals, initialize=1e5, doc="Pressure [Pa]", bounds=(1, 1e9), ) self.enth_mol = Var( initialize=1000, doc="Total molar enthalpy (J/mol)", bounds=(1, 1e5) ) self.scaling_factor[self.enth_mol] = 1e-3 P = self.pressure / 1000.0 # Pressure expr [kPA] (for external func) h_mass = self.enth_mol / self.mw / 1000 # enthalpy expr [kJ/kg] phase_set = self.config.parameters.config.phase_presentation self.temperature = Expression( expr=self.temperature_crit / self.func_tau(h_mass, P), doc="Temperature (K)", ) if phase_set == PhaseType.MIX or phase_set == PhaseType.LG: self.vapor_frac = Expression( expr=self.func_vf(h_mass, P), doc="Vapor mole fraction (mol vapor/mol total)", ) elif phase_set == PhaseType.L: self.vapor_frac = Expression( expr=0.0, doc="Vapor mole fraction (mol vapor/mol total)" ) elif phase_set == PhaseType.G: self.vapor_frac = Expression( expr=1.0, doc="Vapor mole fraction (mol vapor/mol total)" ) # For variables that show up in ports specify extensive/intensive self.extensive_set = ComponentSet((self.flow_mol,)) self.intensive_set = ComponentSet((self.enth_mol, self.pressure)) elif self.state_vars == StateVars.TPX: self.temperature = Var( initialize=300, doc="Temperature [K]", bounds=(200, 3e3) ) self.pressure = Var( domain=PositiveReals, initialize=1e5, doc="Pressure [Pa]", bounds=(1, 1e9), ) self.vapor_frac = Var(initialize=0.0, doc="Vapor fraction [none]") # enth_mol is defined later, since in this case it needs # enth_mol_phase to be defined first # For variables that show up in ports specify extensive/intensive self.extensive_set = ComponentSet((self.flow_mol,)) self.intensive_set = ComponentSet( (self.temperature, self.pressure, self.vapor_frac) ) self.scaling_factor[self.temperature] = 1e-1 self.scaling_factor[self.pressure] = 1e-6 self.scaling_factor[self.vapor_frac] = 1e1 def _tpx_phase_eq(self): # Saturation pressure eps_pu = self.config.parameters.smoothing_pressure_under eps_po = self.config.parameters.smoothing_pressure_over priv_plist = self.config.parameters.private_phase_list plist = self.config.parameters.phase_list rhoc = self.config.parameters.dens_mass_crit P = self.pressure / 1000 # expression for pressure in kPa Psat = self.pressure_sat / 1000.0 # expression for Psat in kPA vf = self.vapor_frac tau = self.tau # Terms for determining if you are above, below, or at the Psat self.P_under_sat = Expression( expr=smooth_max(0, Psat - P, eps_pu), doc="pressure above Psat, 0 if liqid exists [kPa]", ) self.P_over_sat = Expression( expr=smooth_max(0, P - Psat, eps_po), doc="pressure below Psat, 0 if vapor exists [kPa]", ) # Calculate liquid and vapor density. If the phase doesn't exist, # density will be calculated at the saturation or critical pressure def rule_dens_mass(b, p): if p == "Liq": self.scaling_factor[self.dens_mass_phase[p]] = 1e-2 return rhoc * self.func_delta_liq(P + self.P_under_sat, tau) else: self.scaling_factor[self.dens_mass_phase[p]] = 1e1 return rhoc * self.func_delta_vap(P - self.P_over_sat, tau) self.dens_mass_phase = Expression(priv_plist, rule=rule_dens_mass) # Reduced Density (no _mass_ identifier because mass or mol is same) def rule_dens_red(b, p): return self.dens_mass_phase[p] / rhoc self.dens_phase_red = Expression( priv_plist, rule=rule_dens_red, doc="reduced density [unitless]" ) # If there is only one phase fix the vapor fraction appropriately if len(plist) == 1: if "Vap" in plist: self.vapor_frac.fix(1.0) else: self.vapor_frac.fix(0.0) elif not self.config.defined_state: self.eq_complementarity = Constraint( expr=0 == (vf * self.P_over_sat - (1 - vf) * self.P_under_sat) ) self.scaling_expression[self.eq_complementarity] = 10 / self.pressure # eq_sat can activated to force the pressure to be the saturation # pressure, if you use this constraint deactivate eq_complementarity self.eq_sat = Constraint(expr=P / 1000.0 == Psat / 1000.0) self.scaling_expression[self.eq_sat] = 1000 / self.pressure self.eq_sat.deactivate() def build(self, *args): """ Callable method for Block construction """ super().build(*args) # Create the scaling suffixes for the state block self.scaling_factor = Suffix(direction=Suffix.EXPORT) self.scaling_expression = Suffix() # Check if the library is available. self.available = self.config.parameters.available if not self.available: _log.error("Library file '{}' not found. Was it installed?".format( self.config.parameter.plib ) ) # Add external functions self._external_functions() # Which state vars to use self.state_vars = self.config.parameters.state_vars # The private phase list contains phases that may be present and is # used internally. If using the single mixed phase option the phase # list would be mixed while the private phase list would be ["Liq, "Vap"] phlist = self.config.parameters.private_phase_list pub_phlist = self.config.parameters.phase_list component_list = self.config.parameters.component_list phase_set = self.config.parameters.config.phase_presentation self.phase_equilibrium_list = self.config.parameters.phase_equilibrium_list # Expressions that link to some parameters in the param block, which # are commonly needed, this lets you get the parameters with scale # factors directly from the state block self.temperature_crit = Expression(expr=self.config.parameters.temperature_crit) self.scaling_factor[self.temperature_crit] = 1e-2 self.pressure_crit = Expression(expr=self.config.parameters.pressure_crit) self.scaling_factor[self.pressure_crit] = 1e-6 self.dens_mass_crit = Expression(expr=self.config.parameters.dens_mass_crit) self.scaling_factor[self.dens_mass_crit] = 1e-2 self.gas_const = Expression(expr=self.config.parameters.gas_const) self.scaling_factor[self.gas_const] = 1e0 self.mw = Expression( expr=self.config.parameters.mw, doc="molecular weight [kg/mol]" ) self.scaling_factor[self.mw] = 1e3 # create the appropriate state variables self._state_vars() # Some parameters/variables show up in several expressions, so to enhance # readability and compactness, give them short aliases Tc = self.config.parameters.temperature_crit rhoc = self.config.parameters.dens_mass_crit mw = self.mw P = self.pressure / 1000.0 # Pressure expr [kPA] (for external func) T = self.temperature vf = self.vapor_frac # Saturation temperature expression self.temperature_sat = Expression( expr=Tc / self.func_tau_sat(P), doc="Stauration temperature (K)" ) self.scaling_factor[self.temperature_sat] = 1e-2 # Saturation tau (tau = Tc/T) self.tau_sat = Expression(expr=self.func_tau_sat(P)) # Reduced temperature self.temperature_red = Expression( expr=T / Tc, doc="reduced temperature T/Tc (unitless)" ) self.scaling_factor[self.temperature_red] = 1 self.tau = Expression(expr=Tc / T, doc="Tc/T (unitless)") tau = self.tau # Saturation pressure self.pressure_sat = Expression( expr=1000 * self.func_p_sat(tau), doc="Saturation pressure (Pa)" ) self.scaling_factor[self.pressure_sat] = 1e-5 if self.state_vars == StateVars.PH: # If TPx state vars the expressions are given in _tpx_phase_eq # Calculate liquid and vapor density. If the phase doesn't exist, # density will be calculated at the saturation or critical pressure # depending on whether the temperature is above the critical # temperature supercritical fluid is considered to be the liquid # phase def rule_dens_mass(b, p): if p == "Liq": self.scaling_factor[self.dens_mass_phase[p]] = 1e-2 return rhoc * self.func_delta_liq(P, tau) else: self.scaling_factor[self.dens_mass_phase[p]] = 1e1 return rhoc * self.func_delta_vap(P, tau) self.dens_mass_phase = Expression( phlist, rule=rule_dens_mass, doc="Mass density by phase (kg/m3)" ) # Reduced Density (no _mass_ identifier as mass or mol is same) def rule_dens_red(b, p): self.scaling_factor[self.dens_phase_red[p]] = 1 return self.dens_mass_phase[p] / rhoc self.dens_phase_red = Expression( phlist, rule=rule_dens_red, doc="reduced density (unitless)" ) elif self.state_vars == StateVars.TPX: self._tpx_phase_eq() delta = self.dens_phase_red # Phase property expressions all converted to SI # Saturated Enthalpy def rule_enth_mol_sat_phase(b, p): if p == "Liq": self.scaling_factor[self.enth_mol_sat_phase[p]] = 1e-2 return 1000 * mw * self.func_hlpt(P, self.tau_sat) elif p == "Vap": self.scaling_factor[self.enth_mol_sat_phase[p]] = 1e-4 return 1000 * mw * self.func_hvpt(P, self.tau_sat) self.enth_mol_sat_phase = Expression( phlist, rule=rule_enth_mol_sat_phase, doc="Saturated enthalpy of the phases at pressure (J/mol)", ) self.dh_vap_mol = Expression( expr=self.enth_mol_sat_phase["Vap"] - self.enth_mol_sat_phase["Liq"], doc="Enthaply of vaporization at pressure and saturation (J/mol)", ) self.scaling_factor[self.dh_vap_mol] = 1e-4 # Phase Internal Energy def rule_energy_internal_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.energy_internal_mol_phase[p]] = 1e-2 else: self.scaling_factor[self.energy_internal_mol_phase[p]] = 1e-4 return 1000 * mw * self.func_u(delta[p], tau) self.energy_internal_mol_phase = Expression( phlist, rule=rule_energy_internal_mol_phase, doc="Phase internal energy or saturated if phase doesn't exist [J/mol]", ) # Phase Enthalpy def rule_enth_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.enth_mol_phase[p]] = 1e-2 elif p == "Vap": self.scaling_factor[self.enth_mol_phase[p]] = 1e-4 return 1000 * mw * self.func_h(delta[p], tau) self.enth_mol_phase = Expression( phlist, rule=rule_enth_mol_phase, doc="Phase enthalpy or saturated if phase doesn't exist [J/mol]", ) # Phase Entropy def rule_entr_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.entr_mol_phase[p]] = 1e-1 elif p == "Vap": self.scaling_factor[self.entr_mol_phase[p]] = 1e-1 return 1000 * mw * self.func_s(delta[p], tau) self.entr_mol_phase = Expression( phlist, rule=rule_entr_mol_phase, doc="Phase entropy or saturated if phase doesn't exist [J/mol/K]", ) # Phase constant pressure heat capacity, cp def rule_cp_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.cp_mol_phase[p]] = 1e-2 elif p == "Vap": self.scaling_factor[self.cp_mol_phase[p]] = 1e-2 return 1000 * mw * self.func_cp(delta[p], tau) self.cp_mol_phase = Expression( phlist, rule=rule_cp_mol_phase, doc="Phase cp or saturated if phase doesn't exist [J/mol/K]", ) # Phase constant pressure heat capacity, cv def rule_cv_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.cv_mol_phase[p]] = 1e-2 elif p == "Vap": self.scaling_factor[self.cv_mol_phase[p]] = 1e-2 return 1000 * mw * self.func_cv(delta[p], tau) self.cv_mol_phase = Expression( phlist, rule=rule_cv_mol_phase, doc="Phase cv or saturated if phase doesn't exist [J/mol/K]", ) # Phase speed of sound def rule_speed_sound_phase(b, p): if p == "Liq": self.scaling_factor[self.speed_sound_phase[p]] = 1e-2 elif p == "Vap": self.scaling_factor[self.speed_sound_phase[p]] = 1e-2 return self.func_w(delta[p], tau) self.speed_sound_phase = Expression( phlist, rule=rule_speed_sound_phase, doc="Phase speed of sound or saturated if phase doesn't exist [m/s]", ) # Phase Mole density def rule_dens_mol_phase(b, p): if p == "Liq": self.scaling_factor[self.dens_mol_phase[p]] = 1e-2 elif p == "Vap": self.scaling_factor[self.dens_mol_phase[p]] = 1e-4 return self.dens_mass_phase[p] / mw self.dens_mol_phase = Expression( phlist, rule=rule_dens_mol_phase, doc="Phase mole density or saturated if phase doesn't exist [mol/m3]", ) # Phase fraction def rule_phase_frac(b, p): self.scaling_factor[self.phase_frac[p]] = 10 if p == "Vap": return vf elif p == "Liq": return 1.0 - vf self.phase_frac = Expression( phlist, rule=rule_phase_frac, doc="Phase fraction [unitless]" ) # Component flow (for units that need it) def component_flow(b, i): self.scaling_factor[self.flow_mol_comp[i]] = 1e-3 return self.flow_mol self.flow_mol_comp = Expression( component_list, rule=component_flow, doc="Total flow (both phases) of component [mol/s]", ) # Total (mixed phase) properties # Enthalpy if self.state_vars == StateVars.TPX: self.enth_mol = Expression( expr=sum(self.phase_frac[p] * self.enth_mol_phase[p] for p in phlist) ) self.scaling_factor[self.enth_mol] = 1e-3 # Internal Energy self.energy_internal_mol = Expression( expr=sum( self.phase_frac[p] * self.energy_internal_mol_phase[p] for p in phlist ) ) self.scaling_factor[self.energy_internal_mol] = 1e-3 # Entropy self.entr_mol = Expression( expr=sum(self.phase_frac[p] * self.entr_mol_phase[p] for p in phlist) ) self.scaling_factor[self.entr_mol] = 1e-1 # cp self.cp_mol = Expression( expr=sum(self.phase_frac[p] * self.cp_mol_phase[p] for p in phlist) ) self.scaling_factor[self.cp_mol] = 1e-2 # cv self.cv_mol = Expression( expr=sum(self.phase_frac[p] * self.cv_mol_phase[p] for p in phlist) ) self.scaling_factor[self.cv_mol] = 1e-2 # mass density self.dens_mass = Expression( expr=1.0 / sum(self.phase_frac[p] * 1.0 / self.dens_mass_phase[p] for p in phlist) ) self.scaling_factor[self.dens_mass] = 1e0 # mole density self.dens_mol = Expression( expr=1.0 / sum(self.phase_frac[p] * 1.0 / self.dens_mol_phase[p] for p in phlist) ) self.scaling_factor[self.dens_mol] = 1e-3 # heat capacity ratio self.heat_capacity_ratio = Expression(expr=self.cp_mol / self.cv_mol) self.scaling_factor[self.heat_capacity_ratio] = 1e1 # Flows self.flow_vol = Expression( expr=self.flow_mol / self.dens_mol, doc="Total liquid + vapor volumetric flow (m3/s)", ) self.scaling_factor[self.flow_vol] = 100 self.flow_mass = Expression( expr=self.mw * self.flow_mol, doc="mass flow rate [kg/s]" ) self.scaling_factor[self.flow_mass] = 1 self.enth_mass = Expression(expr=self.enth_mol / mw, doc="Mass enthalpy (J/kg)") self.scaling_factor[self.enth_mass] = 1 # Set the state vars dictionary if self.state_vars == StateVars.PH: self._state_vars_dict = { "flow_mol": self.flow_mol, "enth_mol": self.enth_mol, "pressure": self.pressure, } elif self.state_vars == StateVars.TPX and phase_set in ( PhaseType.MIX, PhaseType.LG, ): self._state_vars_dict = { "flow_mol": self.flow_mol, "temperature": self.temperature, "pressure": self.pressure, "vapor_frac": self.vapor_frac, } elif self.state_vars == StateVars.TPX and phase_set in ( PhaseType.G, PhaseType.L, ): self._state_vars_dict = { "flow_mol": self.flow_mol, "temperature": self.temperature, "pressure": self.pressure, } # Define some expressions for the balance terms returned by functions # This is just to allow assigning scale factors to the expressions # returned # # Marterial flow term exprsssions def rule_material_flow_terms(b, p): self.scaling_expression[b.material_flow_terms[p]] = 1 / self.flow_mol if p == "Mix": return self.flow_mol else: return self.flow_mol * self.phase_frac[p] self.material_flow_terms = Expression(pub_phlist, rule=rule_material_flow_terms) # Enthaply flow term expressions def rule_enthalpy_flow_terms(b, p): if p == "Mix": self.scaling_expression[b.enthalpy_flow_terms[p]] = 1 / ( self.enth_mol * self.flow_mol ) return self.enth_mol * self.flow_mol else: self.scaling_expression[b.enthalpy_flow_terms[p]] = 1 / ( self.enth_mol_phase[p] * self.phase_frac[p] * self.flow_mol ) return self.enth_mol_phase[p] * self.phase_frac[p] * self.flow_mol self.enthalpy_flow_terms = Expression(pub_phlist, rule=rule_enthalpy_flow_terms) # Energy density term expressions def rule_energy_density_terms(b, p): if p == "Mix": self.scaling_expression[b.energy_density_terms[p]] = 1 / ( self.energy_internal_mol * self.flow_mol ) return self.dens_mol * self.energy_internal_mol else: self.scaling_expression[b.energy_density_terms[p]] = 1 / ( self.dens_mol_phase[p] * self.energy_internal_mol_phase[p] ) return self.dens_mol_phase[p] * self.energy_internal_mol_phase[p] self.energy_density_terms = Expression( pub_phlist, rule=rule_energy_density_terms ) def get_material_flow_terms(self, p, j): return self.material_flow_terms[p] def get_enthalpy_flow_terms(self, p): return self.enthalpy_flow_terms[p] def get_material_density_terms(self, p, j): if p == "Mix": return self.dens_mol else: return self.dens_mol_phase[p] def get_energy_density_terms(self, p): return self.energy_density_terms[p] def default_material_balance_type(self): return MaterialBalanceType.componentTotal def default_energy_balance_type(self): return EnergyBalanceType.enthalpyTotal def define_state_vars(self): return self._state_vars_dict def define_display_vars(self): return { "Molar Flow (mol/s)": self.flow_mol, "Mass Flow (kg/s)": self.flow_mass, "T (K)": self.temperature, "P (Pa)": self.pressure, "Vapor Fraction": self.vapor_frac, "Molar Enthalpy (J/mol)": self.enth_mol_phase, } def extensive_state_vars(self): return self.extensive_set def intensive_state_vars(self): return self.intensive_set def model_check(self): pass
class TurbineInletStageData(PressureChangerData): # Same settings as the default pressure changer, but force to expander with # isentropic efficiency CONFIG = PressureChangerData.CONFIG() CONFIG.compressor = False CONFIG.get("compressor")._default = False CONFIG.get("compressor")._domain = In([False]) CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption")._default = \ ThermodynamicAssumption.isentropic CONFIG.get("thermodynamic_assumption")._domain = In( [ThermodynamicAssumption.isentropic]) def build(self): super(TurbineInletStageData, self).build() umeta = self.config.property_package.get_metadata().get_derived_units t_units = umeta("temperature") self.flow_coeff = Var( self.flowsheet().config.time, initialize=1.053 / 3600.0, doc="Turbine flow coefficient", units=(umeta("mass") * umeta("temperature")**0.5 * umeta("pressure")**-1 * umeta("time")**-1)) self.delta_enth_isentropic = Var( self.flowsheet().config.time, initialize=-1000, doc="Specific enthalpy change of isentropic process", units=umeta("energy") / umeta("amount")) self.blade_reaction = Var(initialize=0.9, doc="Blade reaction parameter") self.blade_velocity = Var(initialize=110.0, doc="Design blade velocity", units=umeta("length") / umeta("time")) self.eff_nozzle = Var( initialize=0.95, bounds=(0.0, 1.0), doc="Nozzel efficiency (typically 0.90 to 0.95)", ) self.efficiency_mech = Var(initialize=0.98, doc="Turbine mechanical efficiency") self.flow_scale = Param( mutable=True, default=1e3, doc="Scaling factor for pressure flow relation should be " "approximately the same order of magnitude as the expected flow.", ) self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() self.efficiency_mech.fix() self.ratioP[:] = 1 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize @self.Expression( self.flowsheet().config.time, doc="Entering steam velocity calculation", ) def steam_entering_velocity(b, t): # 1.414 = 44.72/sqrt(1000) for SI if comparing to Liese (2014) # b.delta_enth_isentropic[t] = -(hin - hiesn), the mw converts # enthalpy to a mass basis return 1.414 * sqrt( -(1 - b.blade_reaction) * b.delta_enth_isentropic[t] / b.control_volume.properties_in[t].mw * self.eff_nozzle) @self.Constraint(self.flowsheet().config.time, doc="Equation: Turbine inlet flow") def inlet_flow_constraint(b, t): # Some local vars to make the equation more readable g = b.control_volume.properties_in[t].heat_capacity_ratio mw = b.control_volume.properties_in[t].mw flow = b.control_volume.properties_in[t].flow_mol Tin = b.control_volume.properties_in[t].temperature cf = b.flow_coeff[t] Pin = b.control_volume.properties_in[t].pressure Pratio = b.ratioP[t] return ((1 / b.flow_scale**2) * flow**2 * mw**2 * (Tin - 273.15 * t_units) == (1 / b.flow_scale**2) * cf**2 * Pin**2 * (g / (g - 1) * (Pratio**(2.0 / g) - Pratio**((g + 1) / g)))) @self.Constraint(self.flowsheet().config.time, doc="Equation: Isentropic enthalpy change") def isentropic_enthalpy(b, t): return b.work_isentropic[t] == ( b.delta_enth_isentropic[t] * b.control_volume.properties_in[t].flow_mol) @self.Constraint(self.flowsheet().config.time, doc="Equation: Efficiency") def efficiency_correlation(b, t): Vr = b.blade_velocity / b.steam_entering_velocity[t] eff = b.efficiency_isentropic[t] R = b.blade_reaction return eff == 2 * Vr * ( (sqrt(1 - R) - Vr) + sqrt((sqrt(1 - R) - Vr)**2 + R)) @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def _get_performance_contents(self, time_point=0): pc = super()._get_performance_contents(time_point=time_point) pc["vars"]["Mechanical Efficiency"] = self.efficiency_mech pc["vars"]["Flow Coefficient"] = self.flow_coeff[time_point] pc["vars"]["Isentropic Specific Enthalpy"] = \ self.delta_enth_isentropic[time_point] pc["vars"]["Blade Reaction"] = self.blade_reaction pc["vars"]["Blade Velocity"] = self.blade_velocity pc["vars"]["Nozzel Efficiency"] = self.eff_nozzle pc["exprs"] = {} pc["exprs"]["Thermodynamic Power"] = self.power_thermo[time_point] pc["exprs"]["Shaft Power"] = self.power_shaft[time_point] pc["exprs"]["Inlet Steam Velocity"] = \ self.steam_entering_velocity[time_point] pc["params"] = {} pc["params"]["Flow Scaling"] = self.flow_scale return pc def initialize( self, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={ "tol": 1e-6, "max_iter": 30 }, ): """ Initialize the inlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but # initializes the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Deactivate special constraints self.inlet_flow_constraint.deactivate() self.isentropic_enthalpy.deactivate() self.efficiency_correlation.deactivate() self.deltaP.unfix() self.ratioP.unfix() # Fix turbine parameters + eff_isen self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() # fix inlet and free outlet for t in self.flowsheet().config.time: for k, v in self.inlet.vars.items(): v[t].fix() for k, v in self.outlet.vars.items(): v[t].unfix() # If there isn't a good guess for efficeny or outlet pressure # provide something reasonable. eff = self.efficiency_isentropic[t] eff.fix( eff.value if value(eff) > 0.3 and value(eff) < 1.0 else 0.8) # for outlet pressure try outlet pressure, pressure ratio, delta P, # then if none of those look reasonable use a pressure ratio of 0.8 # to calculate outlet pressure Pout = self.outlet.pressure[t] Pin = self.inlet.pressure[t] prdp = value((self.deltaP[t] - Pin) / Pin) if value(Pout / Pin) > 0.98 or value(Pout / Pin) < 0.3: if (value(self.ratioP[t]) < 0.98 and value(self.ratioP[t]) > 0.3): Pout.fix(value(Pin * self.ratioP)) elif prdp < 0.98 and prdp > 0.3: Pout.fix(value(prdp * Pin)) else: Pout.fix(value(Pin * 0.8)) else: Pout.fix() self.deltaP[:] = value(Pout - Pin) self.ratioP[:] = value(Pout / Pin) for t in self.flowsheet().config.time: self.properties_isentropic[t].pressure.value = value( self.outlet.pressure[t]) self.properties_isentropic[t].flow_mol.value = value( self.inlet.flow_mol[t]) self.properties_isentropic[t].enth_mol.value = value( self.inlet.enth_mol[t] * 0.95) self.outlet.flow_mol[t].value = value(self.inlet.flow_mol[t]) self.outlet.enth_mol[t].value = value(self.inlet.enth_mol[t] * 0.95) # Make sure the initialization problem has no degrees of freedom # This shouldn't happen here unless there is a bug in this dof = degrees_of_freedom(self) try: assert dof == 0 except: init_log.exception("degrees_of_freedom = {}".format(dof)) raise # one bad thing about reusing this is that the log messages aren't # really compatible with being nested inside another initialization super().initialize(state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg) # Free eff_isen and activate sepcial constarints self.efficiency_isentropic.unfix() self.outlet.pressure.unfix() self.inlet_flow_constraint.activate() self.isentropic_enthalpy.activate() self.efficiency_correlation.activate() slvr = SolverFactory(solver) slvr.options = optarg with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = slvr.solve(self, tee=slc.tee) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) # reload original spec from_json(self, sd=istate, wts=sp)
class HelmTurbineInletStageData(HelmIsentropicTurbineData): CONFIG = HelmIsentropicTurbineData.CONFIG() def build(self): super().build() self.flow_coeff = Var( self.flowsheet().config.time, initialize=1.053 / 3600.0, doc="Turbine flow coefficient [kg*C^0.5/Pa/s]", ) self.blade_reaction = Var( initialize=0.9, doc="Blade reaction parameter" ) self.blade_velocity = Var( initialize=110.0, doc="Design blade velocity [m/s]" ) self.eff_nozzle = Var( initialize=0.95, bounds=(0.0, 1.0), doc="Nozzel efficiency (typically 0.90 to 0.95)", ) self.efficiency_mech = Var( initialize=1.0, doc="Turbine mechanical efficiency" ) self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() self.efficiency_mech.fix() self.efficiency_isentropic.unfix() self.ratioP[:] = 0.9 # make sure these have a number value self.deltaP[:] = 0 # to avoid an error later in initialize @self.Expression( self.flowsheet().config.time, doc="Entering steam velocity calculation [m/s]", ) def steam_entering_velocity(b, t): # 1.414 = 44.72/sqrt(1000) for SI if comparing to Liese (2014), # b.delta_enth_isentropic[t] = -(hin - hiesn), the mw converts # enthalpy to a mass basis return 1.414 * sqrt( (b.blade_reaction - 1)*b.delta_enth_isentropic[t]*self.eff_nozzle / b.control_volume.properties_in[t].mw ) @self.Expression(self.flowsheet().config.time, doc="Efficiency expression") def efficiency_isentropic_expr(b, t): Vr = b.blade_velocity / b.steam_entering_velocity[t] R = b.blade_reaction return 2*Vr*((sqrt(1 - R) - Vr) + sqrt((sqrt(1 - R) - Vr)**2 + R)) @self.Constraint( self.flowsheet().config.time, doc="Equation: Turbine inlet flow") def inlet_flow_constraint(b, t): # Some local vars to make the equation more readable g = b.control_volume.properties_in[t].heat_capacity_ratio mw = b.control_volume.properties_in[t].mw flow = b.control_volume.properties_in[t].flow_mol Tin = b.control_volume.properties_in[t].temperature cf = b.flow_coeff[t] Pin = b.control_volume.properties_in[t].pressure Pratio = b.ratioP[t] return flow ** 2 * mw ** 2 * Tin == ( cf ** 2 * Pin ** 2 * g / (g - 1) * (Pratio ** (2.0 / g) - Pratio ** ((g + 1) / g))) @self.Constraint(self.flowsheet().config.time, doc="Equation: Efficiency") def efficiency_correlation(b, t): return b.efficiency_isentropic[t] == b.efficiency_isentropic_expr[t] @self.Expression(self.flowsheet().config.time, doc="Thermodynamic power [J/s]") def power_thermo(b, t): return b.control_volume.work[t] @self.Expression(self.flowsheet().config.time, doc="Shaft power [J/s]") def power_shaft(b, t): return b.power_thermo[t] * b.efficiency_mech def initialize( self, state_args={}, outlvl=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6, "max_iter": 30}, calculate_cf=False, ): """ Initialize the inlet turbine stage model. This deactivates the specialized constraints, then does the isentropic turbine initialization, then reactivates the constraints and solves. This initializtion uses a flow value guess, so some reasonable flow guess should be sepecified prior to initializtion. Args: state_args (dict): Initial state for property initialization outlvl (int): Amount of output (0 to 3) 0 is lowest solver (str): Solver to use for initialization optarg (dict): Solver arguments dictionary calculate_cf (bool): If True, use the flow and pressure ratio to calculate the flow coefficient. """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # sp is what to save to make sure state after init is same as the start sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Setup for initializtion step 1 self.inlet_flow_constraint.deactivate() self.efficiency_correlation.deactivate() self.eff_nozzle.fix() self.blade_reaction.fix() self.flow_coeff.fix() self.blade_velocity.fix() self.inlet.fix() self.outlet.unfix() for t in self.flowsheet().config.time: self.efficiency_isentropic[t] = 0.9 super().initialize(outlvl=outlvl, solver=solver, optarg=optarg) # Free eff_isen and activate sepcial constarints self.inlet_flow_constraint.activate() self.efficiency_correlation.activate() if calculate_cf: self.ratioP.fix() self.flow_coeff.unfix() for t in self.flowsheet().config.time: g = self.control_volume.properties_in[t].heat_capacity_ratio mw = self.control_volume.properties_in[t].mw flow = self.control_volume.properties_in[t].flow_mol Tin = self.control_volume.properties_in[t].temperature Pin = self.control_volume.properties_in[t].pressure Pratio = self.ratioP[t] self.flow_coeff[t].value = value( flow * mw * sqrt( Tin/(g/(g - 1) *(Pratio**(2.0/g) - Pratio**((g + 1)/g))) )/Pin ) slvr = SolverFactory(solver) slvr.options = optarg with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = slvr.solve(self, tee=slc.tee) init_log.info("Initialization Complete: {}".format(idaeslog.condition(res))) # reload original spec if calculate_cf: cf = {} for t in self.flowsheet().config.time: cf[t] = value(self.flow_coeff[t]) from_json(self, sd=istate, wts=sp) if calculate_cf: # cf was probably fixed, so will have to set the value agian here # if you ask for it to be calculated. for t in self.flowsheet().config.time: self.flow_coeff[t] = cf[t] def calculate_scaling_factors(self): super().calculate_scaling_factors() for t, c in self.inlet_flow_constraint.items(): s = iscale.get_scaling_factor( self.control_volume.properties_in[t].flow_mol)**2 iscale.constraint_scaling_transform(c, s)
class CoagulationFlocculationData(UnitModelBlockData): """ Zero order Coagulation-Flocculation model based on Jar Tests """ # CONFIG are options for the unit model CONFIG = ConfigBlock() CONFIG.declare("dynamic", ConfigValue( domain=In([False]), default=False, description="Dynamic model flag - must be False", doc="""Indicates whether this model will be dynamic or not, **default** = False. The filtration unit does not support dynamic behavior, thus this must be False.""")) CONFIG.declare("has_holdup", ConfigValue( default=False, domain=In([False]), description="Holdup construction flag - must be False", doc="""Indicates whether holdup terms should be constructed or not. **default** - False. The filtration unit does not have defined volume, thus this must be False.""")) CONFIG.declare("material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""")) # NOTE: This option is temporarily disabled ''' CONFIG.declare("energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""")) ''' CONFIG.declare("momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""")) CONFIG.declare("property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""")) CONFIG.declare("property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property packages", doc="""A ConfigBlock with arguments to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""")) CONFIG.declare("chemical_additives", ConfigValue( default={}, domain=dict, description="""Dictionary of chemical additives used in coagulation process, along with their molecular weights, the moles of salt produced per mole of chemical added, and the molecular weights of the salt produced by the chemical additive with the format of: \n {'chem_name_1': {'parameter_data': { 'mw_additive': (value, units), 'moles_salt_per_mole_additive': value, 'mw_salt': (value, units) } }, 'chem_name_2': {'parameter_data': { 'mw_additive': (value, units), 'moles_salt_per_mole_additive': value, 'mw_salt': (value, units) } }, } """)) def build(self): # build always starts by calling super().build() # This triggers a lot of boilerplate in the background for you super().build() # this creates blank scaling factors, which are populated later self.scaling_factor = Suffix(direction=Suffix.EXPORT) # Next, get the base units of measurement from the property definition units_meta = self.config.property_package.get_metadata().get_derived_units # check the optional config arg 'chemical_additives' common_msg = "The 'chemical_additives' dict MUST contain a dict of 'parameter_data' for " + \ "each chemical name. That 'parameter_data' dict MUST contain 'mw_chem', " + \ "'moles_salt_per_mole_additive', and 'mw_salt' as keys. Users are also " + \ "required to provide the values for the molecular weights and the units " + \ "within a tuple arg. Example format provided below.\n\n" + \ "{'chem_name_1': \n" + \ " {'parameter_data': \n" + \ " {'mw_additive': (value, units), \n" + \ " 'moles_salt_per_mole_additive': value, \n" + \ " 'mw_salt': (value, units)} \n" + \ " }, \n" + \ "}\n\n" mw_adds = {} mw_salts = {} molar_rat = {} for j in self.config.chemical_additives: if type(self.config.chemical_additives[j]) != dict: raise ConfigurationError("\n Did not provide a 'dict' for chemical \n" + common_msg) if 'parameter_data' not in self.config.chemical_additives[j]: raise ConfigurationError("\n Did not provide a 'parameter_data' for chemical \n" + common_msg) if 'mw_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_additive' for chemical \n" + common_msg) if 'moles_salt_per_mole_additive' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'moles_salt_per_mole_additive' for chemical \n" + common_msg) if 'mw_salt' not in self.config.chemical_additives[j]['parameter_data']: raise ConfigurationError("\n Did not provide a 'mw_salt' for chemical \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_additive']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_additive' \n" + common_msg) if type(self.config.chemical_additives[j]['parameter_data']['mw_salt']) != tuple: raise ConfigurationError("\n Did not provide a tuple for 'mw_salt' \n" + common_msg) if not isinstance(self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'], (int,float)): raise ConfigurationError("\n Did not provide a number for 'moles_salt_per_mole_additive' \n" + common_msg) #Populate temp dicts for parameter and variable setting mw_adds[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_additive'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_additive'][1], to_units=pyunits.kg/pyunits.mol) mw_salts[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_salt'][0], from_units=self.config.chemical_additives[j]['parameter_data']['mw_salt'][1], to_units=pyunits.kg/pyunits.mol) molar_rat[j] = self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'] # Add unit variables # Linear relationship between TSS (mg/L) and Turbidity (NTU) # TSS (mg/L) = Turbidity (NTU) * slope + intercept # Default values come from the following paper: # H. Rugner, M. Schwientek,B. Beckingham, B. Kuch, P. Grathwohl, # Environ. Earth Sci. 69 (2013) 373-380. DOI: 10.1007/s12665-013-2307-1 self.slope = Var( self.flowsheet().config.time, initialize=1.86, bounds=(1e-8, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Slope relation between TSS (mg/L) and Turbidity (NTU)') self.intercept = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 10), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Intercept relation between TSS (mg/L) and Turbidity (NTU)') self.initial_turbidity_ntu = Var( self.flowsheet().config.time, initialize=50, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Initial measured Turbidity (NTU) from Jar Test') self.final_turbidity_ntu = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 10000), domain=NonNegativeReals, units=pyunits.dimensionless, doc='Final measured Turbidity (NTU) from Jar Test') self.chemical_doses = Var( self.flowsheet().config.time, self.config.chemical_additives.keys(), initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=pyunits.mg/pyunits.L, doc='Dosages of the set of chemical additives') self.chemical_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_adds, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the set of chemical additives') self.salt_mw = Param( self.config.chemical_additives.keys(), mutable=True, initialize=mw_salts, domain=NonNegativeReals, units=pyunits.kg/pyunits.mol, doc='Molecular weights of the produced salts from chemical additives') self.salt_from_additive_mole_ratio = Param( self.config.chemical_additives.keys(), mutable=True, initialize=molar_rat, domain=NonNegativeReals, units=pyunits.mol/pyunits.mol, doc='Moles of the produced salts from 1 mole of chemical additives') # Build control volume for feed side self.control_volume = ControlVolume0DBlock(default={ "dynamic": False, "has_holdup": False, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_state_blocks( has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, has_mass_transfer=True) # NOTE: This checks for if an energy_balance_type is defined if hasattr(self.config, "energy_balance_type"): self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_enthalpy_transfer=False) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=False) # Add ports self.add_inlet_port(name='inlet', block=self.control_volume) self.add_outlet_port(name='outlet', block=self.control_volume) # Check _phase_component_set for required items if ('Liq', 'TDS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TDS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'Sludge') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','Sludge') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) if ('Liq', 'TSS') not in self.config.property_package._phase_component_set: raise ConfigurationError( "Coagulation-Flocculation model MUST contain ('Liq','TSS') as a component, but " "the property package has only specified the following components {}" .format([p for p in self.config.property_package._phase_component_set])) # -------- Add constraints --------- # Adds isothermal constraint if no energy balance present if not hasattr(self.config, "energy_balance_type"): @self.Constraint(self.flowsheet().config.time, doc="Isothermal condition") def eq_isothermal(self, t): return (self.control_volume.properties_out[t].temperature == self.control_volume.properties_in[t].temperature) # Constraint for tss loss rate based on measured final turbidity self.tss_loss_rate = Var( self.flowsheet().config.time, initialize=1, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time loss rate of TSS based on the measured final turbidity') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tss_loss_rate(self, t): tss_out = pyunits.convert(self.slope[t]*self.final_turbidity_ntu[t] + self.intercept[t], to_units=units_meta('mass')*units_meta('length')**-3) input_rate = self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS'] exit_rate = self.control_volume.properties_out[t].flow_vol_phase['Liq']*tss_out return (self.tss_loss_rate[t] == input_rate - exit_rate) # Constraint for tds gain rate based on 'chemical_doses' and 'chemical_additives' if self.config.chemical_additives: self.tds_gain_rate = Var( self.flowsheet().config.time, initialize=0, bounds=(0, 100), domain=NonNegativeReals, units=units_meta('mass')*units_meta('time')**-1, doc='Mass per time gain rate of TDS based on the chemicals added for coagulation') @self.Constraint(self.flowsheet().config.time, doc="Constraint for the loss rate of TSS to be used in mass_transfer_term") def eq_tds_gain_rate(self, t): sum = 0 for j in self.config.chemical_additives.keys(): chem_dose = pyunits.convert(self.chemical_doses[t, j], to_units=units_meta('mass')*units_meta('length')**-3) chem_dose = chem_dose/self.chemical_mw[j] * \ self.salt_from_additive_mole_ratio[j] * \ self.salt_mw[j]*self.control_volume.properties_out[t].flow_vol_phase['Liq'] sum = sum+chem_dose return (self.tds_gain_rate[t] == sum) # Add constraints for mass transfer terms @self.Constraint(self.flowsheet().config.time, self.config.property_package.phase_list, self.config.property_package.component_list, doc="Mass transfer term") def eq_mass_transfer_term(self, t, p, j): if (p, j) == ('Liq', 'TSS'): return self.control_volume.mass_transfer_term[t, p, j] == -self.tss_loss_rate[t] elif (p, j) == ('Liq', 'Sludge'): return self.control_volume.mass_transfer_term[t, p, j] == self.tss_loss_rate[t] elif (p, j) == ('Liq', 'TDS'): if self.config.chemical_additives: return self.control_volume.mass_transfer_term[t, p, j] == self.tds_gain_rate[t] else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0 else: return self.control_volume.mass_transfer_term[t, p, j] == 0.0 # Return a scalar expression for the inlet concentration of TSS def compute_inlet_tss_mass_concentration(self, t): """ Function to generate an expression that would represent the mass concentration of TSS at the inlet port of the unit. Inlet ports are generally established upstream, but this will be useful for establishing the inlet TSS when an upstream TSS is unknown. This level of inlet TSS is based off of measurements made of Turbidity during the Jar Test. Keyword Arguments: self : this unit model object t : time index on the flowsheet Returns: Expression Recover the numeric value by using 'value(Expression)' """ units_meta = self.config.property_package.get_metadata().get_derived_units return pyunits.convert(self.slope[t]*self.initial_turbidity_ntu[t] + self.intercept[t], to_units=units_meta('mass')*units_meta('length')**-3) # Return a scale expression for the inlet mass flow rate of TSS def compute_inlet_tss_mass_flow(self, t): """ Function to generate an expression that would represent the mass flow rate of TSS at the inlet port of the unit. Inlet ports are generally established upstream, but this will be useful for establishing the inlet TSS when an upstream TSS is unknown. This level of inlet TSS is based off of measurements made of Turbidity during the Jar Test. Keyword Arguments: self : this unit model object t : time index on the flowsheet Returns: Expression Recover the numeric value by using 'value(Expression)' """ return self.control_volume.properties_in[t].flow_vol_phase['Liq']*self.compute_inlet_tss_mass_concentration(t) # Function to automate fixing of the Turbidity v TSS relation params to defaults def fix_tss_turbidity_relation_defaults(self): self.slope.fix() self.intercept.fix() # initialize method def initialize_build( blk, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): """ General wrapper for pressure changer initialization routines Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). outlvl : sets output level of initialization routine optarg : solver options dictionary object (default=None) solver : str indicating which solver to use during initialization (default = None) Returns: None """ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Set solver options opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize holdup block flags = blk.control_volume.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high( "Initialization Step 2 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.control_volume.release_state(flags, outlvl + 1) init_log.info( "Initialization Complete: {}".format(idaeslog.condition(res)) ) def calculate_scaling_factors(self): super().calculate_scaling_factors() units_meta = self.config.property_package.get_metadata().get_derived_units # scaling factors for turbidity relationship # Supressing warning (these factors are not very important) if iscale.get_scaling_factor(self.slope) is None: sf = iscale.get_scaling_factor(self.slope, default=1, warning=False) iscale.set_scaling_factor(self.slope, sf) if iscale.get_scaling_factor(self.intercept) is None: sf = iscale.get_scaling_factor(self.intercept, default=1, warning=False) iscale.set_scaling_factor(self.intercept, sf) # scaling factors for turbidity measurements and chemical doses # Supressing warning if iscale.get_scaling_factor(self.initial_turbidity_ntu) is None: sf = iscale.get_scaling_factor(self.initial_turbidity_ntu, default=1, warning=False) iscale.set_scaling_factor(self.initial_turbidity_ntu, sf) if iscale.get_scaling_factor(self.final_turbidity_ntu) is None: sf = iscale.get_scaling_factor(self.final_turbidity_ntu, default=1, warning=False) iscale.set_scaling_factor(self.final_turbidity_ntu, sf) if iscale.get_scaling_factor(self.chemical_doses) is None: sf = iscale.get_scaling_factor(self.chemical_doses, default=1, warning=False) iscale.set_scaling_factor(self.chemical_doses, sf) # set scaling for tss_loss_rate if iscale.get_scaling_factor(self.tss_loss_rate) is None: sf = 0 for t in self.control_volume.properties_in: sf += value(self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS']) sf = sf / len(self.control_volume.properties_in) if sf < 0.01: sf = 0.01 iscale.set_scaling_factor(self.tss_loss_rate, 1/sf) for ind, c in self.eq_tss_loss_rate.items(): iscale.constraint_scaling_transform(c, 1/sf) # set scaling for tds_gain_rate if self.config.chemical_additives: if iscale.get_scaling_factor(self.tds_gain_rate) is None: sf = 0 for t in self.control_volume.properties_in: sum = 0 for j in self.config.chemical_additives.keys(): chem_dose = pyunits.convert(self.chemical_doses[t, j], to_units=units_meta('mass')*units_meta('length')**-3) chem_dose = chem_dose/self.chemical_mw[j] * \ self.salt_from_additive_mole_ratio[j] * \ self.salt_mw[j]*self.control_volume.properties_in[t].flow_vol_phase['Liq'] sum = sum+chem_dose sf += value(sum) sf = sf / len(self.control_volume.properties_in) if sf < 0.001: sf = 0.001 iscale.set_scaling_factor(self.tds_gain_rate, 1/sf) for ind, c in self.eq_tds_gain_rate.items(): iscale.constraint_scaling_transform(c, 1/sf) # set scaling for mass transfer terms for ind, c in self.eq_mass_transfer_term.items(): if ind[2] == "TDS": if self.config.chemical_additives: sf = iscale.get_scaling_factor(self.tds_gain_rate) else: sf = 1 elif ind[2] == "TSS": sf = iscale.get_scaling_factor(self.tss_loss_rate) elif ind[2] == "Sludge": sf = iscale.get_scaling_factor(self.tss_loss_rate) else: sf = 1 iscale.constraint_scaling_transform(c, sf) iscale.set_scaling_factor(self.control_volume.mass_transfer_term[ind] , sf) # set scaling factors for control_volume.properties_in based on control_volume.properties_out for t in self.control_volume.properties_in: if iscale.get_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase) is None: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase) iscale.set_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase, sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp) is None: for ind in self.control_volume.properties_in[t].flow_mass_phase_comp: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp[ind], sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp) is None: for ind in self.control_volume.properties_in[t].mass_frac_phase_comp: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp[ind], sf) if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase) is None: for ind in self.control_volume.properties_in[t].flow_vol_phase: sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_vol_phase[ind]) iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase[ind], sf) # update scaling for control_volume.properties_out for t in self.control_volume.properties_out: if iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase) is None: iscale.set_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase, 1e-3) # need to update scaling factors for TSS, Sludge, and TDS to account for the # expected change in their respective values from the loss/gain rates for ind in self.control_volume.properties_out[t].flow_mass_phase_comp: if ind[1] == "TSS": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) if ind[1] == "Sludge": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) for ind in self.control_volume.properties_out[t].mass_frac_phase_comp: if ind[1] == "TSS": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og)) if ind[1] == "Sludge": sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind]) sf_new = iscale.get_scaling_factor(self.tss_loss_rate) iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og))
class PhysicalParameterData(PhysicalParameterBlock): """ Property Parameter Block Class Contains parameters and indexing sets associated with properties for methane CLC. """ def build(self): ''' Callable method for Block construction. ''' super(PhysicalParameterData, self).build() self._state_block_class = SolidPhaseStateBlock # Create Phase object self.Sol = SolidPhase() # Create Component objects self.Fe2O3 = Component() self.Fe3O4 = Component() self.Al2O3 = Component() # ------------------------------------------------------------------------- """ Pure solid component properties""" # Mol. weights of solid components - units = kg/mol. ref: NIST webbook mw_comp_dict = {'Fe2O3': 0.15969, 'Fe3O4': 0.231533, 'Al2O3': 0.10196} # Molecular weight should be defined in default units # (default mass units)/(default amount units) # per the define_meta.add_default_units method below self.mw_comp = Param( self.component_list, mutable=False, initialize=mw_comp_dict, doc="Molecular weights of solid components [kg/mol]", units=pyunits.kg / pyunits.mol) # Skeletal density of solid components - units = kg/m3. ref: NIST dens_mass_comp_skeletal_dict = { 'Fe2O3': 5250, 'Fe3O4': 5000, 'Al2O3': 3987 } self.dens_mass_comp_skeletal = Param( self.component_list, mutable=False, initialize=dens_mass_comp_skeletal_dict, doc='Skeletal density of solid components [kg/m3]', units=pyunits.kg / pyunits.m**3) # Ideal gas spec. heat capacity parameters(Shomate) of # components - ref: NIST webbook. Shomate equations from NIST. # Parameters A-E are used for cp calcs while A-H are used for enthalpy # calc. # cp_comp = A + B*T + C*T^2 + D*T^3 + E/(T^2) # where T = Temperature (K)/1000, and cp_comp = (J/mol.K) # H_comp = H - H(298.15) = A*T + B*T^2/2 + C*T^3/3 + # D*T^4/4 - E/T + F - H where T = Temp (K)/1000 and H_comp = (kJ/mol) cp_param_dict = { ('Al2O3', 1): 102.4290, ('Al2O3', 2): 38.74980, ('Al2O3', 3): -15.91090, ('Al2O3', 4): 2.628181, ('Al2O3', 5): -3.007551, ('Al2O3', 6): -1717.930, ('Al2O3', 7): 146.9970, ('Al2O3', 8): -1675.690, ('Fe3O4', 1): 200.8320000, ('Fe3O4', 2): 1.586435e-7, ('Fe3O4', 3): -6.661682e-8, ('Fe3O4', 4): 9.452452e-9, ('Fe3O4', 5): 3.18602e-8, ('Fe3O4', 6): -1174.1350000, ('Fe3O4', 7): 388.0790000, ('Fe3O4', 8): -1120.8940000, ('Fe2O3', 1): 110.9362000, ('Fe2O3', 2): 32.0471400, ('Fe2O3', 3): -9.1923330, ('Fe2O3', 4): 0.9015060, ('Fe2O3', 5): 5.4336770, ('Fe2O3', 6): -843.1471000, ('Fe2O3', 7): 228.3548000, ('Fe2O3', 8): -825.5032000 } self.cp_param_1 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 1}, doc="Shomate equation heat capacity coeff 1", units=pyunits.J / pyunits.mol / pyunits.K) self.cp_param_2 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 2}, doc="Shomate equation heat capacity coeff 2", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK) self.cp_param_3 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 3}, doc="Shomate equation heat capacity coeff 3", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**2) self.cp_param_4 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 4}, doc="Shomate equation heat capacity coeff 4", units=pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**3) self.cp_param_5 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 5}, doc="Shomate equation heat capacity coeff 5", units=pyunits.J / pyunits.mol / pyunits.K * pyunits.kK**2) self.cp_param_6 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 6}, doc="Shomate equation heat capacity coeff 6", units=pyunits.kJ / pyunits.mol) self.cp_param_7 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 7}, doc="Shomate equation heat capacity coeff 7", units=pyunits.J / pyunits.mol / pyunits.K) self.cp_param_8 = Param( self.component_list, mutable=False, initialize={k: v for (k, j), v in cp_param_dict.items() if j == 8}, doc="Shomate equation heat capacity coeff 8", units=pyunits.kJ / pyunits.mol) # Std. heat of formation of comp. - units = J/(mol comp) - ref: NIST enth_mol_form_comp_dict = { 'Fe2O3': -825.5032E3, 'Fe3O4': -1120.894E3, 'Al2O3': -1675.690E3 } self.enth_mol_form_comp = Param( self.component_list, mutable=False, initialize=enth_mol_form_comp_dict, doc="Component molar heats of formation [J/mol]", units=pyunits.J / pyunits.mol) # Set default scaling for mass fractions for comp in self.component_list: self.set_default_scaling("mass_frac_comp", 1e2, index=comp) # ------------------------------------------------------------------------- """ Mixed solid properties""" # These are setup as fixed vars to allow for parameter estimation # Particle size self.particle_dia = Var(domain=Reals, initialize=1.5e-3, doc='Diameter of solid particles [m]', units=pyunits.m) self.particle_dia.fix() # TODO -provide reference # Minimum fluidization velocity - EPAT value used for Davidson model self.velocity_mf = Var(domain=Reals, initialize=0.039624, doc='Velocity at minimum fluidization [m/s]', units=pyunits.m / pyunits.s) self.velocity_mf.fix() # Minimum fluidization voidage - educated guess as rough # estimate from ergun equation results (0.4) are suspicious self.voidage_mf = Var(domain=Reals, initialize=0.45, doc='Voidage at minimum fluidization [-]', units=pyunits.m**3 / pyunits.m**3) self.voidage_mf.fix() # Voidage of the bed self.voidage = Var(domain=Reals, initialize=0.35, doc='Voidage [-]', units=pyunits.m**3 / pyunits.m**3) self.voidage.fix() # Particle thermal conductivity self.therm_cond_sol = Var( domain=Reals, initialize=12.3e-0, doc='Thermal conductivity of solid particles [J/m.K.s]', units=pyunits.J / pyunits.m / pyunits.K / pyunits.s) self.therm_cond_sol.fix() @classmethod def define_metadata(cls, obj): obj.add_properties({ 'flow_mass': { 'method': None, 'units': 'kg/s' }, 'particle_porosity': { 'method': None, 'units': None }, 'temperature': { 'method': None, 'units': 'K' }, 'mass_frac_comp': { 'method': None, 'units': None }, 'dens_mass_skeletal': { 'method': '_dens_mass_skeletal', 'units': 'kg/m3' }, 'dens_mass_particle': { 'method': '_dens_mass_particle', 'units': 'kg/m3' }, 'cp_mol_comp': { 'method': '_cp_mol_comp', 'units': 'J/mol.K' }, 'cp_mass': { 'method': '_cp_mass', 'units': 'J/kg.K' }, 'enth_mass': { 'method': '_enth_mass', 'units': 'J/kg' }, 'enth_mol_comp': { 'method': '_enth_mol_comp', 'units': 'J/mol' } }) obj.add_default_units({ 'time': pyunits.s, 'length': pyunits.m, 'mass': pyunits.kg, 'amount': pyunits.mol, 'temperature': pyunits.K })