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,812 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReaxFF force-field parameter (ffield) handler.
|
|
3
|
+
|
|
4
|
+
This module provides a handler for parsing ReaxFF ``ffield`` files,
|
|
5
|
+
which define all force-field parameters used in ReaxFF simulations.
|
|
6
|
+
|
|
7
|
+
Unlike most handlers, ``ffield`` data is inherently sectional rather
|
|
8
|
+
than tabular and is therefore exposed through per-section tables
|
|
9
|
+
instead of a single summary DataFrame.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Tuple, Any, Optional
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from reaxkit.io.base_handler import BaseHandler
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FFieldHandler(BaseHandler):
|
|
23
|
+
"""
|
|
24
|
+
Parser for ReaxFF force-field parameter files (``ffield``).
|
|
25
|
+
|
|
26
|
+
This class parses ReaxFF ``ffield`` files and exposes all force-field
|
|
27
|
+
parameters as structured, section-specific tables suitable for
|
|
28
|
+
inspection, modification, and analysis.
|
|
29
|
+
|
|
30
|
+
Parsed Data
|
|
31
|
+
-----------
|
|
32
|
+
Summary table
|
|
33
|
+
The main ``dataframe()`` is intentionally empty for ``ffield`` files.
|
|
34
|
+
All meaningful data is stored in per-section DataFrames.
|
|
35
|
+
|
|
36
|
+
Section tables
|
|
37
|
+
Accessible via ``sections`` or ``section_df(name)``, with one table
|
|
38
|
+
per force-field section:
|
|
39
|
+
|
|
40
|
+
- ``general``:
|
|
41
|
+
Global ReaxFF parameters (39 fixed parameters), columns:
|
|
42
|
+
["index", "name", "value", "raw_comment"]
|
|
43
|
+
|
|
44
|
+
- ``atom``:
|
|
45
|
+
Atom-type parameters, indexed by atom number, with columns:
|
|
46
|
+
["symbol", <atom parameter names>]
|
|
47
|
+
|
|
48
|
+
- ``bond``:
|
|
49
|
+
Bond parameters, indexed by bond index, with columns:
|
|
50
|
+
["i", "j", <bond parameter names>]
|
|
51
|
+
|
|
52
|
+
- ``off_diagonal``:
|
|
53
|
+
Off-diagonal interaction parameters, indexed by entry number, with columns:
|
|
54
|
+
["i", "j", <off-diagonal parameter names>]
|
|
55
|
+
|
|
56
|
+
- ``angle``:
|
|
57
|
+
Angle parameters, indexed by angle index, with columns:
|
|
58
|
+
["i", "j", "k", <angle parameter names>]
|
|
59
|
+
|
|
60
|
+
- ``torsion``:
|
|
61
|
+
Torsion parameters, indexed by torsion index, with columns:
|
|
62
|
+
["i", "j", "k", "l", <torsion parameter names>]
|
|
63
|
+
|
|
64
|
+
- ``hbond``:
|
|
65
|
+
Hydrogen-bond parameters, indexed by hbond index, with columns:
|
|
66
|
+
["i", "j", "k", <hbond parameter names>]
|
|
67
|
+
|
|
68
|
+
Metadata
|
|
69
|
+
Returned by ``metadata()``, containing counts per section:
|
|
70
|
+
["n_general_params", "n_atoms", "n_bonds", "n_off_diagonal",
|
|
71
|
+
"n_angles", "n_torsions", "n_hbonds"]
|
|
72
|
+
|
|
73
|
+
Notes
|
|
74
|
+
-----
|
|
75
|
+
- Parameter names follow canonical ReaxFF ordering and numbering.
|
|
76
|
+
- Unused parameters are labeled ``n.u.`` with numeric suffixes.
|
|
77
|
+
- Inline comments in the original file are preserved where applicable.
|
|
78
|
+
- Section headers and ordering are detected automatically.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
SECTION_GENERAL = "general"
|
|
82
|
+
SECTION_ATOM = "atom"
|
|
83
|
+
SECTION_BOND = "bond"
|
|
84
|
+
SECTION_OFF_DIAGONAL = "off_diagonal"
|
|
85
|
+
SECTION_ANGLE = "angle"
|
|
86
|
+
SECTION_TORSION = "torsion"
|
|
87
|
+
SECTION_HBOND = "hbond"
|
|
88
|
+
|
|
89
|
+
# ---------------- General parameter names --------------------
|
|
90
|
+
# Fixed order, 39 parameters. "Not used" ones are tagged by
|
|
91
|
+
# their 1-based line number in the general section.
|
|
92
|
+
_GENERAL_PARAM_NAMES: List[str] = [
|
|
93
|
+
"overcoord_1", # 1
|
|
94
|
+
"overcoord_2", # 2
|
|
95
|
+
"valency_angle_conj_1", # 3
|
|
96
|
+
"triple_bond_stab_1", # 4
|
|
97
|
+
"triple_bond_stab_2", # 5
|
|
98
|
+
"not_used_line_num_6", # 6
|
|
99
|
+
"undercoord_1", # 7
|
|
100
|
+
"triple_bond_stab_3", # 8
|
|
101
|
+
"undercoord_2", # 9
|
|
102
|
+
"undercoord_3", # 10
|
|
103
|
+
"triple_bond_stab_energy", # 11
|
|
104
|
+
"taper_radius_lower", # 12
|
|
105
|
+
"taper_radius_upper", # 13
|
|
106
|
+
"not_used_line_num_14", # 14
|
|
107
|
+
"valency_undercoord", # 15
|
|
108
|
+
"valency_angle_lonepair", # 16
|
|
109
|
+
"valency_angle", # 17
|
|
110
|
+
"valency_angle_param", # 18
|
|
111
|
+
"not_used_line_num_19", # 19
|
|
112
|
+
"double_bond_angle", # 20
|
|
113
|
+
"double_bond_angle_overcoord_1", # 21
|
|
114
|
+
"double_bond_angle_overcoord_2", # 22
|
|
115
|
+
"not_used_line_num_23", # 23
|
|
116
|
+
"torsion_bo", # 24
|
|
117
|
+
"torsion_overcoord_1", # 25
|
|
118
|
+
"torsion_overcoord_2", # 26
|
|
119
|
+
"conj_0_not_used", # 27
|
|
120
|
+
"conj", # 28
|
|
121
|
+
"vdw_shielding", # 29
|
|
122
|
+
"bo_cutoff_scaled", # 30
|
|
123
|
+
"valency_angle_conj_2", # 31
|
|
124
|
+
"overcoord_3", # 32
|
|
125
|
+
"overcoord_4", # 33
|
|
126
|
+
"valency_lonepair", # 34
|
|
127
|
+
"not_used_line_num_35", # 35
|
|
128
|
+
"not_used_line_num_36", # 36
|
|
129
|
+
"molecular_energy_1_not_used", # 37
|
|
130
|
+
"molecular_energy_2_not_used", # 38
|
|
131
|
+
"valency_angle_conj_3", # 39
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# ---------------- parameter name file_templates --------------------
|
|
135
|
+
# Atom: 4 × 8 parameters
|
|
136
|
+
_ATOM_PARAM_NAMES_BASE: List[str] = [
|
|
137
|
+
# line 1
|
|
138
|
+
"cov.r", "valency", "a.m", "Rvdw", "Evdw", "gammaEEM", "cov.r2", "#el",
|
|
139
|
+
# line 2
|
|
140
|
+
"alfa", "gammavdW", "valency(2)", "Eunder", "n.u.", "chiEEM", "etaEEM", "n.u.",
|
|
141
|
+
# line 3
|
|
142
|
+
"cov r3", "Elp", "Heat inc.", "13BO1", "13BO2", "13BO3", "n.u.", "n.u.",
|
|
143
|
+
# line 4
|
|
144
|
+
"ov/un", "val1", "n.u.", "val3", "vval4", "n.u.", "n.u.", "n.u.",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# Bond: 2 × 8 parameters
|
|
148
|
+
_BOND_PARAM_NAMES_BASE: List[str] = [
|
|
149
|
+
# line 1
|
|
150
|
+
"Edis1", "Edis2", "Edis3", "pbe1", "pbo5", "13corr", "pbo6", "kov",
|
|
151
|
+
# line 2
|
|
152
|
+
"pbe2", "pbo3", "pbo4", "n.u.", "pbo1", "pbo2", "ovcorr", "n.u.",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Off-diagonal
|
|
156
|
+
_OFF_DIAGONAL_PARAM_NAMES: List[str] = [
|
|
157
|
+
"Evdw", "Rvdw", "alfa", "cov.r", "cov.r2", "cov.r3",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Angle
|
|
161
|
+
_ANGLE_PARAM_NAMES: List[str] = [
|
|
162
|
+
"Theta0", "ka", "kb", "pconj", "pv2", "kpenal", "pv3",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
# Torsion
|
|
166
|
+
_TORSION_PARAM_NAMES_BASE: List[str] = [
|
|
167
|
+
"V1", "V2", "V3", "V2(BO)", "vconj", "n.u.", "n.u.",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# H-bond
|
|
171
|
+
_HBOND_PARAM_NAMES: List[str] = [
|
|
172
|
+
"Rhb", "Dehb", "vhb1", "vhb2",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
# ---------------- init / public API ---------------------------
|
|
176
|
+
def __init__(self, file_path: str | Path = "ffield") -> None:
|
|
177
|
+
super().__init__(file_path)
|
|
178
|
+
self._sections: Dict[str, pd.DataFrame] = {}
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def sections(self) -> Dict[str, pd.DataFrame]:
|
|
182
|
+
if not self._parsed:
|
|
183
|
+
self.parse()
|
|
184
|
+
return self._sections
|
|
185
|
+
|
|
186
|
+
def section_df(self, name: str) -> pd.DataFrame:
|
|
187
|
+
if not self._parsed:
|
|
188
|
+
self.parse()
|
|
189
|
+
return self._sections[name]
|
|
190
|
+
|
|
191
|
+
# ---------------- core parsing -------------------------------
|
|
192
|
+
def _parse(self) -> Tuple[pd.DataFrame, Dict[str, Any]]:
|
|
193
|
+
path = self.path
|
|
194
|
+
lines = path.read_text().splitlines()
|
|
195
|
+
|
|
196
|
+
meta: Dict[str, Any] = {}
|
|
197
|
+
|
|
198
|
+
# description: first non-empty line
|
|
199
|
+
for ln in lines:
|
|
200
|
+
if ln.strip():
|
|
201
|
+
meta["description"] = ln.strip()
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
sections: Dict[str, pd.DataFrame] = {}
|
|
205
|
+
|
|
206
|
+
i = 0
|
|
207
|
+
n_lines = len(lines)
|
|
208
|
+
|
|
209
|
+
while i < n_lines:
|
|
210
|
+
line = lines[i]
|
|
211
|
+
lower = line.lower()
|
|
212
|
+
|
|
213
|
+
if not line.strip():
|
|
214
|
+
i += 1
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# --- detect section headers ---
|
|
218
|
+
if "general" in lower and "parameter" in lower:
|
|
219
|
+
n = self._first_int_in_line(line)
|
|
220
|
+
if n is None:
|
|
221
|
+
i += 1
|
|
222
|
+
continue
|
|
223
|
+
general_df, i = self._parse_general_section(lines, i + 1, n)
|
|
224
|
+
sections[self.SECTION_GENERAL] = general_df
|
|
225
|
+
meta["n_general_params"] = len(general_df)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if "atom" in lower:
|
|
229
|
+
n = self._first_int_in_line(line)
|
|
230
|
+
if n is None:
|
|
231
|
+
i += 1
|
|
232
|
+
continue
|
|
233
|
+
atom_df, i = self._parse_atom_section(lines, i + 1, n)
|
|
234
|
+
sections[self.SECTION_ATOM] = atom_df
|
|
235
|
+
meta["n_atoms"] = len(atom_df)
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if "bond" in lower and "off" not in lower and "hydrogen" not in lower:
|
|
239
|
+
n = self._first_int_in_line(line)
|
|
240
|
+
if n is None:
|
|
241
|
+
i += 1
|
|
242
|
+
continue
|
|
243
|
+
bond_df, i = self._parse_bond_section(lines, i + 1, n)
|
|
244
|
+
sections[self.SECTION_BOND] = bond_df
|
|
245
|
+
meta["n_bonds"] = len(bond_df)
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if "off-diagonal" in lower or "off diagonal" in lower:
|
|
249
|
+
n = self._first_int_in_line(line)
|
|
250
|
+
if n is None:
|
|
251
|
+
i += 1
|
|
252
|
+
continue
|
|
253
|
+
off_df, i = self._parse_off_diagonal_section(lines, i + 1, n)
|
|
254
|
+
sections[self.SECTION_OFF_DIAGONAL] = off_df
|
|
255
|
+
meta["n_off_diagonal"] = len(off_df)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if "angle" in lower:
|
|
259
|
+
n = self._first_int_in_line(line)
|
|
260
|
+
if n is None:
|
|
261
|
+
i += 1
|
|
262
|
+
continue
|
|
263
|
+
angle_df, i = self._parse_angle_section(lines, i + 1, n)
|
|
264
|
+
sections[self.SECTION_ANGLE] = angle_df
|
|
265
|
+
meta["n_angles"] = len(angle_df)
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
if "torsion" in lower:
|
|
269
|
+
n = self._first_int_in_line(line)
|
|
270
|
+
if n is None:
|
|
271
|
+
i += 1
|
|
272
|
+
continue
|
|
273
|
+
torsion_df, i = self._parse_torsion_section(lines, i + 1, n)
|
|
274
|
+
sections[self.SECTION_TORSION] = torsion_df
|
|
275
|
+
meta["n_torsions"] = len(torsion_df)
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
if "hydrogen" in lower and "bond" in lower:
|
|
279
|
+
n = self._first_int_in_line(line)
|
|
280
|
+
if n is None:
|
|
281
|
+
i += 1
|
|
282
|
+
continue
|
|
283
|
+
hbond_df, i = self._parse_hbond_section(lines, i + 1, n)
|
|
284
|
+
sections[self.SECTION_HBOND] = hbond_df
|
|
285
|
+
meta["n_hbonds"] = len(hbond_df)
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
i += 1
|
|
289
|
+
|
|
290
|
+
self._sections = sections
|
|
291
|
+
|
|
292
|
+
# Summary DataFrame for ffield is intentionally empty
|
|
293
|
+
df = pd.DataFrame()
|
|
294
|
+
return df, meta
|
|
295
|
+
|
|
296
|
+
# ---------------- section parsers ----------------------------
|
|
297
|
+
def _parse_general_section(
|
|
298
|
+
self, lines: List[str], start: int, n_params: int
|
|
299
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
300
|
+
"""Parse General parameters using fixed names, not inline comments."""
|
|
301
|
+
expected = len(self._GENERAL_PARAM_NAMES)
|
|
302
|
+
if n_params == expected:
|
|
303
|
+
print("[FFieldHandler Check] Number of general parameters is 39 (expected).")
|
|
304
|
+
else:
|
|
305
|
+
print(
|
|
306
|
+
f"[FFieldHandler Check] WARNING: expected {expected} general parameters, "
|
|
307
|
+
f"but header says {n_params}."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
records: List[Dict[str, Any]] = []
|
|
311
|
+
|
|
312
|
+
for idx in range(n_params):
|
|
313
|
+
if start + idx >= len(lines):
|
|
314
|
+
break
|
|
315
|
+
raw_line = lines[start + idx]
|
|
316
|
+
|
|
317
|
+
if "!" in raw_line:
|
|
318
|
+
left, comment = raw_line.split("!", 1)
|
|
319
|
+
raw_comment = comment.strip()
|
|
320
|
+
else:
|
|
321
|
+
left = raw_line
|
|
322
|
+
raw_comment = ""
|
|
323
|
+
|
|
324
|
+
tokens = left.split()
|
|
325
|
+
value = float(tokens[0]) if tokens else float("nan")
|
|
326
|
+
|
|
327
|
+
if idx < expected:
|
|
328
|
+
name = self._GENERAL_PARAM_NAMES[idx]
|
|
329
|
+
else:
|
|
330
|
+
name = f"general_param_{idx + 1}"
|
|
331
|
+
|
|
332
|
+
records.append(
|
|
333
|
+
{
|
|
334
|
+
"index": idx + 1,
|
|
335
|
+
"name": name,
|
|
336
|
+
"value": value,
|
|
337
|
+
"raw_comment": raw_comment,
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
df = pd.DataFrame.from_records(records).set_index("index")
|
|
342
|
+
end = start + n_params
|
|
343
|
+
return df, end
|
|
344
|
+
|
|
345
|
+
def _parse_atom_section(
|
|
346
|
+
self, lines: List[str], start: int, n_atoms: int
|
|
347
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
348
|
+
names = self._number_unused_titles(self._ATOM_PARAM_NAMES_BASE)
|
|
349
|
+
n_per_atom = len(names)
|
|
350
|
+
|
|
351
|
+
records: List[Dict[str, Any]] = []
|
|
352
|
+
n_lines = len(lines)
|
|
353
|
+
|
|
354
|
+
# Skip the 4 description lines after the atom header
|
|
355
|
+
i = min(start + 3, n_lines)
|
|
356
|
+
|
|
357
|
+
for atom_idx in range(1, n_atoms + 1):
|
|
358
|
+
values: List[float] = []
|
|
359
|
+
atom_no: Optional[int] = None
|
|
360
|
+
symbol: Optional[str] = None
|
|
361
|
+
first_line = True
|
|
362
|
+
|
|
363
|
+
while len(values) < n_per_atom and i < n_lines:
|
|
364
|
+
line = lines[i]
|
|
365
|
+
i += 1
|
|
366
|
+
|
|
367
|
+
data_part = line.split("!", 1)[0]
|
|
368
|
+
tokens = data_part.split()
|
|
369
|
+
if not tokens:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
if first_line:
|
|
373
|
+
j = 0
|
|
374
|
+
try:
|
|
375
|
+
atom_no = int(tokens[0])
|
|
376
|
+
j = 1
|
|
377
|
+
except ValueError:
|
|
378
|
+
atom_no = atom_idx
|
|
379
|
+
j = 0
|
|
380
|
+
|
|
381
|
+
if j < len(tokens):
|
|
382
|
+
try:
|
|
383
|
+
float(tokens[j])
|
|
384
|
+
except ValueError:
|
|
385
|
+
symbol = tokens[j]
|
|
386
|
+
j += 1
|
|
387
|
+
|
|
388
|
+
for tok in tokens[j:]:
|
|
389
|
+
try:
|
|
390
|
+
values.append(float(tok))
|
|
391
|
+
except ValueError:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
first_line = False
|
|
395
|
+
else:
|
|
396
|
+
for tok in tokens:
|
|
397
|
+
try:
|
|
398
|
+
values.append(float(tok))
|
|
399
|
+
except ValueError:
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if len(values) < n_per_atom:
|
|
403
|
+
values.extend([float("nan")] * (n_per_atom - len(values)))
|
|
404
|
+
|
|
405
|
+
record: Dict[str, Any] = {
|
|
406
|
+
"atom_index": atom_no if atom_no is not None else atom_idx,
|
|
407
|
+
"symbol": symbol,
|
|
408
|
+
}
|
|
409
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
410
|
+
records.append(record)
|
|
411
|
+
|
|
412
|
+
df = pd.DataFrame.from_records(records).set_index("atom_index")
|
|
413
|
+
return df, i
|
|
414
|
+
|
|
415
|
+
def _parse_bond_section(
|
|
416
|
+
self, lines: List[str], start: int, n_bonds: int
|
|
417
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
418
|
+
names = self._number_unused_titles(self._BOND_PARAM_NAMES_BASE)
|
|
419
|
+
n_per_bond = len(names)
|
|
420
|
+
|
|
421
|
+
records: List[Dict[str, Any]] = []
|
|
422
|
+
n_lines = len(lines)
|
|
423
|
+
|
|
424
|
+
# Skip the 2 description lines after the bond header
|
|
425
|
+
i = min(start + 1, n_lines)
|
|
426
|
+
|
|
427
|
+
for bond_idx in range(1, n_bonds + 1):
|
|
428
|
+
values: List[float] = []
|
|
429
|
+
at_i: Optional[int] = None
|
|
430
|
+
at_j: Optional[int] = None
|
|
431
|
+
first_line = True
|
|
432
|
+
|
|
433
|
+
while len(values) < n_per_bond and i < n_lines:
|
|
434
|
+
line = lines[i]
|
|
435
|
+
i += 1
|
|
436
|
+
|
|
437
|
+
data_part = line.split("!", 1)[0]
|
|
438
|
+
tokens = data_part.split()
|
|
439
|
+
if not tokens:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
if first_line:
|
|
443
|
+
j = 0
|
|
444
|
+
if len(tokens) >= 1:
|
|
445
|
+
try:
|
|
446
|
+
at_i = int(tokens[0])
|
|
447
|
+
j = 1
|
|
448
|
+
except ValueError:
|
|
449
|
+
j = 0
|
|
450
|
+
if len(tokens) >= 2 and j == 1:
|
|
451
|
+
try:
|
|
452
|
+
at_j = int(tokens[1])
|
|
453
|
+
j = 2
|
|
454
|
+
except ValueError:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
for tok in tokens[j:]:
|
|
458
|
+
try:
|
|
459
|
+
values.append(float(tok))
|
|
460
|
+
except ValueError:
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
first_line = False
|
|
464
|
+
else:
|
|
465
|
+
for tok in tokens:
|
|
466
|
+
try:
|
|
467
|
+
values.append(float(tok))
|
|
468
|
+
except ValueError:
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
if len(values) < n_per_bond:
|
|
472
|
+
values.extend([float("nan")] * (n_per_bond - len(values)))
|
|
473
|
+
|
|
474
|
+
record: Dict[str, Any] = {
|
|
475
|
+
"bond_index": bond_idx,
|
|
476
|
+
"i": at_i,
|
|
477
|
+
"j": at_j,
|
|
478
|
+
}
|
|
479
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
480
|
+
records.append(record)
|
|
481
|
+
|
|
482
|
+
df = pd.DataFrame.from_records(records).set_index("bond_index")
|
|
483
|
+
return df, i
|
|
484
|
+
|
|
485
|
+
def _parse_off_diagonal_section(
|
|
486
|
+
self, lines: List[str], start: int, n_entries: int
|
|
487
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
488
|
+
names = list(self._OFF_DIAGONAL_PARAM_NAMES)
|
|
489
|
+
n_per = len(names)
|
|
490
|
+
|
|
491
|
+
records: List[Dict[str, Any]] = []
|
|
492
|
+
i = start
|
|
493
|
+
n_lines = len(lines)
|
|
494
|
+
|
|
495
|
+
for idx in range(1, n_entries + 1):
|
|
496
|
+
values: List[float] = []
|
|
497
|
+
at_i: Optional[int] = None
|
|
498
|
+
at_j: Optional[int] = None
|
|
499
|
+
first_line = True
|
|
500
|
+
|
|
501
|
+
while len(values) < n_per and i < n_lines:
|
|
502
|
+
line = lines[i]
|
|
503
|
+
i += 1
|
|
504
|
+
|
|
505
|
+
data_part = line.split("!", 1)[0]
|
|
506
|
+
tokens = data_part.split()
|
|
507
|
+
if not tokens:
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
if first_line:
|
|
511
|
+
j = 0
|
|
512
|
+
if len(tokens) >= 1:
|
|
513
|
+
try:
|
|
514
|
+
at_i = int(tokens[0])
|
|
515
|
+
j = 1
|
|
516
|
+
except ValueError:
|
|
517
|
+
j = 0
|
|
518
|
+
if len(tokens) >= 2 and j == 1:
|
|
519
|
+
try:
|
|
520
|
+
at_j = int(tokens[1])
|
|
521
|
+
j = 2
|
|
522
|
+
except ValueError:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
for tok in tokens[j:]:
|
|
526
|
+
try:
|
|
527
|
+
values.append(float(tok))
|
|
528
|
+
except ValueError:
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
first_line = False
|
|
532
|
+
else:
|
|
533
|
+
for tok in tokens:
|
|
534
|
+
try:
|
|
535
|
+
values.append(float(tok))
|
|
536
|
+
except ValueError:
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
if len(values) < n_per:
|
|
540
|
+
values.extend([float("nan")] * (n_per - len(values)))
|
|
541
|
+
|
|
542
|
+
record: Dict[str, Any] = {
|
|
543
|
+
"offdiag_index": idx,
|
|
544
|
+
"i": at_i,
|
|
545
|
+
"j": at_j,
|
|
546
|
+
}
|
|
547
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
548
|
+
records.append(record)
|
|
549
|
+
|
|
550
|
+
df = pd.DataFrame.from_records(records).set_index("offdiag_index")
|
|
551
|
+
return df, i
|
|
552
|
+
|
|
553
|
+
def _parse_angle_section(
|
|
554
|
+
self, lines: List[str], start: int, n_angles: int
|
|
555
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
556
|
+
names = list(self._ANGLE_PARAM_NAMES)
|
|
557
|
+
n_per = len(names)
|
|
558
|
+
|
|
559
|
+
records: List[Dict[str, Any]] = []
|
|
560
|
+
i = start
|
|
561
|
+
n_lines = len(lines)
|
|
562
|
+
|
|
563
|
+
for idx in range(1, n_angles + 1):
|
|
564
|
+
values: List[float] = []
|
|
565
|
+
at_i: Optional[int] = None
|
|
566
|
+
at_j: Optional[int] = None
|
|
567
|
+
at_k: Optional[int] = None
|
|
568
|
+
first_line = True
|
|
569
|
+
|
|
570
|
+
while len(values) < n_per and i < n_lines:
|
|
571
|
+
line = lines[i]
|
|
572
|
+
i += 1
|
|
573
|
+
|
|
574
|
+
data_part = line.split("!", 1)[0]
|
|
575
|
+
tokens = data_part.split()
|
|
576
|
+
if not tokens:
|
|
577
|
+
continue
|
|
578
|
+
|
|
579
|
+
if first_line:
|
|
580
|
+
j = 0
|
|
581
|
+
if len(tokens) >= 1:
|
|
582
|
+
try:
|
|
583
|
+
at_i = int(tokens[0])
|
|
584
|
+
j = 1
|
|
585
|
+
except ValueError:
|
|
586
|
+
j = 0
|
|
587
|
+
if len(tokens) >= 2 and j == 1:
|
|
588
|
+
try:
|
|
589
|
+
at_j = int(tokens[1])
|
|
590
|
+
j = 2
|
|
591
|
+
except ValueError:
|
|
592
|
+
pass
|
|
593
|
+
if len(tokens) >= 3 and j == 2:
|
|
594
|
+
try:
|
|
595
|
+
at_k = int(tokens[2])
|
|
596
|
+
j = 3
|
|
597
|
+
except ValueError:
|
|
598
|
+
pass
|
|
599
|
+
|
|
600
|
+
for tok in tokens[j:]:
|
|
601
|
+
try:
|
|
602
|
+
values.append(float(tok))
|
|
603
|
+
except ValueError:
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
first_line = False
|
|
607
|
+
else:
|
|
608
|
+
for tok in tokens:
|
|
609
|
+
try:
|
|
610
|
+
values.append(float(tok))
|
|
611
|
+
except ValueError:
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
if len(values) < n_per:
|
|
615
|
+
values.extend([float("nan")] * (n_per - len(values)))
|
|
616
|
+
|
|
617
|
+
record: Dict[str, Any] = {
|
|
618
|
+
"angle_index": idx,
|
|
619
|
+
"i": at_i,
|
|
620
|
+
"j": at_j,
|
|
621
|
+
"k": at_k,
|
|
622
|
+
}
|
|
623
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
624
|
+
records.append(record)
|
|
625
|
+
|
|
626
|
+
df = pd.DataFrame.from_records(records).set_index("angle_index")
|
|
627
|
+
return df, i
|
|
628
|
+
|
|
629
|
+
def _parse_torsion_section(
|
|
630
|
+
self, lines: List[str], start: int, n_torsions: int
|
|
631
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
632
|
+
names = self._number_unused_titles(self._TORSION_PARAM_NAMES_BASE)
|
|
633
|
+
n_per = len(names)
|
|
634
|
+
|
|
635
|
+
records: List[Dict[str, Any]] = []
|
|
636
|
+
i = start
|
|
637
|
+
n_lines = len(lines)
|
|
638
|
+
|
|
639
|
+
for idx in range(1, n_torsions + 1):
|
|
640
|
+
values: List[float] = []
|
|
641
|
+
at_i: Optional[int] = None
|
|
642
|
+
at_j: Optional[int] = None
|
|
643
|
+
at_k: Optional[int] = None
|
|
644
|
+
at_l: Optional[int] = None
|
|
645
|
+
first_line = True
|
|
646
|
+
|
|
647
|
+
while len(values) < n_per and i < n_lines:
|
|
648
|
+
line = lines[i]
|
|
649
|
+
i += 1
|
|
650
|
+
|
|
651
|
+
data_part = line.split("!", 1)[0]
|
|
652
|
+
tokens = data_part.split()
|
|
653
|
+
if not tokens:
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
if first_line:
|
|
657
|
+
j = 0
|
|
658
|
+
if len(tokens) >= 1:
|
|
659
|
+
try:
|
|
660
|
+
at_i = int(tokens[0])
|
|
661
|
+
j = 1
|
|
662
|
+
except ValueError:
|
|
663
|
+
j = 0
|
|
664
|
+
if len(tokens) >= 2 and j == 1:
|
|
665
|
+
try:
|
|
666
|
+
at_j = int(tokens[1])
|
|
667
|
+
j = 2
|
|
668
|
+
except ValueError:
|
|
669
|
+
pass
|
|
670
|
+
if len(tokens) >= 3 and j == 2:
|
|
671
|
+
try:
|
|
672
|
+
at_k = int(tokens[2])
|
|
673
|
+
j = 3
|
|
674
|
+
except ValueError:
|
|
675
|
+
pass
|
|
676
|
+
if len(tokens) >= 4 and j == 3:
|
|
677
|
+
try:
|
|
678
|
+
at_l = int(tokens[3])
|
|
679
|
+
j = 4
|
|
680
|
+
except ValueError:
|
|
681
|
+
pass
|
|
682
|
+
|
|
683
|
+
for tok in tokens[j:]:
|
|
684
|
+
try:
|
|
685
|
+
values.append(float(tok))
|
|
686
|
+
except ValueError:
|
|
687
|
+
continue
|
|
688
|
+
|
|
689
|
+
first_line = False
|
|
690
|
+
else:
|
|
691
|
+
for tok in tokens:
|
|
692
|
+
try:
|
|
693
|
+
values.append(float(tok))
|
|
694
|
+
except ValueError:
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
if len(values) < n_per:
|
|
698
|
+
values.extend([float("nan")] * (n_per - len(values)))
|
|
699
|
+
|
|
700
|
+
record: Dict[str, Any] = {
|
|
701
|
+
"torsion_index": idx,
|
|
702
|
+
"i": at_i,
|
|
703
|
+
"j": at_j,
|
|
704
|
+
"k": at_k,
|
|
705
|
+
"l": at_l,
|
|
706
|
+
}
|
|
707
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
708
|
+
records.append(record)
|
|
709
|
+
|
|
710
|
+
df = pd.DataFrame.from_records(records).set_index("torsion_index")
|
|
711
|
+
return df, i
|
|
712
|
+
|
|
713
|
+
def _parse_hbond_section(
|
|
714
|
+
self, lines: List[str], start: int, n_hbonds: int
|
|
715
|
+
) -> Tuple[pd.DataFrame, int]:
|
|
716
|
+
names = list(self._HBOND_PARAM_NAMES)
|
|
717
|
+
n_per = len(names)
|
|
718
|
+
|
|
719
|
+
records: List[Dict[str, Any]] = []
|
|
720
|
+
i = start
|
|
721
|
+
n_lines = len(lines)
|
|
722
|
+
|
|
723
|
+
for idx in range(1, n_hbonds + 1):
|
|
724
|
+
values: List[float] = []
|
|
725
|
+
at_i: Optional[int] = None
|
|
726
|
+
at_j: Optional[int] = None
|
|
727
|
+
at_k: Optional[int] = None
|
|
728
|
+
first_line = True
|
|
729
|
+
|
|
730
|
+
while len(values) < n_per and i < n_lines:
|
|
731
|
+
line = lines[i]
|
|
732
|
+
i += 1
|
|
733
|
+
|
|
734
|
+
data_part = line.split("!", 1)[0]
|
|
735
|
+
tokens = data_part.split()
|
|
736
|
+
if not tokens:
|
|
737
|
+
continue
|
|
738
|
+
|
|
739
|
+
if first_line:
|
|
740
|
+
j = 0
|
|
741
|
+
if len(tokens) >= 1:
|
|
742
|
+
try:
|
|
743
|
+
at_i = int(tokens[0])
|
|
744
|
+
j = 1
|
|
745
|
+
except ValueError:
|
|
746
|
+
j = 0
|
|
747
|
+
if len(tokens) >= 2 and j == 1:
|
|
748
|
+
try:
|
|
749
|
+
at_j = int(tokens[1])
|
|
750
|
+
j = 2
|
|
751
|
+
except ValueError:
|
|
752
|
+
pass
|
|
753
|
+
if len(tokens) >= 3 and j == 2:
|
|
754
|
+
try:
|
|
755
|
+
at_k = int(tokens[2])
|
|
756
|
+
j = 3
|
|
757
|
+
except ValueError:
|
|
758
|
+
pass
|
|
759
|
+
|
|
760
|
+
for tok in tokens[j:]:
|
|
761
|
+
try:
|
|
762
|
+
values.append(float(tok))
|
|
763
|
+
except ValueError:
|
|
764
|
+
continue
|
|
765
|
+
|
|
766
|
+
first_line = False
|
|
767
|
+
else:
|
|
768
|
+
for tok in tokens:
|
|
769
|
+
try:
|
|
770
|
+
values.append(float(tok))
|
|
771
|
+
except ValueError:
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
if len(values) < n_per:
|
|
775
|
+
values.extend([float("nan")] * (n_per - len(values)))
|
|
776
|
+
|
|
777
|
+
record: Dict[str, Any] = {
|
|
778
|
+
"hbond_index": idx,
|
|
779
|
+
"i": at_i,
|
|
780
|
+
"j": at_j,
|
|
781
|
+
"k": at_k,
|
|
782
|
+
}
|
|
783
|
+
record.update({name: val for name, val in zip(names, values)})
|
|
784
|
+
records.append(record)
|
|
785
|
+
|
|
786
|
+
df = pd.DataFrame.from_records(records).set_index("hbond_index")
|
|
787
|
+
return df, i
|
|
788
|
+
|
|
789
|
+
# ---------------- helpers ------------------------------------
|
|
790
|
+
@staticmethod
|
|
791
|
+
def _first_int_in_line(line: str) -> Optional[int]:
|
|
792
|
+
for tok in line.split():
|
|
793
|
+
try:
|
|
794
|
+
return int(tok)
|
|
795
|
+
except ValueError:
|
|
796
|
+
continue
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def _number_unused_titles(
|
|
801
|
+
names: List[str],
|
|
802
|
+
label: str = "n.u.",
|
|
803
|
+
) -> List[str]:
|
|
804
|
+
result: List[str] = []
|
|
805
|
+
counter = 0
|
|
806
|
+
for name in names:
|
|
807
|
+
if name == label:
|
|
808
|
+
counter += 1
|
|
809
|
+
result.append(f"{label}{counter}")
|
|
810
|
+
else:
|
|
811
|
+
result.append(name)
|
|
812
|
+
return result
|