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.
Files changed (108) hide show
  1. buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py → buctoolkit-1.0b3/BUCToolkit/Bases/BaseConstraints.py +71 -220
  2. buctoolkit-1.0b3/BUCToolkit/Bases/BaseMotion.py +264 -0
  3. buctoolkit-1.0b3/BUCToolkit/Bases/__init__.py +5 -0
  4. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/_BaseMC.py +5 -14
  5. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/BiasedMD.py +8 -5
  6. {buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md → buctoolkit-1.0b3/BUCToolkit/BatchMD}/ConstrNVE.py +17 -51
  7. {buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md → buctoolkit-1.0b3/BUCToolkit/BatchMD}/ConstrNVT.py +78 -54
  8. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/NVE.py +1 -2
  9. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/NVT.py +8 -64
  10. buctoolkit-1.0b3/BUCToolkit/BatchMD/_BaseConstrMD.py +185 -0
  11. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/_BaseMD.py +41 -104
  12. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/__init__.py +3 -1
  13. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/Dimer.py +4 -3
  14. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_BaseOpt.py +41 -36
  15. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/_line_search.py +286 -136
  16. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/FIRE.py +200 -0
  17. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/QN.py +96 -38
  18. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/__init__.py +2 -2
  19. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/BatchStructuresBase.py +1 -1
  20. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/StructuresIO.py +32 -24
  21. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/write_files.py +5 -2
  22. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/__init__.py +19 -0
  23. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/ConstrainedMolecularDynamics.py +7 -2
  24. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/MolecularDynamics.py +6 -1
  25. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/MonteCarlo.py +6 -1
  26. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/NEB.py +6 -1
  27. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Predictor.py +6 -1
  28. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/StructureOptimization.py +6 -1
  29. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Trainer.py +12 -0
  30. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/VibrationAnalysis.py +5 -1
  31. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/_io.py +6 -24
  32. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/print_logo.py +2 -2
  33. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/Ewald.py +8 -4
  34. buctoolkit-1.0b3/BUCToolkit/utils/ab_initio_wrappers.py +144 -0
  35. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/function_utils.py +19 -2
  36. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/index_ops.py +3 -3
  37. buctoolkit-1.0b3/BUCToolkit/utils/setup_loggers.py +56 -0
  38. buctoolkit-1.0b3/BUCToolkit.egg-info/PKG-INFO +21 -0
  39. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/SOURCES.txt +8 -6
  40. buctoolkit-1.0b3/BUCToolkit.egg-info/top_level.txt +3 -0
  41. buctoolkit-1.0b3/PKG-INFO +21 -0
  42. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/README.md +146 -66
  43. buctoolkit-1.0b3/pyproject.toml +52 -0
  44. buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/README.md +0 -4
  45. buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/__init__.py +0 -18
  46. buctoolkit-1.0b2/BUCToolkit/utils/setup_loggers.py +0 -137
  47. buctoolkit-1.0b2/BUCToolkit.egg-info/PKG-INFO +0 -28
  48. buctoolkit-1.0b2/BUCToolkit.egg-info/top_level.txt +0 -1
  49. buctoolkit-1.0b2/PKG-INFO +0 -28
  50. buctoolkit-1.0b2/setup.py +0 -46
  51. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/CheckStructures.py +0 -0
  52. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/__init__.py +0 -0
  53. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/coords_interp.py +0 -0
  54. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/initial_ads_structures.py +0 -0
  55. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchGenerate/random_replace.py +0 -0
  56. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/MetropolisMC.py +0 -0
  57. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMC/__init__.py +0 -0
  58. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/BiasedMD.py +0 -0
  59. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/NVE.py +0 -0
  60. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/NVT.py +0 -0
  61. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchMD/constrained_md/_BaseMD.py +0 -0
  62. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/CI_NEB.py +0 -0
  63. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/TS/__init__.py +0 -0
  64. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/__init__.py +0 -0
  65. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/__init__.py +0 -0
  66. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/_utils/_warnings.py +0 -0
  67. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/frequency.py +0 -0
  68. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchOptim/minimize/CG.py +0 -0
  69. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/README.md +0 -0
  70. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/__init__.py +0 -0
  71. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/batch.py +0 -0
  72. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/BatchStructures/data.py +0 -0
  73. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/__init__.py +0 -0
  74. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/load_files.py +0 -0
  75. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/preprocessing.py +0 -0
  76. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/sampling.py +0 -0
  77. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/Preprocessing/supercells.py +0 -0
  78. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/StandardTemplate.py +0 -0
  79. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/_version.py +0 -0
  80. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/DataLoaders.py +0 -0
  81. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Losses.py +0 -0
  82. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/Metrics.py +0 -0
  83. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/ModelOptims.py +0 -0
  84. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/api/__init__.py +0 -0
  85. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/__init__.py +0 -0
  86. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/cli.py +0 -0
  87. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/convert_data.py +0 -0
  88. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/input_stub.py +0 -0
  89. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/main.py +0 -0
  90. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/cli/template_model.py +0 -0
  91. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/io.py +0 -0
  92. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/AtomSelector.py +0 -0
  93. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/AtomicNumber2Properties.py +0 -0
  94. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/ElemListReduce.py +0 -0
  95. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/IrregularTensorReformat.py +0 -0
  96. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_CheckModules.py +0 -0
  97. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_Element_info.py +0 -0
  98. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/__init__.py +0 -0
  99. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_para_flatt_list.py +0 -0
  100. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/_print_formatter.py +0 -0
  101. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/check_structures.py +0 -0
  102. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit/utils/grad_functions.py +0 -0
  103. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/dependency_links.txt +0 -0
  104. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/entry_points.txt +0 -0
  105. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/BUCToolkit.egg-info/requires.txt +0 -0
  106. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/LICENSE +0 -0
  107. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/MANIFEST.in +0 -0
  108. {buctoolkit-1.0b2 → buctoolkit-1.0b3}/setup.cfg +0 -0
@@ -1,38 +1,49 @@
1
- """ Molecular Dynamics base framework with constrains """
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: 0.9a
6
- # File: NVE.py
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 bjvp, bhvp
14
+ from BUCToolkit.utils.grad_functions import bhvp
15
+ from BUCToolkit.Bases.BaseMotion import BaseIO
19
16
 
20
17
 
21
- class _rConstrBase(_BaseMD):
18
+ class BaseConstr(BaseIO):
22
19
  """
23
- Constrained Base Dynamics
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
- time_step: float, time per step (ps).
27
- max_step: int, maximum steps.
28
- T_init: float, initial temperature, only to generate initial velocities of atoms by Maxwell-Boltzmann distribution. If V_init is given, T_init will be ignored.
29
- constr_func: Callable, a tuple of Python functions as the constraint functions s_k(X) that map R^n -> R^k. It takes one or more arguments, one of which must be a Tensor, and returns one Tensor with shape (k, ). `None` for identity function. see example below.
30
- constr_val: Callable[th.Tensor[1], th.Tensor] | th.Tensor, the constraint value of `constr_func`, i.e., constraints are `constr_func(X) = constr_val`.
31
- By defining it as a callable constr_val = constr_val(t) where `t` is a scalar Tensor, it can be set to the time-dependent constraints.
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. 0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
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 = 2
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)).item()
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
- # unit conversion
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
- return Fc
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 Px
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
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2026.4.24, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 1.0b
4
+ # File: __init__.py
5
+ # Environment: Python 3.12