Coverage for backend/idaes_service/solver/custom/direct_steam_injection.py: 95%

89 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +0000

1# Import Pyomo libraries 

2from pyomo.environ import ( 

3 Var, 

4 Suffix, 

5 units as pyunits, 

6) 

7from pyomo.common.config import ConfigBlock, ConfigValue, In 

8from idaes.core.util.tables import create_stream_table_dataframe 

9from idaes.core.util.exceptions import ConfigurationError 

10 

11# Import IDAES cores 

12from idaes.core import ( 

13 declare_process_block_class, 

14 UnitModelBlockData, 

15 useDefault, 

16) 

17from idaes.core.util.config import is_physical_parameter_block 

18import idaes.core.util.scaling as iscale 

19import idaes.logger as idaeslog 

20 

21# Set up logger 

22_log = idaeslog.getLogger(__name__) 

23 

24 

25# When using this file the name "Load" is what is imported 

26@declare_process_block_class("Dsi") 

27class dsiData(UnitModelBlockData): 

28 """ 

29 Direct Steam Injection Unit Model 

30 

31 This unit model is used to represent a direct steam injection 

32 process. There are no degrees of freedom, but the steam is mixed with the inlet fluid to heat it up. 

33 It is assumed that the pressure of the fluid doesn't change, i.e the steam loses its pressure. 

34 However, the enthalpy of the steam remains the same. 

35 This allows to use two different property packages for the steam and for the inlet fluid, however, 

36 it only works if the reference enthalpy of the steam and the inlet fluid are the same. 

37 

38 It's basically a combination of a mixer and a translator. 

39 """ 

40 

41 # CONFIG are options for the unit model 

42 CONFIG = ConfigBlock() 

43 

44 CONFIG.declare( 

45 "dynamic", 

46 ConfigValue( 

47 domain=In([False]), 

48 default=False, 

49 description="Dynamic model flag - must be False", 

50 doc="""Indicates whether this model will be dynamic or not, 

51 **default** = False. The Bus unit does not support dynamic 

52 behavior, thus this must be False.""", 

53 ), 

54 ) 

55 CONFIG.declare( 

56 "has_holdup", 

57 ConfigValue( 

58 default=False, 

59 domain=In([False]), 

60 description="Holdup construction flag - must be False", 

61 doc="""Indicates whether holdup terms should be constructed or not. 

62 **default** - False. The Bus unit does not have defined volume, thus 

63 this must be False.""", 

64 ), 

65 ) 

66 CONFIG.declare( 

67 "property_package", 

68 ConfigValue( 

69 default=useDefault, 

70 domain=is_physical_parameter_block, 

71 description="Property package to use for control volume", 

72 doc="""Property parameter object used to define property calculations, 

73 **default** - useDefault. 

74 **Valid values:** { 

75 **useDefault** - use default package from parent model or flowsheet, 

76 **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", 

77 ), 

78 ) 

79 CONFIG.declare( 

80 "property_package_args", 

81 ConfigBlock( 

82 implicit=True, 

83 description="Arguments to use for constructing property packages", 

84 doc="""A ConfigBlock with arguments to be passed to a property block(s) 

85 and used when constructing these, 

86 **default** - None. 

87 **Valid values:** { 

88 see property package for documentation.}""", 

89 ), 

90 ) 

91 CONFIG.declare( 

92 "steam_property_package", 

93 ConfigValue( 

94 default=useDefault, 

95 domain=is_physical_parameter_block, 

96 description="Property package to use for control volume", 

97 doc="""Property parameter object used to define property calculations, 

98 **default** - useDefault. 

99 **Valid values:** { 

100 **useDefault** - use default package from parent model or flowsheet, 

101 **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", 

102 ), 

103 ) 

104 CONFIG.declare( 

105 "steam_property_package_args", 

106 ConfigBlock( 

107 implicit=True, 

108 description="Arguments to use for constructing property packages", 

109 doc="""A ConfigBlock with arguments to be passed to a property block(s) 

110 and used when constructing these, 

111 **default** - None. 

112 **Valid values:** { 

113 see property package for documentation.}""", 

114 ), 

115 ) 

116 

117 def build(self): 

118 # build always starts by calling super().build() 

119 # This triggers a lot of boilerplate in the background for you 

120 super().build() 

121 

122 # This creates blank scaling factors, which are populated later 

123 self.scaling_factor = Suffix(direction=Suffix.EXPORT) 

124 

125 # Add state blocks for inlet, outlet, and waste 

126 # These include the state variables and any other properties on demand 

127 # Add inlet block 

128 tmp_dict = dict(**self.config.property_package_args) 

129 tmp_dict["parameters"] = self.config.property_package 

130 tmp_dict["defined_state"] = True # inlet block is an inlet 

131 self.properties_milk_in = self.config.property_package.state_block_class( 

132 self.flowsheet().config.time, doc="Material properties of inlet", **tmp_dict 

133 ) 

134 

135 # We need to calculate the enthalpy of the composition, before adding additional enthalpy from the temperature difference. 

136 # so we'll add another state block to do that. 

137 tmp_dict["defined_state"] = False 

138 tmp_dict["has_phase_equilibrium"] = False 

139 self.properties_mixed_unheated = self.config.property_package.state_block_class( 

140 self.flowsheet().config.time, 

141 doc="Material properties of mixture, before accounting for temperature difference", 

142 **tmp_dict, 

143 ) 

144 

145 # Add outlet block 

146 tmp_dict["defined_state"] = False 

147 tmp_dict["has_phase_equilibrium"] = False 

148 self.properties_out = self.config.property_package.state_block_class( 

149 self.flowsheet().config.time, 

150 doc="Material properties of outlet", 

151 **tmp_dict, 

152 ) 

153 

154 # Add steam inlet block 

155 steam_dict = dict(**self.config.steam_property_package_args) 

156 steam_dict["parameters"] = self.config.steam_property_package 

157 steam_dict["defined_state"] = True 

158 tmp_dict["has_phase_equilibrium"] = True 

159 

160 self.properties_steam_in = self.config.steam_property_package.state_block_class( 

161 self.flowsheet().config.time, 

162 doc="Material properties of steam inlet", 

163 **steam_dict, 

164 ) 

165 

166 # To calculate the amount of enthalpy to add to the inlet fluid, we need to know the difference in enthalpy between steam at that T and P 

167 # and steam at its inlet conditions. Note this is assuming that effects of composition (the steam will no longer be pure water) are negligible. 

168 # Note that this state block is just for calcuating, and not an actual inlet or outlet. 

169 

170 steam_dict["defined_state"] = False # This doesn't affect pure components. 

171 steam_dict["has_phase_equilibrium"] = True 

172 self.properties_steam_cooled = ( 

173 self.config.steam_property_package.state_block_class( 

174 self.flowsheet().config.time, 

175 doc="Material properties of cooled steam", 

176 **steam_dict, 

177 ) 

178 ) 

179 

180 # Add ports 

181 self.add_port(name="outlet", block=self.properties_out) 

182 self.add_port(name="inlet", block=self.properties_milk_in, doc="Inlet port") 

183 self.add_port( 

184 name="steam_inlet", block=self.properties_steam_in, doc="Steam inlet port" 

185 ) 

186 

187 # CONDITIONS 

188 

189 # STEAM INTERMEDIATE BLOCK 

190 

191 # Temperature (= other inlet temperature) 

192 @self.Constraint( 

193 self.flowsheet().time, 

194 doc="Set the temperature of the cooled steam to be the same as the inlet fluid", 

195 ) 

196 def eq_steam_cooled_temperature(b, t): 

197 return ( 

198 b.properties_steam_cooled[t].temperature 

199 == b.properties_milk_in[t].temperature 

200 ) 

201 

202 # Pressure (= other inlet pressure) 

203 @self.Constraint( 

204 self.flowsheet().time, 

205 doc="Set the pressure of the cooled steam to be the same as the inlet fluid", 

206 ) 

207 def eq_steam_cooled_pressure(b, t): 

208 return ( 

209 b.properties_steam_cooled[t].pressure 

210 == b.properties_milk_in[t].pressure 

211 ) 

212 

213 # Flow = steam_flow 

214 @self.Constraint( 

215 self.flowsheet().time, 

216 self.config.steam_property_package.component_list, 

217 doc="Set the composition of the cooled steam to be the same as the steam inlet", 

218 ) 

219 def eq_steam_cooled_composition(b, t, c): 

220 return 0 == sum( 

221 b.properties_steam_cooled[t].get_material_flow_terms(p, c) 

222 - b.properties_steam_in[t].get_material_flow_terms(p, c) 

223 for p in b.properties_steam_in[t].phase_list 

224 ) 

225 

226 # CALCULATE ENTHALPY DIFFERENCE 

227 @self.Expression( 

228 self.flowsheet().time, 

229 ) 

230 def steam_delta_h(b, t): 

231 """ 

232 Calculate the difference in enthalpy between the steam inlet and the cooled steam. 

233 This is used to calculate the amount of enthalpy to add to the inlet fluid. 

234 """ 

235 return ( 

236 b.properties_steam_in[t].enth_mol 

237 - b.properties_steam_cooled[t].enth_mol 

238 ) * b.properties_steam_in[t].flow_mol 

239 

240 # MIXING (without changing temperature) 

241 

242 # Pressure (= inlet pressure) 

243 @self.Constraint( 

244 self.flowsheet().time, 

245 doc="Equivalent pressure balance", 

246 ) 

247 def eq_mixed_pressure(b, t): 

248 return ( 

249 b.properties_mixed_unheated[t].pressure 

250 == b.properties_milk_in[t].pressure 

251 ) 

252 

253 # Temperature (= inlet temperature) 

254 @self.Constraint( 

255 self.flowsheet().time, 

256 doc="Equivalent temperature balance", 

257 ) 

258 def eq_mixed_temperature(b, t): 

259 return ( 

260 b.properties_mixed_unheated[t].temperature 

261 == b.properties_milk_in[t].temperature 

262 ) 

263 

264 # Flow = inlet flow + steam flow 

265 @self.Constraint( 

266 self.flowsheet().time, 

267 self.config.property_package.component_list, 

268 doc="Mass balance", 

269 ) 

270 def eq_mixed_composition(b, t, c): 

271 return 0 == sum( 

272 b.properties_milk_in[t].get_material_flow_terms(p, c) 

273 + ( 

274 b.properties_steam_in[t].get_material_flow_terms(p, c) 

275 if c 

276 in b.properties_steam_in[ 

277 t 

278 ].component_list # handle the case where a component isn't in the steam inlet (e.g no milk in helmholtz) 

279 else 0 

280 ) 

281 - b.properties_mixed_unheated[t].get_material_flow_terms(p, c) 

282 for p in b.properties_milk_in[t].phase_list 

283 if (p, c) in b.properties_milk_in[t].phase_component_set 

284 ) # handle the case where a component is not in that phase (e.g no milk vapor) 

285 

286 # OUTLET BLOCK 

287 

288 # Pressure (= inlet pressure) 

289 @self.Constraint( 

290 self.flowsheet().time, 

291 doc="Pressure balance", 

292 ) 

293 def eq_outlet_pressure(b, t): 

294 return b.properties_out[t].pressure == b.properties_milk_in[t].pressure 

295 

296 # Enthalpy (= mixed enthalpy + delta steam enthalpy) 

297 @self.Constraint( 

298 self.flowsheet().time, 

299 doc="Energy balance", 

300 ) 

301 def eq_outlet_combined_enthalpy(b, t): 

302 return b.properties_out[t].enth_mol == b.properties_mixed_unheated[ 

303 t 

304 ].enth_mol + (b.steam_delta_h[t] / b.properties_mixed_unheated[t].flow_mol) 

305 

306 # Flow = mixed flow 

307 

308 @self.Constraint( 

309 self.flowsheet().time, 

310 self.config.property_package.component_list, 

311 doc="Mass balance for the outlet", 

312 ) 

313 def eq_outlet_composition(b, t, c): 

314 return 0 == sum( 

315 b.properties_out[t].get_material_flow_terms(p, c) 

316 - b.properties_mixed_unheated[t].get_material_flow_terms(p, c) 

317 for p in b.properties_out[t].phase_list 

318 if (p, c) in b.properties_out[t].phase_component_set 

319 ) # handle the case where a component is not in that phase (e.g no milk vapor) 

320 

321 

322 

323 def calculate_scaling_factors(self): 

324 super().calculate_scaling_factors() 

325 

326 def initialize(blk, *args, **kwargs): 

327 blk.properties_milk_in.initialize() 

328 blk.properties_steam_in.initialize() 

329 

330 for t in blk.flowsheet().time: 

331 # copy temperature and pressure from properties_milk_in to properties_steam_cooled 

332 # blk.properties_steam_cooled[t].temperature.set_value( 

333 # blk.properties_milk_in[t].temperature.value 

334 # ) 

335 blk.properties_steam_cooled[t].pressure.set_value( 

336 blk.properties_milk_in[t].pressure.value 

337 ) 

338 # Copy composition from properties_steam_in to properties_steam_cooled 

339 blk.properties_steam_cooled[t].flow_mol.set_value( 

340 blk.properties_steam_in[t].flow_mol.value 

341 ) 

342 # If it's steam, there's only one component, so we prolly don't need to worry about composition. 

343 # But may want TODO this for other cases. 

344 

345 blk.properties_steam_cooled.initialize() 

346 blk.properties_mixed_unheated.initialize() 

347 

348 blk.properties_out.initialize() 

349 pass 

350 

351 def _get_stream_table_contents(self, time_point=0): 

352 """ 

353 Assume unit has standard configuration of 1 inlet and 1 outlet. 

354 

355 Developers should overload this as appropriate. 

356 """ 

357 try: 

358 return create_stream_table_dataframe( 

359 { 

360 "outlet": self.outlet, 

361 "inlet": self.inlet, 

362 "steam_inlet": self.steam_inlet, 

363 }, 

364 time_point=time_point, 

365 ) 

366 except AttributeError: 

367 raise ConfigurationError( 

368 f"Unit model {self.name} does not have the standard Port " 

369 f"names (inlet and outlet). Please contact the unit model " 

370 f"developer to develop a unit specific stream table." 

371 )