pyrestoolbox 2.2.2__tar.gz → 2.5.0__tar.gz

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.
Files changed (70) hide show
  1. {pyrestoolbox-2.2.2/pyrestoolbox.egg-info → pyrestoolbox-2.5.0}/PKG-INFO +1 -1
  2. pyrestoolbox-2.5.0/pyrestoolbox/brine/_lib_salting_library.py +693 -0
  3. pyrestoolbox-2.5.0/pyrestoolbox/brine/_lib_vle_engine.py +2629 -0
  4. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/brine/brine.py +556 -41
  5. pyrestoolbox-2.5.0/pyrestoolbox/docs/.ipynb_checkpoints/examples-checkpoint.ipynb +3284 -0
  6. pyrestoolbox-2.5.0/pyrestoolbox/docs/brine.rst +481 -0
  7. pyrestoolbox-2.5.0/pyrestoolbox/docs/examples.ipynb +1458 -0
  8. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/gas/gas.py +37 -1
  9. pyrestoolbox-2.5.0/pyrestoolbox/plyasunov/__init__.py +12 -0
  10. pyrestoolbox-2.5.0/pyrestoolbox/plyasunov/iapws_if97.py +143 -0
  11. pyrestoolbox-2.5.0/pyrestoolbox/plyasunov/plyasunov_model.py +353 -0
  12. pyrestoolbox-2.5.0/pyrestoolbox/plyasunov/water_properties.py +47 -0
  13. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_brine.py +47 -6
  14. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_doc_examples.py +37 -8
  15. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_gas.py +27 -0
  16. pyrestoolbox-2.5.0/pyrestoolbox/tests/test_unified_brine_design.py +351 -0
  17. pyrestoolbox-2.5.0/pyrestoolbox/tests/test_viscosity_scaling.py +464 -0
  18. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0/pyrestoolbox.egg-info}/PKG-INFO +1 -1
  19. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox.egg-info/SOURCES.txt +10 -0
  20. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/setup.cfg +1 -1
  21. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/setup.py +1 -1
  22. pyrestoolbox-2.2.2/pyrestoolbox/docs/brine.rst +0 -266
  23. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/LICENSE +0 -0
  24. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/MANIFEST.in +0 -0
  25. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/README.md +0 -0
  26. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/README.rst +0 -0
  27. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyproject.toml +0 -0
  28. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/__init__.py +0 -0
  29. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/brine/__init__.py +0 -0
  30. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/classes/__init__.py +0 -0
  31. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/classes/classes.py +0 -0
  32. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/constants/__init__.py +0 -0
  33. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/constants/constants.py +0 -0
  34. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/changelist.rst +0 -0
  35. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/gas.rst +0 -0
  36. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/bot.png +0 -0
  37. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/bot_PVTO.png +0 -0
  38. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/bot_img.png +0 -0
  39. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/dry_gas.png +0 -0
  40. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/grid_sat_df.png +0 -0
  41. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/influence.png +0 -0
  42. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/properties_df.png +0 -0
  43. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/sgof.png +0 -0
  44. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/img/swof.png +0 -0
  45. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/layer.rst +0 -0
  46. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/library.rst +0 -0
  47. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/oil.rst +0 -0
  48. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/docs/simtools.rst +0 -0
  49. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/gas/__init__.py +0 -0
  50. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/layer/__init__.py +0 -0
  51. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/layer/layer.py +0 -0
  52. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/library/__init__.py +0 -0
  53. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/library/component_library.xlsx +0 -0
  54. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/library/library.py +0 -0
  55. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/oil/__init__.py +0 -0
  56. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/oil/oil.py +0 -0
  57. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/shared_fns/__init__.py +0 -0
  58. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/shared_fns/shared_fns.py +0 -0
  59. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/simtools/__init__.py +0 -0
  60. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/simtools/simtools.py +0 -0
  61. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/__init__.py +0 -0
  62. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/run_all_tests.py +0 -0
  63. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_layer.py +0 -0
  64. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_oil.py +0 -0
  65. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/tests/test_simtools.py +0 -0
  66. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/validate/__init__.py +0 -0
  67. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox/validate/validate.py +0 -0
  68. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox.egg-info/dependency_links.txt +0 -0
  69. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox.egg-info/requires.txt +0 -0
  70. {pyrestoolbox-2.2.2 → pyrestoolbox-2.5.0}/pyrestoolbox.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrestoolbox
3
- Version: 2.2.2
3
+ Version: 2.5.0
4
4
  Summary: pyResToolbox - A collection of Reservoir Engineering Utilities
5
5
  Home-page: https://github.com/mwburgoyne/pyResToolbox
6
6
  Author: Mark W. Burgoyne
@@ -0,0 +1,693 @@
1
+ """
2
+ Library of Sechenov (salting-out) correlations for gases in NaCl brine.
3
+
4
+ All functions return ks on a LOG10 basis with molality (mol/kg H2O) scale:
5
+
6
+ log10(S_water / S_brine) = ks * m_NaCl
7
+
8
+ Sources implemented:
9
+ 1. S&W Eq 8 — Soreide & Whitson 1992, generic (Tb-based), all gases
10
+ 2. Dubessy — Dubessy et al. 2005, extended Sechenov for CO2 and H2S
11
+ 3. Akinfiev — Akinfiev et al. 2016, Pitzer model for H2S (-> effective ks)
12
+ 4. Li et al. — Li, Zhang, Luo & Li 2015, Pitzer for CH4, C2H6, C3H8, nC4H10
13
+ 5. Mao & Duan — Mao & Duan 2006, Pitzer model for N2
14
+ 6. Duan & Sun — Duan & Sun 2003, Pitzer model for CO2
15
+
16
+ Not implemented (too complex for Sechenov extraction):
17
+ - Springer et al. 2014 (OTC-25295-MS): MSE framework for H2S/CO2.
18
+ Requires speciation + Debye-Huckel + virial + UNIQUAC simultaneously.
19
+ Cannot be reduced to an analytical Sechenov form.
20
+
21
+ Convention: ks > 0 means salting-out (lower solubility in brine).
22
+ """
23
+
24
+ import numpy as np
25
+
26
+ LN10 = np.log(10.0) # 2.302585...
27
+
28
+
29
+ # ====================================================================
30
+ # 1. Soreide & Whitson 1992, Equation 8 (generic, Tb-based)
31
+ # ====================================================================
32
+ # Boiling points (K) for S&W gases
33
+ TB_K = {
34
+ 'H2': 20.3, 'N2': 77.4, 'CO2': 194.7, 'H2S': 213.6,
35
+ 'CH4': 111.6, 'C2H6': 184.6, 'C3H8': 231.1,
36
+ 'iC4H10': 261.4, 'nC4H10': 272.7, 'iC5H12': 301.0,
37
+ 'nC5H12': 309.2, 'nC6H14': 341.9, 'nC7H16': 371.6,
38
+ 'nC8H18': 398.8, 'nC10H22': 447.3,
39
+ }
40
+
41
+
42
+ def ks_sw_eq8(T_K, gas_or_Tb):
43
+ """Soreide-Whitson 1992 Equation 8 Sechenov coefficient.
44
+
45
+ Parameters
46
+ ----------
47
+ T_K : float or array
48
+ Temperature in Kelvin.
49
+ gas_or_Tb : str or float
50
+ Gas name (e.g. 'H2', 'CH4') or boiling point in K.
51
+
52
+ Returns
53
+ -------
54
+ ks : float or array
55
+ Sechenov coefficient (log10 basis, molality scale).
56
+ """
57
+ if isinstance(gas_or_Tb, str):
58
+ Tb = TB_K[gas_or_Tb]
59
+ else:
60
+ Tb = float(gas_or_Tb)
61
+ T_F = (np.asarray(T_K, dtype=float) - 273.15) * 9.0 / 5.0 + 32.0
62
+ return (0.13163 + 4.45e-4 * Tb - 7.692e-4 * T_F
63
+ + 2.6614e-6 * T_F**2 - 2.612e-9 * T_F**3)
64
+
65
+
66
+ # ====================================================================
67
+ # 2. Dubessy et al. (2005) — Extended Sechenov for CO2 and H2S
68
+ # Oil & Gas Sci. Technol. - Rev. IFP, Vol. 60, No. 2, pp. 339-355
69
+ # Table 9 coefficients.
70
+ #
71
+ # log10(K_brine/K_water) = m*b1(T) + m^2*b2(T) + m^3*b3(T)
72
+ # b_i(T) = sum(B_ij * T^j, j=0..4)
73
+ #
74
+ # Effective ks = b1(T) + m*b2(T) + m^2*b3(T)
75
+ # ====================================================================
76
+
77
+ # CO2: column scaling 1e0, 1e-2, 1e-4, 1e-8, 1e-11
78
+ _DUB_CO2_B1 = np.array([3.114712456, -2.7655585e-2, 0.9176713976e-4,
79
+ -12.78795941e-8, 6.2704268351e-11])
80
+ _DUB_CO2_B2 = np.array([-2.05637458, 2.081980200e-2, -0.765857702e-4,
81
+ 12.011325315e-8, -6.790343083e-11])
82
+ _DUB_CO2_B3 = np.array([0.253424331, -0.26047432e-2, 0.0972580216e-4,
83
+ -1.551654794e-8, 0.8948557284e-11])
84
+
85
+ # H2S: column scaling 1e0, 1e-2, 1e-4, 1e-7, 1e-10
86
+ _DUB_H2S_B1 = np.array([-12.4617636, 12.69373100e-2, -4.791540697e-4,
87
+ 7.9817223650e-7, -4.931093145e-10])
88
+ _DUB_H2S_B2 = np.array([5.327383011, -5.82779828e-2, 2.3650333285e-4,
89
+ -4.207913036e-7, 2.7628521914e-10])
90
+ _DUB_H2S_B3 = np.array([-0.75715275, 0.831927411e-2, -0.338668040e-4,
91
+ 0.6037602785e-7, -0.397049836e-10])
92
+
93
+
94
+ def _poly4(c, T):
95
+ """Evaluate 4th-order polynomial c[0] + c[1]*T + ... + c[4]*T^4."""
96
+ T = np.asarray(T, dtype=float)
97
+ return c[0] + c[1]*T + c[2]*T**2 + c[3]*T**3 + c[4]*T**4
98
+
99
+
100
+ def ks_dubessy_co2(T_K, m_NaCl=0.0):
101
+ """Dubessy et al. 2005 effective Sechenov for CO2-NaCl (log10).
102
+
103
+ Valid: T <= 543 K (270 C), P <= 300 bar, m <= 6. R^2 = 0.83.
104
+ """
105
+ return (_poly4(_DUB_CO2_B1, T_K) + m_NaCl * _poly4(_DUB_CO2_B2, T_K)
106
+ + m_NaCl**2 * _poly4(_DUB_CO2_B3, T_K))
107
+
108
+
109
+ def ks_dubessy_h2s(T_K, m_NaCl=0.0):
110
+ """Dubessy et al. 2005 effective Sechenov for H2S-NaCl (log10).
111
+
112
+ Valid: T <= 523 K (250 C), P <= 150 bar, m <= 6. R^2 = 0.74.
113
+ """
114
+ return (_poly4(_DUB_H2S_B1, T_K) + m_NaCl * _poly4(_DUB_H2S_B2, T_K)
115
+ + m_NaCl**2 * _poly4(_DUB_H2S_B3, T_K))
116
+
117
+
118
+ # ====================================================================
119
+ # 3. Akinfiev, Majer & Shvarov (2016) — Pitzer model for H2S-NaCl
120
+ # Chemical Geology, 424, 1-11. DOI: 10.1016/j.chemgeo.2016.01.006
121
+ #
122
+ # Pitzer activity coefficient (Eq. 16):
123
+ # ln(gamma_s) = 2*m_s*lam_ss + 3*m_s^2*tau_sss
124
+ # + 2*m_e*B_se + 3*m_e^2*C_see + 6*m_s*m_e*C_sse
125
+ #
126
+ # For dilute H2S (m_s << m_e), the effective Sechenov is:
127
+ # ks_eff ≈ (2*B_se + 6*m_s*C_sse) / ln(10) [on log10/molality]
128
+ #
129
+ # But the rigorous approach uses the full recommended solubility tables
130
+ # or the complete Pitzer framework.
131
+ # ====================================================================
132
+
133
+ # Binary H2S-H2O self-interaction: Eq. (18)
134
+ # lambda_ss = a_lam + b_lam * (100/(T-228)) + c_lam * (T/(T-760))
135
+ _AK_A_LAM = -0.19515
136
+ _AK_B_LAM = 0.102822
137
+ _AK_C_LAM = -0.033726
138
+ _AK_TAU_SSS = 0.004900
139
+
140
+ # Ternary H2S-H2O-NaCl: Eq. (20)
141
+ # B_se = b_B * (100/(T-228)) + c_B * (T/(T-760))
142
+ _AK_B_B = 0.03568
143
+ _AK_C_B = -0.02354
144
+ _AK_C_SEE = 0.0 # set to zero by authors
145
+ _AK_C_SSE = 0.002558
146
+
147
+
148
+ def _akinfiev_lambda_ss(T_K):
149
+ """H2S-H2S self-interaction parameter (Akinfiev Eq. 18)."""
150
+ T = np.asarray(T_K, dtype=float)
151
+ return _AK_A_LAM + _AK_B_LAM * (100.0 / (T - 228.0)) + _AK_C_LAM * (T / (T - 760.0))
152
+
153
+
154
+ def _akinfiev_B_se(T_K):
155
+ """H2S-NaCl interaction parameter B_se (Akinfiev Eq. 20)."""
156
+ T = np.asarray(T_K, dtype=float)
157
+ return _AK_B_B * (100.0 / (T - 228.0)) + _AK_C_B * (T / (T - 760.0))
158
+
159
+
160
+ def akinfiev_ln_gamma_h2s(T_K, m_h2s, m_NaCl):
161
+ """Natural log of H2S activity coefficient in H2S-H2O-NaCl (Eq. 16).
162
+
163
+ Parameters
164
+ ----------
165
+ T_K : float
166
+ Temperature in Kelvin (283-573 K).
167
+ m_h2s : float
168
+ H2S molality (mol/kg H2O).
169
+ m_NaCl : float
170
+ NaCl molality (mol/kg H2O), 0-6.
171
+
172
+ Returns
173
+ -------
174
+ ln_gamma : float
175
+ Natural log of activity coefficient on Henry scale.
176
+ """
177
+ lam = _akinfiev_lambda_ss(T_K)
178
+ B = _akinfiev_B_se(T_K)
179
+ return (2.0 * m_h2s * lam + 3.0 * m_h2s**2 * _AK_TAU_SSS
180
+ + 2.0 * m_NaCl * B + 3.0 * m_NaCl**2 * _AK_C_SEE
181
+ + 6.0 * m_h2s * m_NaCl * _AK_C_SSE)
182
+
183
+
184
+ def ks_akinfiev_h2s(T_K, m_NaCl=1.0, m_h2s_approx=0.1):
185
+ """Effective Sechenov coefficient for H2S-NaCl from Akinfiev Pitzer (log10).
186
+
187
+ Computes the effective ks from the Pitzer activity coefficient difference
188
+ between saline and fresh solutions, at a specified approximate H2S molality.
189
+
190
+ For dilute H2S, ln(gamma_saline/gamma_fresh) ≈ 2*m_e*B_se + 6*m_s*m_e*C_sse
191
+ so ks ≈ (2*B_se + 6*m_s*C_sse) / ln(10).
192
+
193
+ Parameters
194
+ ----------
195
+ T_K : float or array
196
+ Temperature in Kelvin (283-573 K).
197
+ m_NaCl : float
198
+ NaCl molality for computing the effective ks. Default 1 m.
199
+ m_h2s_approx : float
200
+ Approximate H2S molality (affects C_sse contribution). Default 0.1 m.
201
+
202
+ Returns
203
+ -------
204
+ ks : float or array
205
+ Effective Sechenov coefficient (log10 basis, molality scale).
206
+ """
207
+ B = _akinfiev_B_se(T_K)
208
+ # ln(gamma_brine) - ln(gamma_fresh) at given m_h2s:
209
+ # = 2*m_NaCl*B_se + 3*m_NaCl^2*C_see + 6*m_h2s*m_NaCl*C_sse
210
+ # Dividing by m_NaCl gives the ln-based Sechenov:
211
+ ks_ln = 2.0 * B + 3.0 * m_NaCl * _AK_C_SEE + 6.0 * m_h2s_approx * _AK_C_SSE
212
+ return ks_ln / LN10
213
+
214
+
215
+ def ks_akinfiev_h2s_from_tables(T_K, m_NaCl):
216
+ """Effective ks for H2S from Akinfiev 2016 recommended solubility tables.
217
+
218
+ Uses linearly interpolated Table 3 (pure water) and Table 4 (brine)
219
+ values at 5 MPa to compute ks = log10(m_water/m_brine) / m_NaCl.
220
+
221
+ This is the most reliable method — uses the model output directly,
222
+ avoids any approximation in the Pitzer-to-Sechenov conversion.
223
+
224
+ Parameters
225
+ ----------
226
+ T_K : float or array
227
+ Temperature in Kelvin. Supported: 298-523 K.
228
+ m_NaCl : float
229
+ Must be one of: 1, 2, 4, or 6 mol/kg.
230
+
231
+ Returns
232
+ -------
233
+ ks : float or array
234
+ Effective Sechenov coefficient (log10 basis).
235
+ """
236
+ # Tables 3 & 4 at P = 5 MPa (mid-range, well within experimental coverage)
237
+ # T_K: 298.15 323.15 373.15 423.15 473.15 523.15
238
+ _T = np.array([298.15, 323.15, 373.15, 423.15, 473.15, 523.15])
239
+ _m0 = np.array([np.nan, 2.026, 1.651, 1.271, 0.956, 0.300]) # pure water, 5 MPa
240
+ # NaCl = 1 m
241
+ _m1 = np.array([np.nan, 1.752, 1.437, 1.110, 0.831, 0.260])
242
+ # NaCl = 2 m
243
+ _m2 = np.array([np.nan, 1.523, 1.259, 0.975, 0.726, 0.226])
244
+ # NaCl = 4 m
245
+ _m4 = np.array([np.nan, 1.168, 0.983, 0.763, 0.561, 0.171])
246
+ # NaCl = 6 m
247
+ _m6 = np.array([np.nan, 0.907, 0.779, 0.606, 0.438, 0.130])
248
+
249
+ brine_tables = {1: _m1, 2: _m2, 4: _m4, 6: _m6}
250
+ if m_NaCl not in brine_tables:
251
+ raise ValueError(f"m_NaCl must be 1, 2, 4, or 6. Got {m_NaCl}")
252
+
253
+ m_brine = brine_tables[m_NaCl]
254
+ # Compute ks at each table temperature (skip 298.15 where data missing at 5 MPa)
255
+ valid = ~np.isnan(_m0) & ~np.isnan(m_brine)
256
+ ks_pts = np.full_like(_T, np.nan)
257
+ ks_pts[valid] = np.log10(_m0[valid] / m_brine[valid]) / m_NaCl
258
+
259
+ # Interpolate to requested temperatures
260
+ T_valid = _T[valid]
261
+ ks_valid = ks_pts[valid]
262
+ T_K = np.asarray(T_K, dtype=float)
263
+ scalar = T_K.ndim == 0
264
+ T_K = np.atleast_1d(T_K)
265
+ result = np.interp(T_K, T_valid, ks_valid)
266
+ return float(result[0]) if scalar else result
267
+
268
+
269
+ # ====================================================================
270
+ # 4. Li, Zhang, Luo & Li (2015) — Pitzer model for CH4-C2H6-C3H8-nC4H10
271
+ # Applied Geochemistry. DOI: 10.1016/j.apgeochem.2015.02.006
272
+ #
273
+ # Eq. 15 (for NaCl, with lambda_{i-Cl-} = 0):
274
+ # ln(gamma_i) = 2 * m * lambda_{i-Na+}(T,P) + m^2 * zeta_{i-Na+-Cl-}
275
+ #
276
+ # Effective Sechenov (log10):
277
+ # ks = (2*lambda + zeta*m) / ln(10)
278
+ #
279
+ # Validity: 273-473 K, 1-1000 bar, 0-6 m NaCl
280
+ # NOTE: C3H8 and nC4H10 have very limited brine data (1 atm only).
281
+ # ====================================================================
282
+
283
+ def _li2015_lambda_ch4(T_K, P_bar=100.0):
284
+ """CH4-Na+ Pitzer lambda (Li et al. 2015 Table 7). T in K, P in bar."""
285
+ T = np.asarray(T_K, dtype=float)
286
+ P = float(P_bar)
287
+ return (-5.7066455e-1 + 7.2997588e-4 * T + 1.5176903e2 / T
288
+ + 3.1927112e-5 * P - 1.642651e-5 * P / T)
289
+
290
+
291
+ def _li2015_lambda_c2h6(T_K, P_bar=100.0):
292
+ """C2H6-Na+ Pitzer lambda (Li et al. 2015 Table 7). T in K, P in bar."""
293
+ T = np.asarray(T_K, dtype=float)
294
+ P = float(P_bar)
295
+ return (-2.143686 + 2.598765e-3 * T + 4.6942351e2 / T
296
+ - 4.6849541e-5 * P - 8.4616602e-10 * P**2 * T
297
+ + 1.095219e-6 * P * T)
298
+
299
+
300
+ def _li2015_lambda_c3h8(T_K, P_bar=100.0):
301
+ """C3H8-Na+ Pitzer lambda (Li et al. 2015 Table 7). T-only (no P dep)."""
302
+ T = np.asarray(T_K, dtype=float)
303
+ return 0.513068 - 0.000958 * T
304
+
305
+
306
+ def _li2015_lambda_nc4h10(T_K, P_bar=100.0):
307
+ """nC4H10-Na+ Pitzer lambda (Li et al. 2015 Table 7). T-only (no P dep)."""
308
+ T = np.asarray(T_K, dtype=float)
309
+ return 0.52862384 - 1.0298104e-3 * T
310
+
311
+
312
+ # Zeta (third virial) — all constants, no T or P dependence
313
+ _LI2015_ZETA = {
314
+ 'CH4': -2.9990084e-3,
315
+ 'C2H6': -1.0165947e-2,
316
+ 'C3H8': -0.007485,
317
+ 'NC4H10': 0.0206946, # NOTE: positive (unlike the others)
318
+ }
319
+
320
+ _LI2015_LAMBDA = {
321
+ 'CH4': _li2015_lambda_ch4,
322
+ 'C2H6': _li2015_lambda_c2h6,
323
+ 'C3H8': _li2015_lambda_c3h8,
324
+ 'NC4H10': _li2015_lambda_nc4h10,
325
+ }
326
+
327
+
328
+ def ks_li2015(T_K, gas, m_NaCl=0.0, P_bar=100.0):
329
+ """Effective Sechenov for light HCs from Li et al. 2015 Pitzer (log10).
330
+
331
+ ln(gamma) = 2*m*lambda(T,P) + m^2*zeta
332
+ ks = (2*lambda + zeta*m) / ln(10)
333
+
334
+ Parameters
335
+ ----------
336
+ T_K : float or array
337
+ Temperature in Kelvin (273-473 K).
338
+ gas : str
339
+ 'CH4', 'C2H6', 'C3H8', or 'nC4H10'.
340
+ m_NaCl : float
341
+ NaCl molality. Affects effective ks through zeta term.
342
+ P_bar : float
343
+ Pressure in bar (affects CH4 and C2H6 lambda). Default 100.
344
+
345
+ Returns
346
+ -------
347
+ ks : float or array
348
+ Effective Sechenov coefficient (log10 basis, molality scale).
349
+ """
350
+ g = gas.upper().replace('N', 'N') # normalize
351
+ if g == 'NC4H10' or g == 'NC4':
352
+ g = 'NC4H10'
353
+ elif g not in _LI2015_LAMBDA:
354
+ raise ValueError(f"Li 2015 covers CH4, C2H6, C3H8, nC4H10. Got {gas}")
355
+
356
+ lam = _LI2015_LAMBDA[g](T_K, P_bar)
357
+ zeta = _LI2015_ZETA[g]
358
+ return (2.0 * lam + zeta * m_NaCl) / LN10
359
+
360
+
361
+ # ====================================================================
362
+ # 5. Mao & Duan (2006) — Pitzer model for N2-NaCl
363
+ # Fluid Phase Equilibria, 248, 103-114.
364
+ # DOI: 10.1016/j.fluid.2006.07.020
365
+ #
366
+ # Eq. 8 (for NaCl, with lambda_{N2-Cl-} = 0):
367
+ # ln(gamma_N2) = 2 * m * lambda_{N2-Na+}(T,P) + m^2 * xi_{N2-Na+-Cl-}
368
+ #
369
+ # Effective Sechenov (log10):
370
+ # ks = (2*lambda + xi*m) / ln(10)
371
+ #
372
+ # Validity: 273-400 K, 1-600 bar, 0-6 m NaCl
373
+ # ====================================================================
374
+
375
+ def _mao2006_lambda_n2(T_K, P_bar=100.0):
376
+ """N2-Na+ Pitzer lambda (Mao & Duan 2006 Table 3). T in K, P in bar."""
377
+ T = np.asarray(T_K, dtype=float)
378
+ P = float(P_bar)
379
+ return (-2.4434074 + 0.0036351795 * T + 447.47364 / T
380
+ - 0.000013711527 * P + 0.0000071037217 * P**2 / T)
381
+
382
+
383
+ _MAO2006_XI_N2 = -0.0058071053 # constant, no T or P dependence
384
+
385
+
386
+ def ks_mao2006_n2(T_K, m_NaCl=0.0, P_bar=100.0):
387
+ """Effective Sechenov for N2 from Mao & Duan 2006 Pitzer model (log10).
388
+
389
+ ln(gamma_N2) = 2*m*lambda(T,P) + m^2*xi
390
+ ks = (2*lambda + xi*m) / ln(10)
391
+
392
+ Parameters
393
+ ----------
394
+ T_K : float or array
395
+ Temperature in Kelvin (273-400 K).
396
+ m_NaCl : float
397
+ NaCl molality. Affects effective ks through xi term.
398
+ P_bar : float
399
+ Pressure in bar (1-600 bar). Default 100.
400
+
401
+ Returns
402
+ -------
403
+ ks : float or array
404
+ Effective Sechenov coefficient (log10 basis, molality scale).
405
+ """
406
+ lam = _mao2006_lambda_n2(T_K, P_bar)
407
+ return (2.0 * lam + _MAO2006_XI_N2 * m_NaCl) / LN10
408
+
409
+
410
+ # ====================================================================
411
+ # 6. Duan & Sun (2003) — Pitzer model for CO2-NaCl
412
+ # Chemical Geology, 193, 257-271.
413
+ # DOI: 10.1016/S0009-2541(02)00263-2
414
+ #
415
+ # Same Pitzer framework as Mao 2006 (N2) — Pitzer et al. (1984) form:
416
+ # Par(T,P) = c1 + c2*T + c3/T + c4*T^2 + c5/(630-T)
417
+ # + c6*P + c7*P*ln(T) + c8*P/T + c9*P/(630-T)
418
+ # + c10*P^2/(630-T)^2 + c11*T*ln(P)
419
+ #
420
+ # lambda_{CO2-Na+} uses c1,c2,c3,c8,c9 (5 coeffs)
421
+ # zeta_{CO2-Na+-Cl-} uses c1,c2,c8,c9 (4 coeffs)
422
+ # lambda_{CO2-Cl-} = 0 (set to zero)
423
+ #
424
+ # ln(gamma_CO2) = 2*m*lambda + m^2*zeta
425
+ # Effective Sechenov: ks = (2*lambda + zeta*m) / ln(10)
426
+ #
427
+ # Validity: 273-533 K, 0-2000 bar, 0-4.3 m NaCl
428
+ # ====================================================================
429
+
430
+ def _duan2003_lambda_co2(T_K, P_bar=100.0):
431
+ """CO2-Na+ Pitzer lambda (Duan & Sun 2003 Table 2). T in K, P in bar."""
432
+ T = np.asarray(T_K, dtype=float)
433
+ P = float(P_bar)
434
+ return (-0.411370585 + 6.07632013e-4 * T + 97.5347708 / T
435
+ - 0.0237622469 * P / T + 0.0170656236 * P / (630.0 - T))
436
+
437
+
438
+ def _duan2003_zeta_co2(T_K, P_bar=100.0):
439
+ """CO2-Na+-Cl- Pitzer zeta (Duan & Sun 2003 Table 2). T in K, P in bar."""
440
+ T = np.asarray(T_K, dtype=float)
441
+ P = float(P_bar)
442
+ return (3.36389723e-4 - 1.98298980e-5 * T
443
+ + 2.12220830e-3 * P / T - 5.24873303e-3 * P / (630.0 - T))
444
+
445
+
446
+ def ks_duan2003_co2(T_K, m_NaCl=0.0, P_bar=100.0):
447
+ """Effective Sechenov for CO2 from Duan & Sun 2003 Pitzer model (log10).
448
+
449
+ ln(gamma_CO2) = 2*m*lambda(T,P) + m^2*zeta(T,P)
450
+ ks = (2*lambda + zeta*m) / ln(10)
451
+
452
+ Parameters
453
+ ----------
454
+ T_K : float or array
455
+ Temperature in Kelvin (273-533 K).
456
+ m_NaCl : float
457
+ NaCl molality (0-4.3 m). Affects effective ks through zeta term.
458
+ P_bar : float
459
+ Pressure in bar (0-2000 bar). Default 100.
460
+
461
+ Returns
462
+ -------
463
+ ks : float or array
464
+ Effective Sechenov coefficient (log10 basis, molality scale).
465
+ """
466
+ lam = _duan2003_lambda_co2(T_K, P_bar)
467
+ zeta = _duan2003_zeta_co2(T_K, P_bar)
468
+ return (2.0 * lam + zeta * m_NaCl) / LN10
469
+
470
+
471
+ # ====================================================================
472
+ # Convenience: unified interface
473
+ # ====================================================================
474
+
475
+ def ks_library(T_K, gas, source='sw_eq8', m_NaCl=0.0, **kwargs):
476
+ """Unified Sechenov coefficient lookup (log10 basis, molality scale).
477
+
478
+ Parameters
479
+ ----------
480
+ T_K : float or array
481
+ Temperature in Kelvin.
482
+ gas : str
483
+ Gas name: 'H2', 'N2', 'CH4', 'C2H6', 'C3H8', 'CO2', 'H2S', etc.
484
+ source : str
485
+ 'sw_eq8' — Soreide & Whitson 1992 Eq 8 (all gases)
486
+ 'dubessy' — Dubessy et al. 2005 (CO2, H2S only)
487
+ 'akinfiev' — Akinfiev et al. 2016 Pitzer (H2S only)
488
+ 'akinfiev_table'— Akinfiev et al. 2016 from tables (H2S only)
489
+ 'li2015' — Li et al. 2015 Pitzer (CH4, C2H6, C3H8, nC4H10)
490
+ 'mao2006' — Mao & Duan 2006 Pitzer (N2 only)
491
+ 'duan2003' — Duan & Sun 2003 Pitzer (CO2 only)
492
+ m_NaCl : float
493
+ NaCl molality (needed for non-linear Sechenov models).
494
+
495
+ Returns
496
+ -------
497
+ ks : float or array
498
+ """
499
+ gas = gas.upper()
500
+ if gas == 'NC4H10':
501
+ pass # already normalized
502
+ elif gas == 'NC4':
503
+ gas = 'NC4H10'
504
+ src = source.lower()
505
+
506
+ if src == 'sw_eq8':
507
+ return ks_sw_eq8(T_K, gas)
508
+ elif src == 'dubessy':
509
+ if gas == 'CO2':
510
+ return ks_dubessy_co2(T_K, m_NaCl)
511
+ elif gas == 'H2S':
512
+ return ks_dubessy_h2s(T_K, m_NaCl)
513
+ else:
514
+ raise ValueError(f"Dubessy only covers CO2 and H2S, not {gas}")
515
+ elif src == 'akinfiev':
516
+ if gas == 'H2S':
517
+ return ks_akinfiev_h2s(T_K, m_NaCl, **kwargs)
518
+ else:
519
+ raise ValueError(f"Akinfiev 2016 only covers H2S, not {gas}")
520
+ elif src == 'akinfiev_table':
521
+ if gas == 'H2S':
522
+ return ks_akinfiev_h2s_from_tables(T_K, m_NaCl)
523
+ else:
524
+ raise ValueError(f"Akinfiev tables only for H2S, not {gas}")
525
+ elif src == 'li2015':
526
+ return ks_li2015(T_K, gas, m_NaCl, **kwargs)
527
+ elif src == 'mao2006':
528
+ if gas == 'N2':
529
+ return ks_mao2006_n2(T_K, m_NaCl, **kwargs)
530
+ else:
531
+ raise ValueError(f"Mao & Duan 2006 only covers N2, not {gas}")
532
+ elif src == 'duan2003':
533
+ if gas == 'CO2':
534
+ return ks_duan2003_co2(T_K, m_NaCl, **kwargs)
535
+ else:
536
+ raise ValueError(f"Duan & Sun 2003 only covers CO2, not {gas}")
537
+ else:
538
+ raise ValueError(f"Unknown source: {source}")
539
+
540
+
541
+ # ====================================================================
542
+ # Main: comparison table
543
+ # ====================================================================
544
+ if __name__ == '__main__':
545
+ temps_C = [25, 50, 75, 100, 125, 150, 200, 250]
546
+ temps_K = [T + 273.15 for T in temps_C]
547
+
548
+ print("=" * 80)
549
+ print("Sechenov Coefficient Library — All values on log10 / molality basis")
550
+ print(" ks > 0 means salting-out; log10(S_water/S_brine) = ks * m_NaCl")
551
+ print("=" * 80)
552
+
553
+ # --- CO2 comparison ---
554
+ print("\n--- CO2: S&W vs Dubessy vs Duan 2003 ---")
555
+ print(f"{'T(C)':>6} {'S&W Eq8':>8} {'Dub m=0':>8} {'Dub m=1':>8} "
556
+ f"{'Dub m=4':>8} {'Duan m=0':>9} {'Duan m=1':>9} {'Duan m=4':>9}")
557
+ print("-" * 82)
558
+ for Tc, Tk in zip(temps_C, temps_K):
559
+ sw = ks_sw_eq8(Tk, 'CO2')
560
+ d0 = ks_dubessy_co2(Tk, 0)
561
+ d1 = ks_dubessy_co2(Tk, 1)
562
+ d4 = ks_dubessy_co2(Tk, 4)
563
+ if Tk <= 533:
564
+ dn0 = ks_duan2003_co2(Tk, 0, 100)
565
+ dn1 = ks_duan2003_co2(Tk, 1, 100)
566
+ dn4 = ks_duan2003_co2(Tk, 4, 100)
567
+ print(f"{Tc:>6} {sw:8.4f} {d0:8.4f} {d1:8.4f} {d4:8.4f} "
568
+ f"{dn0:9.4f} {dn1:9.4f} {dn4:9.4f}")
569
+ else:
570
+ print(f"{Tc:>6} {sw:8.4f} {d0:8.4f} {d1:8.4f} {d4:8.4f} "
571
+ f"{'n/a':>9} {'n/a':>9} {'n/a':>9}")
572
+
573
+ # --- H2S comparison ---
574
+ print("\n--- H2S ---")
575
+ print(f"{'T(C)':>6} {'S&W Eq8':>8} {'Dub m=0':>8} {'Dub m=1':>8} "
576
+ f"{'Akin m=1':>9} {'Akin m=4':>9} {'AkTbl m=1':>10} {'AkTbl m=4':>10}")
577
+ print("-" * 90)
578
+ for Tc, Tk in zip(temps_C, temps_K):
579
+ sw = ks_sw_eq8(Tk, 'H2S')
580
+ d0 = ks_dubessy_h2s(Tk, 0)
581
+ d1 = ks_dubessy_h2s(Tk, 1)
582
+ ak1 = ks_akinfiev_h2s(Tk, m_NaCl=1.0, m_h2s_approx=0.1)
583
+ ak4 = ks_akinfiev_h2s(Tk, m_NaCl=4.0, m_h2s_approx=0.1)
584
+ # Table-based (only for T >= 323.15 K)
585
+ if Tk >= 323.15:
586
+ akt1 = ks_akinfiev_h2s_from_tables(Tk, 1)
587
+ akt4 = ks_akinfiev_h2s_from_tables(Tk, 4)
588
+ print(f"{Tc:>6} {sw:8.4f} {d0:8.4f} {d1:8.4f} "
589
+ f"{ak1:9.4f} {ak4:9.4f} {akt1:10.4f} {akt4:10.4f}")
590
+ else:
591
+ print(f"{Tc:>6} {sw:8.4f} {d0:8.4f} {d1:8.4f} "
592
+ f"{ak1:9.4f} {ak4:9.4f} {'n/a':>10} {'n/a':>10}")
593
+
594
+ # --- Light HC comparison: S&W vs Li 2015 ---
595
+ print("\n--- Light Hydrocarbons: S&W Eq 8 vs Li et al. 2015 Pitzer ---")
596
+ print(" Li 2015 at P=100 bar, m=0 (infinite dilution) and m=2")
597
+ hc_gases = ['CH4', 'C2H6', 'C3H8', 'nC4H10']
598
+ print(f"{'T(C)':>6}", end="")
599
+ for g in hc_gases:
600
+ print(f" {'SW '+g:>11} {'Li m=0':>7} {'Li m=2':>7}", end="")
601
+ print()
602
+ print("-" * 118)
603
+ for Tc, Tk in zip(temps_C, temps_K):
604
+ print(f"{Tc:>6}", end="")
605
+ for g in hc_gases:
606
+ sw = ks_sw_eq8(Tk, g)
607
+ li0 = ks_li2015(Tk, g, m_NaCl=0.0, P_bar=100.0)
608
+ li2 = ks_li2015(Tk, g, m_NaCl=2.0, P_bar=100.0)
609
+ print(f" {sw:11.4f} {li0:7.4f} {li2:7.4f}", end="")
610
+ print()
611
+
612
+ # --- All S&W gases ---
613
+ print("\n--- S&W Eq 8 for all gases (m-independent) ---")
614
+ gases = ['H2', 'N2', 'CH4', 'C2H6', 'C3H8', 'nC4H10', 'CO2', 'H2S']
615
+ header = f"{'T(C)':>6}" + "".join(f" {g:>8}" for g in gases)
616
+ print(header)
617
+ print("-" * (8 + 10 * len(gases)))
618
+ for Tc, Tk in zip(temps_C, temps_K):
619
+ row = f"{Tc:>6}"
620
+ for g in gases:
621
+ row += f" {ks_sw_eq8(Tk, g):8.4f}"
622
+ print(row)
623
+
624
+ # --- N2 comparison: S&W vs Mao & Duan 2006 ---
625
+ print("\n--- N2: S&W Eq 8 vs Mao & Duan 2006 Pitzer ---")
626
+ print(" Mao 2006 at P=100 bar, various m_NaCl")
627
+ print(f"{'T(C)':>6} {'S&W Eq8':>8} {'Mao m=0':>8} {'Mao m=1':>8} "
628
+ f"{'Mao m=2':>8} {'Mao m=4':>8}")
629
+ print("-" * 56)
630
+ for Tc, Tk in zip(temps_C, temps_K):
631
+ if Tk > 400:
632
+ # Mao 2006 valid to 400 K only
633
+ sw = ks_sw_eq8(Tk, 'N2')
634
+ print(f"{Tc:>6} {sw:8.4f} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>8}")
635
+ else:
636
+ sw = ks_sw_eq8(Tk, 'N2')
637
+ m0 = ks_mao2006_n2(Tk, m_NaCl=0.0, P_bar=100.0)
638
+ m1 = ks_mao2006_n2(Tk, m_NaCl=1.0, P_bar=100.0)
639
+ m2 = ks_mao2006_n2(Tk, m_NaCl=2.0, P_bar=100.0)
640
+ m4 = ks_mao2006_n2(Tk, m_NaCl=4.0, P_bar=100.0)
641
+ print(f"{Tc:>6} {sw:8.4f} {m0:8.4f} {m1:8.4f} {m2:8.4f} {m4:8.4f}")
642
+
643
+ # --- N2: Pressure sensitivity (Mao 2006) ---
644
+ print("\n--- N2: Mao 2006 ks at different pressures (m=0) ---")
645
+ print(f"{'T(C)':>6} {'1 bar':>8} {'50 bar':>8} {'100 bar':>8} "
646
+ f"{'200 bar':>8} {'500 bar':>8} {'S&W':>8}")
647
+ print("-" * 62)
648
+ for Tc, Tk in zip(temps_C, temps_K):
649
+ if Tk > 400:
650
+ sw = ks_sw_eq8(Tk, 'N2')
651
+ print(f"{Tc:>6} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>8} {'n/a':>8} {sw:8.4f}")
652
+ else:
653
+ vals = [ks_mao2006_n2(Tk, 0, P) for P in [1, 50, 100, 200, 500]]
654
+ sw = ks_sw_eq8(Tk, 'N2')
655
+ print(f"{Tc:>6}" + "".join(f" {v:8.4f}" for v in vals) + f" {sw:8.4f}")
656
+
657
+ # --- CH4: Pressure sensitivity (Li 2015) ---
658
+ print("\n--- CH4: Li 2015 ks at different pressures (m=0) ---")
659
+ print(f"{'T(C)':>6} {'1 bar':>8} {'50 bar':>8} {'100 bar':>8} "
660
+ f"{'200 bar':>8} {'500 bar':>8} {'S&W':>8}")
661
+ print("-" * 62)
662
+ for Tc, Tk in zip(temps_C, temps_K):
663
+ vals = [ks_li2015(Tk, 'CH4', 0, P) for P in [1, 50, 100, 200, 500]]
664
+ sw = ks_sw_eq8(Tk, 'CH4')
665
+ print(f"{Tc:>6}" + "".join(f" {v:8.4f}" for v in vals) + f" {sw:8.4f}")
666
+
667
+ # --- H2S model comparison summary ---
668
+ print("\n--- H2S: Akinfiev table ks at multiple pressures (log10) ---")
669
+ print(" Showing that ks is largely P-independent (as expected)")
670
+ # Tables at 1 MPa and 10 MPa for m=2
671
+ T_tab = [323.15, 373.15, 423.15, 473.15, 523.15]
672
+ m0_1 = [0.596, 0.297, 0.136, np.nan, np.nan] # pure water, 1 MPa
673
+ m2_1 = [0.467, 0.241, 0.111, np.nan, np.nan] # 2m NaCl, 1 MPa
674
+ m0_5 = [2.026, 1.651, 1.271, 0.956, 0.300] # pure water, 5 MPa
675
+ m2_5 = [1.523, 1.259, 0.975, 0.726, 0.226] # 2m NaCl, 5 MPa
676
+ m0_10 = [2.100, 2.703, 2.752, 2.622, 2.045] # pure water, 10 MPa
677
+ m2_10 = [1.577, 1.980, 1.969, 1.834, 1.411] # 2m NaCl, 10 MPa
678
+ m0_20 = [2.237, 3.065, 4.407, 5.395, 5.458] # pure water, 20 MPa
679
+ m2_20 = [1.676, 2.224, 3.039, 3.659, 3.677] # 2m NaCl, 20 MPa
680
+
681
+ print(f"{'T(C)':>6} {'1 MPa':>8} {'5 MPa':>8} {'10 MPa':>8} {'20 MPa':>8}")
682
+ print("-" * 46)
683
+ for i, Tk in enumerate(T_tab):
684
+ Tc = Tk - 273.15
685
+ vals = []
686
+ for mw, mb in [(m0_1[i], m2_1[i]), (m0_5[i], m2_5[i]),
687
+ (m0_10[i], m2_10[i]), (m0_20[i], m2_20[i])]:
688
+ if np.isnan(mw) or np.isnan(mb) or mb <= 0:
689
+ vals.append(" n/a ")
690
+ else:
691
+ ks = np.log10(mw / mb) / 2.0
692
+ vals.append(f"{ks:8.4f}")
693
+ print(f"{Tc:>6.0f} {' '.join(vals)}")