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,706 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atomic connectivity and bond-event analysis utilities.
|
|
3
|
+
|
|
4
|
+
This module provides tools for extracting, aggregating, and analyzing
|
|
5
|
+
atomic connectivities derived from ReaxFF ``fort.7`` data.
|
|
6
|
+
|
|
7
|
+
Connectivities describe the bonding network between atoms in each frame,
|
|
8
|
+
including bond partners and bond orders. The utilities here convert raw
|
|
9
|
+
fort.7 connectivity columns into tidy edge lists, adjacency tables,
|
|
10
|
+
time-series bond traces, and discrete bond formation/breakage events.
|
|
11
|
+
|
|
12
|
+
Typical use cases include:
|
|
13
|
+
|
|
14
|
+
- building bond (edge) lists with bond orders for chemical analysis
|
|
15
|
+
- aggregating connectivity statistics across frames
|
|
16
|
+
- tracking bond-order time series for specific atom pairs
|
|
17
|
+
- detecting bond formation and breakage events with noise suppression
|
|
18
|
+
- visual debugging of bond-event detection parameters
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
from typing import Iterable, List, Optional, Union, Literal, Tuple
|
|
24
|
+
import pandas as pd
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from reaxkit.utils.frame_utils import resolve_indices
|
|
28
|
+
from reaxkit.utils.numerical.moving_average import simple_moving_average, exponential_moving_average
|
|
29
|
+
from reaxkit.utils.numerical.signal_ops import schmitt_hysteresis, clean_flicker
|
|
30
|
+
from reaxkit.utils.media.plotter import single_plot
|
|
31
|
+
|
|
32
|
+
Indexish = Union[int, Iterable[int], None]
|
|
33
|
+
|
|
34
|
+
# ---- moved from fort7_analyzer: connectivity ----
|
|
35
|
+
def connection_list(
|
|
36
|
+
handler,
|
|
37
|
+
frames: Indexish = None,
|
|
38
|
+
iterations: Indexish = None,
|
|
39
|
+
min_bo: float = 0.0,
|
|
40
|
+
undirected: bool = True,
|
|
41
|
+
aggregate: Literal["max", "mean"] = "max",
|
|
42
|
+
include_self: bool = False,
|
|
43
|
+
) -> pd.DataFrame:
|
|
44
|
+
"""Build a tidy bond (edge) list with bond orders as weights.
|
|
45
|
+
|
|
46
|
+
Works on
|
|
47
|
+
--------
|
|
48
|
+
Fort7Handler — ``fort.7``
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
handler : Fort7Handler
|
|
53
|
+
Parsed ``fort.7`` handler.
|
|
54
|
+
frames, iterations
|
|
55
|
+
Frame indices or iteration numbers to include.
|
|
56
|
+
min_bo : float, default=0.0
|
|
57
|
+
Minimum bond order to keep.
|
|
58
|
+
undirected : bool, default=True
|
|
59
|
+
If True, treat bonds as undirected and merge A–B / B–A duplicates.
|
|
60
|
+
aggregate : {"max", "mean"}, default="max"
|
|
61
|
+
Aggregation rule for bond order when merging duplicates.
|
|
62
|
+
include_self : bool, default=False
|
|
63
|
+
If True, keep self-edges (usually False).
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
pandas.DataFrame
|
|
68
|
+
Tidy edge list with columns:
|
|
69
|
+
``frame_idx``, ``iter``, ``src``, ``dst``, ``bo``, ``j``.
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> edges = connection_list(f7, frames=[0, 1], min_bo=0.3)
|
|
74
|
+
"""
|
|
75
|
+
sim_df = handler.dataframe()
|
|
76
|
+
idx_list = resolve_indices(handler, frames=frames, iterations=iterations)
|
|
77
|
+
|
|
78
|
+
all_edges: List[pd.DataFrame] = []
|
|
79
|
+
|
|
80
|
+
for fi in idx_list:
|
|
81
|
+
atoms = handler._frames[fi]
|
|
82
|
+
iter = int(sim_df.iloc[fi]["iter"])
|
|
83
|
+
# Find how many cnn/BO columns exist for this frame
|
|
84
|
+
nb = int(sim_df.iloc[fi]["num_of_bonds"])
|
|
85
|
+
cnn_cols = [f"atom_cnn{j}" for j in range(1, nb + 1)]
|
|
86
|
+
bo_cols = [f"BO{j}" for j in range(1, nb + 1)]
|
|
87
|
+
|
|
88
|
+
# Sanity: skip if columns are missing
|
|
89
|
+
missing = [c for c in cnn_cols + bo_cols if c not in atoms.columns]
|
|
90
|
+
if missing:
|
|
91
|
+
# Skip silently (or raise) — here we skip this frame
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Build edge blocks for each neighbor slot j
|
|
95
|
+
blocks: List[pd.DataFrame] = []
|
|
96
|
+
src_series = atoms["atom_num"].astype(int)
|
|
97
|
+
|
|
98
|
+
for j, (cnn_c, bo_c) in enumerate(zip(cnn_cols, bo_cols), start=1):
|
|
99
|
+
dst_series = atoms[cnn_c].astype(int)
|
|
100
|
+
bo_series = atoms[bo_c].astype(float)
|
|
101
|
+
|
|
102
|
+
dfj = pd.DataFrame(
|
|
103
|
+
{
|
|
104
|
+
"src": src_series.values,
|
|
105
|
+
"dst": dst_series.values,
|
|
106
|
+
"bo": bo_series.values,
|
|
107
|
+
"j": j,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
blocks.append(dfj)
|
|
111
|
+
|
|
112
|
+
edges = pd.concat(blocks, ignore_index=True)
|
|
113
|
+
|
|
114
|
+
# Filter invalid/empty connections:
|
|
115
|
+
# - Some datasets mark no-connection with 0 or negative dst or bo<=0
|
|
116
|
+
mask = edges["dst"] > 0
|
|
117
|
+
if not include_self:
|
|
118
|
+
mask &= edges["dst"] != edges["src"]
|
|
119
|
+
if min_bo is not None:
|
|
120
|
+
mask &= edges["bo"] >= float(min_bo)
|
|
121
|
+
edges = edges.loc[mask].copy()
|
|
122
|
+
|
|
123
|
+
# Attach frame metadata
|
|
124
|
+
edges.insert(0, "iter", iter)
|
|
125
|
+
edges.insert(0, "frame_idx", fi)
|
|
126
|
+
|
|
127
|
+
# Canonicalize for undirected graphs and collapse duplicates
|
|
128
|
+
if undirected:
|
|
129
|
+
# Ensure src <= dst
|
|
130
|
+
src_min = edges[["src", "dst"]].min(axis=1)
|
|
131
|
+
dst_max = edges[["src", "dst"]].max(axis=1)
|
|
132
|
+
edges["src"], edges["dst"] = src_min, dst_max
|
|
133
|
+
|
|
134
|
+
# Combine duplicates (the same bond appears from both atoms)
|
|
135
|
+
by = ["frame_idx", "iter", "src", "dst"]
|
|
136
|
+
if aggregate == "mean":
|
|
137
|
+
agg = edges.groupby(by, as_index=False)["bo"].mean()
|
|
138
|
+
else:
|
|
139
|
+
agg = edges.groupby(by, as_index=False)["bo"].max()
|
|
140
|
+
|
|
141
|
+
# Keep one representative j (optional; not meaningful when aggregated)
|
|
142
|
+
agg["j"] = -1 # indicates aggregated
|
|
143
|
+
edges = agg
|
|
144
|
+
|
|
145
|
+
all_edges.append(edges)
|
|
146
|
+
|
|
147
|
+
if not all_edges:
|
|
148
|
+
return pd.DataFrame(columns=["frame_idx", "iter", "src", "dst", "bo", "j"])
|
|
149
|
+
|
|
150
|
+
# Concatenate all frames
|
|
151
|
+
out = pd.concat(all_edges, ignore_index=True)
|
|
152
|
+
# Sort for stability
|
|
153
|
+
out = out.sort_values(["frame_idx", "src", "dst", "j"], kind="stable").reset_index(drop=True)
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
def connection_table(
|
|
157
|
+
handler,
|
|
158
|
+
frame: int,
|
|
159
|
+
min_bo: float = 0.0,
|
|
160
|
+
undirected: bool = True,
|
|
161
|
+
fill_value: float = 0.0,
|
|
162
|
+
) -> pd.DataFrame:
|
|
163
|
+
"""Build a dense adjacency (connectivity) table for a single frame.
|
|
164
|
+
|
|
165
|
+
Works on
|
|
166
|
+
--------
|
|
167
|
+
Fort7Handler — ``fort.7``
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
handler : Fort7Handler
|
|
172
|
+
Parsed ``fort.7`` handler.
|
|
173
|
+
frame : int
|
|
174
|
+
Frame index to extract.
|
|
175
|
+
min_bo : float, default=0.0
|
|
176
|
+
Minimum bond order to include.
|
|
177
|
+
undirected : bool, default=True
|
|
178
|
+
Treat bonds as undirected.
|
|
179
|
+
fill_value : float, default=0.0
|
|
180
|
+
Value used for absent bonds.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
pandas.DataFrame
|
|
185
|
+
Adjacency-like table with index=source atom and columns=destination atom,
|
|
186
|
+
values equal to bond order.
|
|
187
|
+
|
|
188
|
+
Examples
|
|
189
|
+
--------
|
|
190
|
+
>>> tbl = connection_table(f7, frame=0, min_bo=0.2)
|
|
191
|
+
"""
|
|
192
|
+
edges = connection_list(
|
|
193
|
+
handler,
|
|
194
|
+
frames=[frame],
|
|
195
|
+
iterations=None,
|
|
196
|
+
min_bo=min_bo,
|
|
197
|
+
undirected=undirected,
|
|
198
|
+
)
|
|
199
|
+
if edges.empty:
|
|
200
|
+
return pd.DataFrame()
|
|
201
|
+
|
|
202
|
+
# Use pivot to form a (possibly sparse) adjacency matrix
|
|
203
|
+
tbl = edges.pivot_table(
|
|
204
|
+
index="src",
|
|
205
|
+
columns="dst",
|
|
206
|
+
values="bo",
|
|
207
|
+
aggfunc="max",
|
|
208
|
+
fill_value=fill_value,
|
|
209
|
+
)
|
|
210
|
+
# Make it a regular DataFrame with sorted axes
|
|
211
|
+
tbl = tbl.sort_index(axis=0).sort_index(axis=1)
|
|
212
|
+
return tbl
|
|
213
|
+
|
|
214
|
+
def connection_stats_over_frames(
|
|
215
|
+
handler,
|
|
216
|
+
frames: Indexish = None,
|
|
217
|
+
iterations: Indexish = None,
|
|
218
|
+
min_bo: float = 0.0,
|
|
219
|
+
undirected: bool = True,
|
|
220
|
+
how: Literal["mean", "max", "count"] = "mean",
|
|
221
|
+
) -> pd.DataFrame:
|
|
222
|
+
"""Aggregate bond statistics across selected frames.
|
|
223
|
+
|
|
224
|
+
Works on
|
|
225
|
+
--------
|
|
226
|
+
Fort7Handler — ``fort.7``
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
handler : Fort7Handler
|
|
231
|
+
Parsed ``fort.7`` handler.
|
|
232
|
+
frames, iterations
|
|
233
|
+
Frame indices or iteration numbers to include.
|
|
234
|
+
min_bo : float, default=0.0
|
|
235
|
+
Minimum bond order to consider.
|
|
236
|
+
undirected : bool, default=True
|
|
237
|
+
Treat bonds as undirected.
|
|
238
|
+
how : {"mean", "max", "count"}, default="mean"
|
|
239
|
+
Aggregation rule across frames.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
pandas.DataFrame
|
|
244
|
+
Table with columns ``src``, ``dst``, ``value`` representing the
|
|
245
|
+
aggregated bond metric.
|
|
246
|
+
|
|
247
|
+
Examples
|
|
248
|
+
--------
|
|
249
|
+
>>> stats = connection_stats_over_frames(f7, how="count")
|
|
250
|
+
"""
|
|
251
|
+
edges = connection_list(
|
|
252
|
+
handler,
|
|
253
|
+
frames=frames,
|
|
254
|
+
iterations=iterations,
|
|
255
|
+
min_bo=min_bo,
|
|
256
|
+
undirected=undirected,
|
|
257
|
+
)
|
|
258
|
+
if edges.empty:
|
|
259
|
+
return pd.DataFrame(columns=["src", "dst", "value"])
|
|
260
|
+
|
|
261
|
+
by = ["src", "dst"]
|
|
262
|
+
if how == "count":
|
|
263
|
+
out = edges.groupby(by, as_index=False).size().rename(columns={"size": "value"})
|
|
264
|
+
elif how == "max":
|
|
265
|
+
out = edges.groupby(by, as_index=False)["bo"].max().rename(columns={"bo": "value"})
|
|
266
|
+
else: # mean
|
|
267
|
+
out = edges.groupby(by, as_index=False)["bo"].mean().rename(columns={"bo": "value"})
|
|
268
|
+
return out.sort_values(["src", "dst"], kind="stable").reset_index(drop=True)
|
|
269
|
+
|
|
270
|
+
# ---- moved helper (keep here since it’s bond-table specific) ----
|
|
271
|
+
def _pivot_to_tidy_bo(pivot: pd.DataFrame) -> pd.DataFrame:
|
|
272
|
+
"""
|
|
273
|
+
Convert a (frame_idx, iter) x (src,dst) pivot to tidy:
|
|
274
|
+
columns: frame_idx, iter, src, dst, bo
|
|
275
|
+
Works across pandas versions (Series/DataFrame stack behavior).
|
|
276
|
+
"""
|
|
277
|
+
# Try the modern stack; fall back to legacy
|
|
278
|
+
try:
|
|
279
|
+
stacked = pivot.stack(future_stack=True)
|
|
280
|
+
except TypeError:
|
|
281
|
+
stacked = pivot.stack()
|
|
282
|
+
|
|
283
|
+
# Case 1: Series → easy
|
|
284
|
+
if isinstance(stacked, pd.Series):
|
|
285
|
+
tidy = stacked.rename("bo").reset_index()
|
|
286
|
+
else:
|
|
287
|
+
# Case 2: DataFrame (some pandas builds) → if 1 col, rename; else manual
|
|
288
|
+
if stacked.shape[1] == 1:
|
|
289
|
+
value_col = stacked.columns[0]
|
|
290
|
+
tidy = stacked.rename(columns={value_col: "bo"}).reset_index()
|
|
291
|
+
else:
|
|
292
|
+
# Manual, bulletproof & fast enough for moderate sizes
|
|
293
|
+
rows = []
|
|
294
|
+
for (fi, it), row in pivot.iterrows():
|
|
295
|
+
for (src, dst), bo in row.items():
|
|
296
|
+
rows.append([fi, it, src, dst, bo])
|
|
297
|
+
tidy = pd.DataFrame(rows, columns=["frame_idx", "iter", "src", "dst", "bo"])
|
|
298
|
+
return tidy
|
|
299
|
+
|
|
300
|
+
# Normalize column names for stacked levels
|
|
301
|
+
# After reset_index we expect: frame_idx, iter, <src_level>, <dst_level>, bo
|
|
302
|
+
want = {"src", "dst"}
|
|
303
|
+
got = set(tidy.columns)
|
|
304
|
+
if not want.issubset(got):
|
|
305
|
+
mi_names = pivot.columns.names or ["src", "dst"]
|
|
306
|
+
rename_map = {}
|
|
307
|
+
if mi_names[0] in tidy.columns:
|
|
308
|
+
rename_map[mi_names[0]] = "src"
|
|
309
|
+
if mi_names[1] in tidy.columns:
|
|
310
|
+
rename_map[mi_names[1]] = "dst"
|
|
311
|
+
tidy = tidy.rename(columns=rename_map)
|
|
312
|
+
|
|
313
|
+
# Final fallback for unnamed levels (e.g., level_2/level_3)
|
|
314
|
+
if "src" not in tidy.columns or "dst" not in tidy.columns:
|
|
315
|
+
for c in list(tidy.columns):
|
|
316
|
+
if c.startswith("level_") and "src" not in tidy.columns:
|
|
317
|
+
tidy = tidy.rename(columns={c: "src"})
|
|
318
|
+
elif c.startswith("level_") and "dst" not in tidy.columns:
|
|
319
|
+
tidy = tidy.rename(columns={c: "dst"})
|
|
320
|
+
|
|
321
|
+
return tidy.sort_values(["frame_idx", "src", "dst"], kind="stable").reset_index(drop=True)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def bond_timeseries(
|
|
325
|
+
handler,
|
|
326
|
+
frames: Indexish = None,
|
|
327
|
+
iterations: Indexish = None,
|
|
328
|
+
undirected: bool = True,
|
|
329
|
+
bo_threshold: float = 0.0,
|
|
330
|
+
as_wide: bool = False,
|
|
331
|
+
) -> pd.DataFrame:
|
|
332
|
+
"""Track bond-order time series for all bonds across selected frames.
|
|
333
|
+
|
|
334
|
+
Missing bonds in a frame are filled with bond order zero.
|
|
335
|
+
|
|
336
|
+
Works on
|
|
337
|
+
--------
|
|
338
|
+
Fort7Handler — ``fort.7``
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
handler : Fort7Handler
|
|
343
|
+
Parsed ``fort.7`` handler.
|
|
344
|
+
frames, iterations
|
|
345
|
+
Frame indices or iteration numbers to include.
|
|
346
|
+
undirected : bool, default=True
|
|
347
|
+
Treat bonds as undirected.
|
|
348
|
+
bo_threshold : float, default=0.0
|
|
349
|
+
Values below this threshold are set to zero.
|
|
350
|
+
as_wide : bool, default=False
|
|
351
|
+
If True, return a wide matrix (frames × bonds).
|
|
352
|
+
|
|
353
|
+
Returns
|
|
354
|
+
-------
|
|
355
|
+
pandas.DataFrame
|
|
356
|
+
Bond-order time series in tidy (long) or wide format.
|
|
357
|
+
|
|
358
|
+
Examples
|
|
359
|
+
--------
|
|
360
|
+
>>> ts = bond_timeseries(f7, bo_threshold=0.1)
|
|
361
|
+
"""
|
|
362
|
+
# 1) Resolve frame indices in order
|
|
363
|
+
idx_list = resolve_indices(handler, frames=frames, iterations=iterations)
|
|
364
|
+
if not idx_list:
|
|
365
|
+
return pd.DataFrame(columns=["frame_idx", "iter", "src", "dst", "bo"] if not as_wide else [])
|
|
366
|
+
|
|
367
|
+
# 2) Build edge list for selected frames (keep all reported edges; we'll threshold later)
|
|
368
|
+
edges = connection_list(
|
|
369
|
+
handler,
|
|
370
|
+
frames=idx_list,
|
|
371
|
+
iterations=None, # idx_list already chosen
|
|
372
|
+
min_bo=0.0, # keep every reported bond; we'll zero later via bo_threshold
|
|
373
|
+
undirected=undirected,
|
|
374
|
+
include_self=False,
|
|
375
|
+
)
|
|
376
|
+
# Ensure unique rows (max BO if duplicated)
|
|
377
|
+
if not edges.empty:
|
|
378
|
+
edges = (
|
|
379
|
+
edges.groupby(["frame_idx", "iter", "src", "dst"], as_index=False)["bo"]
|
|
380
|
+
.max()
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# 3) Build the full frame index (even if a frame reported no edges)
|
|
384
|
+
sim_df = handler.dataframe()
|
|
385
|
+
frame_meta = pd.DataFrame({
|
|
386
|
+
"frame_idx": idx_list,
|
|
387
|
+
"iter": [int(sim_df.iloc[i]["iter"]) for i in idx_list],
|
|
388
|
+
})
|
|
389
|
+
frame_meta = frame_meta.drop_duplicates().sort_values(["frame_idx"]).reset_index(drop=True)
|
|
390
|
+
|
|
391
|
+
# 4) If no edges at all in these frames, return zeros-only structure
|
|
392
|
+
if edges.empty:
|
|
393
|
+
if as_wide:
|
|
394
|
+
# no bonds → empty columns; just return index of frames
|
|
395
|
+
wide = frame_meta.set_index(["frame_idx", "iter"])
|
|
396
|
+
return wide
|
|
397
|
+
else:
|
|
398
|
+
# no bonds → empty tidy table
|
|
399
|
+
return pd.DataFrame(columns=["frame_idx", "iter", "src", "dst", "bo"])
|
|
400
|
+
|
|
401
|
+
# 5) Create a pivot (frames × bonds), then reindex to include *all* bonds and *all* frames
|
|
402
|
+
pivot = edges.pivot_table(
|
|
403
|
+
index=["frame_idx", "iter"],
|
|
404
|
+
columns=["src", "dst"],
|
|
405
|
+
values="bo",
|
|
406
|
+
aggfunc="max",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# All bonds observed in any selected frame
|
|
410
|
+
all_bonds: List[Tuple[int, int]] = sorted(set(zip(edges["src"], edges["dst"])))
|
|
411
|
+
# Reindex rows to ensure all frames present in the matrix (even those w/o edges)
|
|
412
|
+
pivot = pivot.reindex(
|
|
413
|
+
index=pd.MultiIndex.from_frame(frame_meta[["frame_idx", "iter"]]),
|
|
414
|
+
fill_value=0.0
|
|
415
|
+
)
|
|
416
|
+
# Reindex columns to ensure all bonds present
|
|
417
|
+
pivot = pivot.reindex(
|
|
418
|
+
columns=pd.MultiIndex.from_tuples(all_bonds, names=["src", "dst"]),
|
|
419
|
+
fill_value=0.0
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Force any remaining gaps to zero (handles rare pandas edge cases)
|
|
423
|
+
pivot = pivot.fillna(0.0)
|
|
424
|
+
|
|
425
|
+
# 6) Threshold-to-zero for small BO (noise floor)
|
|
426
|
+
if bo_threshold > 0.0:
|
|
427
|
+
pivot = pivot.mask(pivot < float(bo_threshold), 0.0)
|
|
428
|
+
|
|
429
|
+
# 7) Return in desired shape
|
|
430
|
+
if as_wide:
|
|
431
|
+
pivot.columns = [f"{s}-{d}" for (s, d) in pivot.columns.to_list()]
|
|
432
|
+
pivot = pivot.sort_index(level=[0, 1])
|
|
433
|
+
return pivot
|
|
434
|
+
|
|
435
|
+
# Robust tidy conversion
|
|
436
|
+
tidy = _pivot_to_tidy_bo(pivot)
|
|
437
|
+
return tidy
|
|
438
|
+
|
|
439
|
+
# ---- events: inline smoothing, use utils for hysteresis/flicker ----
|
|
440
|
+
def bond_events(
|
|
441
|
+
handler,
|
|
442
|
+
frames: Indexish = None,
|
|
443
|
+
iterations: Indexish = None,
|
|
444
|
+
*,
|
|
445
|
+
src: Optional[int] = None,
|
|
446
|
+
dst: Optional[int] = None,
|
|
447
|
+
threshold: float = 0.35,
|
|
448
|
+
hysteresis: float = 0.05,
|
|
449
|
+
smooth: Optional[Literal["ma","ema"]] = "ma",
|
|
450
|
+
window: int = 7,
|
|
451
|
+
ema_alpha: Optional[float] = None,
|
|
452
|
+
min_run: int = 3,
|
|
453
|
+
xaxis: Literal["iter","frame"] = "iter",
|
|
454
|
+
undirected: bool = True,
|
|
455
|
+
) -> pd.DataFrame:
|
|
456
|
+
"""Detect bond formation and breakage events from bond-order time series.
|
|
457
|
+
|
|
458
|
+
Events are identified using optional smoothing, Schmitt-trigger hysteresis,
|
|
459
|
+
and flicker suppression to avoid noise-induced toggling.
|
|
460
|
+
|
|
461
|
+
Works on
|
|
462
|
+
--------
|
|
463
|
+
Fort7Handler — ``fort.7``
|
|
464
|
+
|
|
465
|
+
Parameters
|
|
466
|
+
----------
|
|
467
|
+
handler : Fort7Handler
|
|
468
|
+
Parsed ``fort.7`` handler.
|
|
469
|
+
src, dst : int, optional
|
|
470
|
+
Restrict analysis to a specific atom pair.
|
|
471
|
+
threshold : float, default=0.35
|
|
472
|
+
Bond-order threshold for bonded state.
|
|
473
|
+
hysteresis : float, default=0.05
|
|
474
|
+
Hysteresis half-width around the threshold.
|
|
475
|
+
smooth : {"ma", "ema"}, optional
|
|
476
|
+
Smoothing method applied before event detection.
|
|
477
|
+
window : int, default=7
|
|
478
|
+
Smoothing window size.
|
|
479
|
+
min_run : int, default=3
|
|
480
|
+
Minimum run length to suppress flicker.
|
|
481
|
+
xaxis : {"iter", "frame"}, default="iter"
|
|
482
|
+
X-axis used in the output.
|
|
483
|
+
undirected : bool, default=True
|
|
484
|
+
Treat bonds as undirected.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
pandas.DataFrame
|
|
489
|
+
Event table with columns:
|
|
490
|
+
``src``, ``dst``, ``event``, ``frame_idx``, ``iter``,
|
|
491
|
+
``x_axis``, ``bo_at_event``.
|
|
492
|
+
|
|
493
|
+
Examples
|
|
494
|
+
--------
|
|
495
|
+
>>> ev = bond_events(f7, src=1, dst=2)
|
|
496
|
+
"""
|
|
497
|
+
ts = bond_timeseries(
|
|
498
|
+
handler,
|
|
499
|
+
frames=frames,
|
|
500
|
+
iterations=iterations,
|
|
501
|
+
undirected=undirected,
|
|
502
|
+
bo_threshold=0.0,
|
|
503
|
+
as_wide=False,
|
|
504
|
+
)
|
|
505
|
+
if ts.empty:
|
|
506
|
+
return pd.DataFrame(columns=["src","dst","event","frame_idx","iter","x_axis","bo_at_event","threshold","hysteresis"])
|
|
507
|
+
|
|
508
|
+
if src is not None and dst is not None:
|
|
509
|
+
a, b = (src, dst) if (not undirected or src <= dst) else (dst, src)
|
|
510
|
+
ts = ts[(ts["src"] == a) & (ts["dst"] == b)].copy()
|
|
511
|
+
if ts.empty:
|
|
512
|
+
return pd.DataFrame(columns=["src","dst","event","frame_idx","iter","x_axis","bo_at_event","threshold","hysteresis"])
|
|
513
|
+
|
|
514
|
+
groups = ts.groupby(["src","dst"], sort=False)
|
|
515
|
+
xcol = "iter" if xaxis == "iter" else "frame_idx"
|
|
516
|
+
out_rows: List[pd.DataFrame] = []
|
|
517
|
+
|
|
518
|
+
for (a, b), g in groups:
|
|
519
|
+
g = g.sort_values(["frame_idx"]).reset_index(drop=True)
|
|
520
|
+
x = g[xcol].to_numpy()
|
|
521
|
+
bo = g["bo"].to_numpy(dtype=float)
|
|
522
|
+
|
|
523
|
+
# --- inline smoothing (replaces _smooth_series) ---
|
|
524
|
+
if smooth is None:
|
|
525
|
+
bo_s = bo
|
|
526
|
+
elif smooth == "ema":
|
|
527
|
+
bo_s = exponential_moving_average(pd.Series(bo), window=window, alpha=ema_alpha, adjust=False).to_numpy()
|
|
528
|
+
else: # "ma"
|
|
529
|
+
bo_s = simple_moving_average(pd.Series(bo), window=window, center=True, min_periods=1).to_numpy()
|
|
530
|
+
|
|
531
|
+
# --- hysteresis & flicker clean via utils ---
|
|
532
|
+
st = schmitt_hysteresis(bo_s, th=threshold, hys=hysteresis)
|
|
533
|
+
st = clean_flicker(st, min_run=min_run)
|
|
534
|
+
|
|
535
|
+
prev = np.r_[st[0], st[:-1]]
|
|
536
|
+
rising = (~prev) & st
|
|
537
|
+
falling = prev & (~st)
|
|
538
|
+
mask = rising | falling
|
|
539
|
+
if not mask.any():
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
ev = pd.DataFrame({
|
|
543
|
+
xcol: x[mask],
|
|
544
|
+
"event": np.where(rising[mask], "formation", "breakage"),
|
|
545
|
+
"bo_at_event": bo_s[mask],
|
|
546
|
+
})
|
|
547
|
+
ev.insert(0, "dst", b)
|
|
548
|
+
ev.insert(0, "src", a)
|
|
549
|
+
|
|
550
|
+
merge_cols = ["frame_idx", "iter"]
|
|
551
|
+
if xcol not in merge_cols:
|
|
552
|
+
merge_cols.append(xcol)
|
|
553
|
+
meta = g[merge_cols].drop_duplicates(subset=[xcol])
|
|
554
|
+
ev = ev.merge(meta, on=xcol, how="left")
|
|
555
|
+
ev["x_axis"] = ev[xcol]
|
|
556
|
+
ev["threshold"] = float(threshold)
|
|
557
|
+
ev["hysteresis"] = float(hysteresis)
|
|
558
|
+
|
|
559
|
+
out_rows.append(ev[["src","dst","event","frame_idx","iter","x_axis","bo_at_event","threshold","hysteresis"]])
|
|
560
|
+
|
|
561
|
+
if not out_rows:
|
|
562
|
+
return pd.DataFrame(columns=["src","dst","event","frame_idx","iter","x_axis","bo_at_event","threshold","hysteresis"])
|
|
563
|
+
|
|
564
|
+
out = pd.concat(out_rows, ignore_index=True)
|
|
565
|
+
return out.sort_values(["src","dst","x_axis","event"], kind="stable").reset_index(drop=True)
|
|
566
|
+
|
|
567
|
+
def _bond_events_single(handler, src: int, dst: int, **kwargs) -> pd.DataFrame:
|
|
568
|
+
"""Convenience wrapper for bond-event detection on a single atom pair.
|
|
569
|
+
|
|
570
|
+
Works on
|
|
571
|
+
--------
|
|
572
|
+
Fort7Handler — ``fort.7``
|
|
573
|
+
|
|
574
|
+
Parameters
|
|
575
|
+
----------
|
|
576
|
+
handler : Fort7Handler
|
|
577
|
+
Parsed ``fort.7`` handler.
|
|
578
|
+
src, dst : int
|
|
579
|
+
Atom indices defining the bond.
|
|
580
|
+
|
|
581
|
+
Returns
|
|
582
|
+
-------
|
|
583
|
+
pandas.DataFrame
|
|
584
|
+
Bond formation and breakage events for the specified pair.
|
|
585
|
+
|
|
586
|
+
Examples
|
|
587
|
+
--------
|
|
588
|
+
>>> ev = _bond_events_single(f7, 1, 2)
|
|
589
|
+
"""
|
|
590
|
+
return bond_events(handler, src=src, dst=dst, **kwargs)
|
|
591
|
+
|
|
592
|
+
def debug_bond_trace_overlay(
|
|
593
|
+
handler,
|
|
594
|
+
src: int,
|
|
595
|
+
dst: int,
|
|
596
|
+
*,
|
|
597
|
+
smooth: str = "ema", # "ema" or "ma"
|
|
598
|
+
window: int = 8,
|
|
599
|
+
hysteresis: float = 0.05,
|
|
600
|
+
threshold: float = 0.10,
|
|
601
|
+
min_run: int = 0, # >=2 to match bond_events behavior
|
|
602
|
+
xaxis: str = "iter", # "iter" or "frame"
|
|
603
|
+
save: str | None = None, # file path OR directory; None -> show
|
|
604
|
+
):
|
|
605
|
+
"""Plot a diagnostic overlay for a single bond-order time series.
|
|
606
|
+
|
|
607
|
+
The plot shows raw and smoothed bond order, hysteresis bands, and
|
|
608
|
+
detected formation/breakage events. This is intended for tuning
|
|
609
|
+
event-detection parameters.
|
|
610
|
+
|
|
611
|
+
Works on
|
|
612
|
+
--------
|
|
613
|
+
Fort7Handler — ``fort.7``
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
handler : Fort7Handler
|
|
618
|
+
Parsed ``fort.7`` handler.
|
|
619
|
+
src, dst : int
|
|
620
|
+
Atom indices defining the bond.
|
|
621
|
+
smooth, window
|
|
622
|
+
Smoothing method and window size.
|
|
623
|
+
hysteresis, threshold
|
|
624
|
+
Event-detection parameters.
|
|
625
|
+
save : str, optional
|
|
626
|
+
File path to save the plot. If None, the plot is shown interactively.
|
|
627
|
+
|
|
628
|
+
Returns
|
|
629
|
+
-------
|
|
630
|
+
None
|
|
631
|
+
|
|
632
|
+
Examples
|
|
633
|
+
--------
|
|
634
|
+
>>> debug_bond_trace_overlay(f7, 1, 2, threshold=0.2)
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
# --- data ---
|
|
638
|
+
a, b = (src, dst) if src <= dst else (dst, src)
|
|
639
|
+
ts = bond_timeseries(handler, as_wide=False)
|
|
640
|
+
g = ts[(ts["src"] == a) & (ts["dst"] == b)].sort_values("iter")
|
|
641
|
+
if g.empty:
|
|
642
|
+
print(f"No data for bond {a}-{b}.")
|
|
643
|
+
return
|
|
644
|
+
|
|
645
|
+
x = g["iter"].to_numpy() if xaxis == "iter" else g["frame_idx"].to_numpy()
|
|
646
|
+
y = g["bo"].to_numpy(dtype=float)
|
|
647
|
+
|
|
648
|
+
# --- smoothing ---
|
|
649
|
+
y_s = (exponential_moving_average(y, window=window)
|
|
650
|
+
if smooth == "ema" else simple_moving_average(y, window=window)).to_numpy()
|
|
651
|
+
|
|
652
|
+
th, hys = float(threshold), float(hysteresis)
|
|
653
|
+
th_on, th_off = th + hys/2.0, th - hys/2.0
|
|
654
|
+
|
|
655
|
+
# --- hysteresis state & events on *smoothed* series ---
|
|
656
|
+
def _schmitt(sig: np.ndarray, base_th: float, band: float) -> np.ndarray:
|
|
657
|
+
on, off = base_th + band/2.0, base_th - band/2.0
|
|
658
|
+
st = np.zeros_like(sig, dtype=bool)
|
|
659
|
+
cur = sig[0] >= on
|
|
660
|
+
for i, v in enumerate(sig):
|
|
661
|
+
if not cur and v >= on:
|
|
662
|
+
cur = True
|
|
663
|
+
elif cur and v <= off:
|
|
664
|
+
cur = False
|
|
665
|
+
st[i] = cur
|
|
666
|
+
return st
|
|
667
|
+
|
|
668
|
+
st = _schmitt(y_s, th, hys)
|
|
669
|
+
if min_run and min_run > 1:
|
|
670
|
+
st = clean_flicker(st, min_run=min_run)
|
|
671
|
+
|
|
672
|
+
prev = np.r_[st[0], st[:-1]]
|
|
673
|
+
rising = (~prev) & st # formation
|
|
674
|
+
falling = prev & (~st) # breakage
|
|
675
|
+
n_form, n_break = int(rising.sum()), int(falling.sum())
|
|
676
|
+
|
|
677
|
+
# --- build series for single_plot (multi-series) ---
|
|
678
|
+
series = [
|
|
679
|
+
{'x': x, 'y': y, 'label': 'raw', 'marker': '.', 'linewidth': 0, 'markersize': 3, 'alpha': 0.75},
|
|
680
|
+
{'x': x, 'y': y_s, 'label': f'{smooth} (w={window})', 'marker': None, 'linewidth': 1.6, 'alpha': 1.0},
|
|
681
|
+
]
|
|
682
|
+
# event markers (as point-only series)
|
|
683
|
+
if n_form:
|
|
684
|
+
series.append({'x': x[rising], 'y': y_s[rising], 'label': f'formation ×{n_form}', 'marker': '^', 'linewidth': 0, 'markersize': 7, 'alpha': 1.0})
|
|
685
|
+
if n_break:
|
|
686
|
+
series.append({'x': x[falling], 'y': y_s[falling], 'label': f'breakage ×{n_break}', 'marker': 'v', 'linewidth': 0, 'markersize': 7, 'alpha': 1.0})
|
|
687
|
+
|
|
688
|
+
# horizontal ON/OFF bands
|
|
689
|
+
hlines = [
|
|
690
|
+
{'y': th_on, 'label': f'ON ≥ {th_on:.3f}', 'linestyle': '--', 'linewidth': 1},
|
|
691
|
+
{'y': th_off, 'label': f'OFF ≤ {th_off:.3f}', 'linestyle': '--', 'linewidth': 1},
|
|
692
|
+
]
|
|
693
|
+
|
|
694
|
+
title = (f"Bond {a}-{b} | th={th:.3f}, hyst={hys:.3f} | "
|
|
695
|
+
f"min_run={min_run} | events: +{n_form}/-{n_break}")
|
|
696
|
+
|
|
697
|
+
single_plot(
|
|
698
|
+
series=series,
|
|
699
|
+
hlines=hlines,
|
|
700
|
+
title=title,
|
|
701
|
+
xlabel=("iter" if xaxis == "iter" else "frame"),
|
|
702
|
+
ylabel="BO",
|
|
703
|
+
save=save,
|
|
704
|
+
legend=True,
|
|
705
|
+
figsize=(9.0, 3.8),
|
|
706
|
+
)
|