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.
Files changed (130) hide show
  1. reaxkit/__init__.py +0 -0
  2. reaxkit/analysis/__init__.py +0 -0
  3. reaxkit/analysis/composed/RDF_analyzer.py +560 -0
  4. reaxkit/analysis/composed/__init__.py +0 -0
  5. reaxkit/analysis/composed/connectivity_analyzer.py +706 -0
  6. reaxkit/analysis/composed/coordination_analyzer.py +144 -0
  7. reaxkit/analysis/composed/electrostatics_analyzer.py +687 -0
  8. reaxkit/analysis/per_file/__init__.py +0 -0
  9. reaxkit/analysis/per_file/control_analyzer.py +165 -0
  10. reaxkit/analysis/per_file/eregime_analyzer.py +108 -0
  11. reaxkit/analysis/per_file/ffield_analyzer.py +305 -0
  12. reaxkit/analysis/per_file/fort13_analyzer.py +79 -0
  13. reaxkit/analysis/per_file/fort57_analyzer.py +106 -0
  14. reaxkit/analysis/per_file/fort73_analyzer.py +61 -0
  15. reaxkit/analysis/per_file/fort74_analyzer.py +65 -0
  16. reaxkit/analysis/per_file/fort76_analyzer.py +191 -0
  17. reaxkit/analysis/per_file/fort78_analyzer.py +154 -0
  18. reaxkit/analysis/per_file/fort79_analyzer.py +83 -0
  19. reaxkit/analysis/per_file/fort7_analyzer.py +393 -0
  20. reaxkit/analysis/per_file/fort99_analyzer.py +411 -0
  21. reaxkit/analysis/per_file/molfra_analyzer.py +359 -0
  22. reaxkit/analysis/per_file/params_analyzer.py +258 -0
  23. reaxkit/analysis/per_file/summary_analyzer.py +84 -0
  24. reaxkit/analysis/per_file/trainset_analyzer.py +84 -0
  25. reaxkit/analysis/per_file/vels_analyzer.py +95 -0
  26. reaxkit/analysis/per_file/xmolout_analyzer.py +528 -0
  27. reaxkit/cli.py +181 -0
  28. reaxkit/count_loc.py +276 -0
  29. reaxkit/data/alias.yaml +89 -0
  30. reaxkit/data/constants.yaml +27 -0
  31. reaxkit/data/reaxff_input_files_contents.yaml +186 -0
  32. reaxkit/data/reaxff_output_files_contents.yaml +301 -0
  33. reaxkit/data/units.yaml +38 -0
  34. reaxkit/help/__init__.py +0 -0
  35. reaxkit/help/help_index_loader.py +531 -0
  36. reaxkit/help/introspection_utils.py +131 -0
  37. reaxkit/io/__init__.py +0 -0
  38. reaxkit/io/base_handler.py +165 -0
  39. reaxkit/io/generators/__init__.py +0 -0
  40. reaxkit/io/generators/control_generator.py +123 -0
  41. reaxkit/io/generators/eregime_generator.py +341 -0
  42. reaxkit/io/generators/geo_generator.py +967 -0
  43. reaxkit/io/generators/trainset_generator.py +1758 -0
  44. reaxkit/io/generators/tregime_generator.py +113 -0
  45. reaxkit/io/generators/vregime_generator.py +164 -0
  46. reaxkit/io/generators/xmolout_generator.py +304 -0
  47. reaxkit/io/handlers/__init__.py +0 -0
  48. reaxkit/io/handlers/control_handler.py +209 -0
  49. reaxkit/io/handlers/eregime_handler.py +122 -0
  50. reaxkit/io/handlers/ffield_handler.py +812 -0
  51. reaxkit/io/handlers/fort13_handler.py +123 -0
  52. reaxkit/io/handlers/fort57_handler.py +143 -0
  53. reaxkit/io/handlers/fort73_handler.py +145 -0
  54. reaxkit/io/handlers/fort74_handler.py +155 -0
  55. reaxkit/io/handlers/fort76_handler.py +195 -0
  56. reaxkit/io/handlers/fort78_handler.py +142 -0
  57. reaxkit/io/handlers/fort79_handler.py +227 -0
  58. reaxkit/io/handlers/fort7_handler.py +264 -0
  59. reaxkit/io/handlers/fort99_handler.py +128 -0
  60. reaxkit/io/handlers/geo_handler.py +224 -0
  61. reaxkit/io/handlers/molfra_handler.py +184 -0
  62. reaxkit/io/handlers/params_handler.py +137 -0
  63. reaxkit/io/handlers/summary_handler.py +135 -0
  64. reaxkit/io/handlers/trainset_handler.py +658 -0
  65. reaxkit/io/handlers/vels_handler.py +293 -0
  66. reaxkit/io/handlers/xmolout_handler.py +174 -0
  67. reaxkit/utils/__init__.py +0 -0
  68. reaxkit/utils/alias.py +219 -0
  69. reaxkit/utils/cache.py +77 -0
  70. reaxkit/utils/constants.py +75 -0
  71. reaxkit/utils/equation_of_states.py +96 -0
  72. reaxkit/utils/exceptions.py +27 -0
  73. reaxkit/utils/frame_utils.py +175 -0
  74. reaxkit/utils/log.py +43 -0
  75. reaxkit/utils/media/__init__.py +0 -0
  76. reaxkit/utils/media/convert.py +90 -0
  77. reaxkit/utils/media/make_video.py +91 -0
  78. reaxkit/utils/media/plotter.py +812 -0
  79. reaxkit/utils/numerical/__init__.py +0 -0
  80. reaxkit/utils/numerical/extrema_finder.py +96 -0
  81. reaxkit/utils/numerical/moving_average.py +103 -0
  82. reaxkit/utils/numerical/numerical_calcs.py +75 -0
  83. reaxkit/utils/numerical/signal_ops.py +135 -0
  84. reaxkit/utils/path.py +55 -0
  85. reaxkit/utils/units.py +104 -0
  86. reaxkit/webui/__init__.py +0 -0
  87. reaxkit/webui/app.py +0 -0
  88. reaxkit/webui/components.py +0 -0
  89. reaxkit/webui/layouts.py +0 -0
  90. reaxkit/webui/utils.py +0 -0
  91. reaxkit/workflows/__init__.py +0 -0
  92. reaxkit/workflows/composed/__init__.py +0 -0
  93. reaxkit/workflows/composed/coordination_workflow.py +393 -0
  94. reaxkit/workflows/composed/electrostatics_workflow.py +587 -0
  95. reaxkit/workflows/composed/xmolout_fort7_workflow.py +343 -0
  96. reaxkit/workflows/meta/__init__.py +0 -0
  97. reaxkit/workflows/meta/help_workflow.py +136 -0
  98. reaxkit/workflows/meta/introspection_workflow.py +235 -0
  99. reaxkit/workflows/meta/make_video_workflow.py +61 -0
  100. reaxkit/workflows/meta/plotter_workflow.py +601 -0
  101. reaxkit/workflows/per_file/__init__.py +0 -0
  102. reaxkit/workflows/per_file/control_workflow.py +110 -0
  103. reaxkit/workflows/per_file/eregime_workflow.py +267 -0
  104. reaxkit/workflows/per_file/ffield_workflow.py +390 -0
  105. reaxkit/workflows/per_file/fort13_workflow.py +86 -0
  106. reaxkit/workflows/per_file/fort57_workflow.py +137 -0
  107. reaxkit/workflows/per_file/fort73_workflow.py +151 -0
  108. reaxkit/workflows/per_file/fort74_workflow.py +88 -0
  109. reaxkit/workflows/per_file/fort76_workflow.py +188 -0
  110. reaxkit/workflows/per_file/fort78_workflow.py +135 -0
  111. reaxkit/workflows/per_file/fort79_workflow.py +314 -0
  112. reaxkit/workflows/per_file/fort7_workflow.py +592 -0
  113. reaxkit/workflows/per_file/fort83_workflow.py +60 -0
  114. reaxkit/workflows/per_file/fort99_workflow.py +223 -0
  115. reaxkit/workflows/per_file/geo_workflow.py +554 -0
  116. reaxkit/workflows/per_file/molfra_workflow.py +577 -0
  117. reaxkit/workflows/per_file/params_workflow.py +135 -0
  118. reaxkit/workflows/per_file/summary_workflow.py +161 -0
  119. reaxkit/workflows/per_file/trainset_workflow.py +356 -0
  120. reaxkit/workflows/per_file/tregime_workflow.py +79 -0
  121. reaxkit/workflows/per_file/vels_workflow.py +309 -0
  122. reaxkit/workflows/per_file/vregime_workflow.py +75 -0
  123. reaxkit/workflows/per_file/xmolout_workflow.py +678 -0
  124. reaxkit-1.0.0.dist-info/METADATA +128 -0
  125. reaxkit-1.0.0.dist-info/RECORD +130 -0
  126. reaxkit-1.0.0.dist-info/WHEEL +5 -0
  127. reaxkit-1.0.0.dist-info/entry_points.txt +2 -0
  128. reaxkit-1.0.0.dist-info/licenses/AUTHORS.md +20 -0
  129. reaxkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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