Coverage for backend/ahuora-compounds/ahuora_property_packages/modular/builder/common_parsers.py: 15%
129 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 math import floor
2from typing import Any, Dict, List
3from ahuora_compounds.packages.chemsep import ChemsepCompound as Compound
4from pyomo.environ import units as pyunits
5from idaes.models.properties.modular_properties.state_definitions import FTPx
6from idaes.models.properties.modular_properties.phase_equil.bubble_dew import (LogBubbleDew)
7from idaes.core import LiquidPhase, VaporPhase, Component, PhaseType as PT
8from idaes.models.properties.modular_properties.phase_equil import (SmoothVLE)
9from idaes.models.properties.modular_properties.phase_equil.forms import log_fugacity
10from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType
11from idaes.models.properties.modular_properties.pure import RPP4, Perrys
12from ahuora_property_packages.modular.builder.data.chem_sep import ChemSep
13from pyomo.common.fileutils import this_file_dir
14from ahuora_property_packages.types import States
15import csv
17def build_base_units(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
18 return {
19 'time': pyunits.s,
20 'length': pyunits.m,
21 'mass': pyunits.kg,
22 'amount': pyunits.mol,
23 'temperature': pyunits.K,
24 }
26def build_bubble_dew_method(compounds: List[Compound], valid_states: List[States]) -> Any:
27 return LogBubbleDew
29def build_components(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
31 def _serialise_component(compound: Compound) -> Dict[str, Any]:
33 # configuration default to all components
34 config = {
35 "type": Component,
36 "parameter_data": {
37 "mw": (compound.MolecularWeight.value, pyunits.kg/pyunits.kilomol),
38 "pressure_crit": (compound.CriticalPressure.value, pyunits.Pa),
39 "temperature_crit": (compound.CriticalTemperature.value, pyunits.K),
40 "omega": compound.AcentricityFactor.value,
41 }
42 }
44 valid_phase = _valid_phases(compound)
45 if valid_phase != PT.vaporPhase:
46 config["phase_equilibrium_form"] = {("Vap", "Liq"): log_fugacity}
47 else:
48 config["valid_phase_types"] = valid_phase
50 # Energies of Formation
51 if compound.HeatOfFormation is not None: # this does not work when passed
52 config["parameter_data"].update({
53 "enth_mol_form_vap_comp_ref": (compound.HeatOfFormation.value, pyunits.J/pyunits.kilomol)
54 })
55 else:
56 raise ValueError("No Heat of Formation Data")
58 if compound.AbsEntropy is not None:
59 config["parameter_data"].update({
60 "entr_mol_form_vap_comp_ref": (-1 * compound.AbsEntropy.value, pyunits.J/pyunits.kilomol/pyunits.K)
61 })
62 else:
63 raise ValueError("No Absolute Entropy Data")
65 # Ideal Gas Molar Calculations
66 # All three properties intrinsically linked together
67 if compound.RPPHeatCapacityCp is not None:
68 if compound.RPPHeatCapacityCp["eqno"] == 4:
69 config["enth_mol_ig_comp"] = RPP4
70 config["entr_mol_ig_comp"] = RPP4
71 config["parameter_data"].update({"cp_mol_ig_comp_coeff": {
72 "A": (float(compound.RPPHeatCapacityCp["A"]), pyunits.J / pyunits.kilomol / pyunits.K),
73 "B": (float(compound.RPPHeatCapacityCp["B"]), pyunits.J / pyunits.kilomol / pyunits.K**2),
74 "C": (float(compound.RPPHeatCapacityCp["C"]), pyunits.J / pyunits.kilomol / pyunits.K**3),
75 "D": (float(compound.RPPHeatCapacityCp["D"]), pyunits.J / pyunits.kilomol / pyunits.K**4),
76 }})
77 elif compound.RPPHeatCapacityCp["eqno"] == 100 or compound.RPPHeatCapacityCp["eqno"] == 5:
78 config["enth_mol_ig_comp"] = ChemSep
79 config["entr_mol_ig_comp"] = ChemSep
80 config["parameter_data"].update({"cp_mol_ig_comp_coeff": {
81 "A": (compound.RPPHeatCapacityCp["A"], pyunits.J / pyunits.kilomol / pyunits.K),
82 "B": (compound.RPPHeatCapacityCp["B"], pyunits.J / pyunits.kilomol / pyunits.K**2),
83 "C": (compound.RPPHeatCapacityCp["C"], pyunits.J / pyunits.kilomol / pyunits.K**3),
84 "D": (compound.RPPHeatCapacityCp["D"], pyunits.J / pyunits.kilomol / pyunits.K**4),
85 "E": (compound.RPPHeatCapacityCp["E"], pyunits.J / pyunits.kilomol / pyunits.K**5),
86 }})
87 else:
88 raise ValueError(f"Invalid equation number for heat capacity {compound.RPPHeatCapacityCp['eqno']}")
89 else:
90 raise ValueError("No Heat Capacity Data")
92 # Saturation Pressure (Vapor)
93 if compound.AntoineVaporPressure is not None:
94 if compound.AntoineVaporPressure["eqno"] == 10:
95 config["pressure_sat_comp"] = ChemSep
96 config["parameter_data"].update({"pressure_sat_comp_coeff": {
97 "A": (compound.AntoineVaporPressure["A"], None),
98 "B": (compound.AntoineVaporPressure["B"], pyunits.K),
99 "C": (compound.AntoineVaporPressure["C"], pyunits.K),
100 }})
101 else:
102 raise ValueError("No Antoine Vapor Pressure Equation Data")
103 else:
104 raise ValueError("No Antoine Vapor Pressure Data")
106 # Liquid Density
107 if compound.LiquidDensity is not None:
108 if compound.LiquidDensity["eqno"] == 105:
109 config["dens_mol_liq_comp"] = Perrys
110 config["parameter_data"].update({"dens_mol_liq_comp_coeff": {
111 "eqn_type": 1,
112 "1": (compound.LiquidDensity["A"], pyunits.kmol / pyunits.m**3),
113 "2": (compound.LiquidDensity["B"], None),
114 "3": (compound.LiquidDensity["C"], pyunits.K),
115 "4": (compound.LiquidDensity["D"], None),
116 }})
117 elif compound.LiquidDensity["eqno"] == 106:
118 config["dens_mol_liq_comp"] = ChemSep
119 config["parameter_data"].update({"dens_mol_liq_comp_coeff": {
120 "A": (compound.LiquidDensity["A"], pyunits.kmol / pyunits.m**3),
121 "B": (compound.LiquidDensity["B"], None),
122 "C": (compound.LiquidDensity["C"], None),
123 "D": (compound.LiquidDensity["D"], None),
124 "E": (compound.LiquidDensity["E"], None),
125 }})
126 else:
127 raise ValueError("No Liquid Density Equation Data")
128 else:
129 raise ValueError("No Liquid Density Data")
131 # Liquid Heat Capacity & Entropy / Enthalpy
132 if compound.LiquidHeatCapacityCp is not None:
133 if compound.LiquidHeatCapacityCp["eqno"] == 100:
134 # Uses correct equations to calculate
135 config["enth_mol_liq_comp"] = Perrys
136 config["entr_mol_liq_comp"] = Perrys
137 config["parameter_data"].update({"cp_mol_liq_comp_coeff": {
138 "1": (compound.LiquidHeatCapacityCp["A"], pyunits.J / pyunits.kilomol / pyunits.K**1),
139 "2": (compound.LiquidHeatCapacityCp["B"], pyunits.J / pyunits.kilomol / pyunits.K**2),
140 "3": (compound.LiquidHeatCapacityCp["C"], pyunits.J / pyunits.kilomol / pyunits.K**3),
141 "4": (compound.LiquidHeatCapacityCp["D"], pyunits.J / pyunits.kilomol / pyunits.K**4),
142 "5": (compound.LiquidHeatCapacityCp["E"], pyunits.J / pyunits.kilomol / pyunits.K**5),
143 }})
145 # ASSUMPTION: Molar heat of formation, liq is zero - given the semi-okay by Ben
146 config["parameter_data"].update({
147 "enth_mol_form_liq_comp_ref": (0, pyunits.J / pyunits.kilomol)
148 })
150 config["parameter_data"].update({
151 "entr_mol_form_liq_comp_ref": (0, pyunits.J / pyunits.kilomol / pyunits.K)
152 })
153 elif compound.LiquidHeatCapacityCp["eqno"] == 16:
154 # Uses correct equations to calculate
155 config["enth_mol_liq_comp"] = ChemSep
156 config["entr_mol_liq_comp"] = ChemSep
157 config["parameter_data"].update({"cp_mol_liq_comp_coeff": {
158 "A": (compound.LiquidHeatCapacityCp["A"], pyunits.J / pyunits.kilomol / pyunits.K**1),
159 "B": (compound.LiquidHeatCapacityCp["B"], pyunits.J / pyunits.kilomol / pyunits.K**2),
160 "C": (compound.LiquidHeatCapacityCp["C"], pyunits.J / pyunits.kilomol / pyunits.K**3),
161 "D": (compound.LiquidHeatCapacityCp["D"], pyunits.J / pyunits.kilomol / pyunits.K**4),
162 "E": (compound.LiquidHeatCapacityCp["E"], pyunits.J / pyunits.kilomol / pyunits.K**5),
163 }})
165 # ASSUMPTION: Molar heat of formation, liq is zero - given the semi-okay by Ben
166 config["parameter_data"].update({
167 "enth_mol_form_liq_comp_ref": (0, pyunits.J / pyunits.kilomol)
168 })
170 config["parameter_data"].update({
171 "entr_mol_form_liq_comp_ref": (0, pyunits.J / pyunits.kilomol / pyunits.K)
172 })
173 else:
174 raise ValueError(f"No Liquid Heat Capacity Equation Data {compound.LiquidHeatCapacityCp['eqno']}")
175 else:
176 # Compound only exists in vapor phase
177 config["parameter_data"].update({"valid_phase_types": PT.vaporPhase})
179 return config
181 def _valid_phases(compound: Compound) -> PT:
182 if compound.NormalMeltingPointTemperature.value > compound.NormalBoilingPointTemperature.value:
183 # no liquid phase exists (sublimation)
184 return PT.vaporPhase
185 # Assumption: Anything above hydrogen can exist as both liquid and vapor
186 elif compound.NormalBoilingPointTemperature.value >= 21:
187 return [PT.liquidPhase, PT.vaporPhase]
188 else:
189 return PT.vaporPhase
191 components_output = {}
192 for compound in compounds:
193 components_output[compound.CompoundID.value] = _serialise_component(compound)
194 return components_output
196def build_phase_equilibrium_state(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
197 return {("Vap", "Liq"): SmoothVLE}
199def build_phases(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
200 phases = {}
201 for state in valid_states:
202 if state == "Liq":
203 phases["Liq"] = {
204 "type": LiquidPhase,
205 "equation_of_state": Cubic,
206 "equation_of_state_options": {"type": CubicType.PR},
207 }
208 elif state == "Vap":
209 phases["Vap"] = {
210 "type": VaporPhase,
211 "equation_of_state": Cubic,
212 "equation_of_state_options": {"type": CubicType.PR},
213 }
214 return phases
216def build_phases_in_equilibrium(compounds: List[Compound], valid_states: List[States]) -> List:
217 return [("Vap", "Liq")]
219def build_pressure_ref(compounds: List[Compound], valid_states: List[States]) -> tuple:
220 return (101325, pyunits.Pa)
222def build_state_bounds(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
223 """
224 State bounds are used to find optimal solution
225 TODO: need to find a way to dynamically determine these
226 """
227 min_melting_point = min([compound.NormalMeltingPointTemperature.value for compound in compounds])
228 min([compound.CriticalTemperature.value for compound in compounds])
230 # TODO: Refactor this logic, need a more versatile approach
231 return {
232 "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s),
233 "temperature": (max(min_melting_point-50,1), 300, 3000, pyunits.K),
234 "pressure": (5e4, 1e5, 1e6, pyunits.Pa),
235 }
237def build_state_definition(compounds: List[Compound], valid_states: List[States]) -> Any:
238 return FTPx
240def build_temperature_ref(compounds: List[Compound], valid_states: List[States]) -> tuple:
241 return (298.15, pyunits.K)
243def build_pr_kappa(compounds: List[Compound], valid_states: List[States]) -> Dict[str, Any]:
244 kappa_parameters = {}
245 compound_id_map = {}
247 for i, compound1 in enumerate(compounds):
248 compound_id_map[str(floor(compound1.LibraryIndex.value))] = compound1
249 for j, compound2 in enumerate(compounds):
250 kappa_parameters[(compound1.CompoundID.value, compound2.CompoundID.value)] = 0.000
251 # Setting all interactions initially to zero
253 # TODO: Adjust method so the multiple kappa values for single pair are supported
254 # Open and read the interaction data file
255 file = open(this_file_dir() + "/data/pr.dat", 'r')
256 reader = csv.reader(file, delimiter=';')
257 for row in reader:
259 # Skip invalid rows
260 if len(row) < 4:
261 continue
263 # Extract ID1, ID2, kappa (k12) and comment
264 id1, id2, kappa, _ = row
266 try:
267 kappa_value = float(kappa)
268 except ValueError:
269 continue
271 # Check if the compound IDs exist in the compound list
272 if id1 in compound_id_map and id2 in compound_id_map:
273 compound1 = compound_id_map[id1]
274 compound2 = compound_id_map[id2]
275 kappa_parameters[(compound1.CompoundID.value, compound2.CompoundID.value)] = kappa_value
276 kappa_parameters[(compound2.CompoundID.value, compound1.CompoundID.value)] = kappa_value
278 return {"PR_kappa": kappa_parameters}