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

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 

20 

21__Author__ = "John Eslick" 

22 

23from enum import Enum 

24 

25import pyomo.environ as pyo 

26from pyomo.common.config import ConfigValue, In 

27 

28from idaes.core import declare_process_block_class 

29from .updated_pressure_changer import ( 

30 ThermodynamicAssumption, 

31 MaterialBalanceType, 

32) 

33 

34from .updated_pressure_changer import PressureChangerData 

35 

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 

40 

41_log = idaeslog.getLogger(__name__) 

42 

43 

44class ValveFunctionType(Enum): 

45 """ 

46 Enum of supported valve types. 

47 """ 

48 

49 linear = 1 

50 quick_opening = 2 

51 equal_percentage = 3 

52 

53 

54def linear_cb(valve): 

55 """ 

56 Linear opening valve function callback. 

57 """ 

58 

59 @valve.Expression(valve.flowsheet().time) 

60 def valve_function(b, t): 

61 return b.valve_opening[t] 

62 

63 

64def quick_cb(valve): 

65 """ 

66 Quick opening valve function callback. 

67 """ 

68 

69 @valve.Expression(valve.flowsheet().time) 

70 def valve_function(b, t): 

71 return pyo.sqrt(b.valve_opening[t]) 

72 

73 

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() 

80 

81 @valve.Expression(valve.flowsheet().time) 

82 def valve_function(b, t): 

83 return b.alpha ** (b.valve_opening[t] - 1) 

84 

85 

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 ) 

94 

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() 

101 

102 valve.flow_var = pyo.Reference(valve.control_volume.properties_in[:].flow_mol) 

103 valve.pressure_flow_equation_scale = lambda x: x**2 

104 

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 

113 

114 

115@declare_process_block_class("Valve", doc="Adiabatic valves") 

116class ValveData(PressureChangerData): 

117 """ 

118 Basic valve model class. 

119 """ 

120 

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 ) 

157 

158 def build(self): 

159 super().build() 

160 

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 ) 

167 

168 

169 #commented out to allow for valve opening to be set by the user 

170 #self.valve_opening.fix() 

171 

172 

173 

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) 

189 

190 # add deltaP_inverted as a property 

191 add_inverted(self,"deltaP") 

192 

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. 

202 

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") 

210 

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) 

235 

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 ) 

241 

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. 

258 

259 super().calculate_scaling_factors() 

260 

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 ) 

277 

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 ) 

289 

290 def _get_performance_contents(self, time_point=0): 

291 pc = super()._get_performance_contents(time_point=time_point) 

292 

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