BUCToolkit 1.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,59 @@
1
+ """
2
+ Configs of Printing Numpy Arrays Format.
3
+
4
+ e.g., numpy.array2string(Array, **FLOAT_ARRAY_FORMAT)
5
+ """
6
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
7
+ # Authors: Pu Pengxin, Song Xin
8
+ # Version: 0.9a
9
+ # File: _print_formatter.py
10
+ # Environment: Python 3.12
11
+
12
+ import numpy as np
13
+
14
+ FLOAT_ARRAY_FORMAT = {
15
+ 'max_line_width': 1024,
16
+ 'precision': 8,
17
+ 'separator': ' ',
18
+ 'sign': ' ',
19
+ 'floatmode': 'fixed',
20
+ 'threshold': 21_4748_3647,
21
+ 'formatter': {'float_kind': lambda x: f'{x:0< 16.8f}', 'int_kind': lambda x: f'{x:> 16d}'}
22
+ }
23
+
24
+ INT_ARRAY_FORMAT = {
25
+ 'max_line_width': 1024,
26
+ 'separator': ' ',
27
+ 'sign': ' ',
28
+ 'threshold': 21_4748_3647,
29
+ 'formatter': {'int_kind': lambda x: f'{x:> 16d}'}
30
+ }
31
+
32
+ STRING_ARRAY_FORMAT = {
33
+ 'max_line_width': 1024,
34
+ 'separator': ' ',
35
+ 'sign': ' ',
36
+ 'threshold': 21_4748_3647,
37
+ 'formatter': {'str_kind': lambda x: f'{x}'}
38
+ }
39
+
40
+ SCIENTIFIC_ARRAY_FORMAT = {
41
+ 'max_line_width': 1024,
42
+ 'precision': 8,
43
+ 'separator': ' ',
44
+ 'sign': ' ',
45
+ 'floatmode': 'fixed',
46
+ 'threshold': 21_4748_3647,
47
+ 'formatter': {'float_kind': lambda x: f'{x:< .8e}', 'int_kind': lambda x: f'{x:> 16d}'}
48
+ }
49
+
50
+ GLOBAL_SCIENTIFIC_ARRAY_FORMAT = {
51
+ 'precision': 8,
52
+ 'sign': ' ',
53
+ 'floatmode': 'fixed',
54
+ 'threshold': 21_4748_3647,
55
+ 'formatter': {'float_kind': lambda x: f'{x:< .8e}', 'int_kind': lambda x: f'{x:> 16d}'}
56
+ }
57
+
58
+ def __as_formatted_str(a: float, ): return f'{a:0< 14.8f}'
59
+ AS_PRINT_COORDS = np.vectorize(__as_formatted_str, otypes='U')
@@ -0,0 +1,204 @@
1
+ """
2
+ Check whether the structures in `BatchStructures` have abnormalities which contain:
3
+ * too large Forces (if forces are not None)
4
+ * too large Coordinates
5
+ * too close distance
6
+ * too large or close distance between adsorbate and slab
7
+
8
+ """
9
+ # Copyright (c) 2025.7.30, BUCToolkit.
10
+ # Authors: Pu Pengxin, Song Xin
11
+ # Version: 0.9a
12
+ # File: check_if_converge.py
13
+ # Environment: Python 3.12
14
+ import warnings
15
+ from typing import List, Callable, Tuple, Any, Dict
16
+ import os
17
+
18
+ import numpy as np
19
+ import joblib as jb
20
+ from BUCToolkit.utils.AtomicNumber2Properties import elements_to_atomic_numbers
21
+
22
+
23
+ def check_if_converge(
24
+ data,
25
+ forces_threshold: float = 0.05,
26
+ ):
27
+ """
28
+
29
+ Args:
30
+ data: BatchStructures data.
31
+ forces_threshold: force threshold for convergence.
32
+
33
+ Returns: list of unconverged structure names.
34
+
35
+ """
36
+ # check
37
+ data.change_mode('L')
38
+ if data.Forces is None:
39
+ warnings.warn('Forces are not available, skipping.', RuntimeWarning)
40
+ return list()
41
+ unconverged_list = list()
42
+ for i, coo in enumerate(data.Forces):
43
+ if np.any(coo > forces_threshold):
44
+ unconverged_list.append(data.Sample_ids[i])
45
+
46
+ return unconverged_list
47
+
48
+ def check_if_abnormal(
49
+ data,
50
+ coo_upper_bound: float = 25.,
51
+ distance_threshold: float = 0.7,
52
+ adsorbate_indices: List[int]|None = None,
53
+ adsorbate_elements: List[str]|None = None,
54
+ exclude_indices: List[int]|None = None,
55
+ exclude_elements: List[str]|None = None,
56
+ ads_slab_dist_upper_bound: float = 5.,
57
+ ads_slab_dist_lower_bound: float = 1.3,
58
+ ):
59
+ """
60
+ Check whether the structures in `BatchStructures` have abnormalities.
61
+ Args:
62
+ data: the data of BatchStructures.
63
+ coo_upper_bound: the upper bound of coordinates.
64
+ distance_threshold: the distance threshold. distance less than it would be set to abnormal.
65
+ adsorbate_indices: atomic indices of absorbate. the selected atoms as adsorbate are the combination of `adsorbate_indices` and `adsorbate_elements`
66
+ adsorbate_elements: elements of selected adsorbates. the selected atoms as adsorbate are the combination of `adsorbate_indices` and `adsorbate_elements`
67
+ exclude_indices: the indices of atoms that do not involve in abnormal check. the REST atom will be considered as SLAB.
68
+ exclude_elements: the indices of atoms that do not involve in abnormal check. the REST atom will be considered as SLAB. it will be combination with `exclude_indices`.
69
+ ads_slab_dist_upper_bound: distance upper bound between adsorbate and slab.
70
+ ads_slab_dist_lower_bound: distance lower bound between adsorbate and slab.
71
+
72
+ Returns:
73
+
74
+ """
75
+ # TODO: setting specific cut-off distance for different atom pairs. for example, use sum of covalent radii in a pre-stored dict.
76
+ def parse_indx2mask(_indices, _elements, all_elements):
77
+ _elements = elements_to_atomic_numbers(_elements) if _elements is not None else list()
78
+ all_elements_set = set(all_elements)
79
+ _mask = np.full_like(all_elements, False, dtype=bool)
80
+ if _indices is not None:
81
+ _mask[_indices] = True
82
+ for ii, _elem in enumerate(_elements):
83
+ if _elem in all_elements_set:
84
+ _tmp_mask = (all_elements == _elem)
85
+ _mask = _mask + _tmp_mask
86
+ else:
87
+ warnings.warn(f"The given element {_elem} not in {all_elements_set}.", RuntimeWarning)
88
+
89
+ return _mask # (n_atoms, )
90
+
91
+ data.change_mode('L')
92
+ data.direct2cartesian()
93
+ data.generate_atomic_number_list()
94
+ data.generate_dist_mat()
95
+ err_config_list = list()
96
+ for i, coo in enumerate(data.Coords):
97
+ is_overflow = np.any(coo > coo_upper_bound)
98
+ is_too_close = np.any(data.Dist_mat[i] + 10. * np.eye(data.Dist_mat[i].shape[-1]) < distance_threshold)
99
+
100
+ atm_num_arr = np.asarray(data.Atomic_number_list[i])
101
+ # parse ads.
102
+ ads_mask = parse_indx2mask(adsorbate_indices, adsorbate_elements, atm_num_arr)
103
+ exclude_mask = parse_indx2mask(exclude_indices, exclude_elements, atm_num_arr)
104
+ slab_mask = np.full_like(atm_num_arr, True, dtype=bool)
105
+ slab_mask[exclude_mask] = False
106
+ slab_mask[ads_mask] = False
107
+ if np.any(ads_mask) and np.any(slab_mask):
108
+ ads_coo = coo[ads_mask]
109
+ rest_coo = coo[slab_mask]
110
+ ads_slab_dist_mat = np.linalg.norm(ads_coo[:, None, ...] - rest_coo[None, ...], axis=-1)
111
+ is_ads_too_far = np.all(ads_slab_dist_mat > ads_slab_dist_upper_bound)
112
+ is_ads_too_close = np.any(ads_slab_dist_mat < ads_slab_dist_lower_bound)
113
+ else:
114
+ is_ads_too_far = False
115
+ is_ads_too_close = False
116
+
117
+ if is_overflow or is_too_close or is_ads_too_far or is_ads_too_close:
118
+ err_config_list.append(data.Sample_ids[i])
119
+
120
+ return err_config_list
121
+
122
+ def batched_check_files(
123
+ path: str,
124
+ file_loader: Callable,
125
+ file_loader_args: Tuple[Any] = tuple(),
126
+ file_loader_kwargs: Dict[str, Any] | None =None,
127
+ file_list: List[str] | None = None,
128
+ n_core: int = 1,
129
+ is_check_convergence: bool = True,
130
+ force_threshold: float = 0.05,
131
+ is_check_configuration: bool = True,
132
+ coo_upper_bound: float = 25.,
133
+ distance_threshold: float = 0.7,
134
+ adsorbate_indices: List[int]|None = None,
135
+ adsorbate_elements: List[str]|None = None,
136
+ exclude_indices: List[int]|None = None,
137
+ exclude_elements: List[str]|None = None,
138
+ ads_slab_dist_upper_bound: float = 5.,
139
+ ads_slab_dist_lower_bound: float = 1.3,
140
+ ) -> Tuple[Dict, Dict]:
141
+ """
142
+ Check multiple files in given path in parallel.
143
+ Args:
144
+ path: the path of files to check.
145
+ file_loader: the function that takes a file path and returns an `BatchStructure` type data. All data will be loaded in memory in priori, and then checking.
146
+ file_loader_args: arguments passed to `file_loader`.
147
+ file_loader_kwargs: keyword arguments passed to `file_loader`.
148
+ file_list: the list of files to check. if None, all files in the given path are checked.
149
+ n_core: the number of CPU cores to use.
150
+ is_check_convergence: whether to check forces convergence.
151
+ force_threshold: if is_check_convergence is True, the force threshold to check.
152
+ is_check_configuration: whether to check configurations abnormalities.
153
+ coo_upper_bound: the upper bound of coordinates.
154
+ distance_threshold: the distance threshold. distance less than it would be set to abnormal.
155
+ adsorbate_indices: atomic indices of absorbate. the selected atoms as adsorbate are the combination of `adsorbate_indices` and `adsorbate_elements`
156
+ adsorbate_elements: elements of selected adsorbates. the selected atoms as adsorbate are the combination of `adsorbate_indices` and `adsorbate_elements`
157
+ exclude_indices: the indices of atoms that do not involve in abnormal check. the REST atom will be considered as SLAB.
158
+ exclude_elements: the indices of atoms that do not involve in abnormal check. the REST atom will be considered as SLAB. it will be combination with `exclude_indices`.
159
+ ads_slab_dist_upper_bound: distance upper bound between adsorbate and slab.
160
+ ads_slab_dist_lower_bound: distance lower bound between adsorbate and slab.
161
+
162
+ Returns: Two Dict of {file_name: name_of_structures_not_converged} and {file_name: name_of_abnormal_structures}
163
+
164
+ """
165
+ if file_loader_kwargs is None:
166
+ file_loader_kwargs = dict()
167
+ if file_list is None:
168
+ file_list = os.listdir(path)
169
+ file_list = [os.path.join(path, _f) for _f in file_list]
170
+
171
+ def _check_single(file_name):
172
+ data = file_loader(file_name, *file_loader_args, **file_loader_kwargs)
173
+ if is_check_convergence:
174
+ converge_info = check_if_converge(data, force_threshold)
175
+ else:
176
+ converge_info = list()
177
+ if is_check_configuration:
178
+ abnormal_info = check_if_abnormal(
179
+ data,
180
+ coo_upper_bound = coo_upper_bound,
181
+ distance_threshold = distance_threshold,
182
+ adsorbate_indices = adsorbate_indices,
183
+ adsorbate_elements = adsorbate_elements,
184
+ exclude_indices = exclude_indices,
185
+ exclude_elements = exclude_elements,
186
+ ads_slab_dist_upper_bound = ads_slab_dist_upper_bound,
187
+ ads_slab_dist_lower_bound = ads_slab_dist_lower_bound,
188
+ )
189
+ else:
190
+ abnormal_info = list()
191
+ basename = os.path.basename(file_name)
192
+ return {basename: converge_info}, {basename: abnormal_info}
193
+
194
+ _para = jb.parallel.Parallel(n_core, )
195
+ check_dict_list = _para(jb.delayed(_check_single)(filename) for filename in file_list)
196
+ if check_dict_list is None:
197
+ raise RuntimeError('Occurred None data.')
198
+ check_dict_conv = dict()
199
+ check_dict_abnormal = dict()
200
+ for _check_dict in check_dict_list:
201
+ check_dict_conv.update(_check_dict[0])
202
+ check_dict_abnormal.update(_check_dict[1])
203
+
204
+ return check_dict_conv, check_dict_abnormal
@@ -0,0 +1,147 @@
1
+ """
2
+ utils used for functions and models, including wrapper, preloader, etc.
3
+ """
4
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
5
+ # Authors: Pu Pengxin, Song Xin
6
+ # Version: 0.9a
7
+ # File: function_utils.py
8
+ # Environment: Python 3.12
9
+
10
+ from typing import Tuple, Dict, List, Any, Mapping, Sequence, Callable
11
+ from abc import ABC, abstractmethod
12
+
13
+ import torch as th
14
+ import torch.nn as nn
15
+
16
+ __all__ = ['MeanModel', 'preload_func']
17
+
18
+ class MeanModel(nn.Module):
19
+ """
20
+ calculate the mean value of multiple models as new output.
21
+
22
+ Args:
23
+ models: sequence of models
24
+ hyperparams: hyperparameters of models. if Dict, all models would share the same hyperparameters.
25
+ """
26
+ def __init__(self, models: Tuple[Any, ...] | List[Any], hyperparams: Tuple[Dict, ...] | Dict, model_output_key: Tuple[str, ...] | None = None):
27
+ super().__init__()
28
+ if isinstance(hyperparams, Sequence):
29
+ if len(models) != len(hyperparams):
30
+ raise RuntimeError(f'Number of hyperparameters {len(hyperparams)} and models {len(models)} does not match.')
31
+ self.models = nn.ModuleList([(models[_])(**hparams) for _, hparams in enumerate(hyperparams)])
32
+ elif isinstance(hyperparams, Dict):
33
+ self.models = nn.ModuleList([_model(**hyperparams) for _model in models])
34
+ self.output_key = model_output_key
35
+
36
+ def load_state_tuple(self, state_dict: Tuple[Mapping[str, Any]] | Mapping[str, Any], strict: bool = True, assign: bool = False):
37
+ """
38
+
39
+ Args:
40
+ state_dict:
41
+ strict:
42
+ assign:
43
+
44
+ Returns:
45
+
46
+ """
47
+ if not isinstance(state_dict, Mapping):
48
+ _state_dict = dict()
49
+ for i, stdict in enumerate(state_dict):
50
+ _state_dict.update({f'models.{i}.{key}': val for key, val in stdict.items()})
51
+ else:
52
+ _state_dict = state_dict
53
+ self.load_state_dict(_state_dict, strict, assign)
54
+
55
+ def forward(self, X: Any):
56
+ if self.output_key is None:
57
+ y = 0.
58
+ if self.training:
59
+ pred_list = list()
60
+ for mod in self.models:
61
+ res = mod(X)
62
+ pred_list.append(res)
63
+ return pred_list
64
+ else:
65
+ for mod in self.models:
66
+ y = y + mod(X)
67
+ else:
68
+ y = {key: 0. for key in self.output_key}
69
+ if self.training:
70
+ pred_list = list()
71
+ for mod in self.models:
72
+ res = mod(X)
73
+ pred_list.append(res)
74
+ return pred_list
75
+ else:
76
+ l = len(self.models)
77
+ for mod in self.models:
78
+ y = {key: y[key] + mod(X)[key]/l for key in self.output_key}
79
+
80
+ return y
81
+
82
+
83
+ class _BaseWrapper(ABC):
84
+
85
+ def __init__(self, model):
86
+ self._model = model
87
+ self.forces = None
88
+ self.X = None
89
+
90
+ @abstractmethod
91
+ def Energy(self, *args, **kwargs) -> th.Tensor:
92
+ pass
93
+
94
+ @abstractmethod
95
+ def Grad(self, *args, **kwargs) -> th.Tensor:
96
+ pass
97
+
98
+ def to(self, *args, **kwargs):
99
+ """
100
+ wrap the `to` method of wrapped model. If the wrapped model has not `to` method, simply passed.
101
+ Args:
102
+ *args:
103
+ **kwargs:
104
+
105
+ Returns: the obj that the wrapped model would return
106
+
107
+ """
108
+ if hasattr(self._model, 'to') and callable(self._model.to):
109
+ self._model = self._model.to(*args, **kwargs)
110
+
111
+ return self
112
+
113
+ def eval(self):
114
+ """
115
+ wrap the `eval` method of wrapped model. If the wrapped model has not `eval` method, simply passed.
116
+ Returns: None
117
+
118
+ """
119
+ if hasattr(self._model, 'eval'):
120
+ self._model.eval()
121
+
122
+
123
+ def preload_func(func: Callable, device: Any) -> Callable:
124
+ """
125
+ Apply the `func.to(device)` and `func.eval()` methods to the input function if it has such method.
126
+ if `func` is just a subclass of torch.nn.Module, `func.zero_grad()` will be called, too.
127
+ Args:
128
+ func:
129
+ device:
130
+
131
+ Returns: callable, the function self.
132
+
133
+ """
134
+ if isinstance(func, nn.Module):
135
+ func = func.to(device)
136
+ func.eval()
137
+ func.zero_grad()
138
+ else:
139
+ if hasattr(func, 'to'):
140
+ _ = func.to(device)
141
+ func = _ if _ is not None else func
142
+ if hasattr(func, 'eval'):
143
+ func.eval()
144
+ if hasattr(func, 'zero_grad'):
145
+ func.zero_grad()
146
+
147
+ return func
@@ -0,0 +1,69 @@
1
+ """ functions of gradient-related calculations. """
2
+ # Copyright (c) 2025.10.17, BUCToolkit.
3
+ # Authors: Pu Pengxin, Song Xin
4
+ # Version: 0.9a
5
+ # File: jac_and_hvp.py
6
+ # Environment: Python 3.12
7
+
8
+ import torch as th
9
+ from typing import Callable, Tuple
10
+
11
+
12
+ def jvp_wrapper(func: Callable, X:Tuple[th.Tensor, ...], v:Tuple[th.Tensor, ...], has_aux=False):
13
+ """
14
+ A function to map func, X, v to J_func_X @ v, i.e., the jvp.
15
+ Args:
16
+ func:
17
+ X:
18
+ v:
19
+ has_aux:
20
+
21
+ Returns:
22
+
23
+ """
24
+ if has_aux:
25
+ _, y, aux = th.func.jvp(func, X, v, has_aux=has_aux)
26
+ return y, aux
27
+ else:
28
+ _, y = th.func.jvp(func, X, v, has_aux=has_aux)
29
+ return y
30
+
31
+ def bjvp(func: Callable, X:Tuple[th.Tensor, ...], v:Tuple[th.Tensor, ...], has_aux=False):
32
+ """
33
+ Batched Jacobian-vector product.
34
+ Args:
35
+ func: The function to calculate the batched JVP.
36
+ X: the input of the function.
37
+ v: the vector that Jacobian product to.
38
+ has_aux: whether to output auxiliary variables.
39
+
40
+ Returns: Tuple[th.Tensor, th.Tensor] | Tuple[th.Tensor, th.Tensor, th.Tensor]
41
+ if `has_aux` is True, return function values y, jvp, aux
42
+ otherwise, return function values y and jvp.
43
+
44
+ """
45
+ if has_aux:
46
+ val, jvp, aux = th.vmap(th.func.jvp, (None, (0, ), (0, )))(func, X, v, has_aux=has_aux)
47
+ return val, jvp, aux
48
+ else:
49
+ val, jvp = th.vmap(th.func.jvp, (None, (0,), (0,)))(func, X, v, has_aux=has_aux)
50
+ return val, jvp
51
+
52
+ def bhvp(func: Callable, X:Tuple[th.Tensor, ...], v:Tuple[th.Tensor, ...], has_aux:bool=False):
53
+ """
54
+ Batched Hessian-vector product.
55
+ Args:
56
+ func: The function to calculate the batched HVP.
57
+ X: the input of the function.
58
+ v: the vector that Hessian product to.
59
+ has_aux: whether to output auxiliary variables.
60
+
61
+ Returns: th.Tensor | Tuple[th.Tensor, th.Tensor], if `has_aux` is True: hvp, jvp; otherwise, hvp.
62
+
63
+ """
64
+ if has_aux:
65
+ y_, jvp_ = th.vmap(th.func.jacrev(jvp_wrapper, argnums=1, has_aux=has_aux), (None, (0,), (0,), None))(func, X, v, has_aux)
66
+ return y_[0], jvp_
67
+ else:
68
+ y_ = th.vmap(th.func.jacrev(jvp_wrapper, argnums=1, has_aux=has_aux), (None, (0,), (0,), None))(func, X, v, has_aux)
69
+ return y_[0]