Coverage for backend/idaes_service/solver/custom/custom_valve.py: 60%
112 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1#################################################################################
2# The Institute for the Design of Advanced Energy Systems Integrated Platform
3# Framework (IDAES IP) was produced under the DOE Institute for the
4# Design of Advanced Energy Systems (IDAES).
5#
6# Copyright (c) 2018-2024 by the software owners: The Regents of the
7# University of California, through Lawrence Berkeley National Laboratory,
8# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
9# University, West Virginia University Research Corporation, et al.
10# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
11# for full copyright and license information.
12#################################################################################
13"""
14This provides standard valve models for adiabatic control valves. Beyond the
15most common valve models, and adiabatic valve model can be added by supplying
16custom callbacks for the pressure-flow relation or valve function.
17"""
18# Changing existing config block attributes
19# pylint: disable=protected-access
21__Author__ = "John Eslick"
23from enum import Enum
25import pyomo.environ as pyo
26from pyomo.common.config import ConfigValue, In
28from idaes.core import declare_process_block_class
29from .updated_pressure_changer import (
30 ThermodynamicAssumption,
31 MaterialBalanceType,
32)
34from .updated_pressure_changer import PressureChangerData
36from idaes.core.util.exceptions import ConfigurationError
37import idaes.logger as idaeslog
38import idaes.core.util.scaling as iscale
39from .inverted import add_inverted, initialise_inverted
41_log = idaeslog.getLogger(__name__)
44class ValveFunctionType(Enum):
45 """
46 Enum of supported valve types.
47 """
49 linear = 1
50 quick_opening = 2
51 equal_percentage = 3
54def linear_cb(valve):
55 """
56 Linear opening valve function callback.
57 """
59 @valve.Expression(valve.flowsheet().time)
60 def valve_function(b, t):
61 return b.valve_opening[t]
64def quick_cb(valve):
65 """
66 Quick opening valve function callback.
67 """
69 @valve.Expression(valve.flowsheet().time)
70 def valve_function(b, t):
71 return pyo.sqrt(b.valve_opening[t])
74def equal_percentage_cb(valve):
75 """
76 Equal percentage valve function callback.
77 """
78 valve.alpha = pyo.Var(initialize=100, doc="Valve function parameter")
79 valve.alpha.fix()
81 @valve.Expression(valve.flowsheet().time)
82 def valve_function(b, t):
83 return b.alpha ** (b.valve_opening[t] - 1)
86def pressure_flow_default_callback(valve):
87 """
88 Add the default pressure flow relation constraint. This will be used in the
89 valve model, a custom callback is provided.
90 """
91 umeta = (
92 valve.control_volume.config.property_package.get_metadata().get_derived_units
93 )
95 valve.Cv = pyo.Var(
96 initialize=0.1,
97 doc="Valve flow coefficient",
98 units=umeta("amount") / umeta("time") / umeta("pressure") ** 0.5,
99 )
100 # valve.Cv.fix()
102 valve.flow_var = pyo.Reference(valve.control_volume.properties_in[:].flow_mol)
103 valve.pressure_flow_equation_scale = lambda x: x**2
105 @valve.Constraint(valve.flowsheet().time)
106 def pressure_flow_equation(b, t):
107 Po = b.control_volume.properties_out[t].pressure
108 Pi = b.control_volume.properties_in[t].pressure
109 F = b.control_volume.properties_in[t].flow_mol
110 Cv = b.Cv
111 fun = b.valve_function[t]
112 return F**2 == Cv**2 * (Pi - Po) * fun**2
115@declare_process_block_class("Valve", doc="Adiabatic valves")
116class ValveData(PressureChangerData):
117 """
118 Basic valve model class.
119 """
121 # Same settings as the default pressure changer, but force to expander with
122 # isentropic efficiency
123 CONFIG = PressureChangerData.CONFIG()
124 CONFIG.compressor = False
125 CONFIG.get("compressor")._default = False
126 CONFIG.get("compressor")._domain = In([False])
127 CONFIG.material_balance_type = MaterialBalanceType.componentTotal
128 CONFIG.get("material_balance_type")._default = MaterialBalanceType.componentTotal
129 CONFIG.thermodynamic_assumption = ThermodynamicAssumption.adiabatic
130 CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.adiabatic
131 CONFIG.get("thermodynamic_assumption")._domain = In(
132 [ThermodynamicAssumption.adiabatic]
133 )
134 CONFIG.declare(
135 "valve_function_callback",
136 ConfigValue(
137 default=ValveFunctionType.linear,
138 description="Valve function type or callback for custom",
139 doc="""This takes either an enumerated valve function type in: {
140ValveFunctionType.linear, ValveFunctionType.quick_opening,
141ValveFunctionType.equal_percentage, ValveFunctionType.custom} or a callback
142function that takes a valve model object as an argument and adds a time-indexed
143valve_function expression to it. Any additional required variables, expressions,
144or constraints required can also be added by the callback.""",
145 ),
146 )
147 CONFIG.declare(
148 "pressure_flow_callback",
149 ConfigValue(
150 default=pressure_flow_default_callback,
151 description="Callback function providing the valve_function expression",
152 doc="""This callback function takes a valve model object as an argument
153and adds a time-indexed valve_function expression to it. Any additional required
154variables, expressions, or constraints required can also be added by the callback.""",
155 ),
156 )
158 def build(self):
159 super().build()
161 self.valve_opening = pyo.Var(
162 self.flowsheet().time,
163 initialize=1,
164 bounds=(0, 1),
165 doc="Fraction open for valve from 0 to 1",
166 )
169 #commented out to allow for valve opening to be set by the user
170 #self.valve_opening.fix()
174 # If the valve function callback is set to one of the known enumerated
175 # types, set the callback appropriately. If not callable and not a known
176 # type raise ConfigurationError.
177 vfcb = self.config.valve_function_callback
178 if not callable(vfcb): 178 ↛ 187line 178 didn't jump to line 187 because the condition on line 178 was always true
179 if vfcb == ValveFunctionType.linear:
180 self.config.valve_function_callback = linear_cb
181 elif vfcb == ValveFunctionType.quick_opening:
182 self.config.valve_function_callback = quick_cb
183 elif vfcb == ValveFunctionType.equal_percentage: 183 ↛ 186line 183 didn't jump to line 186 because the condition on line 183 was always true
184 self.config.valve_function_callback = equal_percentage_cb
185 else:
186 raise ConfigurationError("Invalid valve function callback.")
187 self.config.valve_function_callback(self)
188 self.config.pressure_flow_callback(self)
190 # add deltaP_inverted as a property
191 add_inverted(self,"deltaP")
193 def initialize_build(
194 self,
195 state_args=None,
196 outlvl=idaeslog.NOTSET,
197 solver=None,
198 optarg=None,
199 ):
200 """
201 Initialize the valve based on a deltaP guess.
203 Args:
204 state_args (dict): Initial state for property initialization
205 outlvl : sets output level of initialization routine
206 solver (str): Solver to use for initialization
207 optarg (dict): Solver arguments dictionary
208 """
209 initialise_inverted(self,"deltaP")
211 for t in self.flowsheet().time:
212 if ( 212 ↛ 220line 212 didn't jump to line 220 because the condition on line 212 was always true
213 self.deltaP[t].fixed or self.deltaP_inverted[t].fixed
214 or self.ratioP[t].fixed
215 or self.outlet.pressure[t].fixed
216 ):
217 continue
218 # Generally for the valve initialization pressure drop won't be
219 # fixed, so if there is no good guess on deltaP try to out one in
220 Pout = self.outlet.pressure[t]
221 Pin = self.inlet.pressure[t]
222 if self.deltaP[t].value is not None:
223 prdp = pyo.value((self.deltaP[t] - Pin) / Pin)
224 else:
225 prdp = -100 # crazy number to say don't use deltaP as guess
226 if pyo.value(Pout / Pin) > 1 or pyo.value(Pout / Pin) < 0.0:
227 if pyo.value(self.ratioP[t]) <= 1 and pyo.value(self.ratioP[t]) >= 0:
228 Pout.value = pyo.value(Pin * self.ratioP[t])
229 elif prdp <= 1 and prdp >= 0:
230 Pout.value = pyo.value(prdp * Pin)
231 else:
232 Pout.value = pyo.value(Pin * 0.95)
233 self.deltaP[t] = pyo.value(Pout - Pin)
234 self.ratioP[t] = pyo.value(Pout / Pin)
236 # one bad thing about reusing this is that the log messages aren't
237 # really compatible with being nested inside another initialization
238 super().initialize_build(
239 state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
240 )
242 def calculate_scaling_factors(self):
243 """
244 Calculate pressure flow constraint scaling from flow variable scale.
245 """
246 # The value of the valve opening and the output of the valve function
247 # expression are between 0 and 1, so the only thing that needs to be
248 # scaled here is the pressure-flow constraint, which can be scaled by
249 # using the flow variable scale. The flow variable could be defined
250 # in different ways, so the flow variable is determined here from a
251 # "flow_var[t]" reference set in the pressure-flow callback. The flow
252 # term could be in various forms, so an optional
253 # "pressure_flow_equation_scale" function can be defined in the callback
254 # as well. The pressure-flow function could be flow = f(Pin, Pout), but
255 # it could also be flow**2 = f(Pin, Pout), ... The so
256 # "pressure_flow_equation_scale" provides the form of the LHS side as
257 # a function of the flow variable.
259 super().calculate_scaling_factors()
261 # Do some error trapping.
262 if not hasattr(self, "pressure_flow_equation"):
263 raise AttributeError(
264 "Pressure-flow callback must define pressure_flow_equation[t]"
265 )
266 # Check for flow term form if none assume flow = f(Pin, Pout)
267 if hasattr(self, "pressure_flow_equation_scale"):
268 ff = self.pressure_flow_equation_scale
269 else:
270 # pylint: disable-next=unnecessary-lambda-assignment
271 ff = lambda x: x
272 # if the "flow_var" is not set raise an exception
273 if not hasattr(self, "flow_var"):
274 raise AttributeError(
275 "Pressure-flow callback must define flow_var[t] reference"
276 )
278 # Calculate and set the pressure-flow relation scale.
279 if hasattr(self, "pressure_flow_equation"):
280 for t, c in self.pressure_flow_equation.items():
281 iscale.constraint_scaling_transform(
282 c,
283 ff(
284 iscale.get_scaling_factor(
285 self.flow_var[t], default=1, warning=True
286 )
287 ),
288 )
290 def _get_performance_contents(self, time_point=0):
291 pc = super()._get_performance_contents(time_point=time_point)
293 pc["vars"]["Opening"] = self.valve_opening[time_point]
294 try:
295 pc["vars"]["Valve Coefficient"] = self.Cv
296 except AttributeError:
297 pass
298 if self.config.valve_function_callback == ValveFunctionType.equal_percentage:
299 pc["vars"]["alpha"] = self.alpha
300 return pc