prbs-eos 0.1.0__py3-none-any.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.
prbs_eos/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .constants import R
2
+ from .data import Component, Mixture, get_component, available_components
3
+ from .core import PREOS, PRBSEOS, solve_cubic, select_z
4
+ from .equilibrium import flash_pt, FlashResult, is_stable
5
+ from .thermodynamics import enthalpy_departure, entropy_departure
6
+ from .regression import regress_kij, regress_vpaa
prbs_eos/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Allow running the CLI via ``python -m prbs_eos``."""
2
+ from .cli import main
3
+
4
+ main()
prbs_eos/cli.py ADDED
@@ -0,0 +1,308 @@
1
+ """
2
+ prbs-eos command-line interface.
3
+
4
+ Usage examples
5
+ --------------
6
+ prbs-eos components
7
+ prbs-eos properties methane
8
+ prbs-eos flash --T 250 --P 1e6 --mix methane:0.6 propane:0.4
9
+ prbs-eos zfactor --T 300 --P 2e6 --mix methane:0.7 propane:0.3 --phase vapor
10
+ prbs-eos fugacity --T 300 --P 2e6 --mix methane:0.7 propane:0.3 --phase vapor
11
+ prbs-eos bubble --T 250 --mix methane:0.5 propane:0.5 --P-guess 2e6
12
+ prbs-eos dew --T 250 --mix methane:0.5 propane:0.5 --P-guess 2e6
13
+ prbs-eos enthalpy --T 300 --P 2e6 --mix methane:0.7 propane:0.3 --phase vapor
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ import numpy as np
20
+
21
+ from . import (
22
+ Mixture, PREOS, get_component, available_components,
23
+ flash_pt, enthalpy_departure, entropy_departure,
24
+ )
25
+ from .equilibrium.phase_envelope import bubble_point_P, dew_point_P
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+ def _parse_mix(mix_tokens: list[str]) -> tuple[Mixture, np.ndarray]:
33
+ """
34
+ Parse ``['methane:0.6', 'propane:0.4']`` into a (Mixture, z) pair.
35
+ Mole fractions are normalised to sum to 1.
36
+ """
37
+ comps, fracs = [], []
38
+ for token in mix_tokens:
39
+ if ":" not in token:
40
+ _die(f"Bad mixture token '{token}'. Expected format: name:fraction")
41
+ name, frac_str = token.rsplit(":", 1)
42
+ try:
43
+ frac = float(frac_str)
44
+ except ValueError:
45
+ _die(f"Cannot parse fraction '{frac_str}' in token '{token}'")
46
+ if frac < 0:
47
+ _die(f"Negative mole fraction in token '{token}'")
48
+ comps.append(get_component(name.strip()))
49
+ fracs.append(frac)
50
+ z = np.array(fracs, dtype=float)
51
+ total = z.sum()
52
+ if total <= 0:
53
+ _die("Mole fractions sum to zero.")
54
+ z /= total
55
+ return Mixture(comps), z
56
+
57
+
58
+ def _die(msg: str) -> None:
59
+ print(f"ERROR: {msg}", file=sys.stderr)
60
+ sys.exit(1)
61
+
62
+
63
+ def _out(data: dict, as_json: bool) -> None:
64
+ """Print result as a formatted table or JSON."""
65
+ if as_json:
66
+ print(json.dumps(data, indent=2))
67
+ else:
68
+ for key, val in data.items():
69
+ if isinstance(val, dict):
70
+ print(f" {key}:")
71
+ for k2, v2 in val.items():
72
+ print(f" {k2:<20s} {v2}")
73
+ else:
74
+ print(f" {key:<26s} {val}")
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Sub-command handlers
79
+ # ---------------------------------------------------------------------------
80
+
81
+ def cmd_components(_args) -> None:
82
+ print("Built-in components:")
83
+ for name in available_components():
84
+ print(f" {name}")
85
+
86
+
87
+ def cmd_properties(args) -> None:
88
+ try:
89
+ c = get_component(args.name)
90
+ except KeyError as e:
91
+ _die(str(e))
92
+ data = {
93
+ "name": c.name,
94
+ "Tc (K)": f"{c.Tc:.2f}",
95
+ "Pc (MPa)": f"{c.Pc / 1e6:.4f}",
96
+ "omega": f"{c.omega:.4f}",
97
+ "Mw (g/mol)": f"{c.Mw:.3f}",
98
+ "c (m3/mol)": f"{c.c:.3e}",
99
+ "epsilon (J/mol)": f"{c.epsilon:.2f}",
100
+ "kappa": f"{c.kappa:.4f}",
101
+ "assoc_sites": str(c.assoc_sites),
102
+ }
103
+ _out(data, args.json)
104
+
105
+
106
+ def cmd_flash(args) -> None:
107
+ try:
108
+ mix, z = _parse_mix(args.mix)
109
+ except KeyError as e:
110
+ _die(str(e))
111
+ eos = PREOS(mix)
112
+ res = flash_pt(eos, T=args.T, P=args.P, z=z)
113
+ names = [c.name for c in mix.components]
114
+ data = {
115
+ "T (K)": f"{args.T:.2f}",
116
+ "P (MPa)": f"{args.P / 1e6:.4f}",
117
+ "vapour fraction beta":f"{res.beta:.6f}",
118
+ "converged": str(res.converged),
119
+ "iterations": str(res.iterations),
120
+ "liquid x": {n: f"{v:.6f}" for n, v in zip(names, res.x)},
121
+ "vapour y": {n: f"{v:.6f}" for n, v in zip(names, res.y)},
122
+ "K-factors": {n: f"{v:.6f}" for n, v in zip(names, res.K)},
123
+ }
124
+ _out(data, args.json)
125
+
126
+
127
+ def cmd_zfactor(args) -> None:
128
+ try:
129
+ mix, z = _parse_mix(args.mix)
130
+ except KeyError as e:
131
+ _die(str(e))
132
+ eos = PREOS(mix)
133
+ Z = eos.Z_factor(T=args.T, P=args.P, z=z, phase=args.phase)
134
+ data = {
135
+ "T (K)": f"{args.T:.2f}",
136
+ "P (MPa)": f"{args.P / 1e6:.4f}",
137
+ "phase": args.phase,
138
+ "Z-factor": f"{Z:.6f}",
139
+ }
140
+ _out(data, args.json)
141
+
142
+
143
+ def cmd_fugacity(args) -> None:
144
+ try:
145
+ mix, z = _parse_mix(args.mix)
146
+ except KeyError as e:
147
+ _die(str(e))
148
+ eos = PREOS(mix)
149
+ phi = eos.fugacity_coeff(T=args.T, P=args.P, z=z, phase=args.phase)
150
+ f = eos.fugacity(T=args.T, P=args.P, z=z, phase=args.phase)
151
+ names = [c.name for c in mix.components]
152
+ data = {
153
+ "T (K)": f"{args.T:.2f}",
154
+ "P (MPa)": f"{args.P / 1e6:.4f}",
155
+ "phase": args.phase,
156
+ "fugacity coefficients phi_i": {n: f"{v:.6f}" for n, v in zip(names, phi)},
157
+ "fugacities f_i (Pa)": {n: f"{v:.4e}" for n, v in zip(names, f)},
158
+ }
159
+ _out(data, args.json)
160
+
161
+
162
+ def cmd_bubble(args) -> None:
163
+ try:
164
+ mix, z = _parse_mix(args.mix)
165
+ except KeyError as e:
166
+ _die(str(e))
167
+ eos = PREOS(mix)
168
+ Pb = bubble_point_P(eos, T=args.T, z=z, P_guess=args.P_guess)
169
+ data = {
170
+ "T (K)": f"{args.T:.2f}",
171
+ "bubble-point P (Pa)": "nan (supercritical or out of range)" if np.isnan(Pb) else f"{Pb:.2f}",
172
+ "bubble-point P (MPa)":"nan" if np.isnan(Pb) else f"{Pb / 1e6:.4f}",
173
+ }
174
+ _out(data, args.json)
175
+
176
+
177
+ def cmd_dew(args) -> None:
178
+ try:
179
+ mix, z = _parse_mix(args.mix)
180
+ except KeyError as e:
181
+ _die(str(e))
182
+ eos = PREOS(mix)
183
+ Pd = dew_point_P(eos, T=args.T, z=z, P_guess=args.P_guess)
184
+ data = {
185
+ "T (K)": f"{args.T:.2f}",
186
+ "dew-point P (Pa)": "nan (supercritical or out of range)" if np.isnan(Pd) else f"{Pd:.2f}",
187
+ "dew-point P (MPa)": "nan" if np.isnan(Pd) else f"{Pd / 1e6:.4f}",
188
+ }
189
+ _out(data, args.json)
190
+
191
+
192
+ def cmd_enthalpy(args) -> None:
193
+ try:
194
+ mix, z = _parse_mix(args.mix)
195
+ except KeyError as e:
196
+ _die(str(e))
197
+ eos = PREOS(mix)
198
+ H = enthalpy_departure(eos, T=args.T, P=args.P, z=z, phase=args.phase)
199
+ S = entropy_departure(eos, T=args.T, P=args.P, z=z, phase=args.phase)
200
+ data = {
201
+ "T (K)": f"{args.T:.2f}",
202
+ "P (MPa)": f"{args.P / 1e6:.4f}",
203
+ "phase": args.phase,
204
+ "H_dep (J/mol)": f"{H:.4f}",
205
+ "S_dep (J/mol/K)": f"{S:.6f}",
206
+ "G_dep = H-TS (J/mol)": f"{H - args.T * S:.4f}",
207
+ }
208
+ _out(data, args.json)
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Argument parser
213
+ # ---------------------------------------------------------------------------
214
+
215
+ def _build_parser() -> argparse.ArgumentParser:
216
+ parser = argparse.ArgumentParser(
217
+ prog="prbs-eos",
218
+ description=(
219
+ "PRBS Equation of State — petroleum thermodynamics toolkit.\n"
220
+ "Run 'prbs-eos <command> --help' for per-command options."
221
+ ),
222
+ formatter_class=argparse.RawDescriptionHelpFormatter,
223
+ )
224
+ parser.add_argument("--version", action="version", version="prbs-eos 0.1.0")
225
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
226
+ sub.required = True
227
+
228
+ # ---- shared options ------------------------------------------------
229
+ def add_mix(p):
230
+ p.add_argument(
231
+ "--mix", nargs="+", required=True, metavar="NAME:FRAC",
232
+ help="Mixture components, e.g. methane:0.6 propane:0.4"
233
+ )
234
+ def add_TP(p):
235
+ p.add_argument("--T", type=float, required=True, metavar="K",
236
+ help="Temperature [K]")
237
+ p.add_argument("--P", type=float, required=True, metavar="Pa",
238
+ help="Pressure [Pa] (e.g. 2e6 = 2 MPa)")
239
+ def add_phase(p):
240
+ p.add_argument("--phase", choices=["vapor", "liquid"], default="vapor",
241
+ help="Phase selection (default: vapor)")
242
+ def add_json(p):
243
+ p.add_argument("--json", action="store_true",
244
+ help="Output as JSON")
245
+ def add_P_guess(p):
246
+ p.add_argument("--P-guess", type=float, default=1e6, dest="P_guess",
247
+ metavar="Pa",
248
+ help="Initial pressure guess for bracketing [Pa] (default: 1e6)")
249
+
250
+ # ---- components ----------------------------------------------------
251
+ p = sub.add_parser("components", help="List all built-in components")
252
+ add_json(p)
253
+ p.set_defaults(func=cmd_components)
254
+
255
+ # ---- properties ----------------------------------------------------
256
+ p = sub.add_parser("properties", help="Show critical properties of a component")
257
+ p.add_argument("name", help="Component name (e.g. methane, co2, water)")
258
+ add_json(p)
259
+ p.set_defaults(func=cmd_properties)
260
+
261
+ # ---- flash ---------------------------------------------------------
262
+ p = sub.add_parser("flash", help="Two-phase PT flash (Rachford-Rice)")
263
+ add_TP(p); add_mix(p); add_json(p)
264
+ p.set_defaults(func=cmd_flash)
265
+
266
+ # ---- zfactor -------------------------------------------------------
267
+ p = sub.add_parser("zfactor", help="Compressibility factor Z")
268
+ add_TP(p); add_mix(p); add_phase(p); add_json(p)
269
+ p.set_defaults(func=cmd_zfactor)
270
+
271
+ # ---- fugacity ------------------------------------------------------
272
+ p = sub.add_parser("fugacity", help="Fugacity coefficients and fugacities")
273
+ add_TP(p); add_mix(p); add_phase(p); add_json(p)
274
+ p.set_defaults(func=cmd_fugacity)
275
+
276
+ # ---- bubble --------------------------------------------------------
277
+ p = sub.add_parser("bubble", help="Bubble-point pressure at fixed T")
278
+ p.add_argument("--T", type=float, required=True, metavar="K")
279
+ add_mix(p); add_P_guess(p); add_json(p)
280
+ p.set_defaults(func=cmd_bubble)
281
+
282
+ # ---- dew -----------------------------------------------------------
283
+ p = sub.add_parser("dew", help="Dew-point pressure at fixed T")
284
+ p.add_argument("--T", type=float, required=True, metavar="K")
285
+ add_mix(p); add_P_guess(p); add_json(p)
286
+ p.set_defaults(func=cmd_dew)
287
+
288
+ # ---- enthalpy ------------------------------------------------------
289
+ p = sub.add_parser("enthalpy",
290
+ help="Enthalpy/entropy departure functions (H_dep, S_dep, G_dep)")
291
+ add_TP(p); add_mix(p); add_phase(p); add_json(p)
292
+ p.set_defaults(func=cmd_enthalpy)
293
+
294
+ return parser
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Entry point
299
+ # ---------------------------------------------------------------------------
300
+
301
+ def main(argv: list[str] | None = None) -> None:
302
+ parser = _build_parser()
303
+ args = parser.parse_args(argv)
304
+ args.func(args)
305
+
306
+
307
+ if __name__ == "__main__":
308
+ main()
prbs_eos/constants.py ADDED
@@ -0,0 +1 @@
1
+ R = 8.314462618 # J/(mol·K)
@@ -0,0 +1,4 @@
1
+ from .cubic_solver import solve_cubic, select_z
2
+ from .eos_base import CubicEOS
3
+ from .pr_eos import PREOS
4
+ from .prbs_eos import PRBSEOS
@@ -0,0 +1,30 @@
1
+ """
2
+ Analytical and numerical roots of the cubic EOS in Z-factor form:
3
+ Z³ + p·Z² + q·Z + r = 0
4
+ """
5
+ import numpy as np
6
+
7
+
8
+ def solve_cubic(p: float, q: float, r: float) -> np.ndarray:
9
+ """Return all real roots of Z³ + p·Z² + q·Z + r = 0, sorted ascending."""
10
+ coeffs = [1.0, p, q, r]
11
+ roots = np.roots(coeffs)
12
+ real_roots = roots[np.abs(roots.imag) < 1e-8].real
13
+ return np.sort(real_roots)
14
+
15
+
16
+ def select_z(roots: np.ndarray, phase: str) -> float:
17
+ """
18
+ From the array of real positive Z roots select the thermodynamically
19
+ correct one for the requested phase ('vapor' or 'liquid').
20
+ """
21
+ pos = roots[roots > 0]
22
+ if len(pos) == 0:
23
+ raise ValueError("No positive Z root found.")
24
+ if len(pos) == 1:
25
+ return float(pos[0])
26
+ if phase == "vapor":
27
+ return float(pos.max())
28
+ if phase == "liquid":
29
+ return float(pos.min())
30
+ raise ValueError(f"Unknown phase '{phase}'. Use 'vapor' or 'liquid'.")
@@ -0,0 +1,84 @@
1
+ """Abstract base for cubic EOS implementations."""
2
+ from abc import ABC, abstractmethod
3
+ import numpy as np
4
+ from ..data.mixture import Mixture
5
+
6
+
7
+ class CubicEOS(ABC):
8
+ """
9
+ Subclasses must implement `_ab_pure` and `_mix_rule`.
10
+ Public API: `Z_factor`, `fugacity_coeff`.
11
+ """
12
+
13
+ def __init__(self, mixture: Mixture):
14
+ self.mix = mixture
15
+
16
+ # ------------------------------------------------------------------
17
+ # Abstract
18
+ # ------------------------------------------------------------------
19
+ @abstractmethod
20
+ def _ab_pure(self, T: float) -> tuple[np.ndarray, np.ndarray]:
21
+ """Return arrays (a_i, b_i) for each component at temperature T."""
22
+
23
+ @abstractmethod
24
+ def _mix_rule(
25
+ self, T: float, z: np.ndarray
26
+ ) -> tuple[float, float, np.ndarray]:
27
+ """
28
+ Return (a_mix, b_mix, (da/dni)_T) using the van der Waals mixing rule
29
+ with kij corrections.
30
+ """
31
+
32
+ @abstractmethod
33
+ def _cubic_coeffs(
34
+ self, T: float, P: float, a: float, b: float
35
+ ) -> tuple[float, float, float]:
36
+ """Return (p, q, r) for Z³ + p·Z² + q·Z + r = 0."""
37
+
38
+ @abstractmethod
39
+ def _ln_phi_i(
40
+ self,
41
+ i: int,
42
+ Z: float,
43
+ T: float,
44
+ P: float,
45
+ z: np.ndarray,
46
+ a: float,
47
+ b: float,
48
+ dai_dn: np.ndarray,
49
+ ) -> float:
50
+ """Return ln φ_i for component i."""
51
+
52
+ # ------------------------------------------------------------------
53
+ # Public
54
+ # ------------------------------------------------------------------
55
+ def Z_factor(
56
+ self, T: float, P: float, z: np.ndarray, phase: str = "vapor"
57
+ ) -> float:
58
+ from .cubic_solver import solve_cubic, select_z
59
+ z = np.asarray(z, dtype=float)
60
+ a, b, _ = self._mix_rule(T, z)
61
+ p, q, r = self._cubic_coeffs(T, P, a, b)
62
+ roots = solve_cubic(p, q, r)
63
+ return select_z(roots, phase)
64
+
65
+ def fugacity_coeff(
66
+ self, T: float, P: float, z: np.ndarray, phase: str = "vapor"
67
+ ) -> np.ndarray:
68
+ z = np.asarray(z, dtype=float)
69
+ a, b, dai_dn = self._mix_rule(T, z)
70
+ p, q, r = self._cubic_coeffs(T, P, a, b)
71
+ from .cubic_solver import solve_cubic, select_z
72
+ roots = solve_cubic(p, q, r)
73
+ Z = select_z(roots, phase)
74
+ nc = self.mix.nc
75
+ ln_phi = np.array(
76
+ [self._ln_phi_i(i, Z, T, P, z, a, b, dai_dn) for i in range(nc)]
77
+ )
78
+ return np.exp(ln_phi)
79
+
80
+ def fugacity(
81
+ self, T: float, P: float, z: np.ndarray, phase: str = "vapor"
82
+ ) -> np.ndarray:
83
+ z = np.asarray(z, dtype=float)
84
+ return z * P * self.fugacity_coeff(T, P, z, phase)
@@ -0,0 +1,66 @@
1
+ """
2
+ Peng-Robinson EOS (1976).
3
+ P = RT/(V-b) - a(T)/[V(V+b)+b(V-b)]
4
+ """
5
+ import numpy as np
6
+ from ..constants import R
7
+ from ..data.mixture import Mixture
8
+ from .eos_base import CubicEOS
9
+
10
+
11
+ class PREOS(CubicEOS):
12
+ # PR universal constants
13
+ _OmegaA = 0.45723553
14
+ _OmegaB = 0.07779607
15
+
16
+ def __init__(self, mixture: Mixture):
17
+ super().__init__(mixture)
18
+
19
+ # ------------------------------------------------------------------
20
+ def _kappa(self) -> np.ndarray:
21
+ w = self.mix.omega
22
+ return 0.37464 + 1.54226 * w - 0.26992 * w ** 2
23
+
24
+ def _ab_pure(self, T: float):
25
+ Tc = self.mix.Tc
26
+ Pc = self.mix.Pc
27
+ kappa = self._kappa()
28
+ Tr = T / Tc
29
+ alpha = (1.0 + kappa * (1.0 - np.sqrt(Tr))) ** 2
30
+ a = self._OmegaA * (R * Tc) ** 2 / Pc * alpha
31
+ b = self._OmegaB * R * Tc / Pc
32
+ return a, b
33
+
34
+ def _mix_rule(self, T: float, z: np.ndarray):
35
+ a_i, b_i = self._ab_pure(T)
36
+ kij = self.mix.kij
37
+ nc = self.mix.nc
38
+ # a_ij = sqrt(a_i * a_j) * (1 - kij)
39
+ a_ij = np.sqrt(np.outer(a_i, a_i)) * (1.0 - kij)
40
+ a_mix = float(z @ a_ij @ z)
41
+ b_mix = float(z @ b_i)
42
+ # da_mix/dn_i (at constant n_j≠i, normalised mole fraction)
43
+ dai_dn = 2.0 * (a_ij @ z)
44
+ return a_mix, b_mix, dai_dn
45
+
46
+ def _cubic_coeffs(self, T: float, P: float, a: float, b: float):
47
+ A = a * P / (R * T) ** 2
48
+ B = b * P / (R * T)
49
+ p = B - 1.0
50
+ q = A - 3.0 * B ** 2 - 2.0 * B
51
+ r = B ** 3 + B ** 2 - A * B
52
+ return p, q, r
53
+
54
+ def _ln_phi_i(self, i, Z, T, P, z, a, b, dai_dn):
55
+ a_i, b_i = self._ab_pure(T)
56
+ A = a * P / (R * T) ** 2
57
+ B = b * P / (R * T)
58
+ bi = b_i[i]
59
+ ln_phi = (
60
+ bi / b * (Z - 1.0)
61
+ - np.log(Z - B)
62
+ - A / (2.0 * np.sqrt(2.0) * B)
63
+ * (dai_dn[i] / a - bi / b)
64
+ * np.log((Z + (1.0 + np.sqrt(2.0)) * B) / (Z + (1.0 - np.sqrt(2.0)) * B))
65
+ )
66
+ return ln_phi
@@ -0,0 +1,100 @@
1
+ """
2
+ PRBS-EOS: Peng-Robinson with Boublik-Mansoori-Carnahan-Starling (BMCS)
3
+ hard-sphere repulsion term and a simplified CPA-style association
4
+ contribution, combined with a Peneloux volume shift.
5
+
6
+ For non-associating mixtures this reduces to standard PR with a
7
+ volume shift, so it is backward-compatible with PREOS.
8
+ """
9
+ import numpy as np
10
+ from scipy.optimize import brentq
11
+ from ..constants import R
12
+ from ..data.mixture import Mixture
13
+ from .pr_eos import PREOS
14
+
15
+
16
+ class PRBSEOS(PREOS):
17
+ """
18
+ PRBS = PR + volume-shift (Peneloux) + optional association (CPA-style).
19
+
20
+ Association contribution uses a 4-site model for self-associating
21
+ components (e.g. water). Cross-association is neglected here.
22
+ """
23
+
24
+ def __init__(self, mixture: Mixture):
25
+ super().__init__(mixture)
26
+ self._c = np.array([comp.c for comp in mixture.components])
27
+ self._eps = np.array([comp.epsilon for comp in mixture.components])
28
+ self._kap = np.array([comp.kappa for comp in mixture.components])
29
+ self._sites = np.array([comp.assoc_sites for comp in mixture.components], dtype=int)
30
+
31
+ # ------------------------------------------------------------------
32
+ # Volume shift: shift molar volume after Z-factor calculation
33
+ # ------------------------------------------------------------------
34
+ def _volume_shift(self, z: np.ndarray) -> float:
35
+ """Return molar volume shift c_mix = Σ z_i c_i [m³/mol]."""
36
+ return float(z @ self._c)
37
+
38
+ def molar_volume(
39
+ self, T: float, P: float, z: np.ndarray, phase: str = "vapor"
40
+ ) -> float:
41
+ """Volume-shifted molar volume [m³/mol]."""
42
+ z = np.asarray(z, dtype=float)
43
+ Z = self.Z_factor(T, P, z, phase)
44
+ V_raw = Z * R * T / P
45
+ return V_raw - self._volume_shift(z)
46
+
47
+ # ------------------------------------------------------------------
48
+ # Association (simplified 4-site CPA)
49
+ # ------------------------------------------------------------------
50
+ def _assoc_X(self, T: float, rho_mol: float, z: np.ndarray) -> np.ndarray:
51
+ """
52
+ Solve for fraction of unbonded sites X_i ∈ (0,1] for each component.
53
+ Uses the Michelsen-Hendriks iterative scheme.
54
+
55
+ Association strength: Δ_i = b_i * κ_i * (exp(ε_i / RT) - 1)
56
+ At low density rho_mol → 0, the site-occupancy denominator vanishes
57
+ and X_i → 1 (all sites free), which is the correct ideal-gas limit.
58
+ """
59
+ nc = self.mix.nc
60
+ _, b_i = self._ab_pure(T) # co-volume b_i [m³/mol] for each component
61
+ X = np.ones(nc)
62
+ for _ in range(200):
63
+ X_old = X.copy()
64
+ for i in range(nc):
65
+ if self._sites[i] == 0:
66
+ continue
67
+ # Δ = b_i * κ_i * (exp(ε_i / RT) - 1) [m³/mol]
68
+ delta = b_i[i] * self._kap[i] * (np.exp(self._eps[i] / (R * T)) - 1.0)
69
+ X[i] = 1.0 / (1.0 + self._sites[i] * z[i] * rho_mol * delta * X[i])
70
+ if np.max(np.abs(X - X_old)) < 1e-10:
71
+ break
72
+ return X
73
+
74
+ def _assoc_lnphi_correction(
75
+ self, T: float, P: float, z: np.ndarray, phase: str
76
+ ) -> np.ndarray:
77
+ """ln φ correction from association for each component."""
78
+ nc = self.mix.nc
79
+ if not np.any(self._sites > 0):
80
+ return np.zeros(nc)
81
+ V = self.molar_volume(T, P, z, phase)
82
+ rho_mol = 1.0 / V # mol/m³
83
+ X = self._assoc_X(T, rho_mol, z)
84
+ ln_corr = np.zeros(nc)
85
+ for i in range(nc):
86
+ M = self._sites[i]
87
+ if M == 0:
88
+ continue
89
+ ln_corr[i] = M * (np.log(X[i]) - X[i] / 2.0 + 0.5)
90
+ return ln_corr
91
+
92
+ # ------------------------------------------------------------------
93
+ # Override fugacity_coeff to add association
94
+ # ------------------------------------------------------------------
95
+ def fugacity_coeff(
96
+ self, T: float, P: float, z: np.ndarray, phase: str = "vapor"
97
+ ) -> np.ndarray:
98
+ phi_pr = super().fugacity_coeff(T, P, z, phase)
99
+ ln_assoc = self._assoc_lnphi_correction(T, P, z, phase)
100
+ return phi_pr * np.exp(ln_assoc)
@@ -0,0 +1,3 @@
1
+ from .component import Component
2
+ from .mixture import Mixture
3
+ from .component_db import get_component, available_components
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class Component:
7
+ """Pure-component critical properties and acentric factor."""
8
+ name: str
9
+ Tc: float # critical temperature [K]
10
+ Pc: float # critical pressure [Pa]
11
+ omega: float # acentric factor [-]
12
+ Mw: float # molecular weight [g/mol]
13
+ # optional PRBS volume-shift and association parameters
14
+ c: float = 0.0 # Peneloux volume-shift [m³/mol]
15
+ epsilon: float = 0.0 # association energy [J/mol]
16
+ kappa: float = 0.0 # association volume [-]
17
+ assoc_sites: int = 0 # number of association sites
18
+
19
+ def __post_init__(self):
20
+ if self.Tc <= 0:
21
+ raise ValueError(f"Tc must be positive, got {self.Tc}")
22
+ if self.Pc <= 0:
23
+ raise ValueError(f"Pc must be positive, got {self.Pc}")
24
+ if self.Mw <= 0:
25
+ raise ValueError(f"Mw must be positive, got {self.Mw}")
@@ -0,0 +1,59 @@
1
+ """
2
+ Built-in component database. Properties from DIPPR / Poling et al. (2001).
3
+ Pc in Pa, Tc in K, omega dimensionless, Mw in g/mol.
4
+ """
5
+ from .component import Component
6
+
7
+ _DB: dict[str, Component] = {
8
+ "methane": Component(
9
+ name="methane", Tc=190.56, Pc=4.599e6, omega=0.0115, Mw=16.043
10
+ ),
11
+ "ethane": Component(
12
+ name="ethane", Tc=305.32, Pc=4.872e6, omega=0.0995, Mw=30.070
13
+ ),
14
+ "propane": Component(
15
+ name="propane", Tc=369.83, Pc=4.248e6, omega=0.1523, Mw=44.097
16
+ ),
17
+ "n-butane": Component(
18
+ name="n-butane", Tc=425.12, Pc=3.796e6, omega=0.2002, Mw=58.123
19
+ ),
20
+ "n-pentane": Component(
21
+ name="n-pentane", Tc=469.70, Pc=3.370e6, omega=0.2515, Mw=72.150
22
+ ),
23
+ "n-hexane": Component(
24
+ name="n-hexane", Tc=507.60, Pc=3.025e6, omega=0.3013, Mw=86.177
25
+ ),
26
+ "n-heptane": Component(
27
+ name="n-heptane", Tc=540.20, Pc=2.740e6, omega=0.3495, Mw=100.204
28
+ ),
29
+ "n-octane": Component(
30
+ name="n-octane", Tc=568.70, Pc=2.490e6, omega=0.3996, Mw=114.231
31
+ ),
32
+ "nitrogen": Component(
33
+ name="nitrogen", Tc=126.20, Pc=3.390e6, omega=0.0372, Mw=28.014
34
+ ),
35
+ "co2": Component(
36
+ name="co2", Tc=304.12, Pc=7.374e6, omega=0.2239, Mw=44.010
37
+ ),
38
+ "water": Component(
39
+ name="water", Tc=647.10, Pc=22.064e6, omega=0.3449, Mw=18.015,
40
+ epsilon=1804.0 * 8.314462618, # 1804 K × R [J/mol]
41
+ kappa=0.0692, assoc_sites=4,
42
+ ),
43
+ "hydrogen_sulfide": Component(
44
+ name="hydrogen_sulfide", Tc=373.10, Pc=8.963e6, omega=0.0942, Mw=34.081
45
+ ),
46
+ }
47
+
48
+
49
+ def get_component(name: str) -> Component:
50
+ key = name.strip().lower().replace(" ", "_").replace("-", "-")
51
+ if key not in _DB:
52
+ raise KeyError(
53
+ f"Component '{name}' not found. Available: {list(_DB.keys())}"
54
+ )
55
+ return _DB[key]
56
+
57
+
58
+ def available_components() -> list[str]:
59
+ return list(_DB.keys())
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass, field
2
+ import numpy as np
3
+ from typing import List
4
+ from .component import Component
5
+
6
+
7
+ @dataclass
8
+ class Mixture:
9
+ """Collection of components with binary interaction parameters."""
10
+ components: List[Component]
11
+ kij: np.ndarray = field(default=None) # shape (nc, nc)
12
+
13
+ def __post_init__(self):
14
+ nc = len(self.components)
15
+ if nc == 0:
16
+ raise ValueError("Mixture must have at least one component.")
17
+ if self.kij is None:
18
+ self.kij = np.zeros((nc, nc))
19
+ self.kij = np.asarray(self.kij, dtype=float)
20
+ if self.kij.shape != (nc, nc):
21
+ raise ValueError(
22
+ f"kij shape {self.kij.shape} does not match nc={nc}"
23
+ )
24
+
25
+ @property
26
+ def nc(self) -> int:
27
+ return len(self.components)
28
+
29
+ @property
30
+ def Tc(self) -> np.ndarray:
31
+ return np.array([c.Tc for c in self.components])
32
+
33
+ @property
34
+ def Pc(self) -> np.ndarray:
35
+ return np.array([c.Pc for c in self.components])
36
+
37
+ @property
38
+ def omega(self) -> np.ndarray:
39
+ return np.array([c.omega for c in self.components])
40
+
41
+ @property
42
+ def Mw(self) -> np.ndarray:
43
+ return np.array([c.Mw for c in self.components])
@@ -0,0 +1,3 @@
1
+ from .flash import flash_pt, FlashResult
2
+ from .stability import is_stable, tpd_min
3
+ from .phase_envelope import bubble_point_P, dew_point_P
@@ -0,0 +1,83 @@
1
+ """
2
+ Two-phase PT flash using successive substitution + Newton acceleration.
3
+ Rachford-Rice equation for vapour fraction beta.
4
+ """
5
+ import numpy as np
6
+ from scipy.optimize import brentq
7
+ from dataclasses import dataclass
8
+ from ..constants import R
9
+ from ..core.eos_base import CubicEOS
10
+
11
+
12
+ @dataclass
13
+ class FlashResult:
14
+ beta: float # vapour mole fraction
15
+ x: np.ndarray # liquid mole fractions
16
+ y: np.ndarray # vapour mole fractions
17
+ K: np.ndarray # K-factors (y/x)
18
+ converged: bool
19
+ iterations: int
20
+
21
+
22
+ def _rachford_rice(beta: float, z: np.ndarray, K: np.ndarray) -> float:
23
+ return float(np.sum(z * (K - 1.0) / (1.0 + beta * (K - 1.0))))
24
+
25
+
26
+ def _solve_rr(z: np.ndarray, K: np.ndarray) -> float:
27
+ """Solve Rachford-Rice for beta ∈ (0,1)."""
28
+ f0 = _rachford_rice(0.0, z, K)
29
+ f1 = _rachford_rice(1.0, z, K)
30
+ if f0 <= 0.0:
31
+ return 0.0
32
+ if f1 >= 0.0:
33
+ return 1.0
34
+ return brentq(_rachford_rice, 0.0, 1.0, args=(z, K), xtol=1e-12)
35
+
36
+
37
+ def flash_pt(
38
+ eos: CubicEOS,
39
+ T: float,
40
+ P: float,
41
+ z: np.ndarray,
42
+ max_iter: int = 200,
43
+ tol: float = 1e-10,
44
+ ) -> FlashResult:
45
+ """Two-phase isothermal-isobaric flash."""
46
+ z = np.asarray(z, dtype=float)
47
+ nc = eos.mix.nc
48
+
49
+ # Wilson K-factor initialisation
50
+ Tc = eos.mix.Tc
51
+ Pc = eos.mix.Pc
52
+ omega = eos.mix.omega
53
+ K = (Pc / P) * np.exp(5.373 * (1.0 + omega) * (1.0 - Tc / T))
54
+
55
+ beta = _solve_rr(z, K)
56
+ converged = False
57
+
58
+ for it in range(max_iter):
59
+ beta = _solve_rr(z, K)
60
+ x = z / (1.0 + beta * (K - 1.0))
61
+ x = np.clip(x, 1e-300, 1.0)
62
+ x /= x.sum()
63
+ y = K * x
64
+ y = np.clip(y, 1e-300, 1.0)
65
+ y /= y.sum()
66
+
67
+ phi_L = eos.fugacity_coeff(T, P, x, "liquid")
68
+ phi_V = eos.fugacity_coeff(T, P, y, "vapor")
69
+ K_new = phi_L / phi_V
70
+
71
+ err = np.max(np.abs(np.log(K_new / K)))
72
+ K = K_new
73
+ if err < tol:
74
+ converged = True
75
+ break
76
+
77
+ beta = _solve_rr(z, K)
78
+ x = z / (1.0 + beta * (K - 1.0))
79
+ x /= x.sum()
80
+ y = K * x
81
+ y /= y.sum()
82
+ return FlashResult(beta=beta, x=x, y=y, K=K,
83
+ converged=converged, iterations=it + 1)
@@ -0,0 +1,55 @@
1
+ """
2
+ Simple PT phase-envelope tracer: bubble-point and dew-point pressure solvers.
3
+
4
+ Uses the Wilson K-factor correlation:
5
+ K_i = (Pc_i / P) * exp(5.373 * (1 + omega_i) * (1 - Tc_i / T))
6
+
7
+ Bubble-point condition: sum_i K_i * z_i = 1
8
+ - K_i strictly decreases with P → objective goes from + to - → reliable bracket.
9
+
10
+ Dew-point condition: sum_i z_i / K_i = 1
11
+ - 1/K_i strictly increases with P → objective goes from - to + → reliable bracket.
12
+
13
+ Returns nan when no root lies within [P_guess*0.01, P_guess*100], which naturally
14
+ occurs for supercritical T (where the Wilson P would be far outside the bracket).
15
+ """
16
+ import numpy as np
17
+ from scipy.optimize import brentq
18
+
19
+
20
+ def _wilson_K(eos, T: float, P: float) -> np.ndarray:
21
+ """Wilson K-factor correlation K_i = (Pc_i/P)*exp(5.373*(1+ω_i)*(1-Tc_i/T))."""
22
+ Tc = eos.mix.Tc
23
+ Pc = eos.mix.Pc
24
+ omega = eos.mix.omega
25
+ return (Pc / P) * np.exp(5.373 * (1.0 + omega) * (1.0 - Tc / T))
26
+
27
+
28
+ def bubble_point_P(eos, T, z, P_guess=1e6):
29
+ """Bubble-point pressure [Pa] at temperature T for feed z."""
30
+ z = np.asarray(z, dtype=float)
31
+
32
+ def obj(P):
33
+ K = _wilson_K(eos, T, P)
34
+ return float(np.sum(K * z)) - 1.0
35
+
36
+ try:
37
+ return brentq(obj, P_guess * 0.01, P_guess * 100.0,
38
+ xtol=1.0, rtol=1e-6, maxiter=100)
39
+ except ValueError:
40
+ return float("nan")
41
+
42
+
43
+ def dew_point_P(eos, T, z, P_guess=1e6):
44
+ """Dew-point pressure [Pa] at temperature T for feed z."""
45
+ z = np.asarray(z, dtype=float)
46
+
47
+ def obj(P):
48
+ K = _wilson_K(eos, T, P)
49
+ return float(np.sum(z / K)) - 1.0
50
+
51
+ try:
52
+ return brentq(obj, P_guess * 0.01, P_guess * 100.0,
53
+ xtol=1.0, rtol=1e-6, maxiter=100)
54
+ except ValueError:
55
+ return float("nan")
@@ -0,0 +1,36 @@
1
+ """
2
+ Tangent-plane distance (TPD) stability test (Michelsen 1982).
3
+ """
4
+ import numpy as np
5
+ from scipy.optimize import minimize
6
+ from ..constants import R
7
+ from ..core.eos_base import CubicEOS
8
+
9
+
10
+ def tpd_min(eos, T, P, z, phase_feed="vapor"):
11
+ z = np.asarray(z, dtype=float)
12
+ nc = eos.mix.nc
13
+ ln_phi_z = np.log(eos.fugacity_coeff(T, P, z, phase_feed))
14
+ hi = np.log(z) + ln_phi_z
15
+
16
+ def tpd(W):
17
+ w = np.abs(W)
18
+ w_sum = w.sum()
19
+ if w_sum < 1e-30:
20
+ return 0.0
21
+ y = w / w_sum
22
+ trial_phase = "liquid" if phase_feed == "vapor" else "vapor"
23
+ ln_phi_y = np.log(eos.fugacity_coeff(T, P, y, trial_phase))
24
+ return float(w_sum * (1.0 + np.sum(w / w_sum * (np.log(w / w_sum) + ln_phi_y - hi)) - 1.0))
25
+
26
+ best = np.inf
27
+ for start in [z * 0.5, z * 2.0, np.ones(nc) / nc]:
28
+ res = minimize(tpd, start, method="L-BFGS-B",
29
+ bounds=[(1e-12, None)] * nc)
30
+ if res.fun < best:
31
+ best = res.fun
32
+ return best
33
+
34
+
35
+ def is_stable(eos, T, P, z, phase_feed="vapor", tol=-1e-6):
36
+ return tpd_min(eos, T, P, z, phase_feed) > tol
File without changes
prbs_eos/pvt/export.py ADDED
File without changes
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ from .eos_regression import regress_kij
2
+ from .vpaa_regressor import regress_vpaa
@@ -0,0 +1,39 @@
1
+ """Regression of binary interaction parameters kij to VLE data."""
2
+ import numpy as np
3
+ from scipy.optimize import least_squares
4
+ from ..core.pr_eos import PREOS
5
+ from ..data.mixture import Mixture
6
+
7
+
8
+ def regress_kij(mixture, T_data, P_data, x_data, y_data, kij_bounds=(-0.5, 0.5)):
9
+ from ..equilibrium.flash import flash_pt
10
+ nc = mixture.nc
11
+ n_pairs = nc * (nc - 1) // 2
12
+ if n_pairs == 0:
13
+ return np.zeros((nc, nc))
14
+ pair_idx = [(i, j) for i in range(nc) for j in range(i + 1, nc)]
15
+
16
+ def residuals(params):
17
+ kij = np.zeros((nc, nc))
18
+ for k, (i, j) in enumerate(pair_idx):
19
+ kij[i, j] = kij[j, i] = params[k]
20
+ mix = Mixture(mixture.components, kij=kij)
21
+ eos = PREOS(mix)
22
+ res_list = []
23
+ for n in range(len(T_data)):
24
+ z = x_data[n]
25
+ try:
26
+ fl = flash_pt(eos, T_data[n], P_data[n], z)
27
+ res_list.extend((fl.y - y_data[n]).tolist())
28
+ except Exception:
29
+ res_list.extend([1.0] * nc)
30
+ return np.array(res_list)
31
+
32
+ x0 = np.zeros(n_pairs)
33
+ bounds = ([kij_bounds[0]] * n_pairs, [kij_bounds[1]] * n_pairs)
34
+ result = least_squares(residuals, x0, bounds=bounds, method="trf",
35
+ ftol=1e-8, xtol=1e-8, max_nfev=500)
36
+ kij_out = np.zeros((nc, nc))
37
+ for k, (i, j) in enumerate(pair_idx):
38
+ kij_out[i, j] = kij_out[j, i] = result.x[k]
39
+ return kij_out
@@ -0,0 +1,34 @@
1
+ """
2
+ Vapour-pressure and acentric-factor regression from experimental Psat data.
3
+ """
4
+ import numpy as np
5
+ from scipy.optimize import least_squares
6
+ from ..data.component import Component
7
+ from ..constants import R
8
+
9
+
10
+ def regress_vpaa(
11
+ comp: Component,
12
+ T_data: np.ndarray,
13
+ Psat_data: np.ndarray,
14
+ ) -> tuple[float, float]:
15
+ """
16
+ Fit omega and Pc to vapour-pressure data via Wilson correlation.
17
+
18
+ Returns
19
+ -------
20
+ (omega_fit, Pc_fit)
21
+ """
22
+ T_data = np.asarray(T_data, dtype=float)
23
+ Psat_data = np.asarray(Psat_data, dtype=float)
24
+
25
+ def residuals(params):
26
+ omega, Pc = params
27
+ Psat_calc = Pc * np.exp(5.373 * (1 + omega) * (1 - comp.Tc / T_data))
28
+ return np.log(Psat_calc) - np.log(Psat_data)
29
+
30
+ x0 = [comp.omega, comp.Pc]
31
+ bounds = ([0.0, 1e4], [2.0, 1e8])
32
+ result = least_squares(residuals, x0, bounds=bounds, method="trf",
33
+ ftol=1e-10, xtol=1e-10)
34
+ return float(result.x[0]), float(result.x[1])
@@ -0,0 +1 @@
1
+ from .departure import enthalpy_departure, entropy_departure
@@ -0,0 +1,61 @@
1
+ """
2
+ Departure functions for PR-EOS.
3
+ All quantities are per mole of mixture.
4
+ """
5
+ import numpy as np
6
+ from ..constants import R
7
+ from ..core.pr_eos import PREOS
8
+ from ..core.cubic_solver import solve_cubic, select_z
9
+
10
+
11
+ def _AB(eos: PREOS, T: float, P: float, z: np.ndarray):
12
+ a, b, _ = eos._mix_rule(T, z)
13
+ A = a * P / (R * T) ** 2
14
+ B = b * P / (R * T)
15
+ return a, b, A, B
16
+
17
+
18
+ def enthalpy_departure(
19
+ eos: PREOS, T: float, P: float, z: np.ndarray, phase: str = "vapor"
20
+ ) -> float:
21
+ """H_dep = H - H_ig [J/mol]."""
22
+ z = np.asarray(z, dtype=float)
23
+ a, b, A, B = _AB(eos, T, P, z)
24
+ # da/dT
25
+ a_i, b_i = eos._ab_pure(T)
26
+ kappa = eos._kappa()
27
+ Tc = eos.mix.Tc
28
+ Pc = eos.mix.Pc
29
+ kij = eos.mix.kij
30
+ alpha_i = a_i / (eos._OmegaA * (R * Tc) ** 2 / Pc)
31
+ dadt_i = (
32
+ -eos._OmegaA * (R * Tc) ** 2 / Pc
33
+ * kappa
34
+ * np.sqrt(alpha_i / (T * Tc))
35
+ )
36
+ a_ij = np.sqrt(np.outer(a_i, a_i)) * (1.0 - kij)
37
+ # dadt_ij = 0.5*(dadt_i/sqrt(a_i))*sqrt(a_j)*(1-kij) + sym
38
+ dadt_ij = 0.5 * (
39
+ np.outer(dadt_i / a_i, a_i) + np.outer(a_i, dadt_i / a_i)
40
+ ) * np.sqrt(np.outer(a_i, a_i)) * (1.0 - kij)
41
+ dadt_mix = float(z @ dadt_ij @ z)
42
+
43
+ Z = eos.Z_factor(T, P, z, phase)
44
+ s2 = np.sqrt(2.0)
45
+ H_dep = (
46
+ R * T * (Z - 1.0)
47
+ + (T * dadt_mix - a) / (b * 2.0 * s2)
48
+ * np.log((Z + (1 + s2) * B) / (Z + (1 - s2) * B))
49
+ )
50
+ return H_dep
51
+
52
+
53
+ def entropy_departure(
54
+ eos: PREOS, T: float, P: float, z: np.ndarray, phase: str = "vapor"
55
+ ) -> float:
56
+ """S_dep = S - S_ig [J/(mol·K)]."""
57
+ z = np.asarray(z, dtype=float)
58
+ H_dep = enthalpy_departure(eos, T, P, z, phase)
59
+ ln_phi = np.log(eos.fugacity_coeff(T, P, z, phase))
60
+ G_dep = R * T * float(z @ ln_phi)
61
+ return (H_dep - G_dep) / T
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: prbs-eos
3
+ Version: 0.1.0
4
+ Summary: PRBS Equation of State — Peng-Robinson with volume-shift and CPA association for petroleum thermodynamics
5
+ Author: PRBS-EOS Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/your-org/prbs-eos
8
+ Project-URL: Repository, https://github.com/your-org/prbs-eos
9
+ Project-URL: Issues, https://github.com/your-org/prbs-eos/issues
10
+ Keywords: thermodynamics,equation-of-state,peng-robinson,petroleum,flash,VLE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
18
+ Classifier: Topic :: Scientific/Engineering :: Physics
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: scipy>=1.10
23
+ Requires-Dist: pandas>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.4; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
27
+ Requires-Dist: build>=1.0; extra == "dev"
28
+ Requires-Dist: twine>=4.0; extra == "dev"
29
+ Requires-Dist: pyinstaller>=6.0; extra == "dev"
30
+
31
+ # prbs-eos
32
+
33
+ **PRBS Equation of State** — a Python library for petroleum thermodynamics.
34
+
35
+ ## Features
36
+
37
+ - Peng-Robinson EOS (PR) with van der Waals mixing rules and binary interaction parameters (kij)
38
+ - PRBS-EOS: PR + Peneloux volume-shift + CPA-style association (e.g. water, H₂S)
39
+ - Two-phase PT flash (Rachford-Rice + successive substitution)
40
+ - Tangent-plane stability analysis (Michelsen TPD)
41
+ - Bubble-point / dew-point pressure solvers (Wilson K-factor)
42
+ - Enthalpy and entropy departure functions
43
+ - kij regression from VLE data; vapour-pressure / acentric-factor regression
44
+ - Built-in database: methane, ethane, propane, n-butane … n-octane, N₂, CO₂, water, H₂S
45
+ - Command-line interface: `prbs-eos flash`, `prbs-eos bubble`, `prbs-eos properties`, …
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ import numpy as np
51
+ import prbs_eos as eos
52
+
53
+ mix = eos.Mixture([eos.get_component("methane"), eos.get_component("propane")])
54
+ pr = eos.PREOS(mix)
55
+
56
+ res = eos.flash_pt(pr, T=250.0, P=1e6, z=np.array([0.6, 0.4]))
57
+ print(f"β = {res.beta:.3f} x = {res.x} y = {res.y}")
58
+ ```
59
+
60
+ ## CLI
61
+
62
+ ```
63
+ prbs-eos flash --T 250 --P 1e6 --mix methane:0.6 propane:0.4
64
+ prbs-eos bubble --T 250 --mix methane:0.5 propane:0.5
65
+ prbs-eos properties methane
66
+ prbs-eos components
67
+ ```
@@ -0,0 +1,31 @@
1
+ prbs_eos/__init__.py,sha256=Lc3jtKz-DFYxg9qM40K2mYffLF6GbWASqScDAlXMKp4,335
2
+ prbs_eos/__main__.py,sha256=7ZoLnk_SqsSGgXOkx5gMAAdEm96roAQgRhF0ythsZ1I,86
3
+ prbs_eos/cli.py,sha256=9bWdG0qbob7P8VNV8SVWelhNJGbZ3DHzLFjQypjsEC8,10911
4
+ prbs_eos/constants.py,sha256=qeO6maLY3eAeqQU4dTdbby4QGY0uVSu7qf7khnGhFnk,30
5
+ prbs_eos/core/__init__.py,sha256=dzzwLcnUNs9yUe431DQ9Ety7VciRYKX9BsK4Zm5HqLs,135
6
+ prbs_eos/core/cubic_solver.py,sha256=7mkZo4i2lkXP9WvQWK2MED-IlTuUhL7YNRxwBzyqUJ8,963
7
+ prbs_eos/core/eos_base.py,sha256=4YrB5kKJ5m1OokXKGj3HDJVaev4CgS415_8oDFwyl9s,2677
8
+ prbs_eos/core/pr_eos.py,sha256=RrvvyzZSu6qAU4SvFoR0f3UHRpdOHl3OnFsd6Nt-i34,2010
9
+ prbs_eos/core/prbs_eos.py,sha256=cFa-kmZvfKYuA0qqsWuAP4hi17swMhQj9B-ccUoslNw,4162
10
+ prbs_eos/data/__init__.py,sha256=holrFRrpn-LEb3YHXpAfzV44LGTjhpuOjOcVc_fXD_A,124
11
+ prbs_eos/data/component.py,sha256=JPiLurLhu4-uncczLzNbsJZ8w3JGjf8LfVe_hotuXfA,967
12
+ prbs_eos/data/component_db.py,sha256=Rb-sqv5Iixj2sk_0b6PNi5ggqCieEpvNllF7e5OzKew,1909
13
+ prbs_eos/data/mixture.py,sha256=nvgtzSbxIqq6MoRj8n35Sj3kaH-qCp_EodMnlVjObuU,1241
14
+ prbs_eos/equilibrium/__init__.py,sha256=GR7ADq_KALSaxE0jGotMKXSTGJ-lVGI-adQqj2guL4Y,142
15
+ prbs_eos/equilibrium/flash.py,sha256=e0bCK3YgYO5wy9MbGCazYzyoDk0eLquF2Cm72DixbmE,2206
16
+ prbs_eos/equilibrium/phase_envelope.py,sha256=o2DAmiWSd6dfDM6ZY8L0of2Muo-iwNkJIXdmrGh6ufI,1830
17
+ prbs_eos/equilibrium/stability.py,sha256=qywjEE0AqI36ngyT2bNF4qZ_ofpf2_F5DtqGNGBi-QE,1155
18
+ prbs_eos/pvt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ prbs_eos/pvt/export.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ prbs_eos/pvt/pvt_tables.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ prbs_eos/pvt/separator.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ prbs_eos/regression/__init__.py,sha256=_brdCoDhx1SD6ZAAvKpjHp0ukJVjtxoYUPB8WzgypqI,83
23
+ prbs_eos/regression/eos_regression.py,sha256=tH2jTHIrCu7Ba3-u3H0x2l0qEG_ecL6K23-LF60NfJg,1475
24
+ prbs_eos/regression/vpaa_regressor.py,sha256=lIb1ID6n4_f6diOhJaAEnsGFkuh4MkANKybOKUD9lq0,1019
25
+ prbs_eos/thermodynamics/__init__.py,sha256=EKLFCTDVQ1Vzk4x0sRv2k3UOT_rbefVIv3FWU6s44UM,61
26
+ prbs_eos/thermodynamics/departure.py,sha256=Vv-aSq3G00HUL_OWVzGutyVscZkIsnOln2ZMJYWhrvQ,1779
27
+ prbs_eos-0.1.0.dist-info/METADATA,sha256=l8ies3CVdc3dSmm-uVF_x3I-YrJYxPn0sRjQrTshw0g,2603
28
+ prbs_eos-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
+ prbs_eos-0.1.0.dist-info/entry_points.txt,sha256=I8t_Egguo1J2NeNw-GhLVLnshxFHjf1ElGZ6Di9aZNw,47
30
+ prbs_eos-0.1.0.dist-info/top_level.txt,sha256=KDom8s7RiWwChQoM0uB2_myb_t18-DkRYVqujfuBj3E,9
31
+ prbs_eos-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ prbs-eos = prbs_eos.cli:main
@@ -0,0 +1 @@
1
+ prbs_eos