peritheos 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.
- peritheos/__init__.py +3 -0
- peritheos/constants.py +22 -0
- peritheos/eos/__init__.py +234 -0
- peritheos/eos/_reference/__init__.py +0 -0
- peritheos/eos/_reference/sokolova2016.py +527 -0
- peritheos/eos/rt/__init__.py +7 -0
- peritheos/eos/rt/bm.py +281 -0
- peritheos/eos/rt/holzapfel.py +196 -0
- peritheos/eos/rt/vinet.py +86 -0
- peritheos/eos/thermal/__init__.py +5 -0
- peritheos/eos/thermal/sokolova2016.py +242 -0
- peritheos/utils.py +118 -0
- peritheos-0.1.0.dist-info/METADATA +145 -0
- peritheos-0.1.0.dist-info/RECORD +17 -0
- peritheos-0.1.0.dist-info/WHEEL +5 -0
- peritheos-0.1.0.dist-info/licenses/LICENSE +21 -0
- peritheos-0.1.0.dist-info/top_level.txt +1 -0
peritheos/__init__.py
ADDED
peritheos/constants.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Physical constants used in thermodynamic calculations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from scipy import constants
|
|
6
|
+
|
|
7
|
+
# Universal gas constant (J/(mol·K))
|
|
8
|
+
R = constants.R
|
|
9
|
+
|
|
10
|
+
# Boltzmann constant (J/K)
|
|
11
|
+
k_B = constants.Boltzmann
|
|
12
|
+
|
|
13
|
+
# Avogadro's number (1/mol)
|
|
14
|
+
N_A = constants.Avogadro
|
|
15
|
+
|
|
16
|
+
# Standard temperature and pressure
|
|
17
|
+
STP_TEMPERATURE = constants.zero_Celsius # K (273.15)
|
|
18
|
+
STP_PRESSURE = constants.atm # Pa (101325.0)
|
|
19
|
+
|
|
20
|
+
# Other useful constants
|
|
21
|
+
STANDARD_GRAVITY = constants.g # m/s²
|
|
22
|
+
STEFAN_BOLTZMANN = constants.Stefan_Boltzmann # W/(m²·K⁴)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Equations of state module for Peritheos
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Union
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
from scipy import optimize
|
|
9
|
+
|
|
10
|
+
# Type alias for numeric values (scalar or array)
|
|
11
|
+
NumericType = Union[float, NDArray[np.float64]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_finite_scalar(value: float, name: str) -> float:
|
|
15
|
+
"""Return *value* as a finite float or raise a descriptive error."""
|
|
16
|
+
value = float(value)
|
|
17
|
+
if not np.isfinite(value):
|
|
18
|
+
raise ValueError(f"{name} must be finite")
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_positive_scalar(value: float, name: str) -> float:
|
|
23
|
+
"""Return *value* as a positive finite float."""
|
|
24
|
+
value = validate_finite_scalar(value, name)
|
|
25
|
+
if value <= 0:
|
|
26
|
+
raise ValueError(f"{name} must be greater than zero")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_volume(V: NumericType) -> NumericType:
|
|
31
|
+
"""Validate volume input while preserving scalar or array behaviour."""
|
|
32
|
+
values = np.asarray(V, dtype=float)
|
|
33
|
+
if not np.all(np.isfinite(values)):
|
|
34
|
+
raise ValueError("Volume must be finite")
|
|
35
|
+
if np.any(values <= 0):
|
|
36
|
+
raise ValueError("Volume must be greater than zero")
|
|
37
|
+
if values.ndim == 0:
|
|
38
|
+
return float(values)
|
|
39
|
+
return values
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _scalar_pressure(pressure_function: Callable[[float], NumericType], V: float) -> float:
|
|
43
|
+
"""Evaluate a pressure callable and require a finite scalar result."""
|
|
44
|
+
pressure = np.asarray(pressure_function(V), dtype=float)
|
|
45
|
+
if pressure.ndim != 0:
|
|
46
|
+
raise TypeError("The pressure function must return a scalar for scalar volume")
|
|
47
|
+
result = float(pressure)
|
|
48
|
+
if not np.isfinite(result):
|
|
49
|
+
raise ArithmeticError(f"EOS returned a non-finite pressure at V={V}")
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def solve_volume(
|
|
54
|
+
pressure_function: Callable[[float], NumericType],
|
|
55
|
+
pressure: float,
|
|
56
|
+
reference_volume: float,
|
|
57
|
+
) -> float:
|
|
58
|
+
"""Solve ``pressure_function(V) == pressure`` on the branch nearest V0.
|
|
59
|
+
|
|
60
|
+
The EOS is assumed to be locally monotonic around its reference volume:
|
|
61
|
+
pressure increases on compression and decreases on expansion. Starting at
|
|
62
|
+
``V0``, the search geometrically decreases volume for a higher target
|
|
63
|
+
pressure or increases it for a lower target pressure until the pressure
|
|
64
|
+
residual changes sign. ``scipy.optimize.brentq`` then solves inside that
|
|
65
|
+
positive-volume bracket. If no sign change is found, the requested state is
|
|
66
|
+
reported as outside the model's invertible range.
|
|
67
|
+
"""
|
|
68
|
+
target = validate_finite_scalar(pressure, "Pressure")
|
|
69
|
+
V0 = validate_positive_scalar(reference_volume, "Reference volume")
|
|
70
|
+
p0 = _scalar_pressure(pressure_function, V0)
|
|
71
|
+
f0 = p0 - target
|
|
72
|
+
pressure_tolerance = 1e-10 * max(1.0, abs(target), abs(p0))
|
|
73
|
+
if abs(f0) <= pressure_tolerance:
|
|
74
|
+
return V0
|
|
75
|
+
|
|
76
|
+
if f0 < 0:
|
|
77
|
+
# The target is above P(V0), so search towards compression.
|
|
78
|
+
upper, f_upper = V0, f0
|
|
79
|
+
lower = V0
|
|
80
|
+
f_lower = f0
|
|
81
|
+
for _ in range(160):
|
|
82
|
+
lower *= 0.8
|
|
83
|
+
if lower <= V0 * 1e-14:
|
|
84
|
+
break
|
|
85
|
+
f_lower = _scalar_pressure(pressure_function, lower) - target
|
|
86
|
+
if f_lower >= 0:
|
|
87
|
+
break
|
|
88
|
+
else:
|
|
89
|
+
f_lower = np.nan
|
|
90
|
+
if not np.isfinite(f_lower) or f_lower < 0:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Could not bracket a positive volume for pressure {target}"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
# The target is below P(V0), so search along the first expansion branch.
|
|
96
|
+
lower, f_lower = V0, f0
|
|
97
|
+
upper = V0
|
|
98
|
+
f_upper = f0
|
|
99
|
+
for _ in range(160):
|
|
100
|
+
upper *= 1.05
|
|
101
|
+
if upper >= V0 * 1e4:
|
|
102
|
+
break
|
|
103
|
+
f_upper = _scalar_pressure(pressure_function, upper) - target
|
|
104
|
+
if f_upper <= 0:
|
|
105
|
+
break
|
|
106
|
+
else:
|
|
107
|
+
f_upper = np.nan
|
|
108
|
+
if not np.isfinite(f_upper) or f_upper > 0:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Pressure {target} is outside the invertible expansion range"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
result = optimize.brentq(
|
|
114
|
+
lambda volume: _scalar_pressure(pressure_function, volume) - target,
|
|
115
|
+
lower,
|
|
116
|
+
upper,
|
|
117
|
+
xtol=max(np.finfo(float).eps * V0, 1e-14),
|
|
118
|
+
rtol=1e-12,
|
|
119
|
+
)
|
|
120
|
+
residual = abs(_scalar_pressure(pressure_function, result) - target)
|
|
121
|
+
if residual > 1e-8 * max(1.0, abs(target)):
|
|
122
|
+
raise ArithmeticError(
|
|
123
|
+
f"Volume inversion did not converge to the requested pressure; residual={residual}"
|
|
124
|
+
)
|
|
125
|
+
return float(result)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class EosBase:
|
|
129
|
+
"""
|
|
130
|
+
Base class for equation of state implementations.
|
|
131
|
+
|
|
132
|
+
This abstract class defines the interface that all equation of state
|
|
133
|
+
implementations should follow.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def pressure(self, V: NumericType) -> NumericType:
|
|
137
|
+
"""
|
|
138
|
+
Calculate pressure at a given volume.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
V : float or numpy.ndarray
|
|
143
|
+
Volume (in cubic angstroms or any consistent unit)
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
float or numpy.ndarray
|
|
148
|
+
Pressure (in the same units as K0)
|
|
149
|
+
"""
|
|
150
|
+
raise NotImplementedError("Subclasses must implement the pressure method.")
|
|
151
|
+
|
|
152
|
+
def bulk_modulus(self, V: NumericType) -> NumericType:
|
|
153
|
+
"""
|
|
154
|
+
Calculate the bulk modulus.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
V : float or numpy.ndarray
|
|
159
|
+
Volume (in cubic angstroms or any consistent unit)
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
float or numpy.ndarray
|
|
164
|
+
Bulk modulus (in the same units as K0)
|
|
165
|
+
"""
|
|
166
|
+
raise NotImplementedError("Subclasses must implement the bulk_modulus method.")
|
|
167
|
+
|
|
168
|
+
def calculate_volume(self, P: NumericType) -> NumericType:
|
|
169
|
+
"""
|
|
170
|
+
Calculate volume at a given pressure using a bracketed root solver.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
P : float or numpy.ndarray
|
|
175
|
+
Pressure (in the same units as K0)
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
float or numpy.ndarray
|
|
180
|
+
Volume (in the same units as V0)
|
|
181
|
+
"""
|
|
182
|
+
pressures = np.asarray(P, dtype=float)
|
|
183
|
+
if not np.all(np.isfinite(pressures)):
|
|
184
|
+
raise ValueError("Pressure must be finite")
|
|
185
|
+
if pressures.ndim == 0:
|
|
186
|
+
return solve_volume(self.pressure, float(pressures), self.V0)
|
|
187
|
+
return np.array(
|
|
188
|
+
[solve_volume(self.pressure, value, self.V0) for value in pressures.flat]
|
|
189
|
+
).reshape(pressures.shape)
|
|
190
|
+
|
|
191
|
+
def volume(self, P: NumericType) -> NumericType:
|
|
192
|
+
"""Alias for :meth:`calculate_volume` using the P-to-V terminology."""
|
|
193
|
+
return self.calculate_volume(P)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ThermalEOS(EosBase):
|
|
197
|
+
def __init__(self, rt_eos: EosBase):
|
|
198
|
+
self.rt_eos = rt_eos
|
|
199
|
+
|
|
200
|
+
def thermal_pressure(self, V: NumericType, T: NumericType) -> NumericType:
|
|
201
|
+
raise NotImplementedError("This method should be implemented by the subclass")
|
|
202
|
+
|
|
203
|
+
def pressure(self, V: NumericType, T: NumericType) -> NumericType:
|
|
204
|
+
return self.thermal_pressure(V, T) + self.rt_eos.pressure(V)
|
|
205
|
+
|
|
206
|
+
def calculate_volume(self, P: NumericType, T: NumericType) -> NumericType:
|
|
207
|
+
"""Calculate volume at pressure and temperature using bracketed roots."""
|
|
208
|
+
pressures, temperatures = np.broadcast_arrays(
|
|
209
|
+
np.asarray(P, dtype=float), np.asarray(T, dtype=float)
|
|
210
|
+
)
|
|
211
|
+
if not np.all(np.isfinite(pressures)):
|
|
212
|
+
raise ValueError("Pressure must be finite")
|
|
213
|
+
if not np.all(np.isfinite(temperatures)) or np.any(temperatures <= 0):
|
|
214
|
+
raise ValueError("Temperature must be finite and greater than zero")
|
|
215
|
+
|
|
216
|
+
volumes = np.array(
|
|
217
|
+
[
|
|
218
|
+
solve_volume(
|
|
219
|
+
lambda volume, temperature=float(temperature): self.pressure(
|
|
220
|
+
volume, temperature
|
|
221
|
+
),
|
|
222
|
+
float(pressure),
|
|
223
|
+
self.rt_eos.V0,
|
|
224
|
+
)
|
|
225
|
+
for pressure, temperature in zip(pressures.flat, temperatures.flat)
|
|
226
|
+
]
|
|
227
|
+
).reshape(pressures.shape)
|
|
228
|
+
if volumes.ndim == 0:
|
|
229
|
+
return float(volumes)
|
|
230
|
+
return volumes
|
|
231
|
+
|
|
232
|
+
def volume(self, P: NumericType, T: NumericType) -> NumericType:
|
|
233
|
+
"""Alias for :meth:`calculate_volume` for a thermal EOS."""
|
|
234
|
+
return self.calculate_volume(P, T)
|
|
File without changes
|