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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
BUCToolkit/io.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A fast API for all i/o operations
|
|
3
|
+
"""
|
|
4
|
+
# Copyright (c) 2026.4.13, BUCToolkit.
|
|
5
|
+
# Authors: Pu Pengxin, Song Xin
|
|
6
|
+
# Version: 1.0b
|
|
7
|
+
# File: io.py
|
|
8
|
+
# Environment: Python 3.12
|
|
9
|
+
|
|
10
|
+
from BUCToolkit.Preprocessing import load_files, write_files
|
|
11
|
+
from BUCToolkit.Preprocessing.load_files import OUTCAR2Feat, Xyz2Feat, ExtXyz2Feat, POSCARs2Feat, ASETraj2Feat, Cif2Feat, load_from_structures
|
|
12
|
+
from BUCToolkit.BatchStructures.StructuresIO import read_opt_structures, read_mc_traj, read_md_traj, ArrayDumpReader, ArrayDumper
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'OUTCAR2Feat',
|
|
16
|
+
'Xyz2Feat',
|
|
17
|
+
'ExtXyz2Feat',
|
|
18
|
+
'POSCARs2Feat',
|
|
19
|
+
'ASETraj2Feat',
|
|
20
|
+
'Cif2Feat',
|
|
21
|
+
'read_opt_structures',
|
|
22
|
+
'read_md_traj',
|
|
23
|
+
'read_mc_traj',
|
|
24
|
+
'ArrayDumpReader',
|
|
25
|
+
'ArrayDumper',
|
|
26
|
+
'load_from_structures',
|
|
27
|
+
'load_files',
|
|
28
|
+
'write_files',
|
|
29
|
+
]
|
|
30
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
""" Selecting atoms by given conditions """
|
|
2
|
+
# Copyright (c) 2025.11.26, BUCToolkit.
|
|
3
|
+
# Authors: Pu Pengxin, Song Xin
|
|
4
|
+
# Version: 0.9a
|
|
5
|
+
# File: AtomSelector.py
|
|
6
|
+
# Environment: Python 3.12
|
|
7
|
+
|
|
8
|
+
import torch as th
|
|
9
|
+
from typing import Sequence, List, Literal
|
|
10
|
+
from ._Element_info import ATOMIC_SYMBOL
|
|
11
|
+
|
|
12
|
+
def atom_fix_selector(
|
|
13
|
+
mask: th.Tensor,
|
|
14
|
+
elements: th.Tensor,
|
|
15
|
+
coords: th.Tensor,
|
|
16
|
+
select_element: Sequence[str | int] | None = None,
|
|
17
|
+
select_height: float | None = None,
|
|
18
|
+
atom_index: List[List[float]] | None = None,
|
|
19
|
+
select_mode: Literal['fix', 'inv_fix', 'free'] = 'fix'
|
|
20
|
+
) -> th.Tensor:
|
|
21
|
+
"""
|
|
22
|
+
Selecting which atoms are fixed and updating the input mask accordingly.
|
|
23
|
+
Args:
|
|
24
|
+
mask:
|
|
25
|
+
elements:
|
|
26
|
+
coords:
|
|
27
|
+
select_element:
|
|
28
|
+
select_height:
|
|
29
|
+
atom_index:
|
|
30
|
+
select_mode:
|
|
31
|
+
fix: the atoms satisfied given conditions will be set to 0 in given mask, and others do not change.
|
|
32
|
+
inv_fix: only the atoms satisfied given conditions will be set to 1 in given mask, and others set to 0.
|
|
33
|
+
free: the atoms satisfied given conditions will be set to 1 in given mask, and others do not change.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
# check
|
|
39
|
+
if mask.shape != coords.shape:
|
|
40
|
+
raise ValueError(f'Expected `mask` has the same shape as `coords` ({coords.shape}), but got {mask.shape}.')
|
|
41
|
+
if elements.shape != (mask.shape[0], ):
|
|
42
|
+
raise ValueError(f'Expected `elements` has the same shape as the 1st dim of `mask` ({mask.shape[0]}), but got {elements.shape}.')
|
|
43
|
+
|
|
44
|
+
# initialize
|
|
45
|
+
FIX_DICT = {'fix': 0, 'inv_fix': 1, 'free': 1}
|
|
46
|
+
fill_elem = FIX_DICT[select_mode]
|
|
47
|
+
if select_mode == 'inv_fix':
|
|
48
|
+
_mask = th.zeros_like(mask).to(th.int8)
|
|
49
|
+
else:
|
|
50
|
+
_mask = th.where(mask.to(th.int8) == 0, 0, 1).to(th.int8)
|
|
51
|
+
elements = elements.unsqueeze(-1).broadcast_to(mask.shape)
|
|
52
|
+
|
|
53
|
+
# main
|
|
54
|
+
if select_element is not None:
|
|
55
|
+
sel_elem = [_ if isinstance(_, int) else ATOMIC_SYMBOL[_] for _ in select_element]
|
|
56
|
+
for elem in sel_elem:
|
|
57
|
+
_mask = th.where(elements == elem, fill_elem, _mask)
|
|
58
|
+
if select_height is not None:
|
|
59
|
+
_mask = th.where(coords[:, -1].unsqueeze(-1) <= select_height, fill_elem, _mask)
|
|
60
|
+
if atom_index is not None:
|
|
61
|
+
atom_index = th.tensor(atom_index, dtype=th.int64, device=_mask.device)
|
|
62
|
+
_mask[atom_index] = fill_elem
|
|
63
|
+
|
|
64
|
+
return _mask
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
""" Convert from atomic number to various properties, e.g., masses, elements."""
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2025.7.4, BUCToolkit.
|
|
4
|
+
# Authors: Pu Pengxin, Song Xin
|
|
5
|
+
# Version: 0.9a
|
|
6
|
+
# File: AtomicNumber2Properties.py
|
|
7
|
+
# Environment: Python 3.12
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, List
|
|
10
|
+
from BUCToolkit.utils._Element_info import MASS, N_MASS, ATOMIC_SYMBOL, ATOMIC_NUMBER
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def atomic_numbers_to_masses(Z: Sequence[int]) -> List[float]:
|
|
15
|
+
"""Convert atomic numbers to masses."""
|
|
16
|
+
mass = [N_MASS[int(_)] for _ in Z]
|
|
17
|
+
return mass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def atomic_numbers_to_elements(Z: Sequence[int]) -> List[int]:
|
|
21
|
+
elem = [ATOMIC_NUMBER[int(_)] for _ in Z]
|
|
22
|
+
return elem
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def elements_to_atomic_numbers(Z: Sequence[str]) -> List[int]:
|
|
26
|
+
atomic_numbers = [ATOMIC_SYMBOL[str(_)] for _ in Z]
|
|
27
|
+
return atomic_numbers
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"atomic_numbers_to_masses",
|
|
32
|
+
"atomic_numbers_to_elements",
|
|
33
|
+
"elements_to_atomic_numbers",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
z = [1, 2, 5, 3, 66, 32]
|
|
38
|
+
m = atomic_numbers_to_masses(z)
|
|
39
|
+
pass
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Copyright (c) 2025.7.4, BUCToolkit.
|
|
2
|
+
# Authors: Pu Pengxin, Song Xin
|
|
3
|
+
# Version: 0.9a
|
|
4
|
+
# File: ElemListReduce.py
|
|
5
|
+
# Environment: Python 3.12
|
|
6
|
+
"""
|
|
7
|
+
Convert a dense element list (atomic number corresponds to coordinates one by one) into VASP-like compressed format.
|
|
8
|
+
"""
|
|
9
|
+
from typing import List, Tuple
|
|
10
|
+
from ._Element_info import ATOMIC_NUMBER, ATOMIC_SYMBOL
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def elem_list_reduce(
|
|
15
|
+
atom_list: List[str|int]|np.ndarray
|
|
16
|
+
) -> Tuple[List[str], List[int], List[int]]:
|
|
17
|
+
"""
|
|
18
|
+
Convert a dense element list (atomic number corresponds to coordinates one by one) into VASP-like compressed format.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
atom_list:
|
|
22
|
+
|
|
23
|
+
Returns: element symbol list, atomic numbers list, atom number (count) list
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(atom_list, np.ndarray): atom_list = atom_list.tolist()
|
|
27
|
+
_elem_old = ATOMIC_NUMBER[atom_list[0]] if isinstance(atom_list[0], int) else atom_list[0]
|
|
28
|
+
elements = [_elem_old, ]
|
|
29
|
+
atomic_numbers = [ATOMIC_SYMBOL[_elem_old], ]
|
|
30
|
+
elem_numbers = list()
|
|
31
|
+
count_i = 0
|
|
32
|
+
for elem in atom_list:
|
|
33
|
+
if isinstance(elem, int):
|
|
34
|
+
elem_symb = ATOMIC_NUMBER[elem]
|
|
35
|
+
elem_numb = elem
|
|
36
|
+
elif isinstance(elem, str):
|
|
37
|
+
elem_symb = elem
|
|
38
|
+
elem_numb = ATOMIC_SYMBOL[elem]
|
|
39
|
+
else:
|
|
40
|
+
raise TypeError(f'Expected str or int in `atom_list`, but got {type(elem)}.')
|
|
41
|
+
|
|
42
|
+
if elem_symb == _elem_old:
|
|
43
|
+
count_i += 1
|
|
44
|
+
else:
|
|
45
|
+
_elem_old = elem_symb
|
|
46
|
+
elem_numbers.append(count_i)
|
|
47
|
+
elements.append(elem_symb)
|
|
48
|
+
atomic_numbers.append(elem_numb)
|
|
49
|
+
count_i = 1
|
|
50
|
+
elem_numbers.append(count_i)
|
|
51
|
+
|
|
52
|
+
return elements, atomic_numbers, elem_numbers
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Copyright (c) 2026.3.17, BUCToolkit.
|
|
2
|
+
# Authors: Pu Pengxin, Song Xin
|
|
3
|
+
# Version: 0.9a
|
|
4
|
+
# File: Ewald.py
|
|
5
|
+
# Environment: Python 3.12
|
|
6
|
+
from typing import Callable, Any, List, Tuple, Optional, Dict
|
|
7
|
+
import torch as th
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeneralizedEwald:
|
|
11
|
+
"""
|
|
12
|
+
A Generalized Ewald Summation Algorithm.
|
|
13
|
+
It calculates as follows:
|
|
14
|
+
\sum_n f(r + n R) = \sum_n {f(r + n R) - (a/\pi)^1.5 \int {f(x) exp(-a (x - r - n R)^2) dx}}
|
|
15
|
+
+ 1/V \sum_k {g(k) exp(-k^2/(4 a) + i k \cdot r)}
|
|
16
|
+
"""
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
rfunc: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
20
|
+
gfunc: Callable[[th.Tensor, Any, ...], th.Tensor],
|
|
21
|
+
rfunc_gaussian_conv: Callable[[th.Tensor, Any, ...], th.Tensor] | None = None,
|
|
22
|
+
fft_grids: th.Tensor | List[int] | Tuple[int, int, int] | None = None,
|
|
23
|
+
alpha: float = 0.1,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
rfunc: the function in the real space.
|
|
29
|
+
Its first input should be a Tensor X with shape (B, N, 3), and output a Tensor with shape (B, N),
|
|
30
|
+
where `B` is the batch_size, and `N` is the number of feeler points within a sample.
|
|
31
|
+
For each N, it returns the function value of f at the given point X[B_i, N_i, :].
|
|
32
|
+
gfunc: the corresponding function in the reciprocal space. It has the same format as rfunc.
|
|
33
|
+
rfunc_gaussian_conv: the convolution between rfunc and Gaussian function,
|
|
34
|
+
i.e., rfunc_gaussian_conv(r) = (a/\pi)^1.5 \int {f(x) exp(-a (x - r)^2) dx}.
|
|
35
|
+
If None, a numeric FFT would be applied to calculate the reciprocal production instead of real-space convolution.
|
|
36
|
+
fft_grids: shape of (3, ). If `rfunc_gaussian_conv` is None, this argument is to define the FFT grids,
|
|
37
|
+
and gives the value at input point `r` by interpolation. Otherwise, this argument is ignored.
|
|
38
|
+
alpha: the coefficient of Gaussian exponential in Ewald summation.
|
|
39
|
+
"""
|
|
40
|
+
self.rfunc = rfunc
|
|
41
|
+
self.gfunc = gfunc
|
|
42
|
+
self.rgc_func = rfunc_gaussian_conv
|
|
43
|
+
if rfunc_gaussian_conv is None:
|
|
44
|
+
fft_grids = th.as_tensor(fft_grids, dtype=th.int64)
|
|
45
|
+
if fft_grids.shape != (3, ):
|
|
46
|
+
raise ValueError(f"`fft_grids` must have the shape of (3, ), but got {fft_grids.shape}.")
|
|
47
|
+
if fft_grids.min() <= 0:
|
|
48
|
+
raise ValueError(f"FFT grids number must be positive, but got {fft_grids.tolist()}.")
|
|
49
|
+
self.fft_grids = fft_grids
|
|
50
|
+
else:
|
|
51
|
+
self.fft_grids = None
|
|
52
|
+
|
|
53
|
+
if alpha <= 1e-7:
|
|
54
|
+
raise ValueError(f"`alpha` must be positive, but got {alpha}.")
|
|
55
|
+
self.alpha = alpha
|
|
56
|
+
|
|
57
|
+
def run(
|
|
58
|
+
self,
|
|
59
|
+
r: th.Tensor,
|
|
60
|
+
R: th.Tensor,
|
|
61
|
+
n_r: int = 20,
|
|
62
|
+
n_g: int | None = None,
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Run the summation.
|
|
66
|
+
Args:
|
|
67
|
+
r: Tensor(B, N, 3), the coordinates of the feeler points.
|
|
68
|
+
R: Tensor(B, 3, 3), the real-space lattice of each sample in the batch. Each row is a lattice vector.
|
|
69
|
+
n_r: the number of real-space summation terms.
|
|
70
|
+
n_g: the number of reciprocal-space summation terms. If None, it will be set to `n_r`.
|
|
71
|
+
|
|
72
|
+
Returns: Tensor(B, N), the summations.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
def _run_anal(
|
|
78
|
+
self,
|
|
79
|
+
r: th.Tensor,
|
|
80
|
+
R: th.Tensor,
|
|
81
|
+
rfunc_args: Tuple[Any, ...] = tuple(),
|
|
82
|
+
rfunc_kwargs: Dict[str, Any] | None = None,
|
|
83
|
+
gfunc_args: Tuple[Any, ...] = tuple(),
|
|
84
|
+
gfunc_kwargs: Dict[str, Any] | None = None,
|
|
85
|
+
n_r: int = 20,
|
|
86
|
+
n_g: Optional[int] = None,
|
|
87
|
+
chunk_size: int = 10000, # 分块大小,避免内存溢出
|
|
88
|
+
) -> th.Tensor:
|
|
89
|
+
"""
|
|
90
|
+
Run the Ewald summation using analytical convolution function.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
r: (B, N, 3) query points.
|
|
94
|
+
R: (B, 3, 3) real-space lattice matrices (each row is a lattice vector).
|
|
95
|
+
n_r: number of real-space cells in each direction (half-width). Total cells = (2*n_r+1)^3.
|
|
96
|
+
rfunc_args:
|
|
97
|
+
rfunc_kwargs:
|
|
98
|
+
gfunc_args:
|
|
99
|
+
gfunc_kwargs:
|
|
100
|
+
n_g: number of reciprocal-space cells in each direction. If None, set to n_r.
|
|
101
|
+
chunk_size: max number of real-space cells processed at once to control memory.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
(B, N) tensor of summed values.
|
|
105
|
+
"""
|
|
106
|
+
if rfunc_kwargs is None:
|
|
107
|
+
rfunc_kwargs = {}
|
|
108
|
+
if gfunc_kwargs is None:
|
|
109
|
+
gfunc_kwargs = {}
|
|
110
|
+
|
|
111
|
+
device = r.device
|
|
112
|
+
B, N, _ = r.shape
|
|
113
|
+
n_g = n_g if n_g is not None else n_r
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# 1. Real-space part: sum over lattice vectors nR
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Generate integer lattice indices (i,j,k) for real space
|
|
119
|
+
idx_r = th.arange(-n_r, n_r + 1, device=device)
|
|
120
|
+
grid_r = th.stack(th.meshgrid(idx_r, idx_r, idx_r, indexing='ij'), dim=-1) # (Mr,3)
|
|
121
|
+
Mr = grid_r.shape[0] # = (2*n_r+1)^3
|
|
122
|
+
grid_r = grid_r.view(-1, 3) # (Mr, 3)
|
|
123
|
+
|
|
124
|
+
# Precompute offsets = i*R[:,0] + j*R[:,1] + k*R[:,2] for all batch items
|
|
125
|
+
# offsets shape: (B, Mr, 3)
|
|
126
|
+
offsets = th.einsum('mi,bij->bmj', grid_r, R) # sum over i (grid dim), multiply with R rows
|
|
127
|
+
|
|
128
|
+
# Prepare real-space sum accumulator
|
|
129
|
+
real_sum = th.zeros(B, N, device=device)
|
|
130
|
+
|
|
131
|
+
# Process real-space cells in chunks to avoid OOM
|
|
132
|
+
for start in range(0, Mr, chunk_size):
|
|
133
|
+
end = min(start + chunk_size, Mr)
|
|
134
|
+
offsets_chunk = offsets[:, start:end, :] # (B, chunk, 3)
|
|
135
|
+
|
|
136
|
+
# Shift query points by each offset: (B, N, chunk, 3)
|
|
137
|
+
r_shifted = r.unsqueeze(2) + offsets_chunk.unsqueeze(1)
|
|
138
|
+
|
|
139
|
+
# Reshape to (B, N*chunk, 3) for function evaluation
|
|
140
|
+
r_flat = r_shifted.reshape(B, N * (end - start), 3)
|
|
141
|
+
|
|
142
|
+
# Evaluate f and its Gaussian convolution
|
|
143
|
+
f_vals = self.rfunc(r_flat).reshape(B, N, end - start) # (B, N, chunk)
|
|
144
|
+
conv_vals = self.rgc_func(r_flat).reshape(B, N, end - start) # (B, N, chunk)
|
|
145
|
+
|
|
146
|
+
# Short-range contribution: f - conv, sum over chunk
|
|
147
|
+
real_sum += (f_vals - conv_vals).sum(dim=2)
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# 2. Reciprocal-space part: sum over reciprocal vectors k
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
# Compute cell volume V = |det(R)|
|
|
153
|
+
det_R = th.det(R) # (B,)
|
|
154
|
+
V = det_R.abs().unsqueeze(-1).unsqueeze(-1) # (B,1,1) for broadcasting
|
|
155
|
+
|
|
156
|
+
# Compute reciprocal lattice vectors (rows of the matrix)
|
|
157
|
+
# b1 = 2π * (a2 × a3) / V, b2 = 2π * (a3 × a1) / V, b3 = 2π * (a1 × a2) / V
|
|
158
|
+
a1 = R[:, 0, :] # (B,3)
|
|
159
|
+
a2 = R[:, 1, :] # (B,3)
|
|
160
|
+
a3 = R[:, 2, :] # (B,3)
|
|
161
|
+
cross_a2_a3 = th.cross(a2, a3, dim=-1) # (B,3)
|
|
162
|
+
cross_a3_a1 = th.cross(a3, a1, dim=-1) # (B,3)
|
|
163
|
+
cross_a1_a2 = th.cross(a1, a2, dim=-1) # (B,3)
|
|
164
|
+
|
|
165
|
+
two_pi = 2 * th.pi
|
|
166
|
+
b1 = two_pi * cross_a2_a3 / V.squeeze(-1) # (B,3)
|
|
167
|
+
b2 = two_pi * cross_a3_a1 / V.squeeze(-1)
|
|
168
|
+
b3 = two_pi * cross_a1_a2 / V.squeeze(-1)
|
|
169
|
+
|
|
170
|
+
# Stack to form reciprocal matrix (B,3,3) where each row is a reciprocal vector
|
|
171
|
+
recip_matrix = th.stack([b1, b2, b3], dim=1) # (B,3,3)
|
|
172
|
+
|
|
173
|
+
# Generate integer indices for reciprocal space
|
|
174
|
+
idx_g = th.arange(-n_g, n_g + 1, device=device)
|
|
175
|
+
grid_g = th.stack(th.meshgrid(idx_g, idx_g, idx_g, indexing='ij'), dim=-1) # (Mg,3)
|
|
176
|
+
Mg = grid_g.shape[0]
|
|
177
|
+
grid_g = grid_g.view(-1, 3) # (Mg,3)
|
|
178
|
+
|
|
179
|
+
# Compute all k vectors: (B, Mg, 3)
|
|
180
|
+
k_vec = th.einsum('mi,bij->bmj', grid_g, recip_matrix)
|
|
181
|
+
|
|
182
|
+
# Compute k^2 = ||k||^2
|
|
183
|
+
k_sq = (k_vec ** 2).sum(dim=-1) # (B, Mg)
|
|
184
|
+
|
|
185
|
+
# Gaussian attenuation factor exp(-k^2/(4α))
|
|
186
|
+
atten = th.exp(-k_sq / (4 * self.alpha)) # (B, Mg)
|
|
187
|
+
|
|
188
|
+
# Evaluate g(k) for all k points
|
|
189
|
+
# gfunc expects input (B, N, 3), but here we have (B, Mg, 3). Mg may be large, so we call in one go.
|
|
190
|
+
g_vals = self.gfunc(k_vec) # (B, Mg) – assuming gfunc returns real or complex tensor
|
|
191
|
+
|
|
192
|
+
# Compute phase factor exp(i k·r) for all query points and all k
|
|
193
|
+
# k·r: (B, Mg, 3) dot (B, N, 3) -> (B, N, Mg)
|
|
194
|
+
k_dot_r = th.einsum('bmj,bnj->bnm', k_vec, r) # (B, N, Mg)
|
|
195
|
+
phase = th.exp(1j * k_dot_r) # complex (B, N, Mg)
|
|
196
|
+
|
|
197
|
+
# Combine: g(k) * attenuation * phase, sum over k, and divide by V
|
|
198
|
+
# g_vals may be real or complex; we promote to complex if needed.
|
|
199
|
+
if not th.is_complex(g_vals):
|
|
200
|
+
g_vals = g_vals.to(th.complex64)
|
|
201
|
+
contrib = g_vals.unsqueeze(1) * atten.unsqueeze(1) * phase # (B, N, Mg)
|
|
202
|
+
recip_sum = contrib.sum(dim=2) / V # (B, N) complex
|
|
203
|
+
|
|
204
|
+
# Take real part (should be real due to symmetry, but numerically small imaginary may remain)
|
|
205
|
+
recip_sum = recip_sum.real
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# 3. Combine and return
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
return real_sum + recip_sum
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
""" regularize Sequence of Tensors by padding """
|
|
2
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
3
|
+
# Authors: Pu Pengxin, Song Xin
|
|
4
|
+
# Version: 0.9a
|
|
5
|
+
# File: IrregularTensorReformat.py
|
|
6
|
+
# Environment: Python 3.12
|
|
7
|
+
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
import torch as th
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IrregularTensorReformat:
|
|
14
|
+
"""
|
|
15
|
+
Regularize List[Tensor] into one Tensor by padding, and then recover them into List[Tensor].
|
|
16
|
+
The input tensors can only be irregular at 1st dim.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, ):
|
|
19
|
+
self._length_record = None
|
|
20
|
+
self.__shape_check = None
|
|
21
|
+
|
|
22
|
+
def regularize(self, X: List[th.Tensor], padding_value:float=0.) -> Tuple[th.Tensor, th.Tensor]:
|
|
23
|
+
"""
|
|
24
|
+
Regularize the List of Tensors.
|
|
25
|
+
Args:
|
|
26
|
+
X: the input sequence of Tensors.
|
|
27
|
+
padding_value: the padding_value.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
X_r: Tensor, the regularized tensor.
|
|
31
|
+
mask: Tensor, the mask of paddings. The part of `False` is paddings.
|
|
32
|
+
"""
|
|
33
|
+
if th.any(th.isnan(th.cat(X, dim=0))):
|
|
34
|
+
raise ValueError('Occurred NaN in input `X`.')
|
|
35
|
+
self._length_record = [len(xi) for xi in X]
|
|
36
|
+
# padding
|
|
37
|
+
X = th.nn.utils.rnn.pad_sequence(X, True, padding_value=padding_value)
|
|
38
|
+
pad_mask = ~th.isnan(X)
|
|
39
|
+
# shape check
|
|
40
|
+
self.__shape_check = len(X)
|
|
41
|
+
|
|
42
|
+
return X, pad_mask
|
|
43
|
+
|
|
44
|
+
def recover(self, X:th.Tensor) -> List[th.Tensor]:
|
|
45
|
+
"""
|
|
46
|
+
Recover the regularized tensor into List[Tensor]
|
|
47
|
+
Args:
|
|
48
|
+
X: the Tensor that have same shape of X
|
|
49
|
+
Returns:
|
|
50
|
+
List[Tensor], the recovered Tensors.
|
|
51
|
+
"""
|
|
52
|
+
if self._length_record is None:
|
|
53
|
+
raise RuntimeError('`X` has not regularized yet.')
|
|
54
|
+
elif len(X) != self.__shape_check:
|
|
55
|
+
raise ValueError(f'Wrong shape of `X`. The length of `X` should be {self.__shape_check}, but occurred {len(X)}.')
|
|
56
|
+
|
|
57
|
+
X = [X[i, :self._length_record[i]] for i in range(self.__shape_check)]
|
|
58
|
+
|
|
59
|
+
return X
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
""" check whether imported module exists """
|
|
2
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
3
|
+
# Authors: Pu Pengxin, Song Xin
|
|
4
|
+
# Version: 0.9a
|
|
5
|
+
# File: _CheckModules.py
|
|
6
|
+
# Environment: Python 3.12
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Check modules func
|
|
13
|
+
def check_module(module_name: str, pkg_name: None | str = None):
|
|
14
|
+
"""
|
|
15
|
+
Check whether imported module exists.
|
|
16
|
+
Args:
|
|
17
|
+
module_name: imported module name.
|
|
18
|
+
pkg_name: package name. It is required when performing a relative import.
|
|
19
|
+
It specifies the package to use as the anchor point from which to resolve the relative import to an absolute import.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
the imported module if it exists, else return None.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
pkg = importlib.import_module(module_name, pkg_name)
|
|
27
|
+
return pkg
|
|
28
|
+
except ImportError:
|
|
29
|
+
#warnings.warn(f'Package {module_name} was not found, therefore some related methods would be unavailable.')
|
|
30
|
+
return None
|