reaxkit 1.0.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.
- reaxkit/__init__.py +0 -0
- reaxkit/analysis/__init__.py +0 -0
- reaxkit/analysis/composed/RDF_analyzer.py +560 -0
- reaxkit/analysis/composed/__init__.py +0 -0
- reaxkit/analysis/composed/connectivity_analyzer.py +706 -0
- reaxkit/analysis/composed/coordination_analyzer.py +144 -0
- reaxkit/analysis/composed/electrostatics_analyzer.py +687 -0
- reaxkit/analysis/per_file/__init__.py +0 -0
- reaxkit/analysis/per_file/control_analyzer.py +165 -0
- reaxkit/analysis/per_file/eregime_analyzer.py +108 -0
- reaxkit/analysis/per_file/ffield_analyzer.py +305 -0
- reaxkit/analysis/per_file/fort13_analyzer.py +79 -0
- reaxkit/analysis/per_file/fort57_analyzer.py +106 -0
- reaxkit/analysis/per_file/fort73_analyzer.py +61 -0
- reaxkit/analysis/per_file/fort74_analyzer.py +65 -0
- reaxkit/analysis/per_file/fort76_analyzer.py +191 -0
- reaxkit/analysis/per_file/fort78_analyzer.py +154 -0
- reaxkit/analysis/per_file/fort79_analyzer.py +83 -0
- reaxkit/analysis/per_file/fort7_analyzer.py +393 -0
- reaxkit/analysis/per_file/fort99_analyzer.py +411 -0
- reaxkit/analysis/per_file/molfra_analyzer.py +359 -0
- reaxkit/analysis/per_file/params_analyzer.py +258 -0
- reaxkit/analysis/per_file/summary_analyzer.py +84 -0
- reaxkit/analysis/per_file/trainset_analyzer.py +84 -0
- reaxkit/analysis/per_file/vels_analyzer.py +95 -0
- reaxkit/analysis/per_file/xmolout_analyzer.py +528 -0
- reaxkit/cli.py +181 -0
- reaxkit/count_loc.py +276 -0
- reaxkit/data/alias.yaml +89 -0
- reaxkit/data/constants.yaml +27 -0
- reaxkit/data/reaxff_input_files_contents.yaml +186 -0
- reaxkit/data/reaxff_output_files_contents.yaml +301 -0
- reaxkit/data/units.yaml +38 -0
- reaxkit/help/__init__.py +0 -0
- reaxkit/help/help_index_loader.py +531 -0
- reaxkit/help/introspection_utils.py +131 -0
- reaxkit/io/__init__.py +0 -0
- reaxkit/io/base_handler.py +165 -0
- reaxkit/io/generators/__init__.py +0 -0
- reaxkit/io/generators/control_generator.py +123 -0
- reaxkit/io/generators/eregime_generator.py +341 -0
- reaxkit/io/generators/geo_generator.py +967 -0
- reaxkit/io/generators/trainset_generator.py +1758 -0
- reaxkit/io/generators/tregime_generator.py +113 -0
- reaxkit/io/generators/vregime_generator.py +164 -0
- reaxkit/io/generators/xmolout_generator.py +304 -0
- reaxkit/io/handlers/__init__.py +0 -0
- reaxkit/io/handlers/control_handler.py +209 -0
- reaxkit/io/handlers/eregime_handler.py +122 -0
- reaxkit/io/handlers/ffield_handler.py +812 -0
- reaxkit/io/handlers/fort13_handler.py +123 -0
- reaxkit/io/handlers/fort57_handler.py +143 -0
- reaxkit/io/handlers/fort73_handler.py +145 -0
- reaxkit/io/handlers/fort74_handler.py +155 -0
- reaxkit/io/handlers/fort76_handler.py +195 -0
- reaxkit/io/handlers/fort78_handler.py +142 -0
- reaxkit/io/handlers/fort79_handler.py +227 -0
- reaxkit/io/handlers/fort7_handler.py +264 -0
- reaxkit/io/handlers/fort99_handler.py +128 -0
- reaxkit/io/handlers/geo_handler.py +224 -0
- reaxkit/io/handlers/molfra_handler.py +184 -0
- reaxkit/io/handlers/params_handler.py +137 -0
- reaxkit/io/handlers/summary_handler.py +135 -0
- reaxkit/io/handlers/trainset_handler.py +658 -0
- reaxkit/io/handlers/vels_handler.py +293 -0
- reaxkit/io/handlers/xmolout_handler.py +174 -0
- reaxkit/utils/__init__.py +0 -0
- reaxkit/utils/alias.py +219 -0
- reaxkit/utils/cache.py +77 -0
- reaxkit/utils/constants.py +75 -0
- reaxkit/utils/equation_of_states.py +96 -0
- reaxkit/utils/exceptions.py +27 -0
- reaxkit/utils/frame_utils.py +175 -0
- reaxkit/utils/log.py +43 -0
- reaxkit/utils/media/__init__.py +0 -0
- reaxkit/utils/media/convert.py +90 -0
- reaxkit/utils/media/make_video.py +91 -0
- reaxkit/utils/media/plotter.py +812 -0
- reaxkit/utils/numerical/__init__.py +0 -0
- reaxkit/utils/numerical/extrema_finder.py +96 -0
- reaxkit/utils/numerical/moving_average.py +103 -0
- reaxkit/utils/numerical/numerical_calcs.py +75 -0
- reaxkit/utils/numerical/signal_ops.py +135 -0
- reaxkit/utils/path.py +55 -0
- reaxkit/utils/units.py +104 -0
- reaxkit/webui/__init__.py +0 -0
- reaxkit/webui/app.py +0 -0
- reaxkit/webui/components.py +0 -0
- reaxkit/webui/layouts.py +0 -0
- reaxkit/webui/utils.py +0 -0
- reaxkit/workflows/__init__.py +0 -0
- reaxkit/workflows/composed/__init__.py +0 -0
- reaxkit/workflows/composed/coordination_workflow.py +393 -0
- reaxkit/workflows/composed/electrostatics_workflow.py +587 -0
- reaxkit/workflows/composed/xmolout_fort7_workflow.py +343 -0
- reaxkit/workflows/meta/__init__.py +0 -0
- reaxkit/workflows/meta/help_workflow.py +136 -0
- reaxkit/workflows/meta/introspection_workflow.py +235 -0
- reaxkit/workflows/meta/make_video_workflow.py +61 -0
- reaxkit/workflows/meta/plotter_workflow.py +601 -0
- reaxkit/workflows/per_file/__init__.py +0 -0
- reaxkit/workflows/per_file/control_workflow.py +110 -0
- reaxkit/workflows/per_file/eregime_workflow.py +267 -0
- reaxkit/workflows/per_file/ffield_workflow.py +390 -0
- reaxkit/workflows/per_file/fort13_workflow.py +86 -0
- reaxkit/workflows/per_file/fort57_workflow.py +137 -0
- reaxkit/workflows/per_file/fort73_workflow.py +151 -0
- reaxkit/workflows/per_file/fort74_workflow.py +88 -0
- reaxkit/workflows/per_file/fort76_workflow.py +188 -0
- reaxkit/workflows/per_file/fort78_workflow.py +135 -0
- reaxkit/workflows/per_file/fort79_workflow.py +314 -0
- reaxkit/workflows/per_file/fort7_workflow.py +592 -0
- reaxkit/workflows/per_file/fort83_workflow.py +60 -0
- reaxkit/workflows/per_file/fort99_workflow.py +223 -0
- reaxkit/workflows/per_file/geo_workflow.py +554 -0
- reaxkit/workflows/per_file/molfra_workflow.py +577 -0
- reaxkit/workflows/per_file/params_workflow.py +135 -0
- reaxkit/workflows/per_file/summary_workflow.py +161 -0
- reaxkit/workflows/per_file/trainset_workflow.py +356 -0
- reaxkit/workflows/per_file/tregime_workflow.py +79 -0
- reaxkit/workflows/per_file/vels_workflow.py +309 -0
- reaxkit/workflows/per_file/vregime_workflow.py +75 -0
- reaxkit/workflows/per_file/xmolout_workflow.py +678 -0
- reaxkit-1.0.0.dist-info/METADATA +128 -0
- reaxkit-1.0.0.dist-info/RECORD +130 -0
- reaxkit-1.0.0.dist-info/WHEEL +5 -0
- reaxkit-1.0.0.dist-info/entry_points.txt +2 -0
- reaxkit-1.0.0.dist-info/licenses/AUTHORS.md +20 -0
- reaxkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- reaxkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fort.99 (training-set and EOS) analysis utilities.
|
|
3
|
+
|
|
4
|
+
This module provides helpers for analyzing ReaxFF training-set output
|
|
5
|
+
stored in ``fort.99`` files, including error inspection, two-body ENERGY
|
|
6
|
+
term parsing, and equation-of-state (EOS) analysis for bulk modulus
|
|
7
|
+
extraction.
|
|
8
|
+
|
|
9
|
+
Typical use cases include:
|
|
10
|
+
|
|
11
|
+
- inspecting QM–FF energy differences for training targets
|
|
12
|
+
- parsing pairwise ENERGY terms from fort.99 titles
|
|
13
|
+
- constructing energy–volume datasets using fort.74
|
|
14
|
+
- computing bulk modulus via a Vinet equation-of-state fit
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
import pandas as pd
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from reaxkit.io.base_handler import BaseHandler
|
|
23
|
+
from reaxkit.utils.constants import const
|
|
24
|
+
from reaxkit.utils.equation_of_states import vinet_energy_ev
|
|
25
|
+
|
|
26
|
+
def get_fort99_data(
|
|
27
|
+
handler: BaseHandler,
|
|
28
|
+
*,
|
|
29
|
+
sortby: str = "lineno",
|
|
30
|
+
ascending: bool = True
|
|
31
|
+
) -> pd.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
Retrieve fort.99 data and compute QM–FF energy differences.
|
|
34
|
+
|
|
35
|
+
A new column ``qm_ff_difference`` is added as:
|
|
36
|
+
``qm_value - ffield_value``.
|
|
37
|
+
|
|
38
|
+
Works on
|
|
39
|
+
--------
|
|
40
|
+
Fort99Handler — ``fort.99``
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
handler : TemplateHandler
|
|
45
|
+
Parsed ``fort.99`` handler.
|
|
46
|
+
sortby : str, default="lineno"
|
|
47
|
+
Column name to sort by (e.g. ``error``, ``lineno``).
|
|
48
|
+
ascending : bool, default=True
|
|
49
|
+
Sort order.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
pandas.DataFrame
|
|
54
|
+
fort.99 table including the additional ``qm_ff_difference`` column.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> df = get_fort99_data(h, sortby="error", ascending=False)
|
|
59
|
+
"""
|
|
60
|
+
df = handler.dataframe().copy()
|
|
61
|
+
|
|
62
|
+
# Add qm_ff_difference column
|
|
63
|
+
df["qm_ff_difference"] = df["qm_value"] - df["ffield_value"]
|
|
64
|
+
|
|
65
|
+
# Validate requested sort column
|
|
66
|
+
if sortby not in df.columns:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Invalid sort key: '{sortby}'. "
|
|
69
|
+
f"Available columns: {list(df.columns)}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Sort based on user choice
|
|
73
|
+
df = df.sort_values(sortby, ascending=ascending)
|
|
74
|
+
|
|
75
|
+
return df
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------------------------------------------------------
|
|
78
|
+
# getting the EOS data (i.e., rows where section = ENERGY and have 2 identifiers in their title
|
|
79
|
+
# --------------------------------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def parse_fort99_two_body_energy_terms(handler: BaseHandler) -> pd.DataFrame:
|
|
82
|
+
"""
|
|
83
|
+
Parse pairwise ENERGY terms from the ``ENERGY`` section of fort.99.
|
|
84
|
+
|
|
85
|
+
Only ENERGY titles containing exactly two ``/`` separators are kept,
|
|
86
|
+
corresponding to two-body interaction terms.
|
|
87
|
+
|
|
88
|
+
Works on
|
|
89
|
+
--------
|
|
90
|
+
Fort99Handler — ``fort.99``
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
handler : TemplateHandler
|
|
95
|
+
Parsed ``fort.99`` handler.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
pandas.DataFrame
|
|
100
|
+
ENERGY rows augmented with parsed columns:
|
|
101
|
+
``opt1``, ``iden1``, ``n1``, ``opt2``, ``iden2``, ``n2``.
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
>>> df = parse_fort99_two_body_energy_terms(h)
|
|
106
|
+
"""
|
|
107
|
+
import re
|
|
108
|
+
import numpy as np
|
|
109
|
+
import pandas as pd
|
|
110
|
+
|
|
111
|
+
df = handler.dataframe().copy()
|
|
112
|
+
|
|
113
|
+
# 1) Basic sanity check
|
|
114
|
+
if "section" not in df.columns or "title" not in df.columns:
|
|
115
|
+
raise KeyError("Expected 'section' and 'title' columns in fort.99 DataFrame.")
|
|
116
|
+
|
|
117
|
+
# 2) Restrict to ENERGY section (case-insensitive)
|
|
118
|
+
energy_df = df[df["section"].astype(str).str.upper() == "ENERGY"].copy()
|
|
119
|
+
|
|
120
|
+
# 3) Keep rows with *at least* 2 "/" (some triple-body lines may be truncated)
|
|
121
|
+
energy_df = energy_df[energy_df["title"].astype(str).str.count("/") == 2].copy()
|
|
122
|
+
|
|
123
|
+
# Regex:
|
|
124
|
+
# Energy +Zn_h2o-1_P2/1.00 -Zn_oh-1_P1/1.00 ...
|
|
125
|
+
#
|
|
126
|
+
# - allow any non-"/" chars in identifiers: [^/]+?
|
|
127
|
+
# - allow ints or floats for n1, n2: \d+(?:\.\d+)?
|
|
128
|
+
# - ignore case on "Energy"
|
|
129
|
+
pattern = re.compile(
|
|
130
|
+
r"^Energy\s+"
|
|
131
|
+
r"(?P<sign1>[+-])(?P<iden1>[^/]+?)\s*/\s*(?P<n1>\d+(?:\.\d+)?)\s+"
|
|
132
|
+
r"(?P<sign2>[+-])(?P<iden2>[^/]+?)\s*/\s*(?P<n2>\d+(?:\.\d+)?)",
|
|
133
|
+
flags=re.IGNORECASE,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _parse_title(title: str) -> dict:
|
|
137
|
+
"""Return parsed components or NaNs if it doesn't match."""
|
|
138
|
+
m = pattern.search(title)
|
|
139
|
+
if not m:
|
|
140
|
+
# Return NaNs so we can drop these rows later instead of raising
|
|
141
|
+
return {
|
|
142
|
+
"opt1": np.nan,
|
|
143
|
+
"iden1": np.nan,
|
|
144
|
+
"n1": np.nan,
|
|
145
|
+
"opt2": np.nan,
|
|
146
|
+
"iden2": np.nan,
|
|
147
|
+
"n2": np.nan,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
g = m.groupdict()
|
|
151
|
+
return {
|
|
152
|
+
"opt1": 1 if g["sign1"] == "+" else -1,
|
|
153
|
+
"iden1": g["iden1"].strip(),
|
|
154
|
+
"n1": float(g["n1"]),
|
|
155
|
+
"opt2": 1 if g["sign2"] == "+" else -1,
|
|
156
|
+
"iden2": g["iden2"].strip(),
|
|
157
|
+
"n2": float(g["n2"]),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Apply parser row-wise
|
|
161
|
+
parsed = energy_df["title"].astype(str).apply(_parse_title)
|
|
162
|
+
parsed_df = pd.DataFrame(list(parsed)) # list-of-dicts -> DataFrame
|
|
163
|
+
|
|
164
|
+
# Merge parsed columns into energy_df
|
|
165
|
+
energy_df = pd.concat([energy_df.reset_index(drop=True), parsed_df], axis=1)
|
|
166
|
+
|
|
167
|
+
# Drop rows where parsing failed (NaNs in iden1/iden2)
|
|
168
|
+
energy_df = energy_df.dropna(subset=["iden1", "iden2"])
|
|
169
|
+
|
|
170
|
+
# (Optional) if you no longer need the raw 'title' column, remove it:
|
|
171
|
+
# energy_df = energy_df.drop(columns=["title"])
|
|
172
|
+
|
|
173
|
+
return energy_df
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def fort99_energy_vs_volume(
|
|
177
|
+
fort99_handler: BaseHandler,
|
|
178
|
+
fort74_handler: BaseHandler,
|
|
179
|
+
) -> pd.DataFrame:
|
|
180
|
+
"""
|
|
181
|
+
Construct an energy–volume table from fort.99 and fort.74 data.
|
|
182
|
+
|
|
183
|
+
Pairwise ENERGY terms from fort.99 are matched with corresponding
|
|
184
|
+
volumes extracted from fort.74 based on shared identifiers.
|
|
185
|
+
|
|
186
|
+
Works on
|
|
187
|
+
--------
|
|
188
|
+
Fort99Handler + Fort74Handler — ``fort.99`` + ``fort.74``
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
fort99_handler : TemplateHandler
|
|
193
|
+
Parsed ``fort.99`` handler.
|
|
194
|
+
fort74_handler : TemplateHandler
|
|
195
|
+
Parsed ``fort.74`` handler providing volumes.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
pandas.DataFrame
|
|
200
|
+
Table with columns:
|
|
201
|
+
``iden1``, ``iden2``, ``ffield_value``, ``qm_value``, ``V_iden2``.
|
|
202
|
+
|
|
203
|
+
Examples
|
|
204
|
+
--------
|
|
205
|
+
>>> df = fort99_energy_vs_volume(f99, f74)
|
|
206
|
+
"""
|
|
207
|
+
from reaxkit.analysis.per_file import fort74_analyzer
|
|
208
|
+
|
|
209
|
+
# 1) Build ENERGY section with parsed two-body terms
|
|
210
|
+
energy_df = parse_fort99_two_body_energy_terms(fort99_handler)
|
|
211
|
+
|
|
212
|
+
if energy_df.empty:
|
|
213
|
+
return pd.DataFrame(
|
|
214
|
+
columns=["iden1", "iden2", "ffield_value", "qm_value", "V_iden2"]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 2) Keep only iden1 values appearing more than once
|
|
218
|
+
repeated = energy_df.groupby("iden1").filter(lambda g: len(g) > 1).copy()
|
|
219
|
+
|
|
220
|
+
if repeated.empty:
|
|
221
|
+
return pd.DataFrame(
|
|
222
|
+
columns=["iden1", "iden2", "ffield_value", "qm_value", "V_iden2"]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 🔥 FIX: remove rows where iden1 == iden2
|
|
226
|
+
repeated = repeated[repeated["iden1"] != repeated["iden2"]]
|
|
227
|
+
|
|
228
|
+
if repeated.empty:
|
|
229
|
+
return pd.DataFrame(
|
|
230
|
+
columns=["iden1", "iden2", "ffield_value", "qm_value", "V_iden2"]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# 3) Load fort.74 data and extract identifier → volume
|
|
234
|
+
fort74_df = fort74_analyzer.get_fort74_data(fort74_handler)
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
fort74_df.empty
|
|
238
|
+
or "identifier" not in fort74_df.columns
|
|
239
|
+
or "V" not in fort74_df.columns
|
|
240
|
+
):
|
|
241
|
+
return pd.DataFrame(
|
|
242
|
+
columns=["iden1", "iden2", "ffield_value", "qm_value", "V_iden2"]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
vol_df = fort74_df[["identifier", "V"]].drop_duplicates()
|
|
246
|
+
|
|
247
|
+
# 4) Attach volume of iden2
|
|
248
|
+
merged = repeated.merge(
|
|
249
|
+
vol_df,
|
|
250
|
+
left_on="iden2",
|
|
251
|
+
right_on="identifier",
|
|
252
|
+
how="left",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# 5) Final output
|
|
256
|
+
out = merged[["iden1", "iden2", "ffield_value", "qm_value", "V"]].rename(
|
|
257
|
+
columns={"V": "V_iden2"}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return out.sort_values(["iden1", "iden2"]).reset_index(drop=True)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
#################################################################################
|
|
264
|
+
# Finding the bulk modulus of a system using Rose–Vinet equation of state
|
|
265
|
+
#################################################################################
|
|
266
|
+
|
|
267
|
+
def get_fort99_bulk_modulus(
|
|
268
|
+
fort99_handler,
|
|
269
|
+
fort74_handler,
|
|
270
|
+
*,
|
|
271
|
+
iden: str,
|
|
272
|
+
source: str = "ffield", # "ffield" or "qm" which defines which source of energy data should be used
|
|
273
|
+
shift_min_to_zero: bool = True, # helps conditioning; doesn't change K0
|
|
274
|
+
flip_sign: bool = False,
|
|
275
|
+
dropna: bool = True,
|
|
276
|
+
) -> dict:
|
|
277
|
+
"""
|
|
278
|
+
Compute the bulk modulus using a Vinet EOS fit to E(V) data.
|
|
279
|
+
|
|
280
|
+
Energy–volume points are obtained from pairwise ENERGY terms in
|
|
281
|
+
``fort.99`` and corresponding volumes from ``fort.74``.
|
|
282
|
+
|
|
283
|
+
Works on
|
|
284
|
+
--------
|
|
285
|
+
Fort99Handler + Fort74Handler — ``fort.99`` + ``fort.74``
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
fort99_handler : TemplateHandler
|
|
290
|
+
Parsed ``fort.99`` handler.
|
|
291
|
+
fort74_handler : TemplateHandler
|
|
292
|
+
Parsed ``fort.74`` handler.
|
|
293
|
+
iden : str
|
|
294
|
+
Identifier (``iden1``) for which the EOS fit is performed.
|
|
295
|
+
source : {"ffield", "qm"}, default="ffield"
|
|
296
|
+
Energy source used for fitting.
|
|
297
|
+
shift_min_to_zero : bool, default=True
|
|
298
|
+
Shift minimum energy to zero to improve numerical conditioning.
|
|
299
|
+
flip_sign : bool, default=False
|
|
300
|
+
Flip the sign of energies before fitting.
|
|
301
|
+
dropna : bool, default=True
|
|
302
|
+
Drop rows with NaN energy or volume values.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
dict
|
|
307
|
+
Dictionary containing:
|
|
308
|
+
``iden``, ``source``, ``n_points``, ``V0_A3``, ``K0_eV_A3``,
|
|
309
|
+
``K0_GPa``, ``E0_eV``, ``C``, ``success``.
|
|
310
|
+
|
|
311
|
+
Examples
|
|
312
|
+
--------
|
|
313
|
+
>>> res = get_fort99_bulk_modulus(
|
|
314
|
+
... f99, f74, iden="bulk_0", source="ffield"
|
|
315
|
+
... )
|
|
316
|
+
>>> res["K0_GPa"]
|
|
317
|
+
"""
|
|
318
|
+
from scipy.optimize import curve_fit # local import to avoid overload
|
|
319
|
+
|
|
320
|
+
df = fort99_energy_vs_volume(
|
|
321
|
+
fort99_handler=fort99_handler,
|
|
322
|
+
fort74_handler=fort74_handler,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if df.empty:
|
|
326
|
+
raise ValueError("No ENERGY vs volume data found (fort99_energy_vs_volume returned empty).")
|
|
327
|
+
|
|
328
|
+
g = df[df["iden1"] == iden].copy()
|
|
329
|
+
if g.empty:
|
|
330
|
+
raise ValueError(f"No rows found for iden1 == {iden!r}.")
|
|
331
|
+
|
|
332
|
+
# Choose energy column
|
|
333
|
+
src = (source or "").strip().lower()
|
|
334
|
+
if src in {"ffield", "ff", "forcefield", "force-field"}:
|
|
335
|
+
e_col = "ffield_value"
|
|
336
|
+
src_name = "ffield"
|
|
337
|
+
elif src in {"qm", "dft", "reference"}:
|
|
338
|
+
e_col = "qm_value"
|
|
339
|
+
src_name = "qm"
|
|
340
|
+
else:
|
|
341
|
+
raise ValueError("source must be one of {'ffield','qm'}.")
|
|
342
|
+
|
|
343
|
+
# Pull V and E
|
|
344
|
+
V = g["V_iden2"].to_numpy(dtype=float)
|
|
345
|
+
E_kcal = g[e_col].to_numpy(dtype=float)
|
|
346
|
+
|
|
347
|
+
if dropna:
|
|
348
|
+
m = np.isfinite(V) & np.isfinite(E_kcal)
|
|
349
|
+
V = V[m]
|
|
350
|
+
E_kcal = E_kcal[m]
|
|
351
|
+
|
|
352
|
+
if len(V) < 6:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Need at least ~6 E(V) points for a stable EOS fit; got {len(V)} for iden={iden!r}."
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Sort by V for stability
|
|
358
|
+
order = np.argsort(V)
|
|
359
|
+
V = V[order]
|
|
360
|
+
E_kcal = E_kcal[order]
|
|
361
|
+
|
|
362
|
+
# Optional sign flip (kept for parity with plotting)
|
|
363
|
+
if flip_sign:
|
|
364
|
+
E_kcal = -E_kcal
|
|
365
|
+
|
|
366
|
+
# Convert kcal/mol -> eV
|
|
367
|
+
E = E_kcal * const("energy_kcalmol_to_eV")
|
|
368
|
+
|
|
369
|
+
# Optional energy shift (doesn't affect K0)
|
|
370
|
+
if shift_min_to_zero:
|
|
371
|
+
E = E - np.nanmin(E)
|
|
372
|
+
|
|
373
|
+
# Initial guesses
|
|
374
|
+
V0_guess = float(V[np.nanargmin(E)])
|
|
375
|
+
E0_guess = float(np.nanmin(E))
|
|
376
|
+
K0_guess = 0.5 / const("eV_per_A3_to_GPa")
|
|
377
|
+
C_guess = 4.0
|
|
378
|
+
|
|
379
|
+
p0 = [E0_guess, K0_guess, V0_guess, C_guess]
|
|
380
|
+
|
|
381
|
+
# Basic bounds to prevent nonsense fits
|
|
382
|
+
# K0 > 0, V0 > 0, C > 0
|
|
383
|
+
bounds = (
|
|
384
|
+
[-np.inf, 1e-12, 1e-9, 1e-6],
|
|
385
|
+
[ np.inf, 1e3, np.inf, 1e3],
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
popt, pcov = curve_fit(
|
|
389
|
+
vinet_energy_ev,
|
|
390
|
+
V,
|
|
391
|
+
E,
|
|
392
|
+
p0=p0,
|
|
393
|
+
bounds=bounds,
|
|
394
|
+
maxfev=20000,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
E0_fit, K0_fit, V0_fit, C_fit = popt
|
|
398
|
+
K0_GPa = float(K0_fit * const("eV_per_A3_to_GPa"))
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
"iden": iden,
|
|
402
|
+
"source": src_name,
|
|
403
|
+
"n_points": int(len(V)),
|
|
404
|
+
"V0_A3": float(V0_fit),
|
|
405
|
+
"K0_eV_A3": float(K0_fit),
|
|
406
|
+
"K0_GPa": K0_GPa,
|
|
407
|
+
"E0_eV": float(E0_fit),
|
|
408
|
+
"C": float(C_fit),
|
|
409
|
+
"success": True,
|
|
410
|
+
}
|
|
411
|
+
|