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.
Files changed (64) hide show
  1. {graphatoms-1.2.1 → graphatoms-1.2.2}/PKG-INFO +1 -1
  2. {graphatoms-1.2.1 → graphatoms-1.2.2}/pyproject.toml +1 -1
  3. {graphatoms-1.2.1 → graphatoms-1.2.2}/pyproject.toml.orig +1 -1
  4. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/__init__.py +1 -1
  5. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/base/__init__.py +1 -1
  6. graphatoms-1.2.2/src/graphatoms/reaction/base/move.py +17 -0
  7. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/base/rtgp.py +4 -6
  8. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/__init__.py +5 -2
  9. graphatoms-1.2.2/src/graphatoms/reaction/event/_event.py +358 -0
  10. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/adsorption.py +1 -1
  11. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/desorption.py +1 -1
  12. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/event/reaction.py +1 -1
  13. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/sysCluster.py +21 -8
  14. graphatoms-1.2.1/src/graphatoms/reaction/base/move.py +0 -14
  15. graphatoms-1.2.1/src/graphatoms/reaction/event/_event.py +0 -87
  16. {graphatoms-1.2.1 → graphatoms-1.2.2}/README.md +0 -0
  17. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/LICENSE +0 -0
  18. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/__init__.py +0 -0
  19. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/__init__.py +0 -0
  20. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/compat.py +0 -0
  21. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/arrayapi/typing.py +0 -0
  22. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/__init__.py +0 -0
  23. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_numpydantic.py +0 -0
  24. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydantic2pyarrow.py +0 -0
  25. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydanticMixin.py +0 -0
  26. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/_pydanticModel.py +0 -0
  27. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/dataclasses/graph.py +0 -0
  28. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/__init__.py +0 -0
  29. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_bond_list.py +0 -0
  30. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_distance_pairs.py +0 -0
  31. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_inner_outer.py +0 -0
  32. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/_neighbor_list.py +0 -0
  33. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/mic.py +0 -0
  34. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/rotation.py +0 -0
  35. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/geometry/sample.py +0 -0
  36. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/__init__.py +0 -0
  37. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/base.py +0 -0
  38. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/hdf5.py +0 -0
  39. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/database/sqliteASE.py +0 -0
  40. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/__init__.py +0 -0
  41. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_abc.py +0 -0
  42. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_base.py +0 -0
  43. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveFB.py +0 -0
  44. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveSP.py +0 -0
  45. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_moveSwap.py +0 -0
  46. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/_simNVT.py +0 -0
  47. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/test_mmc.py +0 -0
  48. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/test_move.py +0 -0
  49. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mcmove/zzz.py +0 -0
  50. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/mdwarpper/__init__.py +0 -0
  51. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/network/__init__.py +0 -0
  52. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/reaction/network/_base.py +0 -0
  53. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/__init__.py +0 -0
  54. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/atoms.py +0 -0
  55. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/bonds.py +0 -0
  56. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/graph.py +0 -0
  57. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/sysGas.py +0 -0
  58. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/system/system.py +0 -0
  59. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/__init__.py +0 -0
  60. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/bytestool.py +0 -0
  61. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/logger.py +0 -0
  62. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/parser.py +0 -0
  63. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/rdutils.py +0 -0
  64. {graphatoms-1.2.1 → graphatoms-1.2.2}/src/graphatoms/utils/subgraph.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphatoms
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: The Chemical Core Class for Graph Theory Analysis.
5
5
  Keywords: chemistry,graph theory,reaction,event,KMC
6
6
  Author: LiuGaoyong
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "graphatoms"
7
- version = "1.2.1"
7
+ version = "1.2.2"
8
8
  readme = "README.md"
9
9
  description = "The Chemical Core Class for Graph Theory Analysis."
10
10
  license-files = ["src/LICENSE"]
@@ -9,7 +9,7 @@ Issues = "https://github.com/LiuGaoyong/GraphAtoms/issues/"
9
9
 
10
10
  [project]
11
11
  name = "graphatoms"
12
- version = "1.2.1"
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" }]
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ruff: noqa: F401
4
4
  from graphatoms.reaction.base import RTGP, MoveABC
5
- from graphatoms.reaction.base.rtgp import (
5
+ from graphatoms.reaction.event._event import (
6
6
  DEFAULT_CHECK_MINIMA_FMAX,
7
7
  DEFAULT_CHECK_MINIMA_FQMIN,
8
8
  DEFAULT_CHECK_TS_FMAX,
@@ -1,7 +1,7 @@
1
1
  """The abstract base class for reaction classes."""
2
2
 
3
3
  from graphatoms.reaction.base.move import MoveABC
4
- from graphatoms.reaction.base.rtgp import RTGP
4
+ from graphatoms.reaction.event._event import RTGP
5
5
 
6
6
  __all__ = [
7
7
  "MoveABC",
@@ -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
- raise NotImplementedError
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
- "Desorption",
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
+ )
@@ -2,7 +2,7 @@ from typing import Self
2
2
 
3
3
  from pydantic import model_validator
4
4
 
5
- from graphatoms.reaction.base.rtgp import RTGP
5
+ from graphatoms.reaction.event._event import RTGP
6
6
 
7
7
 
8
8
  class Adsorption(RTGP):
@@ -2,7 +2,7 @@ from typing import Self
2
2
 
3
3
  from pydantic import model_validator
4
4
 
5
- from graphatoms.reaction.base.rtgp import RTGP
5
+ from graphatoms.reaction.event._event import RTGP
6
6
 
7
7
 
8
8
  class Desorption(RTGP):
@@ -2,7 +2,7 @@ from typing import Self
2
2
 
3
3
  from pydantic import model_validator
4
4
 
5
- from graphatoms.reaction.base.rtgp import RTGP
5
+ from graphatoms.reaction.event._event import RTGP
6
6
 
7
7
 
8
8
  class Reaction(RTGP):
@@ -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.__select(system, sub_idxs=idxs, movefixtag=tag)
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.__select(system, sub_idxs=idxs, movefixtag=tag)
142
+ return cls.select(system, sub_idxs=idxs, movefixtag=tag)
143
143
 
144
144
  @classmethod
145
- def __select(
145
+ def select(
146
146
  cls,
147
- sys: System,
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(Energetics.__pydantic_fields__.keys())
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
- return super().from_dict(dct | dict(move_fix_tag=movefixtag))
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