BUCToolkit 1.0b2__tar.gz → 1.0b3__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.
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py → buctoolkit-1.0b3/BUCToolkit/Bases/BaseConstraints.py +71 -220
- buctoolkit-1.0b3/BUCToolkit/Bases/BaseMotion.py +264 -0
- buctoolkit-1.0b3/BUCToolkit/Bases/__init__.py +5 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/_BaseMC.py +5 -14
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/BiasedMD.py +8 -5
- {buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md → buctoolkit-1.0b3/BUCToolkit/BatchMD}/ConstrNVE.py +17 -51
- {buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md → buctoolkit-1.0b3/BUCToolkit/BatchMD}/ConstrNVT.py +78 -54
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/NVE.py +1 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/NVT.py +8 -64
- buctoolkit-1.0b3/BUCToolkit/BatchMD/_BaseConstrMD.py +185 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/_BaseMD.py +41 -104
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/__init__.py +3 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/Dimer.py +4 -3
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_BaseOpt.py +41 -36
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/_line_search.py +286 -136
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/FIRE.py +200 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/QN.py +96 -38
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/__init__.py +2 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/BatchStructuresBase.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/StructuresIO.py +32 -24
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/write_files.py +5 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/__init__.py +19 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/ConstrainedMolecularDynamics.py +7 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/MolecularDynamics.py +6 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/MonteCarlo.py +6 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/NEB.py +6 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Predictor.py +6 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/StructureOptimization.py +6 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Trainer.py +12 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/VibrationAnalysis.py +5 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/_io.py +6 -24
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/print_logo.py +2 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/Ewald.py +8 -4
- buctoolkit-1.0b3/BUCToolkit/utils/ab_initio_wrappers.py +144 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/function_utils.py +19 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/index_ops.py +3 -3
- buctoolkit-1.0b3/BUCToolkit/utils/setup_loggers.py +56 -0
- buctoolkit-1.0b3/BUCToolkit.egg-info/PKG-INFO +21 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/SOURCES.txt +8 -6
- buctoolkit-1.0b3/BUCToolkit.egg-info/top_level.txt +3 -0
- buctoolkit-1.0b3/PKG-INFO +21 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/README.md +146 -66
- buctoolkit-1.0b3/pyproject.toml +52 -0
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/README.md +0 -4
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/__init__.py +0 -18
- buctoolkit-1.0b2/BUCToolkit/utils/setup_loggers.py +0 -137
- buctoolkit-1.0b2/BUCToolkit.egg-info/PKG-INFO +0 -28
- buctoolkit-1.0b2/BUCToolkit.egg-info/top_level.txt +0 -1
- buctoolkit-1.0b2/PKG-INFO +0 -28
- buctoolkit-1.0b2/setup.py +0 -46
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/CheckStructures.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/coords_interp.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/initial_ads_structures.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/random_replace.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/MetropolisMC.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/BiasedMD.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/NVE.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/NVT.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/_BaseMD.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/CI_NEB.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/_warnings.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/frequency.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/CG.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/README.md +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/batch.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/data.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/load_files.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/preprocessing.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/sampling.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/supercells.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/StandardTemplate.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/_version.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/DataLoaders.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Losses.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Metrics.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/ModelOptims.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/cli.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/convert_data.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/input_stub.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/main.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/template_model.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/io.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/AtomSelector.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/AtomicNumber2Properties.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/ElemListReduce.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/IrregularTensorReformat.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_CheckModules.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_Element_info.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/__init__.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_para_flatt_list.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_print_formatter.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/check_structures.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/grad_functions.py +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/dependency_links.txt +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/entry_points.txt +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/requires.txt +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/LICENSE +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/MANIFEST.in +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0b3}/setup.cfg +0 -0
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
1
|
+
# Copyright (c) 2026.4.24, BUCToolkit.
|
|
4
2
|
# Authors: Pu Pengxin, Song Xin
|
|
5
|
-
# Version:
|
|
6
|
-
# File:
|
|
3
|
+
# Version: 1.0b
|
|
4
|
+
# File: BaseConstraints.py
|
|
7
5
|
# Environment: Python 3.12
|
|
8
6
|
|
|
9
|
-
# ruff: noqa: E701, E702, E703
|
|
10
7
|
from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
|
|
11
8
|
|
|
12
9
|
import torch as th
|
|
13
10
|
from torch import nn
|
|
14
11
|
import numpy as np
|
|
15
12
|
|
|
16
|
-
from .._BaseMD import _BaseMD
|
|
17
13
|
from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
|
|
18
|
-
from BUCToolkit.utils.grad_functions import
|
|
14
|
+
from BUCToolkit.utils.grad_functions import bhvp
|
|
15
|
+
from BUCToolkit.Bases.BaseMotion import BaseIO
|
|
19
16
|
|
|
20
17
|
|
|
21
|
-
class
|
|
18
|
+
class BaseConstr(BaseIO):
|
|
22
19
|
"""
|
|
23
|
-
|
|
20
|
+
Base Constraints used for constraints MD or optimizations.
|
|
21
|
+
It can be combined into any normal gradient-based algorithms by applying `project1` to gradient or any other tangent space vectors,
|
|
22
|
+
and `project2` as a retraction mapping to coordinates or any other vectors on the manifold.
|
|
23
|
+
|
|
24
|
+
I strongly suggest that using combination with proxy instead of inheritance when it is applied in other class.
|
|
25
|
+
e.g., define following method in the class with aligning `self._constr = BaseConstr(...)` in the self.__init__ method.
|
|
26
|
+
```python
|
|
27
|
+
def __getattr__(self, name):
|
|
28
|
+
if '_constr' in self.__dict__:
|
|
29
|
+
constr = self._constr
|
|
30
|
+
if hasattr(constr, name):
|
|
31
|
+
return getattr(constr, name)
|
|
32
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
33
|
+
```
|
|
24
34
|
|
|
25
35
|
Args:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
constr_func: Callable, a tuple of Python functions as the constraint functions s_k(X) that map R^n -> R^k.
|
|
37
|
+
It takes one or more arguments, one of which must be a Tensor, and returns one Tensor with shape (k, ).
|
|
38
|
+
`None` for constant function that always return [0., ]. It should support auto-gradient ops. see example below.
|
|
39
|
+
constr_val: Callable[th.Tensor[1], th.Tensor] | th.Tensor, the constraint value of `constr_func`,
|
|
40
|
+
i.e., constraints are `constr_func(X) = constr_val`.
|
|
41
|
+
By defining it as a callable constr_val = constr_val(t) where `t` is a scalar Tensor,
|
|
42
|
+
it can be set to the time-dependent constraints.
|
|
32
43
|
constr_threshold: float, the threshold of constraint convergence (error of manifold violation)
|
|
33
|
-
output_structures_per_step: int, output structures per output_structures_per_step steps.
|
|
34
44
|
device: str|torch.device, device that program rum on.
|
|
35
|
-
verbose: int, control the detailed degree of output information.
|
|
45
|
+
verbose: int, control the detailed degree of output information.
|
|
46
|
+
0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
|
|
36
47
|
|
|
37
48
|
Examples for constrains:
|
|
38
49
|
def constr_func(X):
|
|
@@ -59,29 +70,22 @@ class _rConstrBase(_BaseMD):
|
|
|
59
70
|
"""
|
|
60
71
|
def __init__(
|
|
61
72
|
self,
|
|
62
|
-
time_step: float,
|
|
63
|
-
max_step: int,
|
|
64
|
-
T_init: float = 298.15,
|
|
65
73
|
constr_func: Callable | None = None,
|
|
66
74
|
constr_val: Callable[[th.Tensor], th.Tensor|Tuple[th.Tensor]] | th.Tensor | None = None,
|
|
67
75
|
constr_threshold: float = 1e-5,
|
|
68
|
-
output_file: str | None = None,
|
|
69
|
-
output_structures_per_step: int = 1,
|
|
70
76
|
device: str | th.device = 'cpu',
|
|
71
|
-
verbose: int =
|
|
77
|
+
verbose: int = 0
|
|
72
78
|
):
|
|
73
|
-
super().__init__(
|
|
74
|
-
time_step,
|
|
75
|
-
max_step,
|
|
76
|
-
T_init,
|
|
77
|
-
output_file,
|
|
78
|
-
output_structures_per_step,
|
|
79
|
-
device,
|
|
80
|
-
verbose
|
|
81
|
-
)
|
|
82
79
|
if constr_func is None:
|
|
83
80
|
constr_func = lambda X: th.tensor(0.)
|
|
84
81
|
self._lazy_calc_constr_val = False # use the value of constr_func(X_init) to determine constr_val (and fixed).
|
|
82
|
+
self.device = th.device(device)
|
|
83
|
+
|
|
84
|
+
self.time_now = th.scalar_tensor(0., device=device)
|
|
85
|
+
self.is_const_constr = False
|
|
86
|
+
self.constr_val_func_raw = None
|
|
87
|
+
self.time_step = 1.
|
|
88
|
+
|
|
85
89
|
if isinstance(constr_val, th.Tensor):
|
|
86
90
|
self.is_const_constr = True
|
|
87
91
|
self.constr_val_func_raw = None
|
|
@@ -110,12 +114,18 @@ class _rConstrBase(_BaseMD):
|
|
|
110
114
|
self.constr_thres = constr_threshold
|
|
111
115
|
self.lamb = None # constr force, i.e., the mu of Lagrange multipler
|
|
112
116
|
|
|
117
|
+
self._X_qr_cache = None # check the consistency of X in project1, ensuring it is the new tangent space projection
|
|
118
|
+
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.init_logger('Main.Constraints')
|
|
121
|
+
self.verbose = int(verbose)
|
|
122
|
+
|
|
113
123
|
def initialize(
|
|
114
124
|
self,
|
|
115
125
|
func: Any | nn.Module,
|
|
116
126
|
X: th.Tensor,
|
|
117
|
-
Element_list: List[List[str]] | List[List[int]],
|
|
118
|
-
masses: th.Tensor,
|
|
127
|
+
Element_list: List[List[str]] | List[List[int]] | None,
|
|
128
|
+
masses: th.Tensor | None,
|
|
119
129
|
V_init: th.Tensor | None = None,
|
|
120
130
|
grad_func: Any | nn.Module = None,
|
|
121
131
|
func_args: Sequence = tuple(),
|
|
@@ -128,55 +138,28 @@ class _rConstrBase(_BaseMD):
|
|
|
128
138
|
fixed_atom_tensor: Optional[th.Tensor] = None,
|
|
129
139
|
is_fix_mass_center: bool = False
|
|
130
140
|
):
|
|
141
|
+
if masses is None: masses = th.ones_like(X)
|
|
131
142
|
self.sqrtM = th.sqrt(masses) # M^1/2, (n_batch, n_atoms, n_dim)
|
|
132
143
|
self.negsqrtM = 1 / th.sqrt(masses) # M^-1/2
|
|
133
144
|
_y_check = th.vmap(self.constr_func)(X)
|
|
134
145
|
if self._lazy_calc_constr_val:
|
|
135
146
|
self.constr_val_now = _y_check
|
|
147
|
+
if self.verbose:
|
|
148
|
+
self.logger.info(
|
|
149
|
+
f'Constraint values are now {np.array2string(self.constr_val_now.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}'
|
|
150
|
+
)
|
|
136
151
|
# check constr_val shape
|
|
137
152
|
if _y_check.shape != self.constr_val_now.shape:
|
|
138
153
|
raise RuntimeError(
|
|
139
154
|
f'`constr_val` must have the same shape as what constr_func returned {self.constr_val_now.shape}, but got {_y_check.shape}.'
|
|
140
155
|
)
|
|
141
156
|
jac, y = self._jacobian(X)
|
|
142
|
-
if self.verbose:
|
|
143
|
-
self.logger.info(
|
|
144
|
-
f'Constraint values are now {np.array2string(self.constr_val_now.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}'
|
|
145
|
-
)
|
|
146
157
|
if y.ndim != 2:
|
|
147
158
|
raise ValueError(f'`constr_func` must return a 2D tensor of shape (n_batch, n_constr), but got {y.shape}.')
|
|
148
159
|
self._do_qr(jac)
|
|
149
|
-
ProjV = self._project1(V_init)
|
|
150
|
-
Ek = th.sum(
|
|
151
|
-
masses * V_init ** 2,
|
|
152
|
-
dim=(-2, -1),
|
|
153
|
-
keepdim=True
|
|
154
|
-
)
|
|
155
|
-
Ek_p = th.sum(
|
|
156
|
-
masses * ProjV ** 2,
|
|
157
|
-
dim=(-2, -1),
|
|
158
|
-
keepdim=True
|
|
159
|
-
)
|
|
160
|
-
V_init.copy_(th.where(Ek_p < 1e-5, 0., th.sqrt(Ek/Ek_p) * ProjV))
|
|
161
|
-
self.free_degree -= jac.shape[1] # reduce the constr. free deg.
|
|
162
|
-
# recalculate target Ek under constraints
|
|
163
|
-
_, n_atom, n_dim = X.shape
|
|
164
|
-
# target kinetic energy for NVT|NPT ensembles
|
|
165
|
-
if batch_indices: # Unit: eV/atom. Boltzmann constant kB = 8.6173332621e-5 eV/K
|
|
166
|
-
self.EK_TARGET = th.tensor(
|
|
167
|
-
[((self.free_degree / 2.) * 8.617333262145e-5 * self.T_init) for _n_atom in batch_indices],
|
|
168
|
-
dtype=X.dtype,
|
|
169
|
-
device=self.device
|
|
170
|
-
)
|
|
171
|
-
else:
|
|
172
|
-
self.EK_TARGET = (self.free_degree / 2.) * 8.617333262145e-5 * self.T_init
|
|
173
160
|
# calc. constr. intensity
|
|
174
161
|
n_batch, n_constr, _ = self.R.shape
|
|
175
162
|
self.lamb = th.zeros((n_batch, n_constr), device=self.device)
|
|
176
|
-
# debug
|
|
177
|
-
#self._debug_X_check = list()
|
|
178
|
-
#self._debug_V_check = list()
|
|
179
|
-
# ...
|
|
180
163
|
|
|
181
164
|
def _constr_func_wrapped(self, X, constr_val_now):
|
|
182
165
|
"""
|
|
@@ -274,6 +257,12 @@ class _rConstrBase(_BaseMD):
|
|
|
274
257
|
n_batch, n_atoms, n_dim = X.shape
|
|
275
258
|
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
276
259
|
negsqrtM = self.negsqrtM # M^-1/2
|
|
260
|
+
# check consistency
|
|
261
|
+
if (self._X_qr_cache is None) or (not th.allclose(X, self._X_qr_cache)):
|
|
262
|
+
jac, y = self._jacobian(X)
|
|
263
|
+
self._do_qr(jac)
|
|
264
|
+
self._X_qr_cache = X.clone()
|
|
265
|
+
|
|
277
266
|
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
278
267
|
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
279
268
|
Px = (sqrtM * X).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
@@ -301,6 +290,12 @@ class _rConstrBase(_BaseMD):
|
|
|
301
290
|
n_batch, n_atoms, n_dim = X.shape
|
|
302
291
|
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
303
292
|
negsqrtM = self.negsqrtM # M^-1/2
|
|
293
|
+
# check consistency
|
|
294
|
+
if (self._X_qr_cache is None) or (not th.allclose(X, self._X_qr_cache)):
|
|
295
|
+
jac, y = self._jacobian(X)
|
|
296
|
+
self._do_qr(jac)
|
|
297
|
+
self._X_qr_cache = X
|
|
298
|
+
|
|
304
299
|
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
305
300
|
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
306
301
|
Px = (sqrtM * X).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
@@ -327,15 +322,15 @@ class _rConstrBase(_BaseMD):
|
|
|
327
322
|
# update Jacobian
|
|
328
323
|
jac, y = self._jacobian(X)
|
|
329
324
|
# print
|
|
330
|
-
constr_err = th.max(th.abs(y))
|
|
325
|
+
constr_err = th.max(th.abs(y))
|
|
331
326
|
if self.verbose > 0:
|
|
332
327
|
self.logger.info(f'{i: < 3d} Constraint errors are now: {constr_err:.4e}')
|
|
333
328
|
# threshold
|
|
334
|
-
if constr_err < self.constr_thres:
|
|
329
|
+
if th.all(constr_err < self.constr_thres):
|
|
335
330
|
# constr. forces lambda
|
|
336
331
|
Fc = th.linalg.solve_triangular(self.R, _lamb, upper=True)
|
|
337
|
-
# unit conversion
|
|
338
332
|
Fc *= 207.28617 / (self.time_step ** 2) # convert g/mol Angstrom^2 fs^-2 to eV/Atom
|
|
333
|
+
self._X_qr_cache = X.clone()
|
|
339
334
|
return Fc
|
|
340
335
|
# QR factor.
|
|
341
336
|
self._do_qr(jac)
|
|
@@ -345,75 +340,20 @@ class _rConstrBase(_BaseMD):
|
|
|
345
340
|
# (n_batch, n_atoms, n_dim) * (n_batch, n_atoms * n_dim, n_constr) @ (n_batch, n_constr, 1)
|
|
346
341
|
corr = self.negsqrtM * (self.Q @ _lamb).reshape(n_batch, n_atoms, n_dim)
|
|
347
342
|
X.add_(corr, alpha=-1.)
|
|
348
|
-
#V.add_(corr/self.time_step, alpha=-1.)
|
|
349
343
|
|
|
350
344
|
# if not converged
|
|
351
345
|
self.logger.warning("Projection of X to the manifold is not converged.")
|
|
352
346
|
# constr. forces lambda
|
|
353
347
|
Fc = th.linalg.solve_triangular(self.R, _lamb, upper=True)
|
|
354
|
-
|
|
348
|
+
######################
|
|
349
|
+
# FOR MD, the constraint force used for calculate the free energy gradient should be further processed by
|
|
350
|
+
# Fc *= 207.28617 / (self.time_step ** 2) # convert g/mol Angstrom^2 fs^-2 to eV/Atom
|
|
351
|
+
######################
|
|
355
352
|
Fc *= 207.28617 / (self.time_step ** 2) # convert g/mol Angstrom^2 fs^-2 to eV/Atom
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def _project1_std(self, X:th.Tensor) -> th.Tensor:
|
|
359
|
-
"""
|
|
360
|
-
Compute the projector for 1st-order quantity (e.g. velocity) of all constrains by QR factorization.
|
|
361
|
-
for Jacobian matrix J of constraints s_k at X, let J^T M^-1/2 = QR, thus s_k(X + v * dt) = s_k(X) + R^T Q^T M^1/2 v * dt + O(dt^2)
|
|
362
|
-
define the projector: P = M^1/2 Q_2 Q_2^T M^-1/2 = M^1/2 (I - Q_1 Q_1^T) M^-1/2, where Q = [Q_1, Q_2], Q_1 is from the financial QRF.
|
|
363
|
-
Args:
|
|
364
|
-
X: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
365
|
-
|
|
366
|
-
Returns: th.Tensor, the projected X.
|
|
367
|
-
|
|
368
|
-
"""
|
|
369
|
-
n_batch, n_atoms, n_dim = X.shape
|
|
370
|
-
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
371
|
-
negsqrtM = self.negsqrtM # M^-1/2
|
|
372
|
-
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
373
|
-
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
374
|
-
Px = th.diag_embed(self.sqrtM.flatten(-2, -1), dim1=1, dim2=2) @ X.reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
375
|
-
Px = (Q @ (Q.mT.contiguous() @ Px)) # (n_batch, n_atoms, n_dim)
|
|
376
|
-
Px = th.diag_embed(self.negsqrtM.flatten(-2, -1), dim1=1, dim2=2) @ Px
|
|
377
|
-
Px = X - Px.reshape(n_batch, n_atoms, n_dim)
|
|
378
|
-
# manifold veloc. correction
|
|
379
|
-
if not self.is_const_constr: # time-dependent constraints
|
|
380
|
-
th.linalg.solve_triangular(self.R.mT.contiguous(), self.d_constr, upper=False, out=self.q)
|
|
381
|
-
Px.add_(self.negsqrtM * (self.Q @ self.q.unsqueeze(-1)).reshape(n_batch, n_atoms, n_dim))
|
|
353
|
+
self._X_qr_cache = X.clone()
|
|
382
354
|
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
def _project2_std(self, X:th.Tensor) -> None:
|
|
386
|
-
"""
|
|
387
|
-
Compute position corrections by directly solving JM^-1J^T q = s(X)
|
|
388
|
-
Args:
|
|
389
|
-
X:
|
|
390
|
-
|
|
391
|
-
Returns:
|
|
392
|
-
|
|
393
|
-
"""
|
|
394
|
-
n_batch, n_atoms, n_dim = X.shape
|
|
395
|
-
for i in range(0, self.max_proj_iter):
|
|
396
|
-
# update Jacobian
|
|
397
|
-
jac, y = self._jacobian(X) # jac: (n_batch, n_constr, n_atom, n_dim)
|
|
398
|
-
# print
|
|
399
|
-
constr_err = th.max(th.abs(y)).item()
|
|
400
|
-
lamb = 0.
|
|
401
|
-
if self.verbose > 0:
|
|
402
|
-
self.logger.info(f'{i: < 3d} Constraint errors are now: {constr_err:.4e}')
|
|
403
|
-
# threshold
|
|
404
|
-
if constr_err < self.constr_thres:
|
|
405
|
-
return
|
|
406
|
-
# solve J M^-1 J^T q = s
|
|
407
|
-
Z = jac*self.negsqrtM # TODO
|
|
408
|
-
# Q(n_batch, n_atoms * n_dim, n_constr)
|
|
409
|
-
corr = th.linalg.solve_triangular(self.R.mT.contiguous(), y.unsqueeze(-1) , upper=False) # (n_batch, n_constr, 1)
|
|
410
|
-
# (n_batch, n_atoms, n_dim) * (n_batch, n_atoms * n_dim, n_constr) @ (n_batch, n_constr, 1)
|
|
411
|
-
corr = self.negsqrtM * (self.Q @ corr).reshape(n_batch, n_atoms, n_dim)
|
|
412
|
-
X.add_(corr, alpha=-1.)
|
|
413
|
-
#V.add_(corr/self.time_step, alpha=-1.)
|
|
355
|
+
return Fc
|
|
414
356
|
|
|
415
|
-
# if not converged
|
|
416
|
-
self.logger.warning("Projection of X to the manifold is not converged.")
|
|
417
357
|
|
|
418
358
|
def _jacobian_derivative(self, X, v: th.Tensor) -> th.Tensor:
|
|
419
359
|
"""
|
|
@@ -504,7 +444,6 @@ class _rConstrBase(_BaseMD):
|
|
|
504
444
|
# # (n_f, n_f) @ (n_f, n_constr) @ (n_constr, 1) = (n_f, 1)
|
|
505
445
|
# Gamma = M_sqrt_inv @ Q_mid @ _y
|
|
506
446
|
#
|
|
507
|
-
# # 带松弛的更新
|
|
508
447
|
# v_candidate = v_old - dt * Gamma.reshape(n_atom, n_dim)
|
|
509
448
|
# v_next = omega * v_candidate + (1 - omega) * v_new
|
|
510
449
|
#
|
|
@@ -517,91 +456,3 @@ class _rConstrBase(_BaseMD):
|
|
|
517
456
|
#
|
|
518
457
|
#print('Not Converged !')
|
|
519
458
|
#return v_new
|
|
520
|
-
|
|
521
|
-
def implicit_midpoint_step(
|
|
522
|
-
self,
|
|
523
|
-
X0,
|
|
524
|
-
X,
|
|
525
|
-
V0,
|
|
526
|
-
V,
|
|
527
|
-
F,
|
|
528
|
-
lamb,
|
|
529
|
-
func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
|
|
530
|
-
masses, atom_masks, is_grad_func_contain_y,
|
|
531
|
-
):
|
|
532
|
-
"""
|
|
533
|
-
Solve manifold dynamics by implicit midpoint method.
|
|
534
|
-
Here the input X0, V0 are at the endpoint in last step,
|
|
535
|
-
X, V are the predicted endpoint in this step,
|
|
536
|
-
input F (the potential forces) and lamb (the constraint forces) are at the midpoint of last step,
|
|
537
|
-
and returned X_mid, V_mid, Energy, Force and lamb are all at the **midpoint**.
|
|
538
|
-
The X, V in the end point are updated **in-place** to the input variables.
|
|
539
|
-
Args:
|
|
540
|
-
X:
|
|
541
|
-
X0:
|
|
542
|
-
V:
|
|
543
|
-
V0:
|
|
544
|
-
F:
|
|
545
|
-
lamb:
|
|
546
|
-
func:
|
|
547
|
-
grad_func_:
|
|
548
|
-
func_args:
|
|
549
|
-
func_kwargs:
|
|
550
|
-
grad_func_args:
|
|
551
|
-
grad_func_kwargs:
|
|
552
|
-
masses:
|
|
553
|
-
atom_masks:
|
|
554
|
-
is_grad_func_contain_y:
|
|
555
|
-
|
|
556
|
-
Returns:
|
|
557
|
-
|
|
558
|
-
"""
|
|
559
|
-
n_batch, n_atoms, n_dim = X.shape
|
|
560
|
-
# MAIN LOOP
|
|
561
|
-
is_converged = False
|
|
562
|
-
for i in range(self.max_proj_iter):
|
|
563
|
-
X_mid = 0.5 * (X0 + X)
|
|
564
|
-
V_mid = 0.5 * (V0 + V)
|
|
565
|
-
# update Jacobian & QR factor.
|
|
566
|
-
jac, y = self._jacobian(X_mid) # (n_batch, n_constr, n_atoms, n_dim)
|
|
567
|
-
self._do_qr(jac)
|
|
568
|
-
# 1st forces eval
|
|
569
|
-
with th.set_grad_enabled(self.require_grad):
|
|
570
|
-
X_mid.requires_grad_(self.require_grad)
|
|
571
|
-
E_mid = func(X_mid, *func_args, **func_kwargs)
|
|
572
|
-
if is_grad_func_contain_y:
|
|
573
|
-
F_mid = - grad_func_(X_mid, E_mid, *grad_func_args, **grad_func_kwargs) * atom_masks
|
|
574
|
-
else:
|
|
575
|
-
F_mid = - grad_func_(X_mid, *grad_func_args, **grad_func_kwargs) * atom_masks
|
|
576
|
-
# residuals
|
|
577
|
-
f_constr = th.einsum('bijk, bi -> bjk', jac, lamb)
|
|
578
|
-
RX = X - X0 - V_mid * self.time_step
|
|
579
|
-
RV = V - V0 - (F_mid + f_constr) / masses * self.time_step * 9.64853329045427e-3
|
|
580
|
-
# check thres.
|
|
581
|
-
norm_X, norm_V, norm_s = th.linalg.norm(RX), th.linalg.norm(RV), th.linalg.norm(y)
|
|
582
|
-
if self.verbose > 0:
|
|
583
|
-
self.logger.info(
|
|
584
|
-
f'{i} residuals X, V, constraints: {norm_X.item():.4e}, {norm_V.item():.4e}, {norm_s.item():.4e}'
|
|
585
|
-
)
|
|
586
|
-
if (norm_X < self.constr_thres) and (norm_V < self.constr_thres) and (norm_s < self.constr_thres):
|
|
587
|
-
is_converged = True
|
|
588
|
-
break
|
|
589
|
-
|
|
590
|
-
# update V & X by quasi-Newton
|
|
591
|
-
tinvR_y = th.linalg.solve_triangular(self.R.mT.contiguous(), y.unsqueeze(-1) , upper=False) # R^-T y : (n_batch, n_constr, 1)
|
|
592
|
-
d_lambda = (
|
|
593
|
-
- 4./self.time_step * tinvR_y.squeeze(-1)
|
|
594
|
-
+ 2./self.time_step * th.einsum('bij, bi -> bj', self.Q, (self.sqrtM * RX).flatten(-2, -1))
|
|
595
|
-
+ th.einsum('bij, bi -> bj', self.Q, (self.sqrtM * (RV - 2*V_mid)).flatten(-2, -1))
|
|
596
|
-
)
|
|
597
|
-
d_lambda = 1./(self.time_step * 9.64853329045427e-3) * th.linalg.solve_triangular(self.R, d_lambda.unsqueeze(-1), upper=True).squeeze(-1)
|
|
598
|
-
ds = 4./self.time_step * self.negsqrtM * (self.Q @ tinvR_y).reshape(n_batch, n_atoms, n_dim)
|
|
599
|
-
dV = - ds - self._project1(RV) + 2./self.time_step * self._project_norm(RX)
|
|
600
|
-
dX = 0.5 * self.time_step * dV - RX
|
|
601
|
-
lamb.add_(d_lambda, alpha=1.)
|
|
602
|
-
X.add_(dX, alpha=1.)
|
|
603
|
-
V.add_(dV, alpha=1.)
|
|
604
|
-
if not is_converged:
|
|
605
|
-
self.logger.warning('Implicit midpoint method did not converge!')
|
|
606
|
-
|
|
607
|
-
return X_mid, V_mid, E_mid, F_mid, lamb
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Copyright (c) 2026.4.24, BUCToolkit.
|
|
2
|
+
# Authors: Pu Pengxin, Song Xin
|
|
3
|
+
# Version: 1.0b
|
|
4
|
+
# File: BaseMotion.py
|
|
5
|
+
# Environment: Python 3.12
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import warnings
|
|
10
|
+
from typing import Any, Tuple, Dict, Callable, List
|
|
11
|
+
from itertools import accumulate
|
|
12
|
+
|
|
13
|
+
import torch as th
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from BUCToolkit.BatchStructures.StructuresIO import structures_io_dumper
|
|
17
|
+
from BUCToolkit.utils.setup_loggers import has_any_handler, clear_all_handlers
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseIO:
|
|
21
|
+
"""
|
|
22
|
+
Base class for I/O operations.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, output_file: str|None = None) -> None:
|
|
25
|
+
self.logger = None
|
|
26
|
+
self.log_handler = None
|
|
27
|
+
self.output_file = str(output_file) if output_file is not None else None
|
|
28
|
+
self.dumper = structures_io_dumper(
|
|
29
|
+
path=self.output_file,
|
|
30
|
+
mode='x',
|
|
31
|
+
) # as the default. One can use `reset
|
|
32
|
+
|
|
33
|
+
def init_logger(self, logger_name: str):
|
|
34
|
+
# logging
|
|
35
|
+
if self.logger is None:
|
|
36
|
+
# cut off propagation
|
|
37
|
+
supreme_name = logger_name.split('.')[0]
|
|
38
|
+
top_logger = logging.getLogger(supreme_name)
|
|
39
|
+
top_logger.propagate = False
|
|
40
|
+
# set true logger
|
|
41
|
+
self.logger = logging.getLogger(logger_name)
|
|
42
|
+
self.logger.setLevel(logging.INFO)
|
|
43
|
+
formatter = logging.Formatter('%(message)s')
|
|
44
|
+
if not has_any_handler(top_logger):
|
|
45
|
+
self.log_handler = logging.StreamHandler(sys.stdout)
|
|
46
|
+
self.log_handler.setLevel(logging.INFO)
|
|
47
|
+
self.log_handler.setFormatter(formatter)
|
|
48
|
+
top_logger.addHandler(self.log_handler)
|
|
49
|
+
else:
|
|
50
|
+
warnings.warn('Logger has already initialized. Nothing will be done.', RuntimeWarning)
|
|
51
|
+
|
|
52
|
+
def reset_logger_handler(self, handler: str|logging.StreamHandler|logging.FileHandler, level: int = logging.INFO):
|
|
53
|
+
"""
|
|
54
|
+
Clear all logging handlers including current logger and its ancestors, and reset one.
|
|
55
|
+
Args:
|
|
56
|
+
handler: the new handler.
|
|
57
|
+
level: the logger handler output level.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
clear_all_handlers(self.logger)
|
|
63
|
+
# redirect to supreme logger
|
|
64
|
+
self.logger.setLevel(level)
|
|
65
|
+
top_logger = self.logger
|
|
66
|
+
while top_logger.parent and top_logger.propagate:
|
|
67
|
+
top_logger = top_logger.parent
|
|
68
|
+
top_logger.setLevel(level)
|
|
69
|
+
|
|
70
|
+
top_logger.setLevel(level)
|
|
71
|
+
|
|
72
|
+
formatter = logging.Formatter('%(message)s')
|
|
73
|
+
if isinstance(handler, logging.Handler):
|
|
74
|
+
self.log_handler = handler
|
|
75
|
+
elif isinstance(handler, str):
|
|
76
|
+
output_path = os.path.dirname(handler)
|
|
77
|
+
# check whether path exists
|
|
78
|
+
if not os.path.isdir(output_path): os.makedirs(output_path)
|
|
79
|
+
# set log handler
|
|
80
|
+
self.log_handler = logging.FileHandler(handler, 'w', delay=True)
|
|
81
|
+
else:
|
|
82
|
+
raise TypeError("handler must be a string path or a logging.Handler instance")
|
|
83
|
+
|
|
84
|
+
self.log_handler.setLevel(level)
|
|
85
|
+
self.log_handler.setFormatter(formatter)
|
|
86
|
+
top_logger.addHandler(self.log_handler)
|
|
87
|
+
|
|
88
|
+
def reset_dumper(self, dumper: Any) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Reset a new dumper and delete the old one.
|
|
91
|
+
Args:
|
|
92
|
+
dumper: A dumper object that satisfied the protocol as `_ArrayDumperPlaceHolder` class.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
if self.output_file is not None:
|
|
98
|
+
self.dumper.close()
|
|
99
|
+
del self.dumper
|
|
100
|
+
self.dumper = dumper
|
|
101
|
+
else:
|
|
102
|
+
self.logger.error(
|
|
103
|
+
"ERROR: No output file specified. Hence, resetting dumper is meaningless.\n"
|
|
104
|
+
"'reset_dumper': Operation REFUSED."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
class BaseMotion(BaseIO):
|
|
108
|
+
"""
|
|
109
|
+
Base class for all processes that atoms move & evolution.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, output_file: str|None = None):
|
|
113
|
+
super().__init__(output_file)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def handle_motion_mask(
|
|
117
|
+
X,
|
|
118
|
+
fixed_atom_tensor,
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
normalize format of atom_masks
|
|
122
|
+
Returns: the standardized atom_masks
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
if fixed_atom_tensor is None:
|
|
126
|
+
fixed_atom_tensor = th.ones_like(X, device=X.device)
|
|
127
|
+
else:
|
|
128
|
+
fixed_atom_tensor = fixed_atom_tensor.broadcast_to(X.shape)
|
|
129
|
+
if fixed_atom_tensor.shape == X.shape:
|
|
130
|
+
atom_masks = fixed_atom_tensor.to(X.device)
|
|
131
|
+
else:
|
|
132
|
+
raise RuntimeError(f'The shape of fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
|
|
133
|
+
|
|
134
|
+
return atom_masks
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def handle_grad_func(
|
|
138
|
+
grad_func: Callable[[th.Tensor, Any, ...], th.Tensor] | None,
|
|
139
|
+
is_grad_func_contain_y: bool,
|
|
140
|
+
require_grad: bool,
|
|
141
|
+
**kwargs
|
|
142
|
+
):
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
Returns: grad_func_, require_grad, is_grad_func_contain_y
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
if grad_func is None:
|
|
149
|
+
is_grad_func_contain_y = True
|
|
150
|
+
require_grad = True
|
|
151
|
+
def grad_func_(x, y, grad_shape=None):
|
|
152
|
+
if grad_shape is None:
|
|
153
|
+
grad_shape = th.ones_like(y)
|
|
154
|
+
_g = th.autograd.grad(y, x, grad_shape)
|
|
155
|
+
return _g[0]
|
|
156
|
+
else:
|
|
157
|
+
grad_func_ = grad_func
|
|
158
|
+
|
|
159
|
+
return grad_func_, require_grad, is_grad_func_contain_y
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def handle_batch_indices(
|
|
163
|
+
batch_indices,
|
|
164
|
+
n_batch,
|
|
165
|
+
device
|
|
166
|
+
):
|
|
167
|
+
r"""
|
|
168
|
+
Calculating `n_true_batch`, `batch_tensor`, `batch_scatter`, and `batch_slice_indx` from input batch_indices.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
batch_indices: input batch_indices that each element is the atom number of each sample: [n_0, n_1, ..., n_N].
|
|
172
|
+
n_batch: the length of 1st dimension of X.
|
|
173
|
+
device: torch device
|
|
174
|
+
|
|
175
|
+
Returns: n_true_batch, batch_tensor, batch_scatter, batch_slice_indx;
|
|
176
|
+
n_true_batch: the true batch size
|
|
177
|
+
batch_tensor: the batch_indices in torch.Tensor format
|
|
178
|
+
batch_scatter: the batch indices in the form of Tensor[0, 0, 0, ..., 1, 1, ..., N - 1]
|
|
179
|
+
batch_slice_indx: the batch indices in the form of ptr List[0, n_0, n_0 + n_1, ..., \sum n_i]
|
|
180
|
+
"""
|
|
181
|
+
if batch_indices is not None:
|
|
182
|
+
if n_batch != 1:
|
|
183
|
+
raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
|
|
184
|
+
if isinstance(batch_indices, (th.Tensor, np.ndarray)):
|
|
185
|
+
batch_indices = batch_indices.tolist()
|
|
186
|
+
elif not isinstance(batch_indices, (List, Tuple)):
|
|
187
|
+
raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
|
|
188
|
+
f'It must be List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None')
|
|
189
|
+
for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
|
|
190
|
+
batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
|
|
191
|
+
batch_tensor = th.as_tensor(batch_indices, device=device) # the tensor version of batch_indices which is a List.
|
|
192
|
+
batch_scatter = th.repeat_interleave(
|
|
193
|
+
th.arange(0, len(batch_indices), dtype=th.int64, device=device),
|
|
194
|
+
batch_tensor,
|
|
195
|
+
dim=0
|
|
196
|
+
) # scatter mask of the int tensor with the same shape as X.shape[1], which the data in one batch have one index.
|
|
197
|
+
n_true_batch = len(batch_indices) # the true batch size for irregular batches
|
|
198
|
+
else:
|
|
199
|
+
n_true_batch = n_batch
|
|
200
|
+
batch_tensor = None
|
|
201
|
+
batch_scatter = None
|
|
202
|
+
batch_slice_indx = None
|
|
203
|
+
|
|
204
|
+
return n_true_batch, batch_tensor, batch_scatter, batch_slice_indx
|
|
205
|
+
|
|
206
|
+
def _calc_EF(
|
|
207
|
+
self,
|
|
208
|
+
X: th.Tensor,
|
|
209
|
+
func: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
210
|
+
func_args: Tuple,
|
|
211
|
+
func_kwargs: Dict,
|
|
212
|
+
grad_func_: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
213
|
+
grad_func_args: Tuple,
|
|
214
|
+
grad_func_kwargs: Dict,
|
|
215
|
+
require_grad: bool,
|
|
216
|
+
is_grad_func_contain_y: bool,
|
|
217
|
+
) -> Tuple[th.Tensor, th.Tensor]:
|
|
218
|
+
"""
|
|
219
|
+
Calculate the energy and forces. In fact, it is function value y and its NEGATIVE gradient -g.
|
|
220
|
+
|
|
221
|
+
Returns: Tuple[th.Tensor, th.Tensor], energy and forces.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
y, g = self._calc_y_grad(
|
|
225
|
+
X,
|
|
226
|
+
func,
|
|
227
|
+
func_args,
|
|
228
|
+
func_kwargs,
|
|
229
|
+
grad_func_,
|
|
230
|
+
grad_func_args,
|
|
231
|
+
grad_func_kwargs,
|
|
232
|
+
require_grad,
|
|
233
|
+
is_grad_func_contain_y,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return y, g.neg_()
|
|
237
|
+
|
|
238
|
+
@th.compiler.disable
|
|
239
|
+
def _calc_y_grad(
|
|
240
|
+
self,
|
|
241
|
+
X: th.Tensor,
|
|
242
|
+
func: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
243
|
+
func_args: Tuple,
|
|
244
|
+
func_kwargs: Dict,
|
|
245
|
+
grad_func_: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
246
|
+
grad_func_args: Tuple,
|
|
247
|
+
grad_func_kwargs: Dict,
|
|
248
|
+
require_grad: bool,
|
|
249
|
+
is_grad_func_contain_y: bool,
|
|
250
|
+
) -> Tuple[th.Tensor, th.Tensor]:
|
|
251
|
+
"""
|
|
252
|
+
Calculate the function value y and the corresponding gradient of the y.
|
|
253
|
+
|
|
254
|
+
Returns: Tuple[th.Tensor, th.Tensor], y and the gradient of the y
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
with th.set_grad_enabled(require_grad):
|
|
258
|
+
X.requires_grad_(require_grad)
|
|
259
|
+
y = func(X, *func_args, **func_kwargs)
|
|
260
|
+
if is_grad_func_contain_y:
|
|
261
|
+
g = grad_func_(X, y, *grad_func_args, **grad_func_kwargs)
|
|
262
|
+
else:
|
|
263
|
+
g = grad_func_(X, *grad_func_args, **grad_func_kwargs)
|
|
264
|
+
return y, g
|