yaeos 3.1.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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/envelopes.py ADDED
@@ -0,0 +1,407 @@
1
+ """Envelopes.
2
+
3
+ This module contains the classes that wrapp the data structures used to
4
+ represent different kinds of phase envelopes.
5
+ """
6
+
7
+ from IPython.display import display
8
+
9
+ import matplotlib.pyplot as plt
10
+
11
+ import numpy as np
12
+
13
+ import pandas as pd
14
+
15
+
16
+ class PTEnvelope:
17
+ """PTEnvelope.
18
+
19
+ This class represents a pressure-temperature envelope.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ global_composition,
25
+ main_phases_compositions,
26
+ reference_phase_compositions,
27
+ main_phases_molar_fractions,
28
+ pressures,
29
+ temperatures,
30
+ iterations,
31
+ specified_variable,
32
+ ):
33
+
34
+ msk = ~np.isnan(pressures)
35
+ self.number_of_components = len(global_composition)
36
+ self.number_of_phases = main_phases_compositions.shape[1]
37
+ self.global_composition = global_composition
38
+ self.main_phases_compositions = main_phases_compositions[msk, :, :]
39
+ self.reference_phase_compositions = reference_phase_compositions[
40
+ msk, :
41
+ ]
42
+ self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
43
+ self.pressures = pressures[msk]
44
+ self.temperatures = temperatures[msk]
45
+ self.iterations = iterations[msk]
46
+ self.specified_variable = specified_variable[msk]
47
+
48
+ df = pd.DataFrame()
49
+
50
+ df["T"] = self.temperatures
51
+ df["P"] = self.pressures
52
+
53
+ for i in range(self.number_of_components):
54
+ for j in range(self.number_of_phases):
55
+ df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
56
+ df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
57
+
58
+ for i in range(self.number_of_phases):
59
+ df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
60
+
61
+ self.df = df
62
+
63
+ idx = []
64
+ for phase in range(self.number_of_phases):
65
+ cp_idx = []
66
+ lnK = np.log(
67
+ self.reference_phase_compositions
68
+ / self.main_phases_compositions[:, phase]
69
+ )
70
+
71
+ for i in range(1, len(lnK)):
72
+ if all(lnK[i, :] * lnK[i - 1, :] < 0):
73
+ cp_idx.append(i)
74
+
75
+ idx.append(cp_idx)
76
+
77
+ self.cp = idx
78
+
79
+ # lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
80
+ # lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
81
+ # for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
82
+ # lnK2 = np.log(c["w"]/c["x_l"][i, 1])
83
+ # crit = (lnK * lnK2 < 0).all()
84
+ # if crit:
85
+ # print("asd")
86
+
87
+ def __getitem__(self, key):
88
+ if "key" in self.__dict__:
89
+ return self.__dict__["key"]
90
+ elif isinstance(key, np.ndarray):
91
+ return PTEnvelope(
92
+ global_composition=self.global_composition,
93
+ main_phases_compositions=self.main_phases_compositions[key],
94
+ reference_phase_compositions=self.reference_phase_compositions[
95
+ key
96
+ ],
97
+ main_phases_molar_fractions=self.main_phases_molar_fractions[
98
+ key
99
+ ],
100
+ pressures=self.pressures[key],
101
+ temperatures=self.temperatures[key],
102
+ iterations=self.iterations[key],
103
+ specified_variable=self.specified_variable[key],
104
+ )
105
+ elif key == "T":
106
+ return self.temperatures
107
+ elif key == "Tc":
108
+ return self.temperatures[self.cp]
109
+ elif key == "Pc":
110
+ return self.pressures[self.cp]
111
+ elif key == "P":
112
+ return self.pressures
113
+ elif key == "z":
114
+ return self.global_composition
115
+ elif key == "x":
116
+ return self.main_phases_compositions
117
+ elif key == "w":
118
+ return self.reference_phase_compositions
119
+
120
+ def plot(self):
121
+ plt.plot(self.temperatures, self.pressures)
122
+ plt.xlabel("Temperature (K)")
123
+ plt.ylabel("Pressure [bar]")
124
+ plt.title("PT Envelope")
125
+ for cp in self.cp:
126
+ plt.scatter(
127
+ self.temperatures[cp], self.pressures[cp], color="black"
128
+ )
129
+
130
+ def __repr__(self):
131
+ display(self.df)
132
+ return ""
133
+
134
+ def __mul__(self, other):
135
+ return PTEnvelope(
136
+ global_composition=self.global_composition,
137
+ main_phases_compositions=self.main_phases_compositions * other,
138
+ reference_phase_compositions=self.reference_phase_compositions
139
+ * other,
140
+ main_phases_molar_fractions=self.main_phases_molar_fractions
141
+ * other,
142
+ pressures=self.pressures * other,
143
+ temperatures=self.temperatures * other,
144
+ iterations=self.iterations,
145
+ specified_variable=self.specified_variable,
146
+ )
147
+
148
+ def __len__(self):
149
+ return len(self.temperatures)
150
+
151
+
152
+ class PXEnvelope:
153
+ """PXEnvelope.
154
+
155
+ This class represents a pressure-composition envelope.
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ temperature,
161
+ global_composition_0,
162
+ global_composition_i,
163
+ main_phases_compositions,
164
+ reference_phase_compositions,
165
+ main_phases_molar_fractions,
166
+ pressures,
167
+ alphas,
168
+ iterations,
169
+ specified_variable,
170
+ ):
171
+
172
+ msk = ~np.isnan(pressures)
173
+ self.temperature = temperature
174
+ self.number_of_components = len(global_composition_0)
175
+ self.number_of_phases = main_phases_compositions.shape[1]
176
+ self.global_composition_0 = global_composition_0
177
+ self.global_composition_i = global_composition_i
178
+ self.main_phases_compositions = main_phases_compositions[msk, :, :]
179
+ self.reference_phase_compositions = reference_phase_compositions[
180
+ msk, :
181
+ ]
182
+ self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
183
+ self.pressures = pressures[msk]
184
+ self.alphas = alphas[msk]
185
+ self.iterations = iterations[msk]
186
+ self.specified_variable = specified_variable[msk]
187
+
188
+ df = pd.DataFrame()
189
+
190
+ df["alpha"] = self.alphas
191
+ df["P"] = self.pressures
192
+
193
+ for i in range(self.number_of_components):
194
+ for j in range(self.number_of_phases):
195
+ df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
196
+ df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
197
+
198
+ for i in range(self.number_of_phases):
199
+ df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
200
+
201
+ self.df = df
202
+
203
+ idx = []
204
+ for phase in range(self.number_of_phases):
205
+ cp_idx = []
206
+ lnK = np.log(
207
+ self.reference_phase_compositions
208
+ / self.main_phases_compositions[:, phase]
209
+ )
210
+
211
+ for i in range(1, len(lnK)):
212
+ if all(lnK[i, :] * lnK[i - 1, :] < 0):
213
+ cp_idx.append(i)
214
+
215
+ idx.append(cp_idx)
216
+
217
+ self.cp = idx
218
+
219
+ # lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
220
+ # lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
221
+ # for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
222
+ # lnK2 = np.log(c["w"]/c["x_l"][i, 1])
223
+ # crit = (lnK * lnK2 < 0).all()
224
+ # if crit:
225
+ # print("asd")
226
+
227
+ def __getitem__(self, key):
228
+ if "key" in self.__dict__:
229
+ return self.__dict__["key"]
230
+ elif isinstance(key, np.ndarray):
231
+ return PXEnvelope(
232
+ global_composition_0=self.global_composition_0,
233
+ global_composition_i=self.global_composition_i,
234
+ temperature=self.temperature,
235
+ main_phases_compositions=self.main_phases_compositions[key],
236
+ reference_phase_compositions=self.reference_phase_compositions[
237
+ key
238
+ ],
239
+ main_phases_molar_fractions=self.main_phases_molar_fractions[
240
+ key
241
+ ],
242
+ pressures=self.pressures[key],
243
+ alphas=self.alphas[key],
244
+ iterations=self.iterations[key],
245
+ specified_variable=self.specified_variable[key],
246
+ )
247
+ elif key == "alpha" or key == "a":
248
+ return self.alphas
249
+ elif key == "P":
250
+ return self.pressures
251
+ elif key == "z":
252
+ return self.global_composition
253
+ elif key == "x":
254
+ return self.main_phases_compositions
255
+ elif key == "w":
256
+ return self.reference_phase_compositions
257
+
258
+ def __repr__(self):
259
+ return self.df.__repr__()
260
+
261
+ def __mul__(self, other):
262
+ return PXEnvelope(
263
+ global_composition_0=self.global_composition_0,
264
+ global_composition_i=self.global_composition_i,
265
+ temperature=self.temperature,
266
+ main_phases_compositions=self.main_phases_compositions * other,
267
+ reference_phase_compositions=self.reference_phase_compositions
268
+ * other,
269
+ main_phases_molar_fractions=self.main_phases_molar_fractions
270
+ * other,
271
+ pressures=self.pressures * other,
272
+ alphas=self.alphas * other,
273
+ iterations=self.iterations,
274
+ specified_variable=self.specified_variable,
275
+ )
276
+
277
+ def __len__(self):
278
+ return len(self.alphas)
279
+
280
+
281
+ class TXEnvelope:
282
+ """TXEnvelope.
283
+
284
+ This class represents a pressure-composition envelope.
285
+ """
286
+
287
+ def __init__(
288
+ self,
289
+ pressure,
290
+ global_composition_0,
291
+ global_composition_i,
292
+ main_phases_compositions,
293
+ reference_phase_compositions,
294
+ main_phases_molar_fractions,
295
+ temperatures,
296
+ alphas,
297
+ iterations,
298
+ specified_variable,
299
+ ):
300
+
301
+ msk = ~np.isnan(temperatures)
302
+ self.temperature = pressure
303
+ self.number_of_components = len(global_composition_0)
304
+ self.number_of_phases = main_phases_compositions.shape[1]
305
+ self.global_composition_0 = global_composition_0
306
+ self.global_composition_i = global_composition_i
307
+ self.main_phases_compositions = main_phases_compositions[msk, :, :]
308
+ self.reference_phase_compositions = reference_phase_compositions[
309
+ msk, :
310
+ ]
311
+ self.main_phases_molar_fractions = main_phases_molar_fractions[msk]
312
+ self.temperatures = temperatures[msk]
313
+ self.alphas = alphas[msk]
314
+ self.iterations = iterations[msk]
315
+ self.specified_variable = specified_variable[msk]
316
+
317
+ df = pd.DataFrame()
318
+
319
+ df["alpha"] = self.alphas
320
+ df["T"] = self.temperatures
321
+
322
+ for i in range(self.number_of_components):
323
+ for j in range(self.number_of_phases):
324
+ df[f"x_{i+1}^{j+1}"] = self.main_phases_compositions[:, j, i]
325
+ df[f"w_{i+1}"] = self.reference_phase_compositions[:, i]
326
+
327
+ for i in range(self.number_of_phases):
328
+ df[f"beta^{i+1}"] = self.main_phases_molar_fractions[:, i]
329
+
330
+ self.df = df
331
+
332
+ idx = []
333
+ for phase in range(self.number_of_phases):
334
+ cp_idx = []
335
+ lnK = np.log(
336
+ self.reference_phase_compositions
337
+ / self.main_phases_compositions[:, phase]
338
+ )
339
+
340
+ for i in range(1, len(lnK)):
341
+ if all(lnK[i, :] * lnK[i - 1, :] < 0):
342
+ cp_idx.append(i)
343
+
344
+ idx.append(cp_idx)
345
+
346
+ self.cp = idx
347
+
348
+ # lnKs1 = np.log(c["w"][1:]/c["x_l"][1:, 1])
349
+ # lnKs0 = np.log(c["w"][1:]/c["x_l"][1:, 0])
350
+ # for i, lnK in enumerate(np.log(c["w"][1:]/c["x_l"][1:, 1])):
351
+ # lnK2 = np.log(c["w"]/c["x_l"][i, 1])
352
+ # crit = (lnK * lnK2 < 0).all()
353
+ # if crit:
354
+ # print("asd")
355
+
356
+ def __getitem__(self, key):
357
+ if "key" in self.__dict__:
358
+ return self.__dict__["key"]
359
+ elif isinstance(key, np.ndarray):
360
+ return TXEnvelope(
361
+ global_composition_0=self.global_composition_0,
362
+ global_composition_i=self.global_composition_i,
363
+ pressure=self.pressure,
364
+ main_phases_compositions=self.main_phases_compositions[key],
365
+ reference_phase_compositions=self.reference_phase_compositions[
366
+ key
367
+ ],
368
+ main_phases_molar_fractions=self.main_phases_molar_fractions[
369
+ key
370
+ ],
371
+ temperatures=self.temperatures[key],
372
+ alphas=self.alphas[key],
373
+ iterations=self.iterations[key],
374
+ specified_variable=self.specified_variable[key],
375
+ )
376
+ elif key == "alpha" or key == "a":
377
+ return self.alphas
378
+ elif key == "T":
379
+ return self.temperatures
380
+ elif key == "z":
381
+ return self.global_composition
382
+ elif key == "x":
383
+ return self.main_phases_compositions
384
+ elif key == "w":
385
+ return self.reference_phase_compositions
386
+
387
+ def __repr__(self):
388
+ return self.df.__repr__()
389
+
390
+ def __mul__(self, other):
391
+ return TXEnvelope(
392
+ global_composition_0=self.global_composition_0,
393
+ global_composition_i=self.global_composition_i,
394
+ pressure=self.pressure,
395
+ main_phases_compositions=self.main_phases_compositions * other,
396
+ reference_phase_compositions=self.reference_phase_compositions
397
+ * other,
398
+ main_phases_molar_fractions=self.main_phases_molar_fractions
399
+ * other,
400
+ temperatures=self.temperature * other,
401
+ alphas=self.alphas * other,
402
+ iterations=self.iterations,
403
+ specified_variable=self.specified_variable,
404
+ )
405
+
406
+ def __len__(self):
407
+ return len(self.alphas)
@@ -0,0 +1,12 @@
1
+ """yaeos fitting module.
2
+
3
+ This module provides classes and functions for fitting binary interaction
4
+ parameters to experimental data.
5
+ """
6
+
7
+ from yaeos.fitting.core import BinaryFitter
8
+ from yaeos.fitting.model_setters import fit_kij_lij
9
+ from yaeos.fitting.solvers import solve_pt
10
+
11
+
12
+ __all__ = ["BinaryFitter", "fit_kij_lij", "solve_pt"]
yaeos/fitting/core.py ADDED
@@ -0,0 +1,193 @@
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
+ cl = model.critical_line(
73
+ z0=[0, 1], zi=[1, 0], a0=1e-2, s=1e-2, ds0=1e-3, max_points=5000
74
+ )
75
+
76
+ err = 0
77
+
78
+ for _, row in data.iterrows():
79
+ x = [row["x1"], 1 - row["x1"]]
80
+ y = [row["y1"], 1 - row["y1"]]
81
+ t = row["T"]
82
+ p = row["P"]
83
+
84
+ try:
85
+ w = row["weight"]
86
+ except KeyError:
87
+ w = 1
88
+
89
+ error_i = 0
90
+
91
+ # =================================================================
92
+ # Bubble point
93
+ # -----------------------------------------------------------------
94
+ if row["kind"] == "bubble":
95
+ sat = model.saturation_pressure(
96
+ x, kind="bubble", temperature=t, p0=p
97
+ )
98
+ error_i += pressure_error(p, sat["P"])
99
+
100
+ # =================================================================
101
+ # Dew points
102
+ # -----------------------------------------------------------------
103
+ if row["kind"] == "dew":
104
+ sat = model.saturation_pressure(
105
+ x, kind="dew", temperature=t, p0=p
106
+ )
107
+
108
+ error_i += pressure_error(p, sat["P"])
109
+
110
+ if row["kind"] == "PT" or row["kind"] == "liquid-liquid":
111
+ x1, y1 = solve_pt(model, row["P"], row["T"], row["kind"])
112
+
113
+ if np.isnan(x[0]):
114
+ error_i += composition_error(y, [y1, 1 - y1])
115
+
116
+ elif np.isnan(y[0]):
117
+ error_i += composition_error(x, [x1, 1 - x1])
118
+
119
+ else:
120
+ error_i += composition_error(
121
+ x, [x1, 1 - x1]
122
+ ) + composition_error(y, [y1, 1 - y1])
123
+
124
+ # =================================================================
125
+ # Critical point error is calculated by finding the nearest
126
+ # critical point in the critical line to the given critical
127
+ # point in the data.
128
+ # -----------------------------------------------------------------
129
+ if row["kind"] == "critical":
130
+ cp = row
131
+ distances = (
132
+ (cp["T"] - cl["T"]) ** 2
133
+ + ((cp["P"] - cl["P"]) ** 2)
134
+ + ((cp["x1"] - cl["a"]) ** 2)
135
+ )
136
+ nearest = np.argmin(distances)
137
+ t_cl, p_cl, x1 = (
138
+ cl["T"][nearest],
139
+ cl["P"][nearest],
140
+ cl["a"][nearest],
141
+ )
142
+ error_i += temperature_error(cp["T"], t_cl)
143
+ error_i += pressure_error(cp["P"], p_cl)
144
+ error_i += composition_error(
145
+ [cp["x1"], 1 - cp["x1"]], [x1, 1 - x1]
146
+ )
147
+
148
+ if np.isnan(error_i):
149
+ error_i = row["P"]
150
+ err += error_i * w
151
+
152
+ # =====================================================================
153
+ # Normalize the error and save the valuation
154
+ # ---------------------------------------------------------------------
155
+ err = err / len(data)
156
+
157
+ self.evaluations["fobj"].append(err)
158
+ self.evaluations["x"].append(x_values)
159
+ self.evaluations["cl"].append(cl)
160
+
161
+ if self.verbose:
162
+ print(err, x_values)
163
+ return err
164
+
165
+ def fit(self, x0, bounds, method="Nelder-Mead"):
166
+ """Fit the model to the data.
167
+
168
+ Fit the model to the data using the objective function defined in
169
+ the objective_function method. The optimization is performed using
170
+ the scipy.optimize.minimize function.
171
+ The optimization result is stored in the `.solution` property. Which
172
+
173
+ Parameters
174
+ ----------
175
+ x0 : array-like
176
+ Initial guess for the fitting parameters.
177
+ bounds : array-like
178
+ Bounds for the fitting parameters.
179
+ method : str, optional
180
+ The optimization method to use. Default is 'Nelder-Mead'.
181
+
182
+ Returns
183
+ -------
184
+ None
185
+ """
186
+ sol = minimize(
187
+ self.objective_function, x0=x0, bounds=bounds, method=method
188
+ )
189
+ self._solution = sol
190
+
191
+ @property
192
+ def solution(self):
193
+ 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