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

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 

5 

6 

7class StateEvaluation: 

8 

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: 

20 

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.") 

28 

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() 

42 

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) 

48 

49 # Build property package 

50 self.m.fs.properties = build_package( 

51 self.ppKey, 

52 [comp for comp, _ in self.composition] 

53 ) 

54 

55 self.m.fs.state_index = RangeSet(0, self.n_states - 1) 

56 

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 ) 

62 

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) 

70 

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]) 

75 

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]) 

80 

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]) 

88 

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]) 

96 

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 

109 

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 

119 

120 def get_single_property(self, prop: str, state_index: int = 0): 

121 return [value(getattr(self.m.fs.sb[state_index], prop))] 

122 

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()] 

125 

126 def get_pressure(self, state_index: int = None): 

127 return self.get_property('pressure', state_index) 

128 

129 def get_molar_enthalpy(self, state_index: int = None): 

130 return self.get_property('enth_mol', state_index) 

131 

132 def get_mass_enthalpy(self, state_index: int = None): 

133 return self.get_property('enth_mass', state_index) 

134 

135 def get_temperature(self, state_index: int = None): 

136 return self.get_property('temperature', state_index) 

137 

138 def get_molar_entropy(self, state_index: int = None): 

139 return self.get_property('entr_mol', state_index) 

140 

141 def get_mass_entropy(self, state_index: int = None): 

142 return self.get_property('entr_mass', state_index) 

143 

144 def get_vapour_fraction(self, state_index: int = None): 

145 return self.get_property('vap_frac', state_index) 

146 

147 def get_total_energy_flow(self, state_index: int = None): 

148 return self.get_property('total_energy_flow', state_index) 

149 

150 def get_relative_humidity(self, state_index: int = None): 

151 return self.get_property('relative_humidity', state_index) 

152 

153 def get_mass_flow(self, state_index: int = None): 

154 return self.get_property('flow_mass', state_index) 

155 

156 def get_mol_flow(self, state_index: int = None): 

157 return self.get_property('flow_mol', state_index) 

158 

159 def get_specific_volume(self, state_index: int = None): 

160 return self.get_volumetric_flow(state_index) / self.get_mass_flow(state_index) 

161 

162 def get_density(self, state_index: int = None): 

163 return self.get_mass_flow(state_index) / self.get_volumetric_flow(state_index) 

164 

165 def get_volumetric_flow(self, state_index: int = None): 

166 return self.get_property('flow_vol', state_index) 

167 

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 

175 

176 def get_component_molecular_weight(self, comp): 

177 return value(self.m.fs.sb[0].params.mw_comp[comp]) 

178 

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 } 

194 

195 def _check_DOF(self, ls): 

196 return 2 - sum([1 for x in ls if x is not None]) 

197 

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] 

202 

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