Coverage for backend/pinch_service/heat_exchanger_profiler/classes/state_evaluation.py: 68%
132 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +0000
1from property_packages.build_package import build_package
2from pyomo.environ import value, ConcreteModel, RangeSet
3from idaes.core import FlowsheetBlock
4from idaes.core.solvers import get_solver
7class StateEvaluation:
9 def __init__(self,
10 ppKey: str,
11 composition,
12 mole_flow: float,
13 pressure_ls: list = None,
14 temperature_ls: list = None,
15 molar_enthalpy_ls: list = None,
16 molar_entropy_ls: list = None,
17 prev_states_sol: dict = None,
18 solver_name: str = "ipopt",
19 ) -> None:
21 var = [pressure_ls, temperature_ls, molar_enthalpy_ls, molar_entropy_ls]
22 if self._check_DOF(var) > 0: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 raise ValueError("Insufficient state variables provided.")
24 elif self._check_DOF(var) < 0: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true
25 raise ValueError("Too many state variables provided.")
26 if self._check_state_lengths(var) == False: 26 ↛ 27line 26 didn't jump to line 27 because the condition on line 26 was never true
27 raise ValueError("Number of states in the different variables must match.")
29 self.n_states = max([len(x) if x is not None else 0 for x in var ])
30 self.prev_states_sol = self._check_prev_sol_valid(prev_states_sol, self.n_states)
31 self.ppKey = ppKey
32 self.composition = composition
33 self.mole_flow = mole_flow
34 self.pressure_ls = pressure_ls
35 self.temperature_ls = temperature_ls
36 self.molar_enthalpy_ls = molar_enthalpy_ls
37 self.molar_entropy_ls = molar_entropy_ls
38 self.solver_name = solver_name
39 self.build()
40 self.state_properties()
41 self.solve()
43 def build(self):
44 """Build the model to house a series of state blocks within a single flowsheet to be evaluated."""
45 self.m = ConcreteModel()
46 self.m.fs = FlowsheetBlock(dynamic=False)
47 self.solver = get_solver(self.solver_name)
49 # Build property package
50 self.m.fs.properties = build_package(
51 self.ppKey,
52 [comp for comp, _ in self.composition]
53 )
55 self.m.fs.state_index = RangeSet(0, self.n_states - 1)
57 # Build multiple state blocks
58 self.m.fs.sb = self.m.fs.properties.build_state_block(
59 self.m.fs.state_index,
60 defined_state=True
61 )
63 def state_properties(self):
64 """Fix composition and state variables for each state block and initialise properties with the previous solution (if available)."""
65 for i in self.m.fs.state_index:
66 if len(self.composition) > 1: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true
67 for comp, frac in self.composition:
68 self.m.fs.sb[i].mole_frac_comp[comp].fix(frac)
69 self.m.fs.sb[i].flow_mol.fix(self.mole_flow)
71 if self.pressure_ls is not None: 71 ↛ 73line 71 didn't jump to line 73 because the condition on line 71 was always true
72 self.m.fs.sb[i].pressure.fix(self.pressure_ls[i])
73 elif hasattr(self.prev_states_sol, "pressure"):
74 self.m.fs.sb[i].pressure.set_value(self.prev_states_sol.pressure[i])
76 if self.temperature_ls is not None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 self.m.fs.sb[i].temperature.fix(self.temperature_ls[i])
78 elif hasattr(self.prev_states_sol, "temperature"): 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 self.m.fs.sb[i].temperature.set_value(self.prev_states_sol.temperature[i])
81 if self.molar_enthalpy_ls is not None: 81 ↛ 86line 81 didn't jump to line 86 because the condition on line 81 was always true
82 try:
83 self.m.fs.sb[i].enth_mol.fix(self.molar_enthalpy_ls[i])
84 except:
85 self.m.fs.sb[i].constrain("enth_mol", self.molar_enthalpy_ls[i])
86 elif hasattr(self.prev_states_sol, "enth_mol"):
87 self.m.fs.sb[i].enth_mol.set_value(self.prev_states_sol.enth_mol[i])
89 if self.molar_entropy_ls is not None: 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 try:
91 self.m.fs.sb[i].entr_mol.fix(self.molar_entropy_ls[i])
92 except:
93 self.m.fs.sb[i].constrain("entr_mol", self.molar_entropy_ls[i])
94 elif hasattr(self.prev_states_sol, "entr_mol"): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 self.m.fs.sb[i].entr_mol.set_value(self.prev_states_sol.entr_mol[i])
97 def solve(self):
98 # Initialize and solve all state blocks
99 if self.prev_states_sol is None: 99 ↛ 101line 99 didn't jump to line 101 because the condition on line 99 was always true
100 self.m.fs.sb.initialize()
101 try:
102 self.solver.solve(self.m, tee=False)
103 except ValueError as e:
104 if str(e) == "No variables appear in the Pyomo model constraints or objective. This is not supported by the NL file interface": 104 ↛ 107line 104 didn't jump to line 107 because the condition on line 104 was always true
105 print("No need to solve for a solution. Pass")
106 else:
107 print(f"Solver failed with error: {e}")
108 raise
110 def get_property(self, prop, state_index: int = None):
111 try:
112 if state_index is None: 112 ↛ 115line 112 didn't jump to line 115 because the condition on line 112 was always true
113 return self.get_all_properties(prop)
114 else:
115 return self.get_single_property(prop, state_index)
116 except AttributeError:
117 print(f"Property '{prop}' not found in state block.")
118 return None
120 def get_single_property(self, prop: str, state_index: int = 0):
121 return [value(getattr(self.m.fs.sb[state_index], prop))]
123 def get_all_properties(self, prop: str):
124 return [value(getattr(self.m.fs.sb[i], prop)) for i in self.m.fs.state_index.ordered_data()]
126 def get_pressure(self, state_index: int = None):
127 return self.get_property('pressure', state_index)
129 def get_molar_enthalpy(self, state_index: int = None):
130 return self.get_property('enth_mol', state_index)
132 def get_mass_enthalpy(self, state_index: int = None):
133 return self.get_property('enth_mass', state_index)
135 def get_temperature(self, state_index: int = None):
136 return self.get_property('temperature', state_index)
138 def get_molar_entropy(self, state_index: int = None):
139 return self.get_property('entr_mol', state_index)
141 def get_mass_entropy(self, state_index: int = None):
142 return self.get_property('entr_mass', state_index)
144 def get_vapour_fraction(self, state_index: int = None):
145 return self.get_property('vap_frac', state_index)
147 def get_total_energy_flow(self, state_index: int = None):
148 return self.get_property('total_energy_flow', state_index)
150 def get_relative_humidity(self, state_index: int = None):
151 return self.get_property('relative_humidity', state_index)
153 def get_mass_flow(self, state_index: int = None):
154 return self.get_property('flow_mass', state_index)
156 def get_mol_flow(self, state_index: int = None):
157 return self.get_property('flow_mol', state_index)
159 def get_specific_volume(self, state_index: int = None):
160 return self.get_volumetric_flow(state_index) / self.get_mass_flow(state_index)
162 def get_density(self, state_index: int = None):
163 return self.get_mass_flow(state_index) / self.get_volumetric_flow(state_index)
165 def get_volumetric_flow(self, state_index: int = None):
166 return self.get_property('flow_vol', state_index)
168 def get_molecular_weight(self):
169 mw_total = 0.0
170 for comp in self.m.fs.sb[0].mole_frac_comp:
171 mw = value(self.m.fs.sb[0].params.mw_comp[comp])
172 x = value(self.m.fs.sb[0].mole_frac_comp[comp])
173 mw_total += mw * x
174 return mw_total
176 def get_component_molecular_weight(self, comp):
177 return value(self.m.fs.sb[0].params.mw_comp[comp])
179 def serialize_states(self, state_index: int = None) -> dict:
180 return {
181 "pressure": self.get_pressure(state_index),
182 "enthalpy": self.get_molar_enthalpy(state_index),
183 "temperature": self.get_temperature(state_index),
184 "enthalpy_mass": self.get_mass_enthalpy(state_index),
185 "entropy": self.get_molar_entropy(state_index),
186 "entropy_mass": self.get_mass_entropy(state_index),
187 "quality": self.get_vapour_fraction(state_index),
188 "total_energy_flow": self.get_total_energy_flow(state_index),
189 "relative_humidity": self.get_relative_humidity(state_index),
190 "mass_flow": self.get_mass_flow(state_index),
191 "mol_flow": self.get_mol_flow(state_index),
192 "volumetric_flow": self.get_volumetric_flow(state_index),
193 }
195 def _check_DOF(self, ls):
196 return 2 - sum([1 for x in ls if x is not None])
198 def _check_state_lengths(self, ls):
199 l = [len(x) if x is not None else 0 for x in ls]
200 l.sort()
201 return l[-1] == l[-2]
203 def _check_prev_sol_valid(self, prev_states_sol, n_states):
204 if prev_states_sol is not None and len(prev_states_sol["pressure"]) == n_states: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 return prev_states_sol
206 return None