Skip to content

Commit 1ea2fb7

Browse files
authored
Merge pull request #641 from bobmyhill/perplex_build
Perplex build
2 parents 7c2960f + 217f958 commit 1ea2fb7

File tree

10 files changed

+804
-133
lines changed

10 files changed

+804
-133
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
/build/*
77
*.tmp
88
*.pyc
9+
/contrib/perplex/perplex-installer
10+
/contrib/perplex/iron_olivine_lo_res

burnman/classes/composition.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,8 @@ def __init__(self, composition_dictionary, unit_type="mass", normalize=False):
9494
input_dictionary[k] = composition_dictionary[k] / n_total
9595

9696
# Break component formulae into atomic dictionaries
97-
self.component_formulae = {
98-
c: dictionarize_formula(c) for c in composition_dictionary.keys()
99-
}
100-
101-
# Create lists of elemental compositions of components
102-
self.element_list = OrderedCounter()
103-
for component in self.component_formulae.values():
104-
self.element_list += OrderedCounter(
105-
{element: n_atoms for (element, n_atoms) in component.items()}
106-
)
107-
self.element_list = list(self.element_list.keys())
97+
fl = self._component_formulae_and_element_lists(composition_dictionary.keys())
98+
self.component_formulae, self.element_list = fl
10899

109100
if unit_type == "mass" or unit_type == "weight":
110101
self.mass_composition = input_dictionary
@@ -116,6 +107,30 @@ def __init__(self, composition_dictionary, unit_type="mass", normalize=False):
116107
"Should be either mass, weight or molar."
117108
)
118109

110+
def _component_formulae_and_element_lists(self, component_strings):
111+
"""
112+
Converts a list of components in string form into a list of components in dictionary form
113+
and a list of elements.
114+
115+
:param component_strings: A list of strings containing a formula for each component
116+
:type component_strings: list of strings
117+
:return: a list of components in dictionary form and a list of elements.
118+
:rtype: _type_
119+
"""
120+
121+
# Break component formulae into atomic dictionaries
122+
component_formulae = {c: dictionarize_formula(c) for c in component_strings}
123+
124+
# Create lists of elemental compositions of components
125+
element_list = OrderedCounter()
126+
for component in component_formulae.values():
127+
element_list += OrderedCounter(
128+
{element: n_atoms for (element, n_atoms) in component.items()}
129+
)
130+
element_list = list(element_list.keys())
131+
132+
return component_formulae, element_list
133+
119134
def renormalize(self, unit_type, normalization_component, normalization_amount):
120135
"""
121136
Change the total amount of material in the composition
@@ -191,6 +206,14 @@ def change_component_set(self, new_component_list):
191206
:param new_component_list: New set of basis components.
192207
:type new_component_list: list of strings
193208
"""
209+
210+
old_element_list = self.element_list
211+
_, new_element_list = self._component_formulae_and_element_lists(
212+
new_component_list
213+
)
214+
215+
self.element_list = list(set(old_element_list).union(set(new_element_list)))
216+
194217
composition = np.array(
195218
[self.atomic_composition[element] for element in self.element_list]
196219
)
@@ -218,6 +241,24 @@ def change_component_set(self, new_component_list):
218241
# Reinitialize the object
219242
self.__init__(composition, "molar")
220243

244+
def remove_null_components(self, tol=1.0e-12):
245+
"""
246+
Removes components with absolute concentrations less than a given tolerance.
247+
248+
:param tol: zero tolerance, defaults to 1.e-12
249+
:type tol: float, optional
250+
"""
251+
c = deepcopy(self.molar_composition)
252+
del_keys = []
253+
for key, value in c.items():
254+
if np.abs(value) < tol:
255+
del_keys.append(key)
256+
for key in del_keys:
257+
c.pop(key)
258+
259+
# Reinitialize the object
260+
self.__init__(c, "molar")
261+
221262
def _mole_to_mass_composition(self, molar_comp):
222263
"""
223264
Hidden function to returns the mass composition as a counter [kg]

burnman/classes/perplex.py

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
from __future__ import absolute_import
66
from __future__ import print_function
77

8-
import subprocess
9-
from os import rename
10-
118
import numpy as np
129
from scipy.interpolate import RegularGridInterpolator
1310
from scipy.interpolate import griddata
@@ -17,125 +14,6 @@
1714
from ..utils.misc import copy_documentation
1815

1916

20-
def create_perplex_table(
21-
werami_path,
22-
project_name,
23-
outfile,
24-
n_pressures,
25-
n_temperatures,
26-
pressure_range=None,
27-
temperature_range=None,
28-
):
29-
"""
30-
This function uses PerpleX's werami software to output a table file
31-
containing the following material properties.
32-
2 - Density (kg/m3)
33-
4 - Expansivity (1/K, for volume)
34-
5 - Compressibility (1/bar, for volume)
35-
10 - Adiabatic bulk modulus (bar)
36-
11 - Adiabatic shear modulus (bar)
37-
12 - Sound velocity (km/s)
38-
13 - P-wave velocity (Vp, km/s)
39-
14 - S-wave velocity (Vs, km/s)
40-
17 - Entropy (J/K/kg)
41-
18 - Enthalpy (J/kg)
42-
19 - Heat Capacity (J/K/kg)
43-
22 - Molar Volume (J/bar)
44-
45-
The user must already have a PerpleX build file,
46-
and have run vertex on that build file.
47-
"""
48-
49-
print(
50-
"Creating a {0}x{1} P-T table file using werami. Please wait.\n".format(
51-
n_pressures, n_temperatures
52-
)
53-
)
54-
55-
try:
56-
str2 = "y\n{0} {1}\n{2} {3}\n".format(
57-
pressure_range[0] / 1.0e5,
58-
pressure_range[1] / 1.0e5,
59-
temperature_range[0],
60-
temperature_range[1],
61-
)
62-
except TypeError:
63-
print("Keeping P-T range the same as the original project range.\n")
64-
str2 = "n\n"
65-
66-
stdin = (
67-
"{0:s}\n2\n"
68-
"2\nn\n"
69-
"4\nn\n"
70-
"5\nn\n"
71-
"10\nn\n"
72-
"11\nn\n"
73-
"12\nn\n"
74-
"13\nn\n"
75-
"14\nn\n"
76-
"17\nn\n"
77-
"18\nn\n"
78-
"19\nn\n"
79-
"22\nn\n"
80-
"0\n"
81-
"{1:s}"
82-
"{2:d} {3:d}\n"
83-
"0\n".format(project_name, str2, n_pressures, n_temperatures)
84-
)
85-
86-
with subprocess.Popen(
87-
werami_path,
88-
stdin=subprocess.PIPE,
89-
stdout=subprocess.PIPE,
90-
encoding="utf8",
91-
) as process:
92-
process.stdin.write(stdin)
93-
process.stdin.flush()
94-
95-
out = ""
96-
# Grab stdout line by line as it becomes available.
97-
# This will loop until the process terminates.
98-
stdoutput = ""
99-
while process.poll() is not None:
100-
line = process.stdout.readline()
101-
stdoutput += line
102-
103-
# Check if vertex has been run on the build file
104-
if "missing *.tof file" in line:
105-
raise Exception(
106-
"You must run Perple_X vertex "
107-
f"({werami_path[0].split('werami')[0]}vertex) "
108-
"using the PerpleX build file ({project_name}) "
109-
"before running this script."
110-
)
111-
112-
while process.poll() is None:
113-
line = process.stdout.readline()
114-
stdoutput += line
115-
116-
# Check if werami is trying to create a standard resolution grid
117-
# Tell the user to modify their local perplex option file if so.
118-
if "Continue (y/n)?" in line:
119-
raise Exception(
120-
"If you do not want to define your own P-T range for the grid,\n"
121-
"you must set sample_on_grid to F in the perplex option file\n"
122-
"(default is perplex_option.dat)."
123-
)
124-
125-
# Get the output file name
126-
if "Output has been written to the" in line:
127-
out = line.split()[-1]
128-
129-
# Print stdoutput
130-
print(stdoutput)
131-
print(process.stdout.read())
132-
133-
# Rename the file to the user-specified filename
134-
rename(out, outfile)
135-
print("Output file renamed to {0:s}".format(outfile))
136-
print("Processing complete")
137-
138-
13917
class PerplexMaterial(Material):
14018
"""
14119
This is the base class for a PerpleX material. States of the material

burnman/utils/misc.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import absolute_import
77
from __future__ import print_function
88

9+
import subprocess
910
import operator
1011
import bisect
1112
import pkgutil
@@ -237,3 +238,69 @@ def f(x):
237238
return value
238239

239240
return f
241+
242+
243+
def extract_lines_between_markers(file, start_string, end_string, inclusive=False):
244+
"""
245+
Extract lines from a file between two marker strings.
246+
247+
:param file: Path to the input text file.
248+
:type file: str
249+
:param start_string: The marker string indicating where to start collecting lines.
250+
:type start_string: str
251+
:param end_string: The marker string indicating where to stop collecting lines.
252+
:type end_string: str
253+
:param inclusive: Whether to include the lines containing start_string and end_string, defaults to False.
254+
:type inclusive: bool, optional
255+
:return: A list of lines between the two markers.
256+
:rtype: list[str]
257+
"""
258+
lines = []
259+
with open(file, encoding="latin-1") as f:
260+
inside = False
261+
for line in f:
262+
if start_string in line and not inside:
263+
inside = True
264+
if inclusive:
265+
lines.append(line.strip())
266+
continue # Don't reprocess the start line if not inclusive
267+
268+
if inside:
269+
if end_string in line:
270+
if inclusive:
271+
lines.append(line.strip())
272+
break
273+
lines.append(line.strip())
274+
275+
return lines
276+
277+
278+
def run_cli_program_with_input(program_path, stdin, verbose=True):
279+
"""
280+
Run a command-line program with provided input and capture its output.
281+
282+
:param program_path: Path to the CLI executable or a list of command-line arguments.
283+
:type program_path: str | list[str]
284+
:param stdin: The input string to pass to the program via standard input.
285+
:type stdin: str
286+
:param verbose: If True, prints the program's output to stdout, defaults to True.
287+
:type verbose: bool, optional
288+
:return: The standard output produced by the program.
289+
:rtype: str
290+
"""
291+
process = subprocess.Popen(
292+
program_path,
293+
stdin=subprocess.PIPE,
294+
stdout=subprocess.PIPE,
295+
stderr=subprocess.PIPE,
296+
text=True,
297+
)
298+
299+
stdoutput, stderr = process.communicate(stdin)
300+
301+
if verbose:
302+
print(stdoutput)
303+
if stderr:
304+
print("Error output:", stderr)
305+
306+
return stdoutput

contrib/perplex/Readme.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Perple_X P-T Table Generation
2+
3+
This repository contains scripts and resources to download, install, and use **Perple_X** to generate and read a low-resolution thermodynamic table for an iron-olivine system. It can be used as a template to
4+
create and read your own tables.
5+
6+
## Contents
7+
8+
- **download_and_install_perplex.sh**
9+
A shell script to automatically download and install Perple_X in the local directory.
10+
11+
- **create_lo_res_table.py**
12+
A Python script to generate a low-resolution thermodynamic table using Perple_X. This script calls Perple_X programs and configures the run for a simplified system.
13+
14+
- **read_lo_res_table.py**
15+
A Python script to read and parse the generated low-resolution table (e.g., from `table.txt`) for further analysis or plotting.
16+
17+
- **perplex_utils.py**
18+
A python file containing useful Perple_X-related functions.
19+
20+
## Getting Started
21+
22+
1. **Install Perple_X:**
23+
24+
```bash
25+
./download_and_install_perplex.sh
26+
```
27+
28+
This will download the latest Perple_X release and compile it locally.
29+
30+
2. **Create the Thermodynamic Table:**
31+
32+
```bash
33+
python create_lo_res_table.py
34+
```
35+
36+
This will use Perple_X to generate the low-resolution table in a newly created project directory.
37+
38+
3. **Read and Analyze the Table:**
39+
40+
```bash
41+
python read_lo_res_table.py
42+
```
43+
44+
This script parses the output table for further use (e.g., plotting or machine learning workflows).
45+
46+
## Notes
47+
48+
- The resolution and thermodynamic scope in this example are intentionally simplified.
49+
- Make sure to review Perple_X license and citation guidelines when using in published work.

0 commit comments

Comments
 (0)