yaeos 4.0.0__cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
yaeos/fitting/core.py ADDED
@@ -0,0 +1,199 @@
1
+ """Yaeos fitting core module."""
2
+
3
+ import numpy as np
4
+
5
+ from scipy.optimize import minimize
6
+
7
+ from yaeos.fitting.solvers import solve_pt
8
+
9
+
10
+ class BinaryFitter:
11
+ """BinaryFitter class.
12
+
13
+ This class is used to fit binary interaction parameters to experimental
14
+ data. The objective function is defined as the sum of the squared errors
15
+ between the experimental data and the model predictions.
16
+
17
+ Parameters
18
+ ----------
19
+ model_setter : callable
20
+ A function that returns a model object. The function should take the
21
+ optimization parameters as the first argument and any other arguments
22
+ as the following arguments.
23
+ model_setter_args : tuple
24
+ A tuple with the arguments to pass to the model_setter function.
25
+ data : pandas.DataFrame
26
+ A DataFrame with the experimental data.
27
+ The DataFrame should have the following columns:
28
+ - kind: str, the kind of data point (bubble, dew, liquid-liquid, PT,
29
+ critical)
30
+ - x1: float, the mole fraction of component 1
31
+ - y1: float, the mole fraction of component 1
32
+ - T: float, the temperature in K
33
+ - P: float, the pressure in bar
34
+ verbose : bool, optional
35
+ If True, print the objective function value and the optimization
36
+ """
37
+
38
+ def __init__(self, model_setter, model_setter_args, data, verbose=False):
39
+ self._get_model = model_setter
40
+ self._get_model_args = model_setter_args
41
+ self.data = data
42
+ self.verbose = verbose
43
+ self.evaluations = {"fobj": [], "x": [], "cl": []}
44
+
45
+ def objective_function(self, x_values):
46
+ """
47
+ Objective function to minimize when fitting interaction parameters.
48
+
49
+ Parameters
50
+ ----------
51
+ x_values : array-like
52
+ The interaction parameters to fit.
53
+ """
54
+
55
+ def pressure_error(Pexp, Pmodel):
56
+ return (Pexp - Pmodel) ** 2 / Pexp
57
+
58
+ def composition_error(zexp, zmodel):
59
+ return np.abs(np.log(zmodel[0] / zexp[0])) + np.abs(
60
+ np.log(zmodel[1] / zexp[1])
61
+ )
62
+
63
+ def temperature_error(Texp, Tmodel):
64
+ return (Texp - Tmodel) ** 2 / Texp
65
+
66
+ model = self._get_model(x_values, *self._get_model_args)
67
+ data = self.data
68
+
69
+ # =====================================================================
70
+ # Calculate the critical line starting from the heavy component
71
+ # ---------------------------------------------------------------------
72
+ cp_msk = data["kind"] == "critical"
73
+ if len(data[cp_msk]) > 0:
74
+ cl = model.critical_line(
75
+ z0=[0, 1],
76
+ zi=[1, 0],
77
+ a0=1e-2,
78
+ s=1e-2,
79
+ ds0=1e-3,
80
+ max_points=15000,
81
+ )
82
+
83
+ err = 0
84
+
85
+ for _, row in data.iterrows():
86
+ x = [row["x1"], 1 - row["x1"]]
87
+ y = [row["y1"], 1 - row["y1"]]
88
+ t = row["T"]
89
+ p = row["P"]
90
+
91
+ try:
92
+ w = row["weight"]
93
+ except KeyError:
94
+ w = 1
95
+
96
+ error_i = 0
97
+
98
+ # =================================================================
99
+ # Bubble point
100
+ # -----------------------------------------------------------------
101
+ if row["kind"] == "bubble":
102
+ sat = model.saturation_pressure(
103
+ x, kind="bubble", temperature=t, p0=p
104
+ )
105
+ error_i += pressure_error(p, sat["P"])
106
+
107
+ # =================================================================
108
+ # Dew points
109
+ # -----------------------------------------------------------------
110
+ if row["kind"] == "dew":
111
+ sat = model.saturation_pressure(
112
+ x, kind="dew", temperature=t, p0=p
113
+ )
114
+
115
+ error_i += pressure_error(p, sat["P"])
116
+
117
+ if row["kind"] == "PT" or row["kind"] == "liquid-liquid":
118
+ x1, y1 = solve_pt(model, row["P"], row["T"], row["kind"])
119
+
120
+ if np.isnan(x[0]):
121
+ error_i += composition_error(y, [y1, 1 - y1])
122
+
123
+ elif np.isnan(y[0]):
124
+ error_i += composition_error(x, [x1, 1 - x1])
125
+
126
+ else:
127
+ error_i += composition_error(
128
+ x, [x1, 1 - x1]
129
+ ) + composition_error(y, [y1, 1 - y1])
130
+
131
+ # =================================================================
132
+ # Critical point error is calculated by finding the nearest
133
+ # critical point in the critical line to the given critical
134
+ # point in the data.
135
+ # -----------------------------------------------------------------
136
+ if row["kind"] == "critical":
137
+ cp = row
138
+ distances = (
139
+ (cp["T"] - cl["T"]) ** 2
140
+ + ((cp["P"] - cl["P"]) ** 2)
141
+ + ((cp["x1"] - cl["a"]) ** 2)
142
+ )
143
+ nearest = np.argmin(distances)
144
+ t_cl, p_cl, x1 = (
145
+ cl["T"][nearest],
146
+ cl["P"][nearest],
147
+ cl["a"][nearest],
148
+ )
149
+ error_i += temperature_error(cp["T"], t_cl)
150
+ error_i += pressure_error(cp["P"], p_cl)
151
+ error_i += composition_error(
152
+ [cp["x1"], 1 - cp["x1"]], [x1, 1 - x1]
153
+ )
154
+
155
+ if np.isnan(error_i):
156
+ error_i = row["P"]
157
+ err += error_i * w
158
+
159
+ # =====================================================================
160
+ # Normalize the error and save the valuation
161
+ # ---------------------------------------------------------------------
162
+ err = err / len(data)
163
+
164
+ self.evaluations["fobj"].append(err)
165
+ self.evaluations["x"].append(x_values)
166
+
167
+ if self.verbose:
168
+ print(err, x_values)
169
+ return err
170
+
171
+ def fit(self, x0, bounds, method="Nelder-Mead"):
172
+ """Fit the model to the data.
173
+
174
+ Fit the model to the data using the objective function defined in
175
+ the objective_function method. The optimization is performed using
176
+ the scipy.optimize.minimize function.
177
+ The optimization result is stored in the `.solution` property. Which
178
+
179
+ Parameters
180
+ ----------
181
+ x0 : array-like
182
+ Initial guess for the fitting parameters.
183
+ bounds : array-like
184
+ Bounds for the fitting parameters.
185
+ method : str, optional
186
+ The optimization method to use. Default is 'Nelder-Mead'.
187
+
188
+ Returns
189
+ -------
190
+ None
191
+ """
192
+ sol = minimize(
193
+ self.objective_function, x0=x0, bounds=bounds, method=method
194
+ )
195
+ self._solution = sol
196
+
197
+ @property
198
+ def solution(self):
199
+ return self._solution
@@ -0,0 +1,76 @@
1
+ """Model Setters.
2
+
3
+ Compilation of functions that set a model's parameters to the values
4
+ given in the input arguments.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+
10
+ def fit_kij_lij(x, model, fit_kij, fit_lij):
11
+ """Set the kij and/or lij parameter of the model."""
12
+ mr = model.mixrule
13
+
14
+ if fit_kij and fit_lij:
15
+ kij = x[0]
16
+ lij = x[1]
17
+ mr.kij = np.array([[0, kij], [kij, 0]], order="F")
18
+ mr.lij = np.array([[0, lij], [lij, 0]], order="F")
19
+ elif fit_kij:
20
+ kij = x[0]
21
+ mr.kij = np.array([[0, kij], [kij, 0]], order="F")
22
+ elif fit_lij:
23
+ lij = x[0]
24
+ mr.lij = np.array([[0, lij], [lij, 0]], order="F")
25
+
26
+ model.set_mixrule(mr)
27
+ return model
28
+
29
+
30
+ def fit_mhv_nrtl(x, args):
31
+ """Fit the MHV mixing rule for Cubic EoS with NRTL GE."""
32
+ from yaeos.models import NRTL, MHV
33
+
34
+ a12, a21, b12, b21, alpha = x
35
+
36
+ model = args[0]
37
+ q = args[1]
38
+
39
+ a = [
40
+ [
41
+ 0,
42
+ a12,
43
+ ],
44
+ [a21, 0],
45
+ ]
46
+ b = [[0, b12], [b21, 0]]
47
+ c = [[0, alpha], [alpha, 0]]
48
+
49
+ nrtl = NRTL(a, b, c)
50
+ mr = MHV(ge=nrtl, q=q)
51
+ model.set_mixrule(mr)
52
+ return model
53
+
54
+
55
+ def fit_hv_nrtl(x, args):
56
+ """Fit the HV mixing rule for Cubic EoS with NRTL GE."""
57
+ from yaeos.models import NRTL, HV
58
+
59
+ a12, a21, b12, b21, alpha = x
60
+
61
+ model = args[0]
62
+
63
+ a = [
64
+ [
65
+ 0,
66
+ a12,
67
+ ],
68
+ [a21, 0],
69
+ ]
70
+ b = [[0, b12], [b21, 0]]
71
+ c = [[0, alpha], [alpha, 0]]
72
+
73
+ nrtl = NRTL(a, b, c)
74
+ mr = HV(nrtl)
75
+ model.set_mixrule(mr)
76
+ return model
@@ -0,0 +1,87 @@
1
+ """Solvers.
2
+
3
+ Module that contains different solvers for specific porpouses.
4
+ """
5
+
6
+ import numpy as np
7
+
8
+
9
+ def binary_isofugacity_x1y1pt(x, p, t, model):
10
+ """Isofugacity evaluation at a given P and T."""
11
+ y1, x1 = x
12
+
13
+ x = np.array([x1, 1 - x1])
14
+ y = np.array([y1, 1 - y1])
15
+
16
+ lnphi_x = model.lnphi_pt(x, pressure=p, temperature=t, root="stable")
17
+ lnphi_y = model.lnphi_pt(y, pressure=p, temperature=t, root="stable")
18
+
19
+ isofug = np.log(x) + lnphi_x - (np.log(y) + lnphi_y)
20
+
21
+ return isofug**2
22
+
23
+
24
+ def solve_pt(model, p, t, kind):
25
+ """Solve a point at a given P and T."""
26
+ try:
27
+ x10, y10 = find_init_binary_ll(model, p, t, kind)
28
+ except ValueError:
29
+ x10, y10 = 0.1, 0.9
30
+
31
+ mean = (x10 + y10) / 2
32
+
33
+ z = [mean, 1 - mean]
34
+ y0 = np.array([y10, 1 - y10])
35
+ x0 = np.array([x10, 1 - x10])
36
+
37
+ flash = model.flash_pt(z, pressure=p, temperature=t, k0=y0 / x0)
38
+
39
+ x1 = flash["x"][0]
40
+ y1 = flash["y"][0]
41
+
42
+ return x1, y1
43
+
44
+
45
+ def find_init_binary_ll(model, pressure, temperature, kind):
46
+ """Find initial guess for a binary liquid-liquid system."""
47
+ from scipy.signal import argrelmin, argrelmax
48
+
49
+ (
50
+ p,
51
+ t,
52
+ ) = (
53
+ pressure,
54
+ temperature,
55
+ )
56
+
57
+ if kind == "liquid-liquid":
58
+ root = "liquid"
59
+ else:
60
+ root = "stable"
61
+
62
+ zs = np.linspace(1e-15, 1 - 1e-15, 100)
63
+
64
+ phis = np.array(
65
+ [
66
+ model.lnphi_pt([z, 1 - z], temperature=t, pressure=p, root=root)
67
+ for z in zs
68
+ ]
69
+ )
70
+ phis = np.exp(phis)
71
+ fug_1 = zs * phis[:, 0] * p
72
+
73
+ argmin = argrelmin(zs * phis[:, 0] * p)[-1] + 1
74
+ argmax = argrelmax(zs * phis[:, 0] * p)[0] - 1
75
+
76
+ fug = np.mean([fug_1[argmin], fug_1[argmax]])
77
+
78
+ if fug > fug_1[-1]:
79
+ fug = np.mean([fug_1[argmin[0]], fug_1[-1]])
80
+
81
+ msk = zs < zs[argmax]
82
+ x1 = zs[msk][np.argmin(np.abs(fug - fug_1[msk]))]
83
+
84
+ msk = zs > zs[argmin]
85
+ y1 = zs[msk][np.argmin(np.abs(fug - fug_1[msk]))]
86
+
87
+ return x1, y1
yaeos/gpec.py ADDED
@@ -0,0 +1,225 @@
1
+ """Global Phase Equilibria Calculations.
2
+
3
+ Module that implements the GPEC algorithm for calculation of GPEDs and its
4
+ derivatives to obtain isopleths, isotherms and isobars.
5
+ """
6
+
7
+ import matplotlib.pyplot as plt
8
+
9
+ import numpy as np
10
+
11
+ from yaeos.core import ArModel
12
+
13
+ MAX_POINTS = 10000
14
+
15
+
16
+ class GPEC:
17
+
18
+ def __init__(
19
+ self,
20
+ model: ArModel,
21
+ max_pressure=2500,
22
+ max_points=10000,
23
+ stability_analysis=True,
24
+ step_21=1e-2,
25
+ step_12=1e-5,
26
+ ):
27
+ self._z0 = np.array([0, 1])
28
+ self._zi = np.array([1, 0])
29
+ self._model = model
30
+
31
+ # Calculate the pure saturation pressures of each component and
32
+ # save it as an internal variable
33
+ psats = [model.pure_saturation_pressures(i) for i in [1, 2]]
34
+ self._pures = psats
35
+
36
+ # Calculate the critical line starting from the almost pure second
37
+ # component.
38
+ diff = 1e-3
39
+ cl, cep = model.critical_line(
40
+ z0=self._z0,
41
+ zi=self._zi,
42
+ ns=1,
43
+ s=diff,
44
+ a0=diff,
45
+ ds0=step_21,
46
+ stop_pressure=max_pressure,
47
+ max_points=max_points,
48
+ stability_analysis=stability_analysis,
49
+ )
50
+
51
+ self._cl21 = cl
52
+ self._cep21 = cep
53
+
54
+ # Check if the critical line did not reach to the pure first component.
55
+ # if not, calculate the critical line starting from the almost pure
56
+ # first component. It is important to make small steps because this
57
+ # kind of line can be pretty short.
58
+ if not np.isnan(cep["T"]) or (
59
+ abs(cl["T"][-1] - psats[0]["T"][-1]) > 10
60
+ and abs(cl["P"][-1] - psats[0]["P"][-1] > 10)
61
+ ):
62
+ cl, cep = model.critical_line(
63
+ z0=self._z0,
64
+ zi=self._zi,
65
+ ns=1,
66
+ s=1 - diff / 10,
67
+ a0=1 - diff / 10,
68
+ ds0=-step_12,
69
+ stop_pressure=max_pressure,
70
+ max_points=max_points,
71
+ stability_analysis=stability_analysis,
72
+ )
73
+
74
+ self._cl12 = cl
75
+ self._cep12 = cep
76
+ else:
77
+ self._cl12 = None
78
+ self._cep12 = None
79
+
80
+ a, T, V = model.critical_line_liquid_liquid(
81
+ z0=self._z0, zi=self._zi, pressure=max_pressure, t0=500
82
+ )
83
+
84
+ cl, cep = model.critical_line(
85
+ z0=self._z0,
86
+ zi=self._zi,
87
+ ns=4,
88
+ s=np.log(max_pressure),
89
+ a0=a,
90
+ v0=V,
91
+ t0=T,
92
+ p0=max_pressure,
93
+ ds0=-1e-1,
94
+ stop_pressure=max_pressure * 1.1,
95
+ max_points=max_points,
96
+ stability_analysis=stability_analysis,
97
+ )
98
+
99
+ self._cl_ll = cl
100
+ self._cep_ll = cep
101
+
102
+ def plot_gped(self):
103
+ for pure in self._pures:
104
+ plt.plot(pure["T"], pure["P"], color="green")
105
+
106
+ plt.plot(self._cl21["T"], self._cl21["P"], color="black")
107
+ if self._cl12:
108
+ plt.plot(self._cl12["T"], self._cl12["P"], color="black")
109
+ if self._cl_ll:
110
+ plt.plot(self._cl_ll["T"], self._cl_ll["P"], color="black")
111
+
112
+ plt.xlabel("Temperature (K)")
113
+ plt.ylabel("Pressure (bar)")
114
+
115
+ def plot_pxy(self, temperature, a0=1e-5):
116
+ """Plot a Pxy phase diagram"""
117
+ psat_1, psat_2 = self._pures
118
+
119
+ px_12 = px_21 = px_iso = None
120
+
121
+ # Below saturation temperature of light component
122
+ loc = np.argmin(abs(psat_2["T"] - temperature))
123
+ p0 = psat_2["P"][loc]
124
+ px_21 = self._model.phase_envelope_px(
125
+ self._z0,
126
+ self._zi,
127
+ temperature=temperature,
128
+ kind="bubble",
129
+ p0=p0,
130
+ a0=a0,
131
+ ds0=1e-3,
132
+ max_points=MAX_POINTS,
133
+ )
134
+
135
+ pxs = [px_12, px_21, px_iso]
136
+
137
+ for px in pxs:
138
+ if px:
139
+ plt.plot(px.main_phases_compositions[:, 0, 0], px["P"])
140
+ plt.plot(px.reference_phase_compositions[:, 0], px["P"])
141
+
142
+ plt.xlabel(r"$x_1$, $y_1$")
143
+ plt.ylabel("Pressure (bar)")
144
+
145
+ return pxs
146
+
147
+ def plot_txy(self, pressure, a0=1e-5):
148
+ """Plot a Txy phase diagram"""
149
+ psat_1, psat_2 = self._pures
150
+
151
+ tx_12 = tx_21 = tx_ll = None
152
+
153
+ # Below critical pressure of heavy component
154
+ if pressure < psat_2["P"][-1]:
155
+ loc = np.argmin(abs(psat_2["P"] - pressure))
156
+ t0 = psat_2["T"][loc]
157
+
158
+ tx_21 = self._model.phase_envelope_tx(
159
+ self._z0,
160
+ self._zi,
161
+ pressure=pressure,
162
+ kind="dew",
163
+ t0=t0,
164
+ a0=a0,
165
+ ds0=1e-5,
166
+ max_points=MAX_POINTS,
167
+ )
168
+ if pressure < psat_1["P"][-1]:
169
+ # Below critical pressure of the light component
170
+ loc = np.argmin(abs(psat_1["P"] - pressure))
171
+ t0 = psat_1["T"][loc]
172
+
173
+ tx_12 = self._model.phase_envelope_tx(
174
+ self._z0,
175
+ self._zi,
176
+ pressure=pressure,
177
+ kind="bubble",
178
+ t0=t0,
179
+ a0=1 - a0,
180
+ ds0=-1e-5,
181
+ max_points=MAX_POINTS,
182
+ )
183
+
184
+ if self._cl_ll:
185
+ loc = np.argmin(abs(self._cl_ll["P"] - pressure))
186
+ t0, p = self._cl_ll["T"][loc], self._cl_ll["P"][loc]
187
+ if abs(p - pressure) < 1 or pressure > psat_1["P"][-1]:
188
+
189
+ a = self._cl_ll["a"][loc]
190
+ z = a * self._zi + (1 - a) * self._z0
191
+ x_l0 = [z.copy()]
192
+
193
+ x_l0[0][0] += 1e-5
194
+ x_l0[0][1] -= 1e-5
195
+ w0 = z.copy()
196
+ w0[0] -= 1e-5
197
+ w0[1] += 1e-5
198
+
199
+ tx_ll = self._model.phase_envelope_tx_mp(
200
+ z0=self._z0,
201
+ zi=self._zi,
202
+ p=pressure,
203
+ kinds_x=["liquid"],
204
+ kind_w="liquid",
205
+ x_l0=x_l0,
206
+ w0=w0,
207
+ betas0=[1],
208
+ t0=t0,
209
+ alpha0=a + 1e-5,
210
+ ns0=len(z) + 3,
211
+ ds0=1e-4,
212
+ max_points=MAX_POINTS,
213
+ )
214
+
215
+ txs = [tx_12, tx_21, tx_ll]
216
+
217
+ for tx in txs:
218
+ if tx:
219
+ plt.plot(tx.main_phases_compositions[:, 0, 0], tx["T"])
220
+ plt.plot(tx.reference_phase_compositions[:, 0], tx["T"])
221
+
222
+ plt.xlabel(r"$x_1$, $y_1$")
223
+ plt.ylabel("Temperature (K)")
224
+
225
+ return txs
yaeos/lib/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """yaeos_c module.
2
+
3
+ This module provides the yaeos_c module, which is a Python interface to the
4
+ yaeos C API compiled with f2py.
5
+ """
6
+
7
+ from yaeos.lib.yaeos_python import yaeos_c
8
+
9
+ __all__ = ["yaeos_c"]
@@ -0,0 +1,26 @@
1
+ """Models module.
2
+
3
+ Yaeos models module. This module provides the following submodules:
4
+
5
+ - excess_gibbs: Excess Gibbs energy models
6
+ - NRTL: non-random two-liquid model
7
+ - UNIFACVLE: Original UNIFAC VLE model
8
+ - UNIQUAC: UNIversal QUAsiChemical Excess Gibbs free energy model
9
+
10
+ - residual_helmholtz: Residual Helmholtz energy models
11
+ - Cubic EoS:
12
+ - PengRobinson76: Peng-Robinson model (1976)
13
+ - PengRobinson78: Peng-Robinson model (1978)
14
+ - SoaveRedlichKwong: Soave-Redlich-Kwong model
15
+ - RKPR: RKPR model
16
+ - Mixing rules: mixing rules for cubic EoS
17
+ - QMR: cuadratic mixing rule
18
+ - MHV: modified Huron-Vidal mixing rule
19
+ - Multifluid EoS:
20
+ - GERG2008: GERG2008 Residual contribution
21
+ """
22
+
23
+ from . import excess_gibbs, residual_helmholtz
24
+
25
+
26
+ __all__ = ["excess_gibbs", "residual_helmholtz"]
@@ -0,0 +1,20 @@
1
+ """Gibbs Excess Models module.
2
+
3
+ Yaeos Gibbs excess module. This module provides the following submodules:
4
+
5
+ - excess_gibbs: Excess Gibbs energy models
6
+ - NRTL: non-random two-liquid model
7
+ - UNIFACVLE: Original UNIFAC VLE model
8
+ - UNIFACPSRK: UNIFAC-PSRK model (Predictive Soave-Redlich-Kwong)
9
+ - UNIFACDortmund: UNIFAC Dortmund model
10
+ - UNIQUAC: UNIversal QUAsiChemical Excess Gibbs free energy model
11
+ """
12
+
13
+ from .dortmund import UNIFACDortmund
14
+ from .nrtl import NRTL
15
+ from .psrk_unifac import UNIFACPSRK
16
+ from .unifac import UNIFACVLE
17
+ from .uniquac import UNIQUAC
18
+
19
+
20
+ __all__ = ["NRTL", "UNIFACVLE", "UNIFACPSRK", "UNIFACDortmund", "UNIQUAC"]
@@ -0,0 +1,45 @@
1
+ """UNIFAC Dortmund Module."""
2
+
3
+ from typing import List
4
+
5
+ from yaeos.core import GeModel
6
+ from yaeos.lib import yaeos_c
7
+ from yaeos.models.groups import groups_from_dicts
8
+
9
+
10
+ class UNIFACDortmund(GeModel):
11
+ """UNIFAC Dortmund model.
12
+
13
+ Please refer to the `yaeos` user documentation for an in-depth look at the
14
+ model's information: https://ipqa-research.github.io/yaeos/page/index.html
15
+
16
+ Parameters
17
+ ----------
18
+ molecules : list of dict
19
+ List of dicts with the groups and their amounts for each molecule.
20
+
21
+ Example
22
+ -------
23
+ .. code-block:: python
24
+
25
+ from yaeos import UNIFACDortmund
26
+
27
+ # Groups for water and ethanol
28
+ water = {16: 1}
29
+ ethanol = {1: 1, 2: 1, 14: 1}
30
+
31
+ groups = [water, ethanol]
32
+
33
+ model = UNIFACDortmund(groups)
34
+
35
+ model.ln_gamma([0.5, 0.5], 298.15)
36
+ """
37
+
38
+ def __init__(self, molecules: List[dict]) -> None:
39
+
40
+ (number_of_groups, groups_ids, groups_ammounts) = groups_from_dicts(
41
+ molecules
42
+ )
43
+ self.id = yaeos_c.unifac_dortmund(
44
+ ngs=number_of_groups, g_ids=groups_ids, g_v=groups_ammounts
45
+ )