peritheos 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clemens Prescher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: peritheos
3
+ Version: 0.1.0
4
+ Summary: A library for thermodynamic equations of state calculations
5
+ Author-email: Clemens Prescher <clemens.prescher@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cprescher/peritheos
8
+ Project-URL: Bug Tracker, https://github.com/cprescher/peritheos/issues
9
+ Keywords: equation-of-state,high-pressure,thermodynamics
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.20.0
25
+ Requires-Dist: scipy>=1.7.0
26
+ Dynamic: license-file
27
+
28
+ # Peritheos
29
+
30
+ A Python library for thermodynamic equations of state calculations for solid materials.
31
+
32
+ ## Features
33
+
34
+ - Room temperature equations of state (EOS) implementations
35
+ - Birch-Murnaghan
36
+ - Vinet
37
+ - Holzapfel
38
+ - Thermal equations of state (EOS) implementations
39
+ - Sokolova 2016
40
+
41
+ ## Unit conventions
42
+
43
+ - Public pressure and bulk-modulus values are in GPa.
44
+ - Temperatures are in K.
45
+ - Birch-Murnaghan and Vinet accept any consistent volume unit.
46
+ - Holzapfel and Sokolova 2016 require molar volume in J bar^-1, which is
47
+ equivalent to cm^3/mol divided by 10.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install peritheos
53
+ ```
54
+
55
+ The latest development version can instead be installed directly from GitHub:
56
+
57
+ ```bash
58
+ pip install git+https://github.com/CPrescher/peritheos.git
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Room-temperature equations of state
64
+
65
+ Third-order Birch-Murnaghan equation of state:
66
+
67
+ ```python
68
+ from peritheos.eos.rt import BM3
69
+
70
+ # V0 may use any volume unit for a room-temperature EOS; K0 is in GPa here.
71
+ eos = BM3(V0=50, K0=130, K0_prime=4.3)
72
+
73
+ # Calculate pressure and bulk modulus at a given volume.
74
+ pressure = eos.pressure(V=40)
75
+ bulk_modulus = eos.bulk_modulus(V=40)
76
+
77
+ # Invert the EOS to calculate volume at a given pressure.
78
+ volume = eos.volume(P=pressure)
79
+
80
+ print(f"Pressure: {pressure} GPa")
81
+ print(f"Bulk modulus: {bulk_modulus} GPa")
82
+ print(f"Recovered volume: {volume}")
83
+ ```
84
+
85
+ ### Thermal equations of state
86
+
87
+ Diamond thermal equation of state from sokolova et al. 2016
88
+
89
+ ```python
90
+ from peritheos.eos.rt.holzapfel import Holzapfel
91
+ from peritheos.eos.thermal.sokolova2016 import Sokolova2016
92
+
93
+ # Diamond parameters from Sokolova et al. 2016.
94
+ # The thermal model requires molar volume in J bar^-1 (= [cm^3/mol] / 10),
95
+ # pressure parameters in GPa, and temperatures in K.
96
+ V0 = 0.3414
97
+ K0 = 441.5
98
+ K0_prime = 3.9 # pressure derivative of bulk modulus at reference volume
99
+ QE1o = 684 # first Einstein characteristic temperature
100
+ mE1 = 0.564 # first Einstein number
101
+ QE2o = 1561 # second Einstein characteristic temperature
102
+ mE2 = 2.436 # second Einstein number
103
+ delta = -0.506 # additive normalizing constant for the Gruneisen parameter
104
+ t = 1.085 # generalized Gruneisen parameter
105
+ a_0 = 0 # intrinsic anharmonicity parameter
106
+ m = 0 # anharmonic analogue of the Grüneisen parameter
107
+ e_0 = 0 # free electrons parameter
108
+ g = 0 # electronic analogue of the Grüneisen parameter
109
+
110
+ n = 1 # number of atoms in the formula unit
111
+ z = 6 # atomic number of the formula unit
112
+ Tr = 298.15 # in K - Reference temperature
113
+
114
+ # Initialize the Holzapfel EOS
115
+ holzapfel = Holzapfel(V0=V0, K0=K0, K0_prime=K0_prime, n=n, Z=z)
116
+
117
+ # Initialize the Sokolova 2016 EOS
118
+ sokolova = Sokolova2016(
119
+ rt_eos=holzapfel,
120
+ Tr=Tr,
121
+ QE1o=QE1o,
122
+ mE1=mE1,
123
+ QE2o=QE2o,
124
+ mE2=mE2,
125
+ delta=delta,
126
+ t=t,
127
+ a_0=a_0,
128
+ m=m,
129
+ g=g,
130
+ e_0=e_0,
131
+ )
132
+
133
+ # Calculate the thermal pressure at a given volume and temperature
134
+ V = V0 * 0.8
135
+ T = 3000 # in K
136
+ thermal_pressure = sokolova.thermal_pressure(V, T)
137
+ rt_pressure = holzapfel.pressure(V)
138
+ pressure = sokolova.pressure(V, T)
139
+ recovered_volume = sokolova.volume(pressure, T)
140
+
141
+ print(f"Thermal pressure: {thermal_pressure} GPa")
142
+ print(f"RT pressure: {rt_pressure} GPa")
143
+ print(f"Total pressure: {pressure} GPa")
144
+ print(f"Recovered volume: {recovered_volume} J bar^-1")
145
+ ```
@@ -0,0 +1,118 @@
1
+ # Peritheos
2
+
3
+ A Python library for thermodynamic equations of state calculations for solid materials.
4
+
5
+ ## Features
6
+
7
+ - Room temperature equations of state (EOS) implementations
8
+ - Birch-Murnaghan
9
+ - Vinet
10
+ - Holzapfel
11
+ - Thermal equations of state (EOS) implementations
12
+ - Sokolova 2016
13
+
14
+ ## Unit conventions
15
+
16
+ - Public pressure and bulk-modulus values are in GPa.
17
+ - Temperatures are in K.
18
+ - Birch-Murnaghan and Vinet accept any consistent volume unit.
19
+ - Holzapfel and Sokolova 2016 require molar volume in J bar^-1, which is
20
+ equivalent to cm^3/mol divided by 10.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install peritheos
26
+ ```
27
+
28
+ The latest development version can instead be installed directly from GitHub:
29
+
30
+ ```bash
31
+ pip install git+https://github.com/CPrescher/peritheos.git
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Room-temperature equations of state
37
+
38
+ Third-order Birch-Murnaghan equation of state:
39
+
40
+ ```python
41
+ from peritheos.eos.rt import BM3
42
+
43
+ # V0 may use any volume unit for a room-temperature EOS; K0 is in GPa here.
44
+ eos = BM3(V0=50, K0=130, K0_prime=4.3)
45
+
46
+ # Calculate pressure and bulk modulus at a given volume.
47
+ pressure = eos.pressure(V=40)
48
+ bulk_modulus = eos.bulk_modulus(V=40)
49
+
50
+ # Invert the EOS to calculate volume at a given pressure.
51
+ volume = eos.volume(P=pressure)
52
+
53
+ print(f"Pressure: {pressure} GPa")
54
+ print(f"Bulk modulus: {bulk_modulus} GPa")
55
+ print(f"Recovered volume: {volume}")
56
+ ```
57
+
58
+ ### Thermal equations of state
59
+
60
+ Diamond thermal equation of state from sokolova et al. 2016
61
+
62
+ ```python
63
+ from peritheos.eos.rt.holzapfel import Holzapfel
64
+ from peritheos.eos.thermal.sokolova2016 import Sokolova2016
65
+
66
+ # Diamond parameters from Sokolova et al. 2016.
67
+ # The thermal model requires molar volume in J bar^-1 (= [cm^3/mol] / 10),
68
+ # pressure parameters in GPa, and temperatures in K.
69
+ V0 = 0.3414
70
+ K0 = 441.5
71
+ K0_prime = 3.9 # pressure derivative of bulk modulus at reference volume
72
+ QE1o = 684 # first Einstein characteristic temperature
73
+ mE1 = 0.564 # first Einstein number
74
+ QE2o = 1561 # second Einstein characteristic temperature
75
+ mE2 = 2.436 # second Einstein number
76
+ delta = -0.506 # additive normalizing constant for the Gruneisen parameter
77
+ t = 1.085 # generalized Gruneisen parameter
78
+ a_0 = 0 # intrinsic anharmonicity parameter
79
+ m = 0 # anharmonic analogue of the Grüneisen parameter
80
+ e_0 = 0 # free electrons parameter
81
+ g = 0 # electronic analogue of the Grüneisen parameter
82
+
83
+ n = 1 # number of atoms in the formula unit
84
+ z = 6 # atomic number of the formula unit
85
+ Tr = 298.15 # in K - Reference temperature
86
+
87
+ # Initialize the Holzapfel EOS
88
+ holzapfel = Holzapfel(V0=V0, K0=K0, K0_prime=K0_prime, n=n, Z=z)
89
+
90
+ # Initialize the Sokolova 2016 EOS
91
+ sokolova = Sokolova2016(
92
+ rt_eos=holzapfel,
93
+ Tr=Tr,
94
+ QE1o=QE1o,
95
+ mE1=mE1,
96
+ QE2o=QE2o,
97
+ mE2=mE2,
98
+ delta=delta,
99
+ t=t,
100
+ a_0=a_0,
101
+ m=m,
102
+ g=g,
103
+ e_0=e_0,
104
+ )
105
+
106
+ # Calculate the thermal pressure at a given volume and temperature
107
+ V = V0 * 0.8
108
+ T = 3000 # in K
109
+ thermal_pressure = sokolova.thermal_pressure(V, T)
110
+ rt_pressure = holzapfel.pressure(V)
111
+ pressure = sokolova.pressure(V, T)
112
+ recovered_volume = sokolova.volume(pressure, T)
113
+
114
+ print(f"Thermal pressure: {thermal_pressure} GPa")
115
+ print(f"RT pressure: {rt_pressure} GPa")
116
+ print(f"Total pressure: {pressure} GPa")
117
+ print(f"Recovered volume: {recovered_volume} J bar^-1")
118
+ ```
@@ -0,0 +1,3 @@
1
+ """Peritheos: thermodynamic equations of state calculations."""
2
+
3
+ __version__ = "0.1.0"
@@ -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