#################################################################################
# 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.
#################################################################################
"""
This provides standard valve models for adiabatic control valves. Beyond the
most common valve models, and adiabatic valve model can be added by supplying
custom callbacks for the pressure-flow relation or valve function.
"""
# Changing existing config block attributes
# pylint: disable=protected-access
__Author__ = "John Eslick"
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.unit_models.pressure_changer import (
PressureChangerData,
ThermodynamicAssumption,
MaterialBalanceType,
)
from idaes.core.util.exceptions import ConfigurationError
import idaes.logger as idaeslog
import idaes.core.util.scaling as iscale
_log = idaeslog.getLogger(__name__)
class ValveFunctionType(Enum):
"""
Enum of supported valve types.
"""
linear = 1
quick_opening = 2
equal_percentage = 3
def linear_cb(valve):
"""
Linear opening valve function callback.
"""
@valve.Expression(valve.flowsheet().time)
def valve_function(b, t):
return b.valve_opening[t]
def quick_cb(valve):
"""
Quick opening valve function callback.
"""
@valve.Expression(valve.flowsheet().time)
def valve_function(b, t):
return pyo.sqrt(b.valve_opening[t])
def equal_percentage_cb(valve):
"""
Equal percentage valve function callback.
"""
valve.alpha = pyo.Var(initialize=100, doc="Valve function parameter")
valve.alpha.fix()
@valve.Expression(valve.flowsheet().time)
def valve_function(b, t):
return b.alpha ** (b.valve_opening[t] - 1)
def pressure_flow_default_callback(valve):
"""
Add the default pressure flow relation constraint. This will be used in the
valve model, a custom callback is provided.
"""
umeta = (
valve.control_volume.config.property_package.get_metadata().get_derived_units
)
valve.Cv = pyo.Var(
initialize=0.1,
doc="Valve flow coefficient",
units=umeta("amount") / umeta("time") / umeta("pressure") ** 0.5,
)
valve.Cv.fix()
valve.flow_var = pyo.Reference(valve.control_volume.properties_in[:].flow_mol)
valve.pressure_flow_equation_scale = lambda x: x**2
@valve.Constraint(valve.flowsheet().time)
def pressure_flow_equation(b, t):
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
Cv = b.Cv
fun = b.valve_function[t]
return F**2 == Cv**2 * (Pi - Po) * fun**2
[docs]
@declare_process_block_class("Valve", doc="Adiabatic valves")
class ValveData(PressureChangerData):
"""
Basic valve model class.
"""
# 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.material_balance_type = MaterialBalanceType.componentTotal
CONFIG.get("material_balance_type")._default = MaterialBalanceType.componentTotal
CONFIG.thermodynamic_assumption = ThermodynamicAssumption.adiabatic
CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.adiabatic
CONFIG.get("thermodynamic_assumption")._domain = In(
[ThermodynamicAssumption.adiabatic]
)
CONFIG.declare(
"valve_function_callback",
ConfigValue(
default=ValveFunctionType.linear,
description="Valve function type or callback for custom",
doc="""This takes either an enumerated valve function type in: {
ValveFunctionType.linear, ValveFunctionType.quick_opening,
ValveFunctionType.equal_percentage, ValveFunctionType.custom} or a callback
function that takes a valve model object as an argument and adds a time-indexed
valve_function expression to it. Any additional required variables, expressions,
or constraints required can also be added by the callback.""",
),
)
CONFIG.declare(
"pressure_flow_callback",
ConfigValue(
default=pressure_flow_default_callback,
description="Callback function providing the valve_function expression",
doc="""This callback function takes a valve model object as an argument
and adds a time-indexed valve_function expression to it. Any additional required
variables, expressions, or constraints required can also be added by the callback.""",
),
)
[docs]
def build(self):
super().build()
self.valve_opening = pyo.Var(
self.flowsheet().time,
initialize=1,
bounds=(0, 1),
doc="Fraction open for valve from 0 to 1",
)
self.valve_opening.fix()
# If the valve function callback is set to one of the known enumerated
# types, set the callback appropriately. If not callable and not a known
# type raise ConfigurationError.
vfcb = self.config.valve_function_callback
if not callable(vfcb):
if vfcb == ValveFunctionType.linear:
self.config.valve_function_callback = linear_cb
elif vfcb == ValveFunctionType.quick_opening:
self.config.valve_function_callback = quick_cb
elif vfcb == ValveFunctionType.equal_percentage:
self.config.valve_function_callback = equal_percentage_cb
else:
raise ConfigurationError("Invalid valve function callback.")
self.config.valve_function_callback(self)
self.config.pressure_flow_callback(self)
[docs]
def initialize_build(
self,
state_args=None,
outlvl=idaeslog.NOTSET,
solver=None,
optarg=None,
):
"""
Initialize the valve based on a deltaP guess.
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
"""
for t in self.flowsheet().time:
if (
self.deltaP[t].fixed
or self.ratioP[t].fixed
or self.outlet.pressure[t].fixed
):
continue
# Generally for the valve initialization pressure drop won't be
# fixed, so if there is no good guess on deltaP try to out one in
Pout = self.outlet.pressure[t]
Pin = self.inlet.pressure[t]
if self.deltaP[t].value is not None:
prdp = pyo.value((self.deltaP[t] - Pin) / Pin)
else:
prdp = -100 # crazy number to say don't use deltaP as guess
if pyo.value(Pout / Pin) > 1 or pyo.value(Pout / Pin) < 0.0:
if pyo.value(self.ratioP[t]) <= 1 and pyo.value(self.ratioP[t]) >= 0:
Pout.value = pyo.value(Pin * self.ratioP[t])
elif prdp <= 1 and prdp >= 0:
Pout.value = pyo.value(prdp * Pin)
else:
Pout.value = pyo.value(Pin * 0.95)
self.deltaP[t] = pyo.value(Pout - Pin)
self.ratioP[t] = pyo.value(Pout / Pin)
# one bad thing about reusing this is that the log messages aren't
# really compatible with being nested inside another initialization
super().initialize_build(
state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
)
[docs]
def calculate_scaling_factors(self):
"""
Calculate pressure flow constraint scaling from flow variable scale.
"""
# The value of the valve opening and the output of the valve function
# expression are between 0 and 1, so the only thing that needs to be
# scaled here is the pressure-flow constraint, which can be scaled by
# using the flow variable scale. The flow variable could be defined
# in different ways, so the flow variable is determined here from a
# "flow_var[t]" reference set in the pressure-flow callback. The flow
# term could be in various forms, so an optional
# "pressure_flow_equation_scale" function can be defined in the callback
# as well. The pressure-flow function could be flow = f(Pin, Pout), but
# it could also be flow**2 = f(Pin, Pout), ... The so
# "pressure_flow_equation_scale" provides the form of the LHS side as
# a function of the flow variable.
super().calculate_scaling_factors()
# Do some error trapping.
if not hasattr(self, "pressure_flow_equation"):
raise AttributeError(
"Pressure-flow callback must define pressure_flow_equation[t]"
)
# Check for flow term form if none assume flow = f(Pin, Pout)
if hasattr(self, "pressure_flow_equation_scale"):
ff = self.pressure_flow_equation_scale
else:
# pylint: disable-next=unnecessary-lambda-assignment
ff = lambda x: x
# if the "flow_var" is not set raise an exception
if not hasattr(self, "flow_var"):
raise AttributeError(
"Pressure-flow callback must define flow_var[t] reference"
)
# Calculate and set the pressure-flow relation scale.
if hasattr(self, "pressure_flow_equation"):
for t, c in self.pressure_flow_equation.items():
iscale.constraint_scaling_transform(
c,
ff(
iscale.get_scaling_factor(
self.flow_var[t], default=1, warning=True
)
),
)
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]
try:
pc["vars"]["Valve Coefficient"] = self.Cv
except AttributeError:
pass
if self.config.valve_function_callback == ValveFunctionType.equal_percentage:
pc["vars"]["alpha"] = self.alpha
return pc