Coverage for backend/ahuora-builder/src/ahuora_builder/custom/custom_separator.py: 62%
112 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from idaes.models.unit_models.separator import SeparatorData, SplittingType
3from functools import partial
4from pandas import DataFrame
6from pyomo.environ import (
7 Block,
8 check_optimal_termination,
9 Constraint,
10 Param,
11 Reals,
12 Reference,
13 Set,
14 Var,
15 value,
16)
17from pyomo.network import Port
18from pyomo.common.config import ConfigBlock, ConfigValue, In, ListOf, Bool
20from idaes.core import (
21 declare_process_block_class,
22 UnitModelBlockData,
23 useDefault,
24 MaterialBalanceType,
25 MomentumBalanceType,
26 MaterialFlowBasis,
27 VarLikeExpression,
28)
29from idaes.core.util.config import (
30 is_physical_parameter_block,
31 is_state_block,
32)
33from idaes.core.util.exceptions import (
34 BurntToast,
35 ConfigurationError,
36 PropertyNotSupportedError,
37 InitializationError,
38)
39from idaes.core.solvers import get_solver
40from idaes.core.util.tables import create_stream_table_dataframe
41from idaes.core.util.model_statistics import degrees_of_freedom
42import idaes.logger as idaeslog
43import idaes.core.util.scaling as iscale
44from idaes.core.util.units_of_measurement import report_quantity
45from idaes.core.initialization import ModularInitializerBase
48# This only changes a couple of lines in the original SeparatorData class, to not fix state variables by default.
49# The state block initialisation already does this if needed, so we can just set their value.
50# This is because if the state block has extra constraints, such as for flow_mass, then fixing flow_mol will over-define the system.
51# It might be worth making this a pr to idaes.
53@declare_process_block_class("CustomSeparator")
54class CustomSeparatorData(SeparatorData):
56 def initialize_build(
57 blk, outlvl=idaeslog.NOTSET, optarg=None, solver=None, hold_state=False
58 ):
59 """
60 Initialization routine for separator
62 Keyword Arguments:
63 outlvl : sets output level of initialization routine
64 optarg : solver options dictionary object (default=None, use
65 default solver options)
66 solver : str indicating which solver to use during
67 initialization (default = None, use default solver)
68 hold_state : flag indicating whether the initialization routine
69 should unfix any state variables fixed during
70 initialization, **default** - False. **Valid values:**
71 **True** - states variables are not unfixed, and a dict of
72 returned containing flags for which states were fixed
73 during initialization, **False** - state variables are
74 unfixed after initialization by calling the release_state
75 method.
77 Returns:
78 If hold_states is True, returns a dict containing flags for which
79 states were fixed during initialization.
80 """
81 init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
82 solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
84 # Create solver
85 opt = get_solver(solver, optarg)
87 # Initialize mixed state block
88 if blk.config.mixed_state_block is not None: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 mblock = blk.config.mixed_state_block
90 else:
91 mblock = blk.mixed_state
92 flags = mblock.initialize(
93 outlvl=outlvl,
94 optarg=optarg,
95 solver=solver,
96 hold_state=True,
97 )
100 # Solve for split fractions only
101 component_status = {}
102 for c in blk.component_objects((Block, Constraint)):
103 for i in c:
104 if not c[i].local_name == "sum_split_frac": 104 ↛ 103line 104 didn't jump to line 103 because the condition on line 104 was always true
105 # Record current status of components to restore later
106 component_status[c[i]] = c[i].active
107 c[i].deactivate()
109 if degrees_of_freedom(blk) != 0: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
111 res = opt.solve(blk, tee=slc.tee)
112 init_log.info(
113 "Initialization Step 1 Complete: {}".format(idaeslog.condition(res))
114 )
116 for c, s in component_status.items():
117 if s: 117 ↛ 116line 117 didn't jump to line 116 because the condition on line 117 was always true
118 c.activate()
120 if blk.config.ideal_separation: 120 ↛ 122line 120 didn't jump to line 122 because the condition on line 120 was never true
121 # If using ideal splitting, initialization should be complete
122 return flags
124 # Initialize outlet StateBlocks
125 outlet_list = blk.create_outlet_list()
127 # Premises for initializing outlet states:
128 # 1. Intensive states remain unchanged - this is either a valid premise
129 # or the actual state is impossible to calculate without solving the
130 # full separator model.
131 # 2. Extensive states are use split fractions if index matches, or
132 # average of split fractions for outlet otherwise
133 for o in outlet_list:
134 # Get corresponding outlet StateBlock
135 o_block = getattr(blk, o + "_state")
137 # Create dict to store fixed status of state variables
138 o_flags = {}
139 for t in blk.flowsheet().time:
141 # Calculate values for state variables
142 s_vars = o_block[t].define_state_vars()
143 for v in s_vars:
144 for k in s_vars[v]:
145 # Record whether variable was fixed or not
146 o_flags[t, v, k] = s_vars[v][k].fixed
148 # If fixed, use current value
149 # otherwise calculate guess from mixed state and fix
150 if not s_vars[v][k].fixed: 150 ↛ 144line 150 didn't jump to line 144 because the condition on line 150 was always true
151 m_var = getattr(mblock[t], s_vars[v].local_name)
152 if "flow" in v:
153 # If a "flow" variable, is extensive
154 # Apply split fraction
155 if blk.config.split_basis == SplittingType.totalFlow:
156 # All flows split by outlet
157 s_vars[v][k].set_value(
158 value(m_var[k] * blk.split_fraction[(t, o)])
159 )
160 elif "_phase_comp" in v: 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was never true
161 # Need to match indices, but use split frac
162 if (
163 blk.config.split_basis
164 == SplittingType.phaseComponentFlow
165 ):
166 s_vars[v][k].set_value(
167 value(
168 m_var[k]
169 * blk.split_fraction[(t, o) + (k,)]
170 )
171 )
172 elif (
173 blk.config.split_basis
174 == SplittingType.phaseFlow
175 ):
176 s_vars[v][k].set_value(
177 value(
178 m_var[k]
179 * blk.split_fraction[(t, o) + (k[0],)]
180 )
181 )
182 elif (
183 blk.config.split_basis
184 == SplittingType.componentFlow
185 ):
186 s_vars[v][k].set_value(
187 value(
188 m_var[k]
189 * blk.split_fraction[(t, o) + (k[1],)]
190 )
191 )
192 else:
193 raise BurntToast(
194 "{} encountered unrecognised "
195 "SplittingType. This should not "
196 "occur - please send this bug to "
197 "the IDAES developers.".format(blk.name)
198 )
199 elif "_phase" in v: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true
200 if (
201 blk.config.split_basis
202 == SplittingType.phaseComponentFlow
203 ):
204 # Need average split fraction
205 avg_split = value(
206 sum(
207 blk.split_fraction[t, o, k, j]
208 for j in mblock.component_list
209 )
210 / len(mblock.component_list)
211 )
212 s_vars[v][k].set_value(value(m_var[k] * avg_split))
213 elif (
214 blk.config.split_basis
215 == SplittingType.phaseFlow
216 ):
217 s_vars[v][k].set_value(
218 value(
219 m_var[k]
220 * blk.split_fraction[(t, o) + (k,)]
221 )
222 )
223 elif (
224 blk.config.split_basis
225 == SplittingType.componentFlow
226 ):
227 # Need average split fraction
228 avg_split = value(
229 sum(
230 blk.split_fraction[t, o, j]
231 for j in mblock.component_list
232 )
233 / len(mblock.component_list)
234 )
235 s_vars[v][k].set_value(value(m_var[k] * avg_split))
236 else:
237 raise BurntToast(
238 "{} encountered unrecognised "
239 "SplittingType. This should not "
240 "occur - please send this bug to "
241 "the IDAES developers.".format(blk.name)
242 )
243 elif "_comp" in v: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true
244 if (
245 blk.config.split_basis
246 == SplittingType.phaseComponentFlow
247 ):
248 # Need average split fraction
249 avg_split = value(
250 sum(
251 blk.split_fraction[t, o, p, k]
252 for p in mblock.phase_list
253 )
254 / len(mblock.phase_list)
255 )
256 s_vars[v][k].set_value(value(m_var[k] * avg_split))
257 elif (
258 blk.config.split_basis
259 == SplittingType.phaseFlow
260 ):
261 # Need average split fraction
262 avg_split = value(
263 sum(
264 blk.split_fraction[t, o, p]
265 for p in mblock.phase_list
266 )
267 / len(mblock.phase_list)
268 )
269 s_vars[v][k].set_value(value(m_var[k] * avg_split))
270 elif (
271 blk.config.split_basis
272 == SplittingType.componentFlow
273 ):
274 s_vars[v][k].set_value(
275 value(
276 m_var[k]
277 * blk.split_fraction[(t, o) + (k,)]
278 )
279 )
280 else:
281 raise BurntToast(
282 "{} encountered unrecognised "
283 "SplittingType. This should not "
284 "occur - please send this bug to "
285 "the IDAES developers.".format(blk.name)
286 )
287 else:
288 # Assume unindexed extensive state
289 # Need average split
290 if ( 290 ↛ 295line 290 didn't jump to line 295 because the condition on line 290 was never true
291 blk.config.split_basis
292 == SplittingType.phaseComponentFlow
293 ):
294 # Need average split fraction
295 avg_split = value(
296 sum(
297 blk.split_fraction[t, o, p, j]
298 for (p, j) in mblock.phase_component_set
299 )
300 / len(mblock.phase_component_set)
301 )
302 elif (
303 blk.config.split_basis
304 == SplittingType.phaseFlow
305 ):
306 # Need average split fraction
307 avg_split = value(
308 sum(
309 blk.split_fraction[t, o, p]
310 for p in mblock.phase_list
311 )
312 / len(mblock.phase_list)
313 )
314 elif ( 314 ↛ 327line 314 didn't jump to line 327 because the condition on line 314 was always true
315 blk.config.split_basis
316 == SplittingType.componentFlow
317 ):
318 # Need average split fraction
319 avg_split = value(
320 sum(
321 blk.split_fraction[t, o, j]
322 for j in mblock.component_list
323 )
324 / len(mblock.component_list)
325 )
326 else:
327 raise BurntToast(
328 "{} encountered unrecognised "
329 "SplittingType. This should not "
330 "occur - please send this bug to "
331 "the IDAES developers.".format(blk.name)
332 )
333 s_vars[v][k].set_value(value(m_var[k] * avg_split))
334 else:
335 # Otherwise intensive, equate to mixed stream
336 s_vars[v][k].set_value(m_var[k].value)
338 # Call initialization routine for outlet StateBlock
339 o_block.initialize(
340 outlvl=outlvl,
341 optarg=optarg,
342 solver=solver,
343 hold_state=False,
344 )
346 # Revert fixed status of variables to what they were before
347 for t in blk.flowsheet().time:
348 s_vars = o_block[t].define_state_vars()
349 for v in s_vars:
350 for k in s_vars[v]:
351 s_vars[v][k].fixed = o_flags[t, v, k]
353 if blk.config.mixed_state_block is None: 353 ↛ 367line 353 didn't jump to line 367 because the condition on line 353 was always true
354 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
355 res = opt.solve(blk, tee=slc.tee)
357 if not check_optimal_termination(res): 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true
358 raise InitializationError(
359 f"{blk.name} failed to initialize successfully. Please "
360 f"check the output logs for more information."
361 )
363 init_log.info(
364 "Initialization Step 2 Complete: {}".format(idaeslog.condition(res))
365 )
366 else:
367 init_log.info("Initialization Complete.")
369 if hold_state is True: 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true
370 return flags
371 else:
372 blk.release_state(flags, outlvl=outlvl)
375 @staticmethod
376 def ahuora_metadata():
377 from ahuora_unit_ops.json_config import (
378 JsonAdapterArgConfig,
379 JsonFrontendConfig,
380 JsonGraphicObjectConfig,
381 JsonIdaesAdapterConfig,
382 JsonPortAdapterConfig,
383 JsonPortConfig,
384 JsonPropertyConfig,
385 JsonPropertySetGroupConfig,
386 JsonUnitOpConfig,
387 )
389 return JsonUnitOpConfig(
390 key='splitter',
391 objectType='splitter',
392 enumMember='Splitter',
393 displayType='Splitter',
394 displayName='Splitter',
395 categoryPath=['chemical', 'mixer'],
396 ports={
397 'inlet': JsonPortConfig(
398 displayName='Inlet',
399 type='inlet',
400 streamType='stream',
401 many=False,
402 default=1,
403 minimum=1,
404 makeStream=True,
405 streamOffset=0.75,
406 streamName='S',
407 ),
408 'outlet': JsonPortConfig(
409 displayName='Outlet',
410 type='outlet',
411 streamType='stream',
412 many=True,
413 default=2,
414 minimum=2,
415 makeStream=True,
416 streamOffset=0.75,
417 streamName='S',
418 ),
419 },
420 propertyPackagePorts={
421 '': ['inlet', 'outlet'],
422 },
423 graphicObject=JsonGraphicObjectConfig(
424 kind='unitop_graphic',
425 ),
426 indexSets=['splitter_fraction'],
427 properties={
428 'split_fraction': JsonPropertyConfig(
429 propertySetGroup='default',
430 displayName='Split Ratio',
431 indexSets=['splitter_fraction'],
432 sumToOne=True,
433 value=None,
434 unit=None,
435 unitType='ratio',
436 description=None,
437 type='numeric',
438 many=False,
439 default=1,
440 options={},
441 hasTimeIndex=True,
442 ),
443 },
444 propertySetGroups={
445 'default': JsonPropertySetGroupConfig(
446 type='exceptLast',
447 displayName='Properties',
448 stateVars=['split_fraction'],
449 toggle=None,
450 ),
451 },
452 keyProperties=None,
453 splitterFractionName='Outlet',
454 idaesAdapter=JsonIdaesAdapterConfig(
455 constructor='ahuora_builder.custom.custom_separator.CustomSeparator',
456 args={
457 'property_package': JsonAdapterArgConfig(
458 kind='property_package',
459 label=None,
460 ),
461 'num_outlets': JsonAdapterArgConfig(
462 kind='port_count',
463 port='outlet',
464 ),
465 },
466 ports=JsonPortAdapterConfig(
467 kind='splitter',
468 ),
469 properties=None,
470 ),
471 frontend=JsonFrontendConfig(
472 showInPanel=True,
473 variant=None,
474 ),
475 )