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
« 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
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
21# Set up logger
22_log = idaeslog.getLogger(__name__)
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
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.
38 It's basically a combination of a mixer and a translator.
39 """
41 # CONFIG are options for the unit model
42 CONFIG = ConfigBlock()
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 )
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()
122 # This creates blank scaling factors, which are populated later
123 self.scaling_factor = Suffix(direction=Suffix.EXPORT)
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 )
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 )
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 )
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
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 )
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.
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 )
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 )
187 # CONDITIONS
189 # STEAM INTERMEDIATE BLOCK
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 )
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 )
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 )
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
240 # MIXING (without changing temperature)
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 )
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 )
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)
286 # OUTLET BLOCK
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
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)
306 # Flow = mixed flow
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)
323 def calculate_scaling_factors(self):
324 super().calculate_scaling_factors()
326 def initialize(blk, *args, **kwargs):
327 blk.properties_milk_in.initialize()
328 blk.properties_steam_in.initialize()
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.
345 blk.properties_steam_cooled.initialize()
346 blk.properties_mixed_unheated.initialize()
348 blk.properties_out.initialize()
349 pass
351 def _get_stream_table_contents(self, time_point=0):
352 """
353 Assume unit has standard configuration of 1 inlet and 1 outlet.
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 )