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,170 @@
1
+ """
2
+ Build-in Losses function
3
+ """
4
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
5
+ # Authors: Pu Pengxin, Song Xin
6
+ # Version: 0.9a
7
+ # File: Losses.py
8
+ # Environment: Python 3.12
9
+
10
+ from typing import Literal, List, Dict, Sequence, Tuple
11
+ import math
12
+ import numpy as np
13
+ import torch as th
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+ class Energy_Force_Loss(nn.Module):
18
+ """
19
+ A loss function that evaluates both predicted energy and forces.
20
+ Both energy & force losses would be averaged over each atom.
21
+
22
+ Parameters:
23
+ loss_E: the loss function of energy.
24
+ loss_F: the loss function of forces.
25
+ coeff_E: the coefficient of energy.
26
+ coeff_F: the coefficient of forces.
27
+
28
+ forward:
29
+ pred: Dict[Literal['energy'], th.Tensor], output of models;
30
+ label: Dict[Literal['energy'], th.Tensor], labels.
31
+
32
+ """
33
+ def __init__(self,
34
+ loss_E: Literal['MAE', 'MSE']|nn.Module='MAE',
35
+ loss_F: Literal['MAE', 'MSE']|nn.Module='MAE',
36
+ coeff_E: float=1.,
37
+ coeff_F: float=1.) -> None:
38
+ super().__init__()
39
+ if loss_E == 'MAE':
40
+ self.loss_E = nn.SmoothL1Loss(reduction='sum')
41
+ elif loss_E == 'MSE':
42
+ self.loss_E = nn.MSELoss(reduction='sum')
43
+ elif loss_E == 'SmoothMAE':
44
+ self.loss_E = nn.SmoothL1Loss(reduction='sum')
45
+ else:
46
+ self.loss_E = loss_E
47
+
48
+ if loss_F == 'MAE':
49
+ self.loss_F = nn.SmoothL1Loss(reduction='sum')
50
+ elif loss_F == 'MSE':
51
+ self.loss_F = nn.MSELoss(reduction='sum')
52
+ else:
53
+ self.loss_F = loss_F
54
+
55
+ self.coeff_E = coeff_E
56
+ self.coeff_F = coeff_F
57
+
58
+ def forward(self, pred:Dict[Literal['energy', 'forces'], th.Tensor], label:Dict[Literal['energy', 'forces'], th.Tensor]):
59
+ n_atom = math.prod(pred['forces'].shape[:-1])
60
+ loss = (self.coeff_E * self.loss_E(pred['energy'], label['energy']) +
61
+ self.coeff_F * self.loss_F(pred['forces'], label['forces']))/n_atom
62
+ return loss
63
+
64
+ class Energy_Loss(nn.Module):
65
+ """
66
+ A loss function that evaluates both predicted energy and forces.
67
+
68
+ Parameters:
69
+ loss_E: the loss function of energy.
70
+
71
+ forward:
72
+ pred: Dict[Literal['energy'], th.Tensor], output of models;
73
+ label: Dict[Literal['energy'], th.Tensor], labels.
74
+
75
+ """
76
+ def __init__(self, loss_E: Literal['MAE', 'MSE', 'SmoothMAE', 'Huber'] | nn.Module = 'MAE',) -> None:
77
+ super().__init__()
78
+ if loss_E == 'MAE':
79
+ self.loss_E = nn.L1Loss()
80
+ elif loss_E == 'SmoothMAE':
81
+ self.loss_E = nn.SmoothL1Loss()
82
+ elif loss_E == 'MSE':
83
+ self.loss_E = nn.MSELoss()
84
+ elif loss_E == 'Huber':
85
+ self.loss_E = nn.HuberLoss()
86
+ else:
87
+ self.loss_E = loss_E
88
+
89
+ def forward(self, pred: Dict[Literal['energy'], th.Tensor], label: Dict[Literal['energy'], th.Tensor]):
90
+ loss = self.loss_E(pred['energy'], label['energy'])
91
+ return loss
92
+
93
+
94
+ class WrapperBoostLoss(nn.Module):
95
+ def __init__(self, loss_E: Literal['MAE', 'MSE', 'SmoothMAE', 'Huber'] | nn.Module = 'MAE', loss_F = None) -> None:
96
+ super().__init__()
97
+ if loss_E == 'MAE':
98
+ self.loss_E = nn.L1Loss()
99
+ elif loss_E == 'SmoothMAE':
100
+ self.loss_E = nn.SmoothL1Loss()
101
+ elif loss_E == 'MSE':
102
+ self.loss_E = nn.MSELoss()
103
+ elif loss_E == 'Huber':
104
+ self.loss_E = nn.HuberLoss()
105
+ else:
106
+ self.loss_E = loss_E
107
+
108
+ if loss_F is None:
109
+ self.has_loss_f = False
110
+ else:
111
+ if loss_F == 'MAE':
112
+ self.loss_F = nn.SmoothL1Loss(reduction='sum')
113
+ elif loss_F == 'MSE':
114
+ self.loss_F = nn.MSELoss(reduction='sum')
115
+ else:
116
+ self.loss_F = loss_F
117
+
118
+ def forward(self, pred: Dict[Literal['energy', 'forces'], List[th.Tensor]], label: Dict[Literal['energy', 'forces'], th.Tensor]):
119
+ loss = 0.
120
+ res = label['energy']
121
+ for pred_ in pred['energy']:
122
+ loss = loss + self.loss_E(res, pred_)
123
+ res = res - pred_
124
+ if self.has_loss_f:
125
+ res_f = label['forces']
126
+ for pred_ in pred['forces']:
127
+ loss = loss + self.loss_F(res_f, pred_)
128
+ res_f = res - pred_
129
+
130
+ return loss
131
+
132
+
133
+ class WrapperMeanLoss(nn.Module):
134
+ def __init__(
135
+ self,
136
+ loss_E: Literal['MAE', 'MSE', 'SmoothMAE', 'Huber'] | nn.Module = 'MAE',
137
+ loss_F: Literal['MAE', 'MSE', 'SmoothMAE', 'Huber'] | nn.Module | None = None
138
+ ) -> None:
139
+ super().__init__()
140
+ if loss_E == 'MAE':
141
+ self.loss_E = nn.L1Loss()
142
+ elif loss_E == 'SmoothMAE':
143
+ self.loss_E = nn.SmoothL1Loss()
144
+ elif loss_E == 'MSE':
145
+ self.loss_E = nn.MSELoss()
146
+ elif loss_E == 'Huber':
147
+ self.loss_E = nn.HuberLoss()
148
+ else:
149
+ self.loss_E = loss_E
150
+
151
+ if loss_F is None:
152
+ self.has_loss_f = False
153
+ else:
154
+ self.has_loss_f = True
155
+ if loss_F == 'MAE':
156
+ self.loss_F = nn.SmoothL1Loss(reduction='sum')
157
+ elif loss_F == 'MSE':
158
+ self.loss_F = nn.MSELoss(reduction='sum')
159
+ else:
160
+ self.loss_F = loss_F
161
+
162
+ def forward(self, pred: Dict[Literal['energy', 'forces'], List[th.Tensor]], label: Dict[Literal['energy', 'forces'], th.Tensor]):
163
+ loss = 0.
164
+ for pred_ in pred['energy']:
165
+ loss = loss + self.loss_E(label['energy'], pred_)
166
+ if self.has_loss_f:
167
+ for pred_ in pred['forces']:
168
+ loss = loss + self.loss_F(label['forces'], pred_)
169
+
170
+ return loss
@@ -0,0 +1,84 @@
1
+ """ Metrics """
2
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
3
+ # Authors: Pu Pengxin, Song Xin
4
+ # Version: 0.9a
5
+ # File: Metrics.py
6
+ # Environment: Python 3.12
7
+
8
+ from typing import Literal, Dict, List, Tuple
9
+ import warnings
10
+ import torch as th
11
+ import torch.nn.functional as F
12
+
13
+
14
+ def _r2_score(y_pred: th.Tensor, y_true: th.Tensor) -> th.Tensor:
15
+ """
16
+ Calculate R^2
17
+ """
18
+ # initialize
19
+ y1 = y_true.float()
20
+ y2 = y_pred.float()
21
+ if y1.dim() == 1:
22
+ y1 = y1.view(-1, 1)
23
+ y2 = y2.view(-1, 1)
24
+
25
+ # SS_res
26
+ ss_res = th.sum((y1 - y2) ** 2, dim=0)
27
+
28
+ # SS_tol
29
+ y1_mean = th.mean(y1, dim=0, keepdim=True)
30
+ ss_tot = th.sum((y1 - y1_mean) ** 2, dim=0)
31
+
32
+ # Main
33
+ r2 = th.where(
34
+ ss_tot != 0,
35
+ 1 - (ss_res / ss_tot),
36
+ th.where(
37
+ ss_res != 0,
38
+ -th.inf,
39
+ th.ones_like(ss_res)
40
+ )
41
+ )
42
+
43
+ return r2.squeeze()
44
+
45
+ def _rmse(x, y, reduction='mean') -> th.Tensor:
46
+ return th.sqrt(F.mse_loss(x, y, reduction=reduction))
47
+
48
+ def E_MAE(pred: Dict[Literal['energy', 'forces'], th.Tensor],
49
+ label: Dict[Literal['energy', 'forces'], th.Tensor],
50
+ reduction: Literal['mean', 'sum', 'none'] = 'mean'):
51
+ with th.no_grad():
52
+ if isinstance(pred['energy'], th.Tensor):
53
+ mae = F.l1_loss(pred['energy'], label['energy'], reduction=reduction)
54
+ elif isinstance(pred['energy'], List|Tuple):
55
+ mae = sum(F.l1_loss(pred_, label['energy'], reduction=reduction) for pred_ in pred['energy'])
56
+
57
+ return mae
58
+
59
+
60
+ def E_R2(pred: Dict[Literal['energy', 'forces'], th.Tensor],
61
+ label: Dict[Literal['energy', 'forces'], th.Tensor],):
62
+ if len(pred['energy']) <= 2:
63
+ r2 = th.tensor([1.], dtype=th.float32)
64
+ warnings.warn('Input samples less than 2, r2 was set to 1.', RuntimeWarning)
65
+ else:
66
+ with th.no_grad():
67
+ r2 = _r2_score(pred['energy'], label['energy'])
68
+
69
+ return r2
70
+
71
+
72
+ def F_MAE(pred: Dict[Literal['energy', 'forces'], th.Tensor],
73
+ label: Dict[Literal['energy', 'forces'], th.Tensor],
74
+ reduction: Literal['mean', 'sum', 'none'] = 'mean'):
75
+ with th.no_grad():
76
+ mae = F.l1_loss(pred['forces'], label['forces'], reduction=reduction)
77
+ return mae
78
+
79
+
80
+ def F_MaxE(pred: Dict[Literal['energy', 'forces'], th.Tensor],
81
+ label: Dict[Literal['energy', 'forces'], th.Tensor]):
82
+ with th.no_grad():
83
+ max_ = th.max(pred['forces'] - label['forces'])
84
+ return max_
@@ -0,0 +1,296 @@
1
+ # Copyright (c) 2026.3.29, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 1.0b
4
+ # File: ModelOptims.py
5
+ # Environment: Python 3.12
6
+ import math
7
+ from collections import deque
8
+
9
+ import torch as th
10
+ from torch.optim import Optimizer
11
+
12
+ class FIRELikeOptimizer(Optimizer):
13
+ """
14
+ FIRE-like optimizer based on damped Newton dynamics.
15
+ When power = -grad @ velocities > 0, which means loss descents, n_count += 1;
16
+ when power <= 0, apply an infinite pulse that sets velocity and N_min to zero, and lr to initial lr.
17
+ Once n_count >= N_min, lr *= fi (fi > 1.), i.e., increase learning rate.
18
+
19
+ Args:
20
+ params: model parameters.
21
+ lr: initial learning rate, i.e., time steplength.
22
+ mass: hyperparameter of mass.
23
+ N_min: steps that keep lr. If contiguous loss descent step number >= N_min, increase lr.
24
+ tolerance: steps that keep lr & veloc. If contiguous loss increment step number >= tolerance, reset lr to the default and veloc. to 0.
25
+ fi: learning rate increment factor
26
+ max_lr: maximum learning rate.
27
+ """
28
+ def __init__(self, params, lr=1e-2, mass=1.0, N_min=10, tolerance=5, fi=1.1, max_lr=None):
29
+ if lr <= 0:
30
+ raise ValueError(f"Invalid lr: {lr}")
31
+ else:
32
+ lr = float(lr)
33
+ if mass <= 0:
34
+ raise ValueError(f"mass must be positive: {mass}")
35
+ else:
36
+ mass = float(mass)
37
+ if N_min < 1:
38
+ raise ValueError(f"N_min must be >= 1: {N_min}")
39
+ else:
40
+ N_min = int(N_min)
41
+ if tolerance < 0:
42
+ raise ValueError(f"tolerance must be positive: {tolerance}")
43
+ else:
44
+ tolerance = int(tolerance)
45
+ if fi <= 1.0:
46
+ raise ValueError(f"fi must be > 1.0: {fi}")
47
+ else:
48
+ fi = float(fi)
49
+ if max_lr is None:
50
+ max_lr = lr * 10.
51
+ elif max_lr < lr:
52
+ raise ValueError("max_lr must be >= initial lr")
53
+ else:
54
+ max_lr = float(max_lr)
55
+
56
+ defaults = dict(lr=lr, mass=mass, N_min=N_min, tolerance=tolerance, fi=fi, max_lr=max_lr)
57
+ super().__init__(params, defaults)
58
+
59
+ # store 'lr' as the baseline, and 'lr_now' initialized to current really used lr.
60
+ for group in self.param_groups:
61
+ group['lr_now'] = group['lr']
62
+
63
+ # decrement counter
64
+ self.n_count_dec = 0
65
+ # increment counter
66
+ self.n_count_inc = 0
67
+
68
+ def step(self, closure=None):
69
+ loss = None
70
+ if closure is not None:
71
+ loss = closure()
72
+
73
+ # ---------- 1. Power P = -sum(grad · velocity) ----------
74
+ power = None
75
+ for group in self.param_groups:
76
+ for p in group['params']:
77
+ if p.grad is None:
78
+ continue
79
+ grad = p.grad.data
80
+ state = self.state[p]
81
+ # lazy launch initial veloc.
82
+ if 'velocity' not in state:
83
+ state['velocity'] = th.zeros_like(p.data)
84
+ if power is None: power = th.scalar_tensor(0.0, dtype=p.data.dtype, device=p.device)
85
+ vel = state['velocity']
86
+ #power -= th.dot(grad.view(-1), vel.view(-1)).item()
87
+ power: th.Tensor
88
+ power -= th.sum(vel * grad) # DO NOT USE `.item()` which causes global synchronization
89
+
90
+ # ---------- 2. infinite damp pulse ----------
91
+ # scalar Tensor allows directly comparison
92
+ if power <= 0: # loss increment
93
+ # sets velocity and N_min to zero, and lr to initial lr.
94
+ self.n_count_dec = 0
95
+ self.n_count_inc += 1
96
+ for group in self.param_groups:
97
+ if self.n_count_inc >= group['tolerance']:
98
+ group['lr_now'] = group['lr']
99
+ for p in group['params']:
100
+ state = self.state.get(p)
101
+ if (state is not None) and ('velocity' in state):
102
+ state['velocity'].zero_()
103
+ else:
104
+ self.n_count_dec += 1
105
+ self.n_count_inc = 0
106
+ # check if contiguously decreased at least N_min steps
107
+ for i, group in enumerate(self.param_groups):
108
+ if self.n_count_dec >= self.param_groups[i]['N_min']:
109
+ group['lr_now'] = min(group['lr_now'] * group['fi'], group['max_lr'])
110
+
111
+ # ---------- 3. semi-implicit Euler update ----------
112
+ for group in self.param_groups:
113
+ lr = group['lr_now']
114
+ mass = group['mass']
115
+ for p in group['params']:
116
+ if p.grad is None:
117
+ continue
118
+ grad = p.grad.data
119
+ state = self.state[p]
120
+ vel = state['velocity']
121
+ # a = -grad / mass
122
+ neg_acc = grad / mass # here avoid one O(n) tensor ops
123
+ # v: v = v + a * dt
124
+ vel.add_(neg_acc, alpha=-lr)
125
+ # p: p = p + v * dt
126
+ p.data.add_(vel, alpha=lr)
127
+
128
+ return loss
129
+
130
+ # ---------- store/resume states ----------
131
+ def state_dict(self):
132
+ state = super().state_dict()
133
+ state['n'] = self.n_count_dec
134
+ return state
135
+
136
+ def load_state_dict(self, state_dict):
137
+ self.n_count_dec = state_dict.pop('n')
138
+ super().load_state_dict(state_dict)
139
+
140
+
141
+ class LangevinOptimizer(Optimizer):
142
+ r"""
143
+ Langevin optimizer based on Newton dynamics with Langevin thermostat.
144
+ Use Equipartition theorem or Virial theorem <x_i \partial H / \partial x_i> == k_B T to
145
+ judge whether equilibrium reached, wherein k_B = 1. If equilibrium reached, temperature decreases.
146
+
147
+ Args:
148
+ params: model parameters.
149
+ lr: initial learning rate, i.e., time steplength.
150
+ mass: hyperparameter of mass.
151
+ alpha: damping coefficient.
152
+ temperature: initial virtual temperature.
153
+ anneal_coeff: anneal coefficient to descent temperature.
154
+ balance_tol: relative tolerance of differences between k_B T and the Virial, which calculates as `(Virial - T)/T`
155
+ time_window_size: time window size to count the average <x_i \partial H / \partial x_i>.
156
+ heating_max_steps: the maximum heating steps. Once the number of calling `self.step()` over it, use normal SGD with momenta instead.
157
+ momenta_in_sgd: momentum decay coefficient after annealing.
158
+ """
159
+ def __init__(
160
+ self,
161
+ params,
162
+ lr=1e-3,
163
+ mass=1.0,
164
+ alpha=50.,
165
+ temperature=1.,
166
+ anneal_coeff=0.9,
167
+ balance_tol=0.1,
168
+ time_window_size=500,
169
+ heating_max_steps=100000,
170
+ momenta_in_sgd=0.6,
171
+ ):
172
+ if lr <= 0:
173
+ raise ValueError(f"Invalid lr: {lr}")
174
+ else:
175
+ lr = float(lr)
176
+ if mass <= 0:
177
+ raise ValueError(f"mass must be positive: {mass}")
178
+ else:
179
+ mass = th.scalar_tensor(mass, dtype=th.float32)
180
+ if anneal_coeff > 1. or anneal_coeff < 0.:
181
+ raise ValueError(f"anneal_coeff must be in [0, 1]: {anneal_coeff}")
182
+ time_window_size = int(time_window_size)
183
+ if time_window_size < 1:
184
+ raise ValueError(f"time_window_size must be positive: {time_window_size}")
185
+
186
+ # Virial queue
187
+ self.virials = deque([0.] * time_window_size, maxlen=time_window_size)
188
+ self.Virial_now = 0.
189
+ self.heating_max_steps = heating_max_steps
190
+ self.heating_step_now = 0
191
+
192
+ damp = math.exp(- alpha * lr)
193
+ defaults = dict(
194
+ lr=lr,
195
+ mass=mass,
196
+ damp=damp,
197
+ temperature=temperature,
198
+ anneal_coeff=anneal_coeff,
199
+ balance_tol=balance_tol,
200
+ momenta_in_sgd=momenta_in_sgd
201
+ )
202
+ super().__init__(params, defaults)
203
+
204
+ def step(self, closure=None):
205
+ loss = None
206
+ if closure is not None:
207
+ loss = closure()
208
+ # ---------- ABOBA update ----------
209
+ with th.no_grad():
210
+ for group in self.param_groups:
211
+ if self.heating_step_now >= self.heating_max_steps: # enough annealing, use trivial SGD instead
212
+ lr = group['lr']
213
+ for p in group['params']:
214
+ if p.grad is None:
215
+ continue
216
+ grad = p.grad.data
217
+ state = self.state[p]
218
+ # lazy launch initial veloc.
219
+ if 'velocity' not in state:
220
+ state['velocity'] = th.zeros_like(p.data)
221
+ vel = state['velocity']
222
+ vel.mul_(group['momenta_in_sgd']).add_(grad)
223
+ p.add_(vel, alpha=-lr)
224
+
225
+ else: # keep annealing
226
+ lr = group['lr']
227
+ mass = group['mass']
228
+ damp = group['damp']
229
+ temp = group['temperature']
230
+ anneal_coeff = group['anneal_coeff']
231
+ balance_tol = group['balance_tol']
232
+ xpHpx = 0.
233
+ sizes = 0
234
+ _vir = 0.
235
+ for p in group['params']:
236
+ if p.grad is None:
237
+ continue
238
+ grad = p.grad
239
+ state = self.state[p]
240
+ # lazy launch initial veloc.
241
+ if 'velocity' not in state:
242
+ state['velocity'] = th.randn_like(p) * th.sqrt(temp / mass)
243
+ vel = state['velocity']
244
+ # Calc. Virial
245
+ #xpHpx += th.sum(p * grad) # x_i * \partial H / \partial x_i
246
+ sizes += p.numel()
247
+ _vir += (vel.pow(2).sum() * 0.5 * mass).item()
248
+ mass = mass.to(p.dtype).to(p.device)
249
+ # ABOBA
250
+ vel.addcdiv_(grad, mass, value=- 0.5 * lr)
251
+ p.add_(vel, alpha=0.5 * lr)
252
+ # stochastic update velocity
253
+ vel.mul_(damp)
254
+ vel.add_(th.sqrt((temp * (1 - damp ** 2)) / mass) * th.randn_like(vel))
255
+ # V = damp * V + th.sqrt((self.T_init * (1 - damp ** 2)) / masses) * th.randn_like(V)
256
+ # the rest half-step
257
+ p.add_(vel, alpha=0.5 * lr)
258
+ vel.addcdiv_(grad, mass, value=-0.5 * lr)
259
+
260
+ # Annealing, check the Equipartition theorem
261
+ self.Virial_now -= self.virials[0] # sub the head
262
+ #_vir = xpHpx / sizes
263
+ _vir /= sizes
264
+ self.virials.append(_vir)
265
+ self.Virial_now += _vir
266
+ #print(f"Virial: {(self.Virial_now/len(self.virials)): <.4e}, T: {temp}")
267
+ #avg_grad_norm = sum(p.grad.norm().item() for p in group['params'] if p.grad is not None) / len(group['params'])
268
+ #avg_param_norm = sum(p.norm().item() for p in group['params']) / len(group['params'])
269
+ #print(f"Step {self.heating_step_now}: Virial={self.Virial_now/(0.5 * len(self.virials)):.4e}, T={temp:.4e}")
270
+
271
+ #if abs((self.Virial_now/len(self.virials) - temp)/temp) <= balance_tol:
272
+ # group['temperature'] *= anneal_coeff
273
+ if abs((self.Virial_now/(0.5 * len(self.virials)) - temp) / temp) <= balance_tol:
274
+ group['temperature'] *= anneal_coeff
275
+
276
+ self.heating_step_now += 1
277
+
278
+ return loss
279
+
280
+ # ---------- store/resume states ----------
281
+ def state_dict(self):
282
+ state = super().state_dict()
283
+ state.update({
284
+ 'virials': self.virials, 'Virial_now': self.Virial_now,
285
+ 'heating_step_now': self.heating_step_now,
286
+ 'heating_max_steps': self.heating_max_steps,
287
+ })
288
+ return state
289
+
290
+ def load_state_dict(self, state_dict):
291
+ self.virials = state_dict.pop('virials')
292
+ self.virials = deque(self.virials, maxlen=len(self.virials))
293
+ self.Virial_now = state_dict.pop('Virial_now')
294
+ self.heating_step_now = state_dict.pop('heating_step_now')
295
+ self.heating_max_steps = state_dict.pop('heating_max_steps')
296
+ super().load_state_dict(state_dict)