GraphAtoms 1.2.1__tar.gz → 1.2.2__tar.gz
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.
- {graphatoms-1.2.1 → graphatoms-1.2.2}/PKG-INFO +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/pyproject.toml +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/pyproject.toml.orig +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/__init__.py +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/base/__init__.py +1 -1
- graphatoms-1.2.2/src/graphatoms/reaction/base/move.py +17 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/base/rtgp.py +4 -6
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/__init__.py +5 -2
- graphatoms-1.2.2/src/graphatoms/reaction/event/_event.py +358 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/adsorption.py +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/desorption.py +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/reaction.py +1 -1
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/sysCluster.py +21 -8
- graphatoms-1.2.1/src/graphatoms/reaction/base/move.py +0 -14
- graphatoms-1.2.1/src/graphatoms/reaction/event/_event.py +0 -87
- {graphatoms-1.2.1 → graphatoms-1.2.2}/README.md +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/LICENSE +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/compat.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/typing.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_numpydantic.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydantic2pyarrow.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydanticMixin.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydanticModel.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/graph.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_bond_list.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_distance_pairs.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_inner_outer.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_neighbor_list.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/mic.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/rotation.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/sample.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/base.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/hdf5.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/sqliteASE.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_abc.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_base.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveFB.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveSP.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveSwap.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_simNVT.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/test_mmc.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/test_move.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/zzz.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mdwarpper/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/network/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/network/_base.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/atoms.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/bonds.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/graph.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/sysGas.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/system.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/__init__.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/bytestool.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/logger.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/parser.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/rdutils.py +0 -0
- {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/subgraph.py +0 -0
|
@@ -9,7 +9,7 @@ Issues = "https://github.com/LiuGaoyong/GraphAtoms/issues/"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "graphatoms"
|
|
12
|
-
version = "1.2.
|
|
12
|
+
version = "1.2.2"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
description = "The Chemical Core Class for Graph Theory Analysis."
|
|
15
15
|
authors = [{ name = "LiuGaoyong", email = "liugaoyong_88@163.com" }]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from graphatoms.system import System
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MoveABC(ABC):
|
|
8
|
+
"""The base class for all moves."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def apply(
|
|
12
|
+
self,
|
|
13
|
+
atoms: System,
|
|
14
|
+
*args,
|
|
15
|
+
**kwargs,
|
|
16
|
+
) -> Any:
|
|
17
|
+
"""Apply this move to the given atoms."""
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from abc import abstractmethod
|
|
2
1
|
from functools import cached_property
|
|
3
|
-
from pathlib import Path
|
|
4
2
|
from typing import Self, override
|
|
5
3
|
|
|
6
4
|
import numpy as np
|
|
@@ -24,10 +22,10 @@ class RTGP(OurFrozenModel, MoveABC):
|
|
|
24
22
|
G: Gas | None = None
|
|
25
23
|
P: SysGraph
|
|
26
24
|
|
|
27
|
-
@abstractmethod
|
|
28
|
-
@classmethod
|
|
29
|
-
def from_ase_trajectory(cls, traj: list[Atoms] | str | Path) -> Self:
|
|
30
|
-
|
|
25
|
+
# @abstractmethod
|
|
26
|
+
# @classmethod
|
|
27
|
+
# def from_ase_trajectory(cls, traj: list[Atoms] | str | Path) -> Self:
|
|
28
|
+
# raise NotImplementedError
|
|
31
29
|
|
|
32
30
|
########################################################################
|
|
33
31
|
# Validation for the event.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# ruff: noqa: F401
|
|
2
2
|
|
|
3
|
+
from graphatoms.reaction.event._event import Event
|
|
3
4
|
from graphatoms.reaction.event.adsorption import Adsorption
|
|
4
5
|
from graphatoms.reaction.event.desorption import Desorption
|
|
5
|
-
from graphatoms.reaction.event.reaction import ReactionER, ReactionLH
|
|
6
|
+
from graphatoms.reaction.event.reaction import Reaction, ReactionER, ReactionLH
|
|
6
7
|
|
|
7
8
|
__all__ = [
|
|
9
|
+
"Event",
|
|
8
10
|
"Adsorption",
|
|
11
|
+
"Desorption",
|
|
9
12
|
"ReactionLH",
|
|
10
13
|
"ReactionER",
|
|
11
|
-
"
|
|
14
|
+
"Reaction",
|
|
12
15
|
]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from typing import Self, override
|
|
3
|
+
|
|
4
|
+
import igraph
|
|
5
|
+
import numpy as np
|
|
6
|
+
from ase import Atoms
|
|
7
|
+
from pydantic import model_validator
|
|
8
|
+
|
|
9
|
+
from graphatoms.dataclasses import OurFrozenModel
|
|
10
|
+
from graphatoms.geometry.rotation import kabsch
|
|
11
|
+
from graphatoms.reaction.base.move import MoveABC
|
|
12
|
+
from graphatoms.system import (
|
|
13
|
+
DEFAULT_WH_HASH_DEPTH,
|
|
14
|
+
Cluster,
|
|
15
|
+
Gas,
|
|
16
|
+
SysGraph,
|
|
17
|
+
System,
|
|
18
|
+
)
|
|
19
|
+
from graphatoms.utils.bytestool import hash_string
|
|
20
|
+
|
|
21
|
+
DEFAULT_CHECK_MINIMA_FMAX = 0.05 # eV/Å
|
|
22
|
+
DEFAULT_CHECK_MINIMA_FQMIN = 30.0 # cm^-1
|
|
23
|
+
DEFAULT_CHECK_TS_FQMIN = 20.0 # cm^-1
|
|
24
|
+
DEFAULT_CHECK_TS_FMAX = 0.1 # eV/Å
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RTGP(OurFrozenModel, MoveABC):
|
|
28
|
+
R: SysGraph
|
|
29
|
+
T: SysGraph | None = None
|
|
30
|
+
G: Gas | None = None
|
|
31
|
+
P: SysGraph
|
|
32
|
+
|
|
33
|
+
# @classmethod
|
|
34
|
+
# def from_ase_trajectory(cls, traj: list[Atoms] | str | Path) -> Self:
|
|
35
|
+
# raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
########################################################################
|
|
38
|
+
# Validation for the event.
|
|
39
|
+
########################################################################
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="after")
|
|
42
|
+
def __check_something(self) -> Self:
|
|
43
|
+
self.__check_basic()
|
|
44
|
+
self.__chech_gas()
|
|
45
|
+
self.__check_ts()
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __check_basic(self) -> None:
|
|
49
|
+
assert isinstance(self.R, self.P.__class__), (
|
|
50
|
+
"The `R` and `P` should be of the same class."
|
|
51
|
+
)
|
|
52
|
+
assert self.R.check_minima(
|
|
53
|
+
fmax=DEFAULT_CHECK_MINIMA_FMAX,
|
|
54
|
+
fqmin=DEFAULT_CHECK_MINIMA_FQMIN,
|
|
55
|
+
), "The reactant should be a minima."
|
|
56
|
+
assert self.P.check_minima(
|
|
57
|
+
fmax=DEFAULT_CHECK_MINIMA_FMAX,
|
|
58
|
+
fqmin=DEFAULT_CHECK_MINIMA_FQMIN,
|
|
59
|
+
), "The product should be a minima."
|
|
60
|
+
assert self.R.is_connected, "The `R` should be a connected graph."
|
|
61
|
+
assert self.P.is_connected, "The `P` should be a connected graph."
|
|
62
|
+
assert self.R.is_periodic == self.P.is_periodic, (
|
|
63
|
+
"The `R` and `P` should be of the same periodicity."
|
|
64
|
+
)
|
|
65
|
+
assert self.R.is_orthorhombic == self.P.is_orthorhombic, (
|
|
66
|
+
"The `R` and `P` should be of the same orthorhombicity."
|
|
67
|
+
)
|
|
68
|
+
assert np.allclose(self.R.ase_cell, self.P.ase_cell), (
|
|
69
|
+
"The `R` and `P` should have the same cell."
|
|
70
|
+
)
|
|
71
|
+
assert self.R.hash != self.P.hash, (
|
|
72
|
+
"The `R` and `P` should have different hash."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert any(i is not None for i in [self.T, self.G]), (
|
|
76
|
+
"At least one of `T` and `G` should be not None."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __chech_gas(self) -> None:
|
|
80
|
+
if self.G is not None:
|
|
81
|
+
assert self.G.check_minima(
|
|
82
|
+
fmax=DEFAULT_CHECK_MINIMA_FMAX,
|
|
83
|
+
fqmin=DEFAULT_CHECK_MINIMA_FQMIN,
|
|
84
|
+
), "The gas should be a minima."
|
|
85
|
+
n = abs(len(self.P) - len(self.R))
|
|
86
|
+
assert len(self.G) == int(n), (
|
|
87
|
+
"The number of gas atoms should match the difference in "
|
|
88
|
+
"the number of atoms between the product and reactant."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
small, big = sorted([self.R, self.P], key=lambda x: len(x))
|
|
92
|
+
z: np.ndarray = np.append(small.numbers, self.G.numbers)
|
|
93
|
+
assert np.array_equal(z, big.numbers), (
|
|
94
|
+
"The combined numbers of the small `R/P` and "
|
|
95
|
+
"gas should match the numbers of the big `R/P`."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def __check_ts(self) -> None:
|
|
99
|
+
if self.T is not None:
|
|
100
|
+
assert self.T.check_ts(
|
|
101
|
+
fmax=DEFAULT_CHECK_TS_FMAX,
|
|
102
|
+
fqmin=DEFAULT_CHECK_TS_FQMIN,
|
|
103
|
+
), "The `T` should be a transition state."
|
|
104
|
+
assert isinstance(self.T, self.R.__class__), (
|
|
105
|
+
"The `R` and `T` should be of the same class."
|
|
106
|
+
)
|
|
107
|
+
assert len(self.T) == max(len(self.R), len(self.P)), (
|
|
108
|
+
"The number of atoms in the transition state "
|
|
109
|
+
"should be equal to the maximum number of "
|
|
110
|
+
"atoms in the reactant and product."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
########################################################################
|
|
114
|
+
# the magic methods for the event.
|
|
115
|
+
########################################################################
|
|
116
|
+
|
|
117
|
+
@override # for __str__ method of the base class
|
|
118
|
+
def _string(self) -> str:
|
|
119
|
+
r_fml = self.R.symbols.get_chemical_formula("metal")
|
|
120
|
+
p_fml = self.P.symbols.get_chemical_formula("metal")
|
|
121
|
+
before, after = f"{r_fml}:{self.R.hash}", f"{p_fml}:{self.P.hash}"
|
|
122
|
+
if self.G is not None:
|
|
123
|
+
gas_fml = self.G.symbols.get_chemical_formula("metal")
|
|
124
|
+
if len(self.R) < len(self.P):
|
|
125
|
+
before = f"{gas_fml} + {before}"
|
|
126
|
+
else:
|
|
127
|
+
after = f"{gas_fml} + {after}"
|
|
128
|
+
if self.T is None:
|
|
129
|
+
ts = "none"
|
|
130
|
+
else:
|
|
131
|
+
ts = self.T.symbols.get_chemical_formula("metal")
|
|
132
|
+
ts = f"{ts}:{self.T.hash}"
|
|
133
|
+
return f"{before} --> {ts} --> {after}"
|
|
134
|
+
|
|
135
|
+
@cached_property
|
|
136
|
+
@override # for __hash__ method of the base class
|
|
137
|
+
def hash(self) -> str:
|
|
138
|
+
t = self.T.hash if self.T is not None else ""
|
|
139
|
+
g = self.G.hash if self.G is not None else ""
|
|
140
|
+
v = ",".join([*sorted([self.R.hash, self.P.hash]), t, g])
|
|
141
|
+
return hash_string(v, digest_size=DEFAULT_WH_HASH_DEPTH)
|
|
142
|
+
|
|
143
|
+
def simplify(self, env_radius: float = 5.0) -> Self:
|
|
144
|
+
"""Simplify the event by removing the atoms that are not involved."""
|
|
145
|
+
graph_node_moved: set[int] = set()
|
|
146
|
+
g0: igraph.Graph = self.R.get_igraph()
|
|
147
|
+
for g1 in [i.get_igraph() for i in [self.P, self.T] if i is not None]:
|
|
148
|
+
for g2 in [g1.difference(g0), g0.difference(g1)]:
|
|
149
|
+
for e in g2.es:
|
|
150
|
+
for vid in e.tuple:
|
|
151
|
+
graph_node_moved.add(vid)
|
|
152
|
+
# print(f"graph_node_moved: {graph_node_moved}")
|
|
153
|
+
|
|
154
|
+
# Got geometry-based simplification, but it is not used for now.
|
|
155
|
+
# n = min(len(i) for i in [self.R, self.P, self.T] if i is not None)
|
|
156
|
+
# r_geom, p_geom = self.R.positions[:n, :], self.P.positions[:n, :]
|
|
157
|
+
# rp_var = np.linalg.norm(r_geom - p_geom, axis=1) < 0.05 # Angstrom
|
|
158
|
+
# if self.T is not None:
|
|
159
|
+
# t_geom = self.T.positions[:n, :]
|
|
160
|
+
# rt_var = np.linalg.norm(r_geom - t_geom, axis=1) < 0.05
|
|
161
|
+
# tp_var = np.linalg.norm(t_geom - p_geom, axis=1) < 0.05
|
|
162
|
+
# rp_var = rp_var | rt_var | tp_var
|
|
163
|
+
# geom_moved = set(np.argwhere(np.logical_not(rp_var)).flatten())
|
|
164
|
+
# print(f"geom_moved: {geom_moved}")
|
|
165
|
+
|
|
166
|
+
moved: np.ndarray = np.asarray(list(graph_node_moved), dtype=int)
|
|
167
|
+
pos = self.R.positions[moved, :].reshape(-1, len(moved), 3)
|
|
168
|
+
v = self.R.positions[:, np.newaxis, :] - pos
|
|
169
|
+
d = np.linalg.norm(v, axis=-1).min(-1)
|
|
170
|
+
sub = np.argwhere(d < env_radius).flatten()
|
|
171
|
+
|
|
172
|
+
rtgp: list[SysGraph | None] = []
|
|
173
|
+
for i in [self.R, self.T, self.G, self.P]:
|
|
174
|
+
if i is None or isinstance(i, Gas):
|
|
175
|
+
rtgp.append(i)
|
|
176
|
+
elif isinstance(i, SysGraph):
|
|
177
|
+
rtgp.append(
|
|
178
|
+
Cluster.select(
|
|
179
|
+
i,
|
|
180
|
+
sub_idxs=sub,
|
|
181
|
+
exclude_energetics=False,
|
|
182
|
+
) # type: ignore
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
raise TypeError(f"Unknown type: {type(i)}")
|
|
186
|
+
|
|
187
|
+
r, t, g, p = rtgp
|
|
188
|
+
assert isinstance(r, SysGraph)
|
|
189
|
+
assert isinstance(p, SysGraph)
|
|
190
|
+
assert g is None or isinstance(g, Gas)
|
|
191
|
+
assert t is None or isinstance(t, SysGraph)
|
|
192
|
+
return self.__class__(R=r, T=t, G=g, P=p)
|
|
193
|
+
|
|
194
|
+
def __reversed__(self) -> Self: # type: ignore
|
|
195
|
+
return self.__class__(R=self.P, G=self.G, T=self.T, P=self.R)
|
|
196
|
+
|
|
197
|
+
def __eq__(self, other) -> bool:
|
|
198
|
+
if not isinstance(other, self.__class__):
|
|
199
|
+
return False
|
|
200
|
+
elif self.hash == other.hash:
|
|
201
|
+
return False
|
|
202
|
+
else:
|
|
203
|
+
return self.R.hash == other.R.hash
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Event(RTGP):
|
|
207
|
+
"""The base class for all KMC events in the reaction process.
|
|
208
|
+
|
|
209
|
+
An event is a change of the system, which can be a reaction, a diffusion,
|
|
210
|
+
or a desorption, etc. It is defined by the change of the system, which
|
|
211
|
+
can be represented by the change of the graph.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
# @override
|
|
215
|
+
# @classmethod
|
|
216
|
+
# def from_ase_trajectory(cls, traj: list[Atoms] | str | Path) -> Self:
|
|
217
|
+
# if not isinstance(traj, list):
|
|
218
|
+
# traj = list(TrajectoryReader(traj)) # type: ignore
|
|
219
|
+
# assert isinstance(traj, list), "The trajectory must be a list."
|
|
220
|
+
# if any(not isinstance(t, Atoms) for t in traj):
|
|
221
|
+
# raise ValueError("The trajectory must be a list of ase.Atoms.")
|
|
222
|
+
# raise NotImplementedError("Adsorption is not implemented.")
|
|
223
|
+
|
|
224
|
+
@model_validator(mode="after")
|
|
225
|
+
def __check_something(self) -> Self:
|
|
226
|
+
assert any(
|
|
227
|
+
[
|
|
228
|
+
self.is_reaction_LH,
|
|
229
|
+
self.is_reaction_ER,
|
|
230
|
+
self.is_adsorption,
|
|
231
|
+
self.is_desorption,
|
|
232
|
+
]
|
|
233
|
+
), (
|
|
234
|
+
"The event should be either a reaction based on "
|
|
235
|
+
"Langmuir-Hinsher model, a reaction based on "
|
|
236
|
+
"Eley-Rideal model, an adsorption or a desorption."
|
|
237
|
+
)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
@override
|
|
241
|
+
def apply(
|
|
242
|
+
self,
|
|
243
|
+
atoms: System | Atoms,
|
|
244
|
+
*args,
|
|
245
|
+
matched_indxs: list[int] | np.ndarray | None = None,
|
|
246
|
+
**kwargs,
|
|
247
|
+
) -> tuple[Atoms, float]:
|
|
248
|
+
if matched_indxs is None:
|
|
249
|
+
assert isinstance(atoms, System), (
|
|
250
|
+
"The `atoms` should be a System "
|
|
251
|
+
+ "when `matched_indxs` is None."
|
|
252
|
+
)
|
|
253
|
+
matched_indxs = atoms.get_match_mode(self.R) # type: ignore
|
|
254
|
+
elif not isinstance(atoms, Atoms):
|
|
255
|
+
atoms = atoms.to_ase(
|
|
256
|
+
exclude_energetics=True,
|
|
257
|
+
exclude_bond_attibutes=True,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
matched_indxs = np.asarray(matched_indxs, dtype=int)
|
|
261
|
+
|
|
262
|
+
if matched_indxs.ndim == 1:
|
|
263
|
+
matched_indxs = matched_indxs.flatten()
|
|
264
|
+
assert len(matched_indxs) == len(atoms)
|
|
265
|
+
assert isinstance(atoms, Atoms)
|
|
266
|
+
atoms.info = {}
|
|
267
|
+
|
|
268
|
+
_i = np.vectorize(lambda x: np.argwhere(matched_indxs == x).item())(
|
|
269
|
+
np.arange(len(self.R))
|
|
270
|
+
)
|
|
271
|
+
rot, t, rmsd = kabsch(
|
|
272
|
+
A=self.R.positions,
|
|
273
|
+
B=atoms.positions[_i, :],
|
|
274
|
+
) # A = rotate(B) + t
|
|
275
|
+
rot_inv, t_inv = rot.inv(), -t
|
|
276
|
+
|
|
277
|
+
# Old Usage: original atoms will be rotated.
|
|
278
|
+
# # 1. geom --> geom reactant
|
|
279
|
+
# geom = rot.apply(atoms.positions) + t
|
|
280
|
+
# # 2. geom reactant --> geom product
|
|
281
|
+
# geom[_i, :] += self.P.positions - self.R.positions
|
|
282
|
+
# # 3. geom product --> result
|
|
283
|
+
# geom = rot_inv.apply(geom) + t_inv
|
|
284
|
+
|
|
285
|
+
# New Usage: original atoms will not be rotated.
|
|
286
|
+
pos_r = rot_inv.apply(self.R.positions) + t_inv
|
|
287
|
+
pos_p = rot_inv.apply(self.P.positions) + t_inv
|
|
288
|
+
pos_diff = pos_p - pos_r
|
|
289
|
+
geom = atoms.positions.copy()
|
|
290
|
+
geom[_i, :] += pos_diff
|
|
291
|
+
|
|
292
|
+
return Atoms(
|
|
293
|
+
numbers=atoms.numbers,
|
|
294
|
+
positions=geom,
|
|
295
|
+
cell=atoms.cell,
|
|
296
|
+
pbc=atoms.pbc,
|
|
297
|
+
), rmsd
|
|
298
|
+
|
|
299
|
+
elif matched_indxs.ndim == 2:
|
|
300
|
+
res_lst, rmsd_lst = [], []
|
|
301
|
+
for i in range(len(matched_indxs)):
|
|
302
|
+
res, rmsd = self.apply(
|
|
303
|
+
atoms=atoms,
|
|
304
|
+
matched_indxs=matched_indxs[i, :],
|
|
305
|
+
)
|
|
306
|
+
res_lst.append(res)
|
|
307
|
+
rmsd_lst.append(rmsd)
|
|
308
|
+
i = np.argmin(rmsd_lst)
|
|
309
|
+
return res_lst[i], rmsd_lst[i]
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
"The `matched_indxs` should be either a 1D or 2D array."
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
########################################################################
|
|
317
|
+
# Properties for checking the type of the event.
|
|
318
|
+
########################################################################
|
|
319
|
+
@property
|
|
320
|
+
def is_reaction(self) -> bool:
|
|
321
|
+
"""Whether the event is a reaction."""
|
|
322
|
+
n = int(max(len(self.R), len(self.P)))
|
|
323
|
+
return self.T is not None and len(self.T) == n
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def is_reaction_LH(self) -> bool:
|
|
327
|
+
"""Whether the event is a reaction based on Langmuir-Hinsher model."""
|
|
328
|
+
return self.is_reaction and self.G is None
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def is_reaction_ER(self) -> bool:
|
|
332
|
+
"""Whether the event is a reaction based on Eley-Rideal model."""
|
|
333
|
+
return (
|
|
334
|
+
self.is_reaction
|
|
335
|
+
and self.G is not None
|
|
336
|
+
and (
|
|
337
|
+
len(self.P) == len(self.R) + len(self.G) #
|
|
338
|
+
or len(self.R) == len(self.P) + len(self.G)
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def is_adsorption(self) -> bool:
|
|
344
|
+
"""Whether the event is an adsorption."""
|
|
345
|
+
return (
|
|
346
|
+
self.T is None
|
|
347
|
+
and self.G is not None
|
|
348
|
+
and len(self.P) == len(self.R) + len(self.G)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def is_desorption(self) -> bool:
|
|
353
|
+
"""Whether the event is a desorption."""
|
|
354
|
+
return (
|
|
355
|
+
self.T is None
|
|
356
|
+
and self.G is not None
|
|
357
|
+
and len(self.R) == len(self.P) + len(self.G)
|
|
358
|
+
)
|
|
@@ -98,7 +98,7 @@ class Cluster(SysGraph):
|
|
|
98
98
|
idxs = np.where(hop <= int(max_moved_hop + env_hop))[0]
|
|
99
99
|
hop = hop[idxs].astype(int)
|
|
100
100
|
tag = np.where(hop > max_moved_hop, -hop, hop)
|
|
101
|
-
return cls.
|
|
101
|
+
return cls.select(system, sub_idxs=idxs, movefixtag=tag)
|
|
102
102
|
|
|
103
103
|
@classmethod
|
|
104
104
|
def __select_by_distance(
|
|
@@ -139,14 +139,16 @@ class Cluster(SysGraph):
|
|
|
139
139
|
idxs = np.unique(np.where(d1 <= float(env_distance)))
|
|
140
140
|
hop = np.round(d1[idxs]).astype(int)
|
|
141
141
|
tag = np.where(d1[idxs] > max_moved_distance, -hop, hop)
|
|
142
|
-
return cls.
|
|
142
|
+
return cls.select(system, sub_idxs=idxs, movefixtag=tag)
|
|
143
143
|
|
|
144
144
|
@classmethod
|
|
145
|
-
def
|
|
145
|
+
def select(
|
|
146
146
|
cls,
|
|
147
|
-
sys:
|
|
147
|
+
sys: SysGraph,
|
|
148
148
|
sub_idxs: np.ndarray,
|
|
149
|
-
movefixtag: np.ndarray,
|
|
149
|
+
movefixtag: np.ndarray | None = None,
|
|
150
|
+
*,
|
|
151
|
+
exclude_energetics: bool = True,
|
|
150
152
|
) -> Self:
|
|
151
153
|
"""Select a Cluster object from a System object.
|
|
152
154
|
|
|
@@ -154,14 +156,19 @@ class Cluster(SysGraph):
|
|
|
154
156
|
"""
|
|
155
157
|
idxs = cls.get_index(sub_idxs, sys.natoms)
|
|
156
158
|
dct: dict[str, np.ndarray] = sys.to_dict(
|
|
159
|
+
exclude_energetics=False,
|
|
157
160
|
exclude_none=True,
|
|
158
161
|
exclude=(
|
|
159
|
-
set(
|
|
160
|
-
| set(GasMixin.__pydantic_fields__.keys())
|
|
162
|
+
set(GasMixin.__pydantic_fields__.keys())
|
|
161
163
|
| set(AtomTag.__pydantic_fields__.keys())
|
|
162
164
|
| {"coordination", "hashes"}
|
|
163
165
|
),
|
|
164
166
|
) | {"coordination": sys.CN}
|
|
167
|
+
|
|
168
|
+
dct_eng: dict[str, np.ndarray | float | None] = {}
|
|
169
|
+
for k in set(Energetics.__pydantic_fields__.keys()):
|
|
170
|
+
dct_eng[k] = dct.pop(k, None)
|
|
171
|
+
|
|
165
172
|
dct["pair"], _, pair_mask = subgraph( # type: ignore
|
|
166
173
|
subset=idxs, # type: ignore
|
|
167
174
|
edge_index=dct["pair"], # type: ignore
|
|
@@ -177,4 +184,10 @@ class Cluster(SysGraph):
|
|
|
177
184
|
dct[k] = v[idxs]
|
|
178
185
|
elif len(v) == sys.nbonds:
|
|
179
186
|
dct[k] = v[pair_mask]
|
|
180
|
-
|
|
187
|
+
if not exclude_energetics:
|
|
188
|
+
dct = dct | dct_eng # type: ignore
|
|
189
|
+
|
|
190
|
+
if movefixtag is None:
|
|
191
|
+
assert sys.move_fix_tag is not None
|
|
192
|
+
movefixtag = sys.move_fix_tag[idxs]
|
|
193
|
+
return super().from_dict(dct | {"move_fix_tag": movefixtag})
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
|
|
3
|
-
from ase.atoms import Atoms
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MoveABC(ABC):
|
|
7
|
-
"""The base class for all moves."""
|
|
8
|
-
|
|
9
|
-
@abstractmethod
|
|
10
|
-
def __call__(self, atoms: Atoms, *args, **kwargs) -> Atoms: ...
|
|
11
|
-
|
|
12
|
-
def apply(self, atoms: Atoms, *args, **kwargs) -> Atoms:
|
|
13
|
-
"""Apply this move to the given atoms."""
|
|
14
|
-
return self.__call__(atoms, *args, **kwargs)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Self, override
|
|
3
|
-
|
|
4
|
-
from ase import Atoms
|
|
5
|
-
from ase.io.trajectory import TrajectoryReader
|
|
6
|
-
from pydantic import model_validator
|
|
7
|
-
|
|
8
|
-
from graphatoms.reaction.base.rtgp import RTGP
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Event(RTGP):
|
|
12
|
-
"""The base class for all KMC events in the reaction process.
|
|
13
|
-
|
|
14
|
-
An event is a change of the system, which can be a reaction, a diffusion,
|
|
15
|
-
or a desorption, etc. It is defined by the change of the system, which
|
|
16
|
-
can be represented by the change of the graph.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
@override
|
|
20
|
-
@classmethod
|
|
21
|
-
def from_ase_trajectory(cls, traj: list[Atoms] | str | Path) -> Self:
|
|
22
|
-
if not isinstance(traj, list):
|
|
23
|
-
traj = list(TrajectoryReader(traj)) # type: ignore
|
|
24
|
-
assert isinstance(traj, list), "The trajectory must be a list."
|
|
25
|
-
if any(not isinstance(t, Atoms) for t in traj):
|
|
26
|
-
raise ValueError("The trajectory must be a list of ase.Atoms.")
|
|
27
|
-
raise NotImplementedError("Adsorption is not implemented.")
|
|
28
|
-
|
|
29
|
-
@model_validator(mode="after")
|
|
30
|
-
def __check_something(self) -> Self:
|
|
31
|
-
assert any(
|
|
32
|
-
[
|
|
33
|
-
self.is_reaction_LH,
|
|
34
|
-
self.is_reaction_ER,
|
|
35
|
-
self.is_adsorption,
|
|
36
|
-
self.is_desorption,
|
|
37
|
-
]
|
|
38
|
-
), (
|
|
39
|
-
"The event should be either a reaction based on "
|
|
40
|
-
"Langmuir-Hinsher model, a reaction based on "
|
|
41
|
-
"Eley-Rideal model, an adsorption or a desorption."
|
|
42
|
-
)
|
|
43
|
-
return self
|
|
44
|
-
|
|
45
|
-
########################################################################
|
|
46
|
-
# Properties for checking the type of the event.
|
|
47
|
-
########################################################################
|
|
48
|
-
@property
|
|
49
|
-
def is_reaction(self) -> bool:
|
|
50
|
-
"""Whether the event is a reaction."""
|
|
51
|
-
n = int(max(len(self.R), len(self.P)))
|
|
52
|
-
return self.T is not None and len(self.T) == n
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def is_reaction_LH(self) -> bool:
|
|
56
|
-
"""Whether the event is a reaction based on Langmuir-Hinsher model."""
|
|
57
|
-
return self.is_reaction and self.G is None
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def is_reaction_ER(self) -> bool:
|
|
61
|
-
"""Whether the event is a reaction based on Eley-Rideal model."""
|
|
62
|
-
return (
|
|
63
|
-
self.is_reaction
|
|
64
|
-
and self.G is not None
|
|
65
|
-
and (
|
|
66
|
-
len(self.P) == len(self.R) + len(self.G) #
|
|
67
|
-
or len(self.R) == len(self.P) + len(self.G)
|
|
68
|
-
)
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def is_adsorption(self) -> bool:
|
|
73
|
-
"""Whether the event is an adsorption."""
|
|
74
|
-
return (
|
|
75
|
-
self.T is None
|
|
76
|
-
and self.G is not None
|
|
77
|
-
and len(self.P) == len(self.R) + len(self.G)
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def is_desorption(self) -> bool:
|
|
82
|
-
"""Whether the event is a desorption."""
|
|
83
|
-
return (
|
|
84
|
-
self.T is None
|
|
85
|
-
and self.G is not None
|
|
86
|
-
and len(self.R) == len(self.P) + len(self.G)
|
|
87
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|