Skip to content

Commit 88a602b

Browse files
Mixer Scaler (#1676)
* additional test for propagating scaling factors within a block * Control Volume 1D scaler * phase material balance cv0d * test for submodel scaler for blockdata * Tests for flow direction attribute * black and typo * remove unused variables * protected property error message depends on python version * test no override * test propagate scaling factors * additional test coverage * Mixer scaler * test with inherent reactions * test inlet_blocks * Pylint * Apply suggestion from @bpaul4 Co-authored-by: Brandon Paul <[email protected]> * default_scaler attribute should live on indexed block * Apply suggestion from @dallan-keylogic * Apply suggestion from @dallan-keylogic --------- Co-authored-by: Brandon Paul <[email protected]>
1 parent 0a8eed3 commit 88a602b

File tree

5 files changed

+667
-233
lines changed

5 files changed

+667
-233
lines changed

idaes/core/base/control_volume_base.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class ControlVolumeScalerBase(CustomScalerBase):
120120
# terms. Presently (9/25/25), this attribute exists to take into
121121
# account the fact that all the material and energy terms in the
122122
# ControlVolume1D are given on the basis of material or energy per
123-
# unit length, so we want to weigh them accordingly.
123+
# unit length, so we want to weight them accordingly.
124124
_weight_attr_name = None
125125

126126
def variable_scaling_routine(
@@ -517,6 +517,7 @@ def constraint_scaling_routine(
517517
else:
518518
props = getattr(model, self._state_block_ref)
519519
phase_list = props.phase_list
520+
pc_set = props.phase_component_set
520521

521522
if hasattr(model, "reactions"):
522523
self.call_submodel_scaler_method(
@@ -525,7 +526,7 @@ def constraint_scaling_routine(
525526
method="constraint_scaling_routine",
526527
overwrite=overwrite,
527528
)
528-
# Transform constraints in order of appearance
529+
529530
if hasattr(model, "material_holdup_calculation"):
530531
for idx in model.material_holdup_calculation:
531532
self.scale_constraint_by_component(
@@ -550,18 +551,34 @@ def constraint_scaling_routine(
550551
overwrite=overwrite,
551552
)
552553

554+
inh_rxn_con = None
553555
if hasattr(model, "inherent_reaction_stoichiometry_constraint"):
554-
for idx in model.inherent_reaction_stoichiometry_constraint:
556+
# ControlVolume0D and ControlVolume1D
557+
inh_rxn_con = model.inherent_reaction_stoichiometry_constraint
558+
elif hasattr(model, "inherent_reaction_constraint"):
559+
# Mixer
560+
inh_rxn_con = model.inherent_reaction_constraint
561+
562+
if inh_rxn_con is not None:
563+
for idx in inh_rxn_con:
555564
self.scale_constraint_by_component(
556-
model.inherent_reaction_stoichiometry_constraint[idx],
565+
inh_rxn_con[idx],
557566
model.inherent_reaction_generation[idx],
558567
overwrite=overwrite,
559568
)
560569

570+
mb_eqn = None
561571
if hasattr(model, "material_balances"):
572+
# ControlVolume0D and ControlVolume1D
573+
mb_eqn = model.material_balances
574+
elif hasattr(model, "material_mixing_equations"):
575+
# Mixer
576+
mb_eqn = model.material_mixing_equations
577+
578+
if mb_eqn is not None:
562579
mb_type = model._constructed_material_balance_type # pylint: disable=W0212
563580
if mb_type == MaterialBalanceType.componentTotal:
564-
for idx in model.material_balances:
581+
for idx in mb_eqn:
565582
c = idx[-1]
566583
nom_list = []
567584
for p in phase_list:
@@ -572,22 +589,38 @@ def constraint_scaling_routine(
572589
)
573590
nom = max(nom_list)
574591
self.set_component_scaling_factor(
575-
model.material_balances[idx], 1 / nom, overwrite=overwrite
592+
mb_eqn[idx], 1 / nom, overwrite=overwrite
576593
)
577594
elif mb_type == MaterialBalanceType.componentPhase:
578-
for idx in model.material_balances:
595+
for idx in mb_eqn:
579596
p = idx[-2]
580597
c = idx[-1]
581598
nom = self.get_expression_nominal_value(
582599
props[idx[:-2]].get_material_flow_terms(p, c)
583600
)
584601
self.set_component_scaling_factor(
585-
model.material_balances[idx], 1 / nom, overwrite=overwrite
602+
mb_eqn[idx], 1 / nom, overwrite=overwrite
603+
)
604+
elif mb_type == MaterialBalanceType.total:
605+
for idx in mb_eqn:
606+
nom_list = []
607+
for p, c in pc_set:
608+
nom_list.append(
609+
self.get_expression_nominal_value(
610+
props[idx[:-1]].get_material_flow_terms(p, c)
611+
)
612+
)
613+
nom = max(nom_list)
614+
self.set_component_scaling_factor(
615+
mb_eqn[idx], 1 / nom, overwrite=overwrite
586616
)
587617
else:
588618
# There are some other material balance types but they create
589619
# constraints with different names.
590-
_log.warning(f"Unknown material balance type {mb_type}")
620+
_log.warning(
621+
f"Unknown material balance type {mb_type}. It cannot be "
622+
"automatically scaled."
623+
)
591624

592625
# TODO element balances
593626
# if hasattr(self, "element_balances"):
@@ -602,10 +635,16 @@ def constraint_scaling_routine(
602635
# sf = iscale.get_scaling_factor(self.element_holdup[t, e])
603636
# iscale.constraint_scaling_transform(c, sf, overwrite=False)
604637

638+
eb_eqn = None
605639
if hasattr(model, "enthalpy_balances"):
640+
eb_eqn = model.enthalpy_balances
641+
elif hasattr(model, "enthalpy_mixing_equations"):
642+
eb_eqn = model.enthalpy_mixing_equations
643+
644+
if eb_eqn is not None:
606645
# Phase enthalpy balances are not implemented
607646
# as of 9/26/25
608-
for idx in model.enthalpy_balances:
647+
for idx in eb_eqn:
609648
nom_list = []
610649
for p in phase_list:
611650
nom_list.append(
@@ -615,7 +654,7 @@ def constraint_scaling_routine(
615654
)
616655
nom = max(nom_list)
617656
self.set_component_scaling_factor(
618-
model.enthalpy_balances[idx], 1 / nom, overwrite=overwrite
657+
eb_eqn[idx], 1 / nom, overwrite=overwrite
619658
)
620659

621660
if hasattr(model, "energy_holdup_calculation"):
@@ -626,11 +665,16 @@ def constraint_scaling_routine(
626665
overwrite=overwrite,
627666
)
628667

668+
pb_eqn = None
629669
if hasattr(model, "pressure_balance"):
630-
for con in model.pressure_balance.values():
631-
self.scale_constraint_by_nominal_value(
670+
# ControlVolume0D and ControlVolume1D
671+
pb_eqn = model.pressure_balance
672+
673+
if pb_eqn is not None:
674+
for idx, con in pb_eqn.items():
675+
self.scale_constraint_by_component(
632676
con,
633-
scheme=ConstraintScalingScheme.inverseMaximum,
677+
props[idx].pressure,
634678
overwrite=overwrite,
635679
)
636680

idaes/core/scaling/custom_scaler_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def scale_variable_by_component(
299299
variable=target_variable, scaling_factor=sf, overwrite=overwrite
300300
)
301301
else:
302+
# TODO add infrastructure to log a warning
302303
_log.debug(
303304
f"Could not set scaling factor for {target_variable.name}, "
304305
f"no scaling factor set for {scaling_component.name}"

idaes/models/properties/examples/saponification_thermo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ class PhysicalParameterData(PhysicalParameterBlock):
6060
Property Parameter Block Class
6161
6262
Contains parameters and indexing sets associated with properties for
63-
superheated steam.
63+
a dilute solution of NaOH, Ethyl Acetate, Sodium Acetate, and Ethanol
64+
in water.
6465
6566
"""
6667

idaes/models/unit_models/mixer.py

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from pyomo.environ import (
1919
Block,
2020
check_optimal_termination,
21+
ComponentMap,
2122
Constraint,
2223
Param,
2324
PositiveReals,
@@ -34,6 +35,7 @@
3435
MaterialBalanceType,
3536
MaterialFlowBasis,
3637
)
38+
from idaes.core.base.control_volume_base import ControlVolumeScalerBase
3739
from idaes.core.util.config import (
3840
is_physical_parameter_block,
3941
is_state_block,
@@ -52,7 +54,7 @@
5254

5355
import idaes.logger as idaeslog
5456

55-
__author__ = "Andrew Lee"
57+
__author__ = "Andrew Lee, Douglas Allan"
5658

5759

5860
# Set up logger
@@ -80,6 +82,96 @@ class MomentumMixingType(Enum):
8082
minimize_and_equality = 3
8183

8284

85+
class MixerScaler(ControlVolumeScalerBase):
86+
"""
87+
Scaler object for the Mixer unit model
88+
"""
89+
90+
# This attribute gives the parent ControlVolumeScalerBase
91+
# methods a state block with the same index as the material
92+
# and energy balances to get scaling information from
93+
_state_block_ref = "mixed_state"
94+
95+
def variable_scaling_routine(
96+
self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None
97+
):
98+
for _, iblock in model.inlet_blocks:
99+
self.call_submodel_scaler_method(
100+
submodel=iblock,
101+
submodel_scalers=submodel_scalers,
102+
method="variable_scaling_routine",
103+
overwrite=overwrite,
104+
)
105+
106+
if model.config.mixed_state_block is None:
107+
mblock = model.mixed_state
108+
else:
109+
mblock = model.config.mixed_state_block
110+
111+
self.call_submodel_scaler_method(
112+
submodel=mblock,
113+
submodel_scalers=submodel_scalers,
114+
method="variable_scaling_routine",
115+
overwrite=overwrite,
116+
)
117+
118+
# Scale inherent reaction and phase equilibrium
119+
# variables, if they exist
120+
super().variable_scaling_routine(
121+
model, overwrite=overwrite, submodel_scalers=submodel_scalers
122+
)
123+
124+
if hasattr(model, "minimum_pressure"):
125+
for (t, _), v in model.minimum_pressure.items():
126+
self.scale_variable_by_component(
127+
v, model.mixed_state[t].pressure, overwrite=overwrite
128+
)
129+
130+
def constraint_scaling_routine(
131+
self, model, overwrite: bool = False, submodel_scalers: ComponentMap = None
132+
):
133+
for _, iblock in model.inlet_blocks:
134+
self.call_submodel_scaler_method(
135+
submodel=iblock,
136+
submodel_scalers=submodel_scalers,
137+
method="constraint_scaling_routine",
138+
overwrite=overwrite,
139+
)
140+
141+
if model.config.mixed_state_block is None:
142+
mblock = model.mixed_state
143+
else:
144+
mblock = model.config.mixed_state_block
145+
146+
self.call_submodel_scaler_method(
147+
submodel=mblock,
148+
submodel_scalers=submodel_scalers,
149+
method="constraint_scaling_routine",
150+
overwrite=overwrite,
151+
)
152+
153+
# Scale material and energy balance equations
154+
super().constraint_scaling_routine(
155+
model, overwrite=overwrite, submodel_scalers=submodel_scalers
156+
)
157+
158+
if hasattr(model, "pressure_equality_constraints"):
159+
for (t, _), con in model.pressure_equality_constraints.items():
160+
self.scale_constraint_by_component(
161+
con, model.mixed_state[t].pressure, overwrite=overwrite
162+
)
163+
if hasattr(model, "minimum_pressure_constraint"):
164+
for (t, _), con in model.minimum_pressure_constraint.items():
165+
self.scale_constraint_by_component(
166+
con, model.mixed_state[t].pressure, overwrite=overwrite
167+
)
168+
if hasattr(model, "mixture_pressure"):
169+
for t, con in model.mixture_pressure.items():
170+
self.scale_constraint_by_component(
171+
con, model.mixed_state[t].pressure, overwrite=overwrite
172+
)
173+
174+
83175
class MixerInitializer(ModularInitializerBase):
84176
"""
85177
Hierarchical Initializer for Mixer blocks.
@@ -113,15 +205,11 @@ def initialization_routine(
113205
solver = self._get_solver()
114206

115207
# Initialize inlet state blocks
116-
inlet_list = model.create_inlet_list()
117208
i_block_list = []
118-
for i in inlet_list:
119-
i_block = getattr(model, i + "_state")
120-
i_block_list.append(i_block)
121209

122-
# Get initializer for inlet
210+
for _, i_block in model.inlet_blocks:
211+
i_block_list.append(i_block)
123212
iinit = self.get_submodel_initializer(i_block)
124-
125213
iinit.initialize(i_block)
126214

127215
# Initialize mixed state block
@@ -225,6 +313,9 @@ class MixerData(UnitModelBlockData):
225313
"""
226314

227315
default_initializer = MixerInitializer
316+
default_scaler = MixerScaler
317+
318+
_inlet_dict = None
228319

229320
CONFIG = ConfigBlock()
230321
CONFIG.declare(
@@ -397,6 +488,19 @@ class MixerData(UnitModelBlockData):
397488
),
398489
)
399490

491+
@property
492+
def inlet_blocks(self):
493+
"""
494+
Allows the user to iterate over the inlet stream names and state blocks.
495+
496+
Returns:
497+
dict_items with the inlet stream names as keys and the
498+
inlet state blocks as values
499+
"""
500+
# Return an iterator so the user cannot
501+
# mutate _inlet_dict
502+
return self._inlet_dict.items()
503+
400504
def build(self):
401505
"""
402506
General build method for MixerData. This method calls a number
@@ -424,6 +528,8 @@ def build(self):
424528
# Build StateBlocks
425529
inlet_blocks = self.add_inlet_state_blocks(inlet_list)
426530

531+
self._inlet_dict = {key: blk for key, blk in zip(inlet_list, inlet_blocks)}
532+
427533
if self.config.mixed_state_block is None:
428534
mixed_block = self.add_mixed_state_block()
429535
else:
@@ -635,6 +741,7 @@ def add_material_mixing_equations(self, inlet_blocks, mixed_block, mb_type):
635741
else:
636742
# Let this pass for now with no units
637743
flow_units = None
744+
self._constructed_material_balance_type = mb_type
638745

639746
if mixed_block.include_inherent_reactions:
640747
if mb_type == MaterialBalanceType.total:

0 commit comments

Comments
 (0)