Coverage for backend/ahuora-compounds/ahuora_property_packages/helmholtz/helmholtz_extended.py: 27%
67 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1from pyomo.environ import (
2 Expression,
3 SolverFactory,
4 check_optimal_termination,
5 units,
6 Block,
7 Constraint,
8 Var
9)
10from idaes.models.properties.general_helmholtz.helmholtz_state import HelmholtzStateBlockData, _StateBlock
11from idaes.models.properties.general_helmholtz.helmholtz_functions import HelmholtzParameterBlockData
12from idaes.core import declare_process_block_class
13from idaes.core.util.initialization import solve_indexed_blocks, revert_state_vars
14from idaes.core.util.model_statistics import degrees_of_freedom
15from idaes.core.util.exceptions import InitializationError
16import idaes.logger as idaeslog
18from ahuora_property_packages.base.flexible_state_block import FlexibleStateBlockData, FlexibleStateBlock
19from ahuora_property_packages.utils.fix_state_vars import fix_state_vars
22class _ExtendedStateBlock(FlexibleStateBlock,_StateBlock):
24 def initialize(blk, *args, **kwargs):
25 #blk.pre_initialize(*args, **kwargs)
26 # In this case we actually don't actually need to do any of the FlexibleStateBlock pre and post initialisation steps, because we have already
27 # written a custom initialize method that handles the extra constraints for helmholtz.
28 outlvl = kwargs.get("outlvl", idaeslog.NOTSET)
29 init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="properties")
30 solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="properties")
32 set_vapor_frac_guesses(blk)
34 flag_dict = fix_state_vars(blk, kwargs.get("state_args", None))
36 dof = degrees_of_freedom(blk)
37 if dof != 0:
38 raise InitializationError(
39 f"{blk.name} Unexpected degrees of freedom during "
40 f"initialization at property initialization step: {dof}."
41 )
43 res = None
44 opt = SolverFactory('ipopt')
45 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
46 try:
47 res = solve_indexed_blocks(opt, [blk], tee=slc.tee)
48 except ValueError as e:
49 if str(e).startswith("No variables appear"):
50 # https://github.com/Pyomo/pyomo/pull/3445
51 pass
52 else:
53 raise e
55 if res is not None and not check_optimal_termination(res):
56 raise InitializationError(
57 f"{blk.name} failed to initialize successfully. Please check "
58 f"the output logs for more information."
59 )
61 if kwargs.get("hold_state") is True:
62 return flag_dict
63 else:
64 blk.release_state(flag_dict)
66 init_log.info(
67 "Property package initialization: {}.".format(idaeslog.condition(res))
68 )
69 return flag_dict
71 def release_state(blk, flags, outlvl=idaeslog.NOTSET):
72 revert_state_vars(blk, flags)
76def set_vapor_frac_guesses(blk: Block) -> None:
77 """
78 Vapor fraction specified
79 valid combinations: {p, x}, {T, x}
81 We find a guess for h and p based on the specified vapor fraction and either p or T.
83 This is because the initial guess has to be within the vapor fraction region
84 otherwise the solver has a hard time converging.
85 """
86 blk[blk.index_set().first()].config.parameters
88 for sb in blk.values():
89 #vapor_frac_constraint is added by the platform when the user specifies a vapor fraction,
90 # using the FlexibleStateBlock.constrain_component() method. this will run _convert_expression_to_var which will add
91 # vapor_frac_constraint and vapor_frac_var to the state block.
92 if not hasattr(sb, "vapor_frac_constraint"):
93 continue
95 # we remove the vapor fraction constraint added by the platform, and add our own custom smooth constraint below.
96 # This allows the vapor fraction to go below 0 and above 1, which helps with solver convergence as there is a smooth gradient to follow.
97 del sb.vapor_frac_constraint # new smooth constraint will be added later instead
100 # add custom vapor fraction constraint: h = h_sat_liq + x(h_sat_vap - h_sat_liq)
101 # this is to make the vapor fraction continuous, rather than cutting off at 0 and 1. Helps with solving reliability as outside
102 # the vapor fraction region, this will still be smooth and will not have a zero gradient.
103 sb.add_component(
104 "vapor_frac_constraint",
105 Constraint(expr=sb.enth_mol == sb.enth_mol_sat_phase["Liq"] + sb.vapor_frac_var * (sb.enth_mol_sat_phase["Vap"] - sb.enth_mol_sat_phase["Liq"]))
106 )
108@declare_process_block_class("HelmholtzExtendedStateBlock", block_class=_ExtendedStateBlock)
109class HelmholtzExtendedStateBlockData(HelmholtzStateBlockData, FlexibleStateBlockData):
111 def build(blk, *args):
112 HelmholtzStateBlockData.build(blk, *args)
113 FlexibleStateBlockData.build(blk, *args)
115 def constrain_component(blk, component: Var | Expression, value: float) -> Var | None:
116 # We actually ignore mole_frac_comp constraints, because helmholtz is pure so they are not necessary.
118 if component.local_name.startswith("mole_frac_comp"):
119 return None
120 else:
121 return super().constrain_component(component, value)
123 def add_extra_expressions(blk):
124 super().add_extra_expressions()
126 # Generic property packages support temperature_bubble and temperature_dew,
127 # Helmholtz is pure so these are the same, and so it only has
128 # temperature_sat. https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/property_models/helmholtz.html#expressions
129 # We add them here so that the property package interface is consistent.
130 blk.add_component("temperature_bubble", Expression(expr=blk.temperature_sat))
131 blk.add_component("temperature_dew", Expression(expr=blk.temperature_sat))
133 # For numerical stability when constraining temperature, we actually smooth out the temperature equilibrium curve.
134 # Otherwise, the solver has difficulty solving through the vapor fraction region as there is zero gradient.
136 # rename temperature to old_temperature
137 old_temperature = blk.temperature
138 blk.del_component("temperature")
139 blk.add_component("old_temperature", old_temperature)
140 # create smooth temperature expression
141 blk.add_component("temperature", Expression(
142 expr=blk.old_temperature + (blk.enth_mol / (1 * units.J/units.mol))*0.000001 * units.K
143 ))
146@declare_process_block_class("HelmholtzExtendedParameterBlock")
147class HelmholtzExtendedParameterBlockData(HelmholtzParameterBlockData):
148 def build(self):
149 super().build()
150 self._state_block_class = HelmholtzExtendedStateBlock # noqa: F821