Coverage for backend/idaes_service/solver/custom/custom_heat_exchanger_1d.py: 15%
103 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
1from pyomo.environ import Var, Constraint, Reference, units as pyunits
2from pyomo.common.numeric_types import value
3from pyomo.opt.results.solver import check_optimal_termination
4from idaes.core import declare_process_block_class
5from idaes.core.util import scaling as iscale
6from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData, HX1DInitializer
7from idaes.models.unit_models.heat_exchanger import HeatExchangerFlowPattern
8from idaes.core.solvers import get_solver
9from idaes.core.util.exceptions import InitializationError
10import idaes.logger as idaeslog
12class CustomHX1DInitializer(HX1DInitializer):
13 """
14 Use our custom control-volume initialize (no source port-member fixing).
15 """
16 def initialize_control_volume(self, cv, state_args=None):
17 return initialize(
18 cv,
19 state_args=state_args,
20 outlvl=self.get_output_level(),
21 )
23@declare_process_block_class(
24 "CustomHeatExchanger1D",
25 doc="1D Heat Exchanger with overall U tied to local heat_transfer_coefficient.",
26)
27class CustomHeatExchanger1DData(HeatExchanger1DData):
28 # Use our initializer so both sides use the custom CV initialize
29 default_initializer = CustomHX1DInitializer
31 CONFIG = HeatExchanger1DData.CONFIG()
33 def build(self):
34 super().build()
35 # Ends of the tube along the length axis (start and end positions)
36 x_first = self.hot_side.length_domain.first()
37 x_last = self.hot_side.length_domain.last()
39 # Hot side: inlet at start, outlet at end
40 x_hot_in, x_hot_out = x_first, x_last
42 # Cold side: depends on flow pattern
43 if self.config.flow_type == HeatExchangerFlowPattern.cocurrent:
44 x_cold_in, x_cold_out = x_first, x_last
45 else:
46 x_cold_in, x_cold_out = x_last, x_first
48 # Time-only inlet/outlet views of the boundary states (no extra vars/cons)
49 self.hot_side.properties_in = Reference(self.hot_side.properties[:, x_hot_in])
50 self.hot_side.properties_out = Reference(self.hot_side.properties[:, x_hot_out])
51 self.cold_side.properties_in = Reference(self.cold_side.properties[:, x_cold_in])
52 self.cold_side.properties_out = Reference(self.cold_side.properties[:, x_cold_out])
54 # Overall U
55 self.overall_heat_transfer_coefficient = Var(
56 self.flowsheet().time,
57 initialize=500.0,
58 bounds=(1.0, 1e5),
59 units=pyunits.W / pyunits.m**2 / pyunits.K,
60 doc="Overall (constant along length) heat transfer coefficient U.",
61 )
63 @self.Constraint(self.flowsheet().time, self.hot_side.length_domain)
64 def overall_heat_transfer_coefficient_def(b, t, x):
65 return b.overall_heat_transfer_coefficient[t] == b.heat_transfer_coefficient[t, x]
67 iscale.set_scaling_factor(self.overall_heat_transfer_coefficient, 1e-3)
69 def initialize_build(
70 self,
71 hot_side_state_args=None,
72 cold_side_state_args=None,
73 outlvl=idaeslog.NOTSET,
74 solver=None,
75 optarg=None,
76 duty=None,
77 ):
78 init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
79 solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")
80 opt = get_solver(solver, optarg)
82 # Sync length values
83 if self.length.fixed:
84 self.cold_side.length.set_value(self.length)
85 elif self.cold_side.length.fixed:
86 self.length.set_value(self.cold_side.length)
88 # Initialize control volumes with length fixed
89 Lfix = self.hot_side.length.fixed
90 self.hot_side.length.fix()
91 flags_hot_side = initialize(
92 self.hot_side,
93 outlvl=outlvl,
94 optarg=optarg,
95 solver=solver,
96 state_args=hot_side_state_args,
97 )
98 if not Lfix:
99 self.hot_side.length.unfix()
101 Lfix = self.cold_side.length.fixed
102 self.cold_side.length.fix()
103 # Use our custom CV initialize here as well
104 flags_cold_side = initialize(
105 self.cold_side,
106 outlvl=outlvl,
107 optarg=optarg,
108 solver=solver,
109 state_args=cold_side_state_args,
110 )
111 if not Lfix:
112 self.cold_side.length.unfix()
114 init_log.info_high("Initialization Step 1 Complete.")
116 # Fixed-duty solve
117 hot_units = self.hot_side.config.property_package.get_metadata().get_derived_units
118 cold_units = self.cold_side.config.property_package.get_metadata().get_derived_units
119 t0 = self.flowsheet().time.first()
121 # Use inlet indices for each side
122 x_hot_in = self.hot_side.length_domain.first()
123 x_cold_in = self.cold_side.length_domain.first() if self.config.flow_type == HeatExchangerFlowPattern.cocurrent else self.cold_side.length_domain.last()
125 if duty is None:
126 duty = value(
127 0.25
128 * self.heat_transfer_coefficient[t0, x_hot_in]
129 * self.area
130 * (
131 self.hot_side.properties[t0, x_hot_in].temperature
132 - pyunits.convert(
133 self.cold_side.properties[t0, x_cold_in].temperature,
134 to_units=hot_units("temperature"),
135 )
136 )
137 )
138 else:
139 duty = pyunits.convert_value(duty[0], from_units=duty[1], to_units=hot_units("power"))
141 duty_per_length = value(duty / self.length)
143 # Fix heat duties
144 for v in self.hot_side.heat.values():
145 v.fix(-duty_per_length)
146 for v in self.cold_side.heat.values():
147 v.fix(pyunits.convert_value(duty_per_length, to_units=cold_units("power")/cold_units("length"), from_units=hot_units("power")/hot_units("length")))
149 # Deactivate heat duty constraints and solve
150 self.heat_transfer_eq.deactivate()
151 self.heat_conservation.deactivate()
152 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
153 res = opt.solve(self, tee=slc.tee)
154 init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res)))
156 # Unfix heat duties and re-activate constraints
157 for v in self.hot_side.heat.values():
158 v.unfix()
159 for v in self.cold_side.heat.values():
160 v.unfix()
161 self.heat_transfer_eq.activate()
162 self.heat_conservation.activate()
163 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
164 res = opt.solve(self, tee=slc.tee)
165 init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))
167 release_state(self.hot_side, flags_hot_side)
168 release_state(self.cold_side, flags_cold_side)
170 if res is not None and not check_optimal_termination(res):
171 raise InitializationError(f"{self.name} failed to initialize successfully. See logs.")
173 init_log.info("Initialization Complete.")
176def initialize(
177 blk,
178 state_args=None,
179 outlvl=idaeslog.NOTSET,
180 optarg=None,
181 solver=None,
182 hold_state=True,
183 ):
184 """
185 Initialization routine for 1D control volume.
187 Keyword Arguments:
188 state_args: a dict of arguments to be passed to the property
189 package(s) to provide an initial state for initialization
190 (see documentation of the specific property package) (default = {}).
191 outlvl: sets output level of initialization routine
192 optarg: solver options dictionary object (default=None, use
193 default solver options)
194 solver: str indicating which solver to use during initialization
195 (default = None)
196 hold_state: flag indicating whether the initialization routine
197 should unfix any state variables fixed during initialization,
198 (default = True). **Valid values:**
199 **True** - states variables are not unfixed, and a dict of
200 returned containing flags for which states were fixed
201 during initialization, **False** - state variables are
202 unfixed after initialization by calling the release_state
203 method.
205 Returns:
206 If hold_states is True, returns a dict containing flags for which
207 states were fixed during initialization else the release state is
208 triggered.
209 """
210 if optarg is None:
211 optarg = {}
213 # Get inlet state if not provided
214 init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="control_volume")
216 # Provide guesses if none
217 if state_args is None:
218 blk.estimate_states(always_estimate=True)
221 if state_args is None:
222 # If no initial guesses provided, estimate values for states
223 blk.estimate_states(always_estimate=True)
225 # Initialize state blocks
226 flags = blk.properties.initialize(
227 state_args=state_args,
228 outlvl=outlvl,
229 optarg=optarg,
230 solver=solver,
231 hold_state=True,
232 )
234 try:
235 # TODO: setting state_vars_fixed may not work for heterogeneous
236 # systems where a second control volume is involved, as we cannot
237 # assume those state vars are also fixed. For now, heterogeneous
238 # reactions should ignore the state_vars_fixed argument and always
239 # check their state_vars.
240 blk.reactions.initialize(
241 outlvl=outlvl,
242 optarg=optarg,
243 solver=solver,
244 state_vars_fixed=True,
245 )
246 except AttributeError:
247 pass
249 init_log.info("Initialization Complete")
251 # Unfix state variables except for source block
252 blk.properties.release_state(flags)
254 return {}
256def release_state(blk, flags, outlvl=idaeslog.NOTSET):
257 # No-op: nothing was fixed at the CV level in our custom initialize
258 return