Source code for idaes.models_extra.power_generation.unit_models.helm.valve_steam

#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
# TODO: Missing doc strings
# pylint: disable=missing-module-docstring
# pylint: disable=missing-class-docstring

# Changing existing config block attributes
# pylint: disable=protected-access

from enum import Enum

import pyomo.environ as pyo
from pyomo.common.config import ConfigValue, In

from idaes.core import declare_process_block_class
from idaes.models_extra.power_generation.unit_models.balance import BalanceBlockData
from idaes.core.util import from_json, to_json, StoreSpec
from idaes.core.solvers import get_solver
import idaes.models.properties.helmholtz.helmholtz as hltz
import idaes.logger as idaeslog
import idaes.core.util.scaling as iscale
from idaes.core.util.exceptions import ConfigurationError


_log = idaeslog.getLogger(__name__)


class ValveFunctionType(Enum):
    linear = 1
    quick_opening = 2
    equal_percentage = 3
    custom = 4


def _assert_properties(pb):
    """Assert that the properties parameter block conforms to the requirements"""
    try:
        assert isinstance(pb, hltz.HelmholtzParameterBlockData)
        assert pb.config.phase_presentation in {
            hltz.PhaseType.MIX,
            hltz.PhaseType.L,
            hltz.PhaseType.G,
        }
        assert pb.config.state_vars == hltz.StateVars.PH
    except AssertionError:
        _log.error(
            "helm.HelmValve requires a Helmholtz EOS with "
            "a single or mixed phase and pressure-enthalpy state vars."
        )
        raise


def _linear_callback(blk):
    @blk.Expression(blk.flowsheet().time)
    def valve_function(b, t):
        return b.valve_opening[t]


def _quick_open_callback(blk):
    @blk.Expression(blk.flowsheet().time)
    def valve_function(b, t):
        return pyo.sqrt(b.valve_opening[t])


def _equal_percentage_callback(blk):
    blk.alpha = pyo.Var(initialize=1, doc="Valve function parameter")
    blk.alpha.fix()

    @blk.Expression(blk.flowsheet().time)
    def valve_function(b, t):
        return b.alpha ** (b.valve_opening[t] - 1)


def _liquid_pressure_flow_rule(b, t):
    """
    For liquid F = Cv*sqrt(Pi**2 - Po**2)*f(x)
    """
    Po = b.control_volume.properties_out[t].pressure
    Pi = b.control_volume.properties_in[t].pressure
    F = b.control_volume.properties_in[t].flow_mol
    fun = b.valve_function[t]
    return F**2 == b.Cv**2 * (Pi - Po) * fun**2


def _vapor_pressure_flow_rule(b, t):
    """
    For vapor F = Cv*sqrt(Pi**2 - Po**2)*f(x)
    """
    Po = b.control_volume.properties_out[t].pressure
    Pi = b.control_volume.properties_in[t].pressure
    F = b.control_volume.properties_in[t].flow_mol
    fun = b.valve_function[t]
    return F**2 == b.Cv**2 * (Pi**2 - Po**2) * fun**2


[docs] @declare_process_block_class("HelmValve") class HelmValveData(BalanceBlockData): """ Basic adiabatic 0D valve model. This inherits the balance block to get a lot of unit model boilerplate and the mass balance, enegy balance and pressure equations. This model is intended to be used only with Helmholtz EOS property packages in mixed or single phase mode with P-H state vars. Since this inherits BalanceBlockData, and only operates in steady-state or pseudo-steady-state (for dynamic models) the following mass, energy and pressure equations are implicitly written. 1) Mass Balance: 0 = flow_mol_in[t] - flow_mol_out[t] 2) Energy Balance: 0 = (flow_mol[t]*h_mol[t])_in - (flow_mol[t]*h_mol[t])_out 3) Pressure: 0 = P_in[t] + deltaP[t] - P_out[t] """ CONFIG = BalanceBlockData.CONFIG() # For dynamics assume pseudo-steady-state CONFIG.dynamic = False CONFIG.get("dynamic")._default = False CONFIG.get("dynamic")._domain = In([False]) CONFIG.has_holdup = False CONFIG.get("has_holdup")._default = False CONFIG.get("has_holdup")._domain = In([False]) # Rest of config to make this function like a turbine CONFIG.has_pressure_change = True CONFIG.get("has_pressure_change")._default = True CONFIG.get("has_pressure_change")._domain = In([True]) CONFIG.has_work_transfer = False CONFIG.get("has_work_transfer")._default = False CONFIG.get("has_work_transfer")._domain = In([False]) CONFIG.has_heat_transfer = False CONFIG.get("has_heat_transfer")._default = False CONFIG.get("has_heat_transfer")._domain = In([False]) CONFIG.declare( "valve_function", ConfigValue( default=ValveFunctionType.linear, domain=In(ValveFunctionType), description="Valve function type, if custom provide an expression rule", doc="""The type of valve function, if custom provide an expression rule with the valve_function_rule argument. **default** - ValveFunctionType.linear **Valid values** - { ValveFunctionType.linear, ValveFunctionType.quick_opening, ValveFunctionType.equal_percentage, ValveFunctionType.custom}""", ), ) CONFIG.declare( "valve_function_callback", ConfigValue( default=None, description="This is a callback that adds a valve function. The " "callback function takes the valve bock data argument.", ), ) CONFIG.declare( "phase", ConfigValue( default="Vap", domain=In(("Vap", "Liq")), description='Expected phase of fluid in valve in {"Liq", "Vap"}', ), )
[docs] def build(self): """ Add model equations to the unit model. This is called by a default block construnction rule when the unit model is created. """ super().build() # Basic unit model build/read config config = self.config # shorter config pointer # The thermodynamic expression writer object, te, writes expressions # including external function calls to calculate thermodynamic quantities # from a set of state variables. _assert_properties(config.property_package) self.valve_opening = pyo.Var( self.flowsheet().time, initialize=1, doc="Fraction open for valve from 0 to 1", ) self.Cv = pyo.Var( initialize=0.1, doc="Valve flow coefficient, for vapor " "[mol/s/Pa] for liquid [mol/s/Pa]", units=pyo.units.mol / pyo.units.s / pyo.units.Pa, ) # self.Cv.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. vfcb = self.config.valve_function_callback vfselect = self.config.valve_function if vfselect is not ValveFunctionType.custom and vfcb is not None: _log.warning( "A valve function callback was provided but the valve " "function type is not custom." ) if vfselect == ValveFunctionType.linear: _linear_callback(self) elif vfselect == ValveFunctionType.quick_opening: _quick_open_callback(self) elif vfselect == ValveFunctionType.equal_percentage: _equal_percentage_callback(self) else: if vfcb is None: raise ConfigurationError("No custom valve function callback provided") vfcb(self) if self.config.phase == "Liq": rule = _liquid_pressure_flow_rule else: rule = _vapor_pressure_flow_rule self.pressure_flow_equation = pyo.Constraint(self.flowsheet().time, rule=rule)
def _get_performance_contents(self, time_point=0): """This returns a dictionary of quntities to be used in IDAES unit model report generation routines. """ pc = super()._get_performance_contents(time_point=time_point) return pc
[docs] def initialize_build( self, outlvl=idaeslog.NOTSET, solver=None, optarg=None, calculate_cv=False, calculate_opening=False, ): """ For simplicity this initialization requires you to set values for the efficiency, inlet, and one of pressure ratio, pressure change or outlet pressure. """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") init_log.info("Steam valve initialization started") # Create solver opt = get_solver(solver, optarg) # Store original specification so initialization doesn't change the model # This will only restore the values of variables that were originally fixed sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # Check for alternate pressure specs for t in self.flowsheet().time: if self.outlet.pressure[t].fixed: self.deltaP[t].fix( pyo.value(self.outlet.pressure[t] - self.inlet.pressure[t]) ) self.outlet.pressure[t].unfix() elif self.deltaP[t].fixed: # No outlet pressure specified guess a small pressure drop self.outlet.pressure[t] = pyo.value( self.inlet.pressure[t] + self.deltaP[t] ) self.inlet.fix() self.outlet.unfix() for t, v in self.deltaP.items(): if calculate_cv: self.Cv.unfix() elif calculate_opening: self.valve_opening.unfix() elif v.fixed and self.pressure_flow_equation.active: self.inlet.flow_mol[t].unfix() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: opt.solve(self, tee=slc.tee) init_log.info("Steam valve initialization complete") from_json(self, sd=istate, wts=sp)
def calculate_scaling_factors(self): super().calculate_scaling_factors() for t, c in self.pressure_flow_equation.items(): s = iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_mol) s = s**2 iscale.constraint_scaling_transform(c, s, overwrite=False)