BUCToolkit 1.0b2__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buctoolkit-1.0.1/BUCToolkit/Bases/BaseConstraints.py +1207 -0
- buctoolkit-1.0.1/BUCToolkit/Bases/BaseMotion.py +723 -0
- buctoolkit-1.0.1/BUCToolkit/Bases/StdContainer.py +45 -0
- buctoolkit-1.0.1/BUCToolkit/Bases/__init__.py +5 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchGenerate/CheckStructures.py +3 -3
- {buctoolkit-1.0b2/BUCToolkit/BatchOptim/_utils → buctoolkit-1.0.1/BUCToolkit/BatchGenerate}/__init__.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchGenerate/coords_interp.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchGenerate/initial_ads_structures.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchGenerate/random_replace.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchMC/MetropolisMC.py +141 -47
- buctoolkit-1.0.1/BUCToolkit/BatchMC/_BaseMC.py +982 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchMC/__init__.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchMD/BiasedMD.py +9 -6
- buctoolkit-1.0.1/BUCToolkit/BatchMD/ConstrNVE.py +107 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/ConstrNVT.py +115 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/NVE.py +445 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/NVT.py +1139 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchMD/_BaseMD.py +450 -470
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchMD/__init__.py +4 -2
- buctoolkit-1.0.1/BUCToolkit/BatchMD/fused_ops/VelocityVerlet.py +219 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/fused_ops/__init__.py +35 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/fused_ops/thermostats.py +844 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/fused_ops/utils.py +206 -0
- buctoolkit-1.0.1/BUCToolkit/BatchMD/utils.py +200 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/TS/CI_NEB.py +13 -3
- buctoolkit-1.0.1/BUCToolkit/BatchOptim/TS/Dimer.py +432 -0
- buctoolkit-1.0.1/BUCToolkit/BatchOptim/TS/Krylov.py +1248 -0
- buctoolkit-1.0.1/BUCToolkit/BatchOptim/TS/__init__.py +6 -0
- buctoolkit-1.0.1/BUCToolkit/BatchOptim/TS/_eigen_solver.py +954 -0
- buctoolkit-1.0.1/BUCToolkit/BatchOptim/_BaseOpt.py +1252 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/__init__.py +4 -2
- {buctoolkit-1.0b2/BUCToolkit/BatchGenerate → buctoolkit-1.0.1/BUCToolkit/BatchOptim/_utils}/__init__.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/_utils/_line_search.py +287 -137
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/_utils/_warnings.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/frequency.py +217 -166
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/minimize/CG.py +19 -10
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/minimize/FIRE.py +277 -28
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/minimize/QN.py +107 -43
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchOptim/minimize/__init__.py +3 -3
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchStructures/BatchStructuresBase.py +137 -16
- buctoolkit-1.0.1/BUCToolkit/BatchStructures/PropertiesContainer.py +136 -0
- buctoolkit-1.0.1/BUCToolkit/BatchStructures/StructuresIO.py +2342 -0
- buctoolkit-1.0.1/BUCToolkit/BatchStructures/__init__.py +65 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchStructures/batch.py +24 -8
- buctoolkit-1.0.1/BUCToolkit/BatchStructures/convert_dump.py +306 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchStructures/data.py +9 -1
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/__init__.py +25 -0
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/_collective_variables.py +151 -0
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/blue_moon.py +294 -0
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/constraint_work.py +223 -0
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/trajectory.py +242 -0
- buctoolkit-1.0.1/BUCToolkit/Postprocessing/vibrational_spectrum.py +262 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/Preprocessing/__init__.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/Preprocessing/load_files.py +22 -170
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/Preprocessing/preprocessing.py +107 -8
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/Preprocessing/sampling.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/Preprocessing/supercells.py +1 -1
- buctoolkit-1.0.1/BUCToolkit/Preprocessing/write_files.py +1008 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/StandardTemplate.py +2 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/__init__.py +25 -3
- buctoolkit-1.0.1/BUCToolkit/_version.py +11 -0
- buctoolkit-1.0.1/BUCToolkit/api/ConstrainedMolecularDynamics.py +506 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/DataLoaders.py +270 -10
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/Losses.py +4 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/Metrics.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/ModelOptims.py +2 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/MolecularDynamics.py +24 -15
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/MonteCarlo.py +25 -16
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/NEB.py +66 -41
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/Predictor.py +40 -17
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/StructureOptimization.py +197 -88
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/Trainer.py +266 -21
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/VibrationAnalysis.py +39 -40
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/__init__.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/api/_io.py +461 -280
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/cli/__init__.py +1 -1
- buctoolkit-1.0.1/BUCToolkit/cli/_config.py +269 -0
- buctoolkit-1.0.1/BUCToolkit/cli/_vasp_interface.py +57 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/cli/cli.py +369 -315
- buctoolkit-1.0.1/BUCToolkit/cli/convert_data.py +100 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/cli/input_stub.py +72 -17
- buctoolkit-1.0.1/BUCToolkit/cli/main.py +688 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/cli/print_logo.py +2 -2
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/io.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/AtomSelector.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/AtomicNumber2Properties.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/ElemListReduce.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/IrregularTensorReformat.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/_CheckModules.py +1 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/_Element_info.py +15 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/__init__.py +10 -3
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/_para_flatt_list.py +11 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/_print_formatter.py +1 -1
- buctoolkit-1.0.1/BUCToolkit/utils/ab_initio_wrappers.py +136 -0
- buctoolkit-1.0.1/BUCToolkit/utils/batch_updaters.py +72 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/check_structures.py +1 -1
- buctoolkit-1.0.1/BUCToolkit/utils/exceptions.py +6 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/function_utils.py +45 -4
- buctoolkit-1.0.1/BUCToolkit/utils/grad_functions.py +148 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/utils/index_ops.py +181 -18
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/MACE_model_wrapper.py +436 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/VASP_model_wrapper.py +150 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/VASP_plugin_wrapper.py +1181 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/__init__.py +33 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/dgl_model_wrappers.py +38 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/multi_devices_wrappers_mp.py +765 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/multi_devices_wrappers_mt.py +343 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/online_model_wrappers.py +421 -0
- buctoolkit-1.0.1/BUCToolkit/utils/model_wrappers/pyg_model_wrappers.py +114 -0
- buctoolkit-1.0.1/BUCToolkit/utils/operators/__init__.py +0 -0
- buctoolkit-1.0.1/BUCToolkit/utils/operators/element_wise_ops.py +270 -0
- buctoolkit-1.0.1/BUCToolkit/utils/setup_loggers.py +56 -0
- buctoolkit-1.0.1/BUCToolkit.egg-info/PKG-INFO +985 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit.egg-info/SOURCES.txt +40 -13
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit.egg-info/entry_points.txt +2 -1
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit.egg-info/requires.txt +10 -3
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit.egg-info/top_level.txt +1 -0
- buctoolkit-1.0.1/CHANGELOG.md +41 -0
- buctoolkit-1.0.1/MANIFEST.in +2 -0
- buctoolkit-1.0.1/PKG-INFO +985 -0
- buctoolkit-1.0.1/README.md +957 -0
- buctoolkit-1.0.1/pyproject.toml +62 -0
- buctoolkit-1.0b2/BUCToolkit/BatchMC/_BaseMC.py +0 -863
- buctoolkit-1.0b2/BUCToolkit/BatchMD/NVE.py +0 -92
- buctoolkit-1.0b2/BUCToolkit/BatchMD/NVT.py +0 -510
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/BiasedMD.py +0 -273
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +0 -169
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +0 -462
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/NVE.py +0 -78
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/NVT.py +0 -346
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/README.md +0 -4
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/_BaseMD.py +0 -440
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +0 -607
- buctoolkit-1.0b2/BUCToolkit/BatchMD/constrained_md/__init__.py +0 -18
- buctoolkit-1.0b2/BUCToolkit/BatchOptim/TS/Dimer.py +0 -950
- buctoolkit-1.0b2/BUCToolkit/BatchOptim/TS/__init__.py +0 -10
- buctoolkit-1.0b2/BUCToolkit/BatchOptim/_BaseOpt.py +0 -702
- buctoolkit-1.0b2/BUCToolkit/BatchStructures/StructuresIO.py +0 -1394
- buctoolkit-1.0b2/BUCToolkit/BatchStructures/__init__.py +0 -22
- buctoolkit-1.0b2/BUCToolkit/Preprocessing/write_files.py +0 -524
- buctoolkit-1.0b2/BUCToolkit/_version.py +0 -7
- buctoolkit-1.0b2/BUCToolkit/api/ConstrainedMolecularDynamics.py +0 -349
- buctoolkit-1.0b2/BUCToolkit/cli/convert_data.py +0 -56
- buctoolkit-1.0b2/BUCToolkit/cli/main.py +0 -386
- buctoolkit-1.0b2/BUCToolkit/cli/template_model.py +0 -6
- buctoolkit-1.0b2/BUCToolkit/utils/Ewald.py +0 -210
- buctoolkit-1.0b2/BUCToolkit/utils/grad_functions.py +0 -69
- buctoolkit-1.0b2/BUCToolkit/utils/setup_loggers.py +0 -137
- buctoolkit-1.0b2/BUCToolkit.egg-info/PKG-INFO +0 -28
- buctoolkit-1.0b2/MANIFEST.in +0 -1
- buctoolkit-1.0b2/PKG-INFO +0 -28
- buctoolkit-1.0b2/README.md +0 -545
- buctoolkit-1.0b2/setup.py +0 -46
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit/BatchStructures/README.md +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/BUCToolkit.egg-info/dependency_links.txt +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/LICENSE +0 -0
- {buctoolkit-1.0b2 → buctoolkit-1.0.1}/setup.cfg +0 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
# Copyright (c) 2026.4.24, BUCToolkit.
|
|
2
|
+
# Authors: Pu Pengxin, Song Xin
|
|
3
|
+
# Version: 1.0.1
|
|
4
|
+
# File: BaseConstraints.py
|
|
5
|
+
# Environment: Python 3.12
|
|
6
|
+
import inspect
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import warnings
|
|
10
|
+
from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
|
|
11
|
+
|
|
12
|
+
import torch as th
|
|
13
|
+
from torch import nn
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
|
|
17
|
+
from BUCToolkit.utils._Element_info import DTYPE
|
|
18
|
+
from BUCToolkit.utils.grad_functions import bhvp
|
|
19
|
+
from BUCToolkit.Bases.BaseMotion import BaseIO
|
|
20
|
+
|
|
21
|
+
FLOAT_TYPE = os.environ.get('BT_FLOAT_TYPE', 'float32')
|
|
22
|
+
FLOAT_TYPE = DTYPE.get(FLOAT_TYPE, th.float32)
|
|
23
|
+
|
|
24
|
+
# Probe reference precision for the threshold estimate. Kept local to this
|
|
25
|
+
# module: the map only decides which dtype is used to measure the round-off
|
|
26
|
+
# of the user constraint function.
|
|
27
|
+
_PROBE_DTYPE = {
|
|
28
|
+
th.float16: th.float32,
|
|
29
|
+
th.bfloat16: th.float32,
|
|
30
|
+
th.float32: th.float64,
|
|
31
|
+
th.float64: th.float64,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _report_diagnostic(message: str, verbose: int, logger: Any) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Report a constraint diagnostic on the channel selected by verbosity.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
message: Human-readable description of the diagnostic.
|
|
41
|
+
verbose: Diagnostic verbosity of the constraint component.
|
|
42
|
+
logger: Logger used when ``verbose > 0``.
|
|
43
|
+
|
|
44
|
+
Return: None.
|
|
45
|
+
|
|
46
|
+
Note:
|
|
47
|
+
At ``verbose == 0`` the message is emitted as a ``RuntimeWarning`` so
|
|
48
|
+
that it still reaches stderr after the standard logger has been
|
|
49
|
+
redirected to a file; at ``verbose > 0`` it goes to ``logger`` at
|
|
50
|
+
WARNING level. The default warning filter reports each call site once.
|
|
51
|
+
"""
|
|
52
|
+
if verbose > 0:
|
|
53
|
+
logger.warning(message)
|
|
54
|
+
else:
|
|
55
|
+
warnings.warn(message, RuntimeWarning, stacklevel=2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BaseConstr(BaseIO):
|
|
59
|
+
"""
|
|
60
|
+
Base Constraints used for constraints MD or optimizations.
|
|
61
|
+
It can be combined into any normal gradient-based algorithms by applying `project1` to gradient or any other tangent space vectors,
|
|
62
|
+
and `project2` as a retraction mapping to coordinates or any other vectors on the manifold.
|
|
63
|
+
|
|
64
|
+
I strongly suggest composition instead of inheritance when BaseConstr is
|
|
65
|
+
embedded in another class: create the component as ``self._constr`` and
|
|
66
|
+
call its methods directly (for example ``self._constr._project2(...)``).
|
|
67
|
+
Do not reproduce a dynamic ``__getattr__`` proxy, which makes the host
|
|
68
|
+
class opaque to tooling and compilers.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
constr_func: Callable, a tuple of Python functions as the constraint functions s_k(X) that map R^n -> R^k.
|
|
72
|
+
It takes one or more arguments, one of which must be a Tensor, and returns one Tensor with shape (k, ).
|
|
73
|
+
`None` for constant function that always return [0., ]. It should support auto-gradient ops. see example below.
|
|
74
|
+
constr_val: Callable[..., th.Tensor] | th.Tensor, the constraint value of `constr_func`,
|
|
75
|
+
i.e., constraints are `constr_func(X) = constr_val`.
|
|
76
|
+
A time-dependent callback may be `constr_val(t)` or
|
|
77
|
+
`constr_val(t, c0)`, where `t` is a scalar Tensor and `c0` is the
|
|
78
|
+
value of `constr_func(X_init)` computed at initialization.
|
|
79
|
+
constr_threshold: float | Literal['auto'], the threshold of constraint convergence (error of manifold violation).
|
|
80
|
+
``'auto'`` estimates the achievable floor at initialization and
|
|
81
|
+
uses three times that value; it requires threshold detection.
|
|
82
|
+
require_fixman: bool, whether to calculate fixman
|
|
83
|
+
retraction_solver: Literal['newton', 'damped'], the position projection solver.
|
|
84
|
+
``'newton'`` takes the default full Newton step; ``'damped'`` adds
|
|
85
|
+
a backtracking line search that is slower but can recover from an
|
|
86
|
+
ill-conditioned normal-space Jacobian. The solver is bound at
|
|
87
|
+
construction; reassign ``_retraction_map_solver`` to switch later.
|
|
88
|
+
device: str|torch.device, device that program rum on.
|
|
89
|
+
verbose: print level. 0 is silent, 1 permits scalar diagnostics only,
|
|
90
|
+
and 2 or greater also permits array diagnostics.
|
|
91
|
+
0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
|
|
92
|
+
_detect_dynamic_thres: Literal['None', 'Once'] | int, when to re-estimate
|
|
93
|
+
the achievable threshold floor. ``'Once'`` estimates it only during
|
|
94
|
+
initialization, ``'None'`` disables the estimate, and an ``int``
|
|
95
|
+
re-estimates it every that many projection calls. Each estimate
|
|
96
|
+
evaluates ``constr_func`` a few extra times for probing.
|
|
97
|
+
|
|
98
|
+
Examples for constrains:
|
|
99
|
+
def constr_func(X):
|
|
100
|
+
y = list()
|
|
101
|
+
# X: shape(N, D), Note the batch dimension would NOT be considered in constraints calculation of X.
|
|
102
|
+
# fix the distance between atoms (2, 4), (3, 7), (5, 8) into corresponding `constr_val[:3]`
|
|
103
|
+
y.append(th.linalg.norm(X[[2, 3, 5]] - X[[4, 7, 8]], dim=-1))
|
|
104
|
+
|
|
105
|
+
# fix the angle of atom7-atom5-atom8 and atom11-atom9-atom12 into corresponding `constr_val[3:6]`
|
|
106
|
+
x1 = X[[5, 9]]
|
|
107
|
+
x2 = X[[7, 11]]
|
|
108
|
+
x3 = X[[8, 12]]
|
|
109
|
+
y.append(
|
|
110
|
+
(
|
|
111
|
+
th.sum((x2 - x1) * (x3 - x1))
|
|
112
|
+
) / (th.linalg.norm(x2 - x1) * th.linalg.norm(x3 - x1))
|
|
113
|
+
)
|
|
114
|
+
z = th.cat(y)
|
|
115
|
+
return z
|
|
116
|
+
|
|
117
|
+
Methods:
|
|
118
|
+
run: run BatchMD.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
constr_func: Callable | None = None,
|
|
124
|
+
constr_val: Callable[..., th.Tensor|Tuple[th.Tensor]] | th.Tensor | None = None,
|
|
125
|
+
constr_threshold: float | Literal['auto'] = 'auto',
|
|
126
|
+
require_fixman: bool = False,
|
|
127
|
+
retraction_solver: Literal['newton', 'damped'] = 'newton',
|
|
128
|
+
device: str | th.device = 'cpu',
|
|
129
|
+
verbose: int = 0,
|
|
130
|
+
_detect_dynamic_thres: Literal['None', 'Once'] | int = 'Once',
|
|
131
|
+
):
|
|
132
|
+
# var check
|
|
133
|
+
if retraction_solver not in ('newton', 'damped'):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"`retraction_solver` must be 'newton' or 'damped', but got {retraction_solver!r}."
|
|
136
|
+
)
|
|
137
|
+
if _detect_dynamic_thres not in ('None', 'Once'):
|
|
138
|
+
_detect_dynamic_thres = int(_detect_dynamic_thres)
|
|
139
|
+
if _detect_dynamic_thres <= 0:
|
|
140
|
+
raise ValueError(f"`int(_detect_dynamic_thres)` must be > 0, but got {_detect_dynamic_thres}")
|
|
141
|
+
if constr_threshold != 'auto':
|
|
142
|
+
constr_threshold = float(constr_threshold)
|
|
143
|
+
if constr_threshold <= 0:
|
|
144
|
+
raise ValueError(f"`constr_threshold` must be > 0, but got {constr_threshold}")
|
|
145
|
+
elif _detect_dynamic_thres == 'None':
|
|
146
|
+
raise ValueError(
|
|
147
|
+
"`constr_threshold='auto'` requires threshold detection, but "
|
|
148
|
+
"`_detect_dynamic_thres='None'`."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if constr_func is None:
|
|
152
|
+
constr_func = lambda X: th.tensor(0., dtype=X.dtype, device=X.device)
|
|
153
|
+
self._lazy_calc_constr_val = False # use the value of constr_func(X_init) to determine constr_val (and fixed).
|
|
154
|
+
self.device = th.device(device)
|
|
155
|
+
|
|
156
|
+
self.time_now = th.scalar_tensor(0., device=device, dtype=FLOAT_TYPE)
|
|
157
|
+
self.is_const_constr = False
|
|
158
|
+
self.constr_val_func_raw = None
|
|
159
|
+
self._constr_val_func = None
|
|
160
|
+
self._constr_val_uses_c0 = False
|
|
161
|
+
self._constr_val_c0 = None
|
|
162
|
+
self.time_step = 1.
|
|
163
|
+
self.require_fixman = require_fixman
|
|
164
|
+
self.retraction_solver = retraction_solver
|
|
165
|
+
|
|
166
|
+
if isinstance(constr_val, th.Tensor):
|
|
167
|
+
self.is_const_constr = True
|
|
168
|
+
self.constr_val_func_raw = None
|
|
169
|
+
self.constr_val_now = constr_val.to(self.device, dtype=FLOAT_TYPE)
|
|
170
|
+
elif callable(constr_val):
|
|
171
|
+
self.is_const_constr = False
|
|
172
|
+
self.constr_val_func_raw = constr_val
|
|
173
|
+
self.constr_val_now = None
|
|
174
|
+
elif constr_val is None:
|
|
175
|
+
self.is_const_constr = True
|
|
176
|
+
self.constr_val_func_raw = None
|
|
177
|
+
self._lazy_calc_constr_val = True
|
|
178
|
+
else:
|
|
179
|
+
raise TypeError(f'Expected `constr_val` is Callable or torch.Tensor, but got {type(constr_val)}')
|
|
180
|
+
|
|
181
|
+
if not isinstance(constr_func, Callable):
|
|
182
|
+
raise TypeError(f"`constr_func` must consist of callable, but got {type(constr_func)}")
|
|
183
|
+
self.constr_func = constr_func
|
|
184
|
+
self.Q = th.tensor([], device=self.device, dtype=FLOAT_TYPE) # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
185
|
+
self.jac = None # Jacobian Matrix
|
|
186
|
+
self.R = th.tensor([], device=self.device, dtype=FLOAT_TYPE) # R(n_batch, n_constr, n_constr)
|
|
187
|
+
self.q = th.tensor([], device=self.device, dtype=FLOAT_TYPE) # (n_batch, n_constr, 1), solution of `R^T q = d/dt constr_val(t)`
|
|
188
|
+
self.sqrtM = None # M^1/2, (n_batch, n_atoms, n_dim)
|
|
189
|
+
self.negsqrtM = None # M^-1/2
|
|
190
|
+
self.max_proj_iter = 10
|
|
191
|
+
self.retraction_stagnation_factor = 0.05
|
|
192
|
+
self.constr_thres = constr_threshold
|
|
193
|
+
self._is_auto_thres = (constr_threshold == 'auto')
|
|
194
|
+
self._detect_dynamic_thres = _detect_dynamic_thres
|
|
195
|
+
self._proj2_call_number: int = 0 # the count of `project2` call. used for auto-detection sheme.
|
|
196
|
+
self._thres_below = False # flag indices whether current constr thres lower than the estimated infimum.
|
|
197
|
+
self.X_cache = None # constr force, i.e., the mu of Lagrange multipler
|
|
198
|
+
self._compiled_jac = None # pre-compiled Jacobian for replay
|
|
199
|
+
|
|
200
|
+
self._X_qr_cache = None # check the consistency of X in project1, ensuring it is the new tangent space projection
|
|
201
|
+
|
|
202
|
+
super().__init__()
|
|
203
|
+
self.init_logger('Main.Constraints')
|
|
204
|
+
self.verbose = int(verbose)
|
|
205
|
+
# Bind the concrete retraction implementation once. The solver is
|
|
206
|
+
# selected at construction; reassign this attribute to switch later.
|
|
207
|
+
if retraction_solver == 'newton':
|
|
208
|
+
self._retraction_map_solver = self._retraction_newton
|
|
209
|
+
else:
|
|
210
|
+
self._retraction_map_solver = self._retraction_damp_newton
|
|
211
|
+
|
|
212
|
+
def initialize(
|
|
213
|
+
self,
|
|
214
|
+
func: Any | nn.Module,
|
|
215
|
+
X: th.Tensor,
|
|
216
|
+
Element_list: List[List[str]] | List[List[int]] | None,
|
|
217
|
+
masses: th.Tensor | None,
|
|
218
|
+
V_init: th.Tensor | None = None,
|
|
219
|
+
grad_func: Any | nn.Module = None,
|
|
220
|
+
func_args: Sequence = tuple(),
|
|
221
|
+
func_kwargs: Dict | None = None,
|
|
222
|
+
grad_func_args: Sequence = tuple(),
|
|
223
|
+
grad_func_kwargs: Dict | None = None,
|
|
224
|
+
is_grad_func_contain_y: bool = True,
|
|
225
|
+
require_grad: bool = False,
|
|
226
|
+
batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
|
|
227
|
+
fixed_atom_tensor: Optional[th.Tensor] = None,
|
|
228
|
+
is_fix_mass_center: bool = False,
|
|
229
|
+
compile_jacobian: bool = True,
|
|
230
|
+
):
|
|
231
|
+
"""Initialize masses, targets, and the tangent-space basis.
|
|
232
|
+
|
|
233
|
+
This method owns all state required by constraint projection. Proxy
|
|
234
|
+
users such as constrained MD and constrained FIRE should call it rather
|
|
235
|
+
than reproducing the setup or invoking ``_build_compiled_jacobian``
|
|
236
|
+
directly.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
func: Parent algorithm objective. Accepted for compatibility with
|
|
240
|
+
the common motion-initialization hook; constraints do not use it.
|
|
241
|
+
X: Initial coordinates with shape
|
|
242
|
+
``(n_batch, n_atom, n_dim)``.
|
|
243
|
+
Element_list: Atomic symbols or numbers supplied by the parent
|
|
244
|
+
algorithm. Constraint initialization does not use them.
|
|
245
|
+
masses: Per-coordinate masses broadcast to the shape of ``X``. If
|
|
246
|
+
``None``, unit masses are used.
|
|
247
|
+
V_init: Initial velocities accepted by the common initialization
|
|
248
|
+
hook. Constraint initialization does not use them.
|
|
249
|
+
grad_func: Parent objective-gradient callable. Not used here.
|
|
250
|
+
func_args: Positional objective arguments. Not used here.
|
|
251
|
+
func_kwargs: Keyword objective arguments. Not used here.
|
|
252
|
+
grad_func_args: Positional gradient arguments. Not used here.
|
|
253
|
+
grad_func_kwargs: Keyword gradient arguments. Not used here.
|
|
254
|
+
is_grad_func_contain_y: Parent gradient-call convention. Not used
|
|
255
|
+
by constraint initialization.
|
|
256
|
+
require_grad: Parent objective autograd flag. Not used here.
|
|
257
|
+
batch_indices: Irregular-batch atom counts. Not used here because
|
|
258
|
+
``constr_func`` is vectorized over the leading dimension of X.
|
|
259
|
+
fixed_atom_tensor: Parent selective-dynamics mask. Not used here.
|
|
260
|
+
is_fix_mass_center: Parent center-of-mass setting. Not used here.
|
|
261
|
+
compile_jacobian: Whether to compile the batched constraint
|
|
262
|
+
Jacobian. Compilation is enabled by default on both CPU and
|
|
263
|
+
CUDA. Unsupported constraint graphs automatically fall back to
|
|
264
|
+
the eager implementation.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
None. The method updates mass factors, the current constraint
|
|
268
|
+
target, Jacobian/QR state, compilation state, and projection cache.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
TypeError: If ``compile_jacobian`` is not ``bool``.
|
|
272
|
+
Also raised when a time-dependent ``constr_val`` callback has
|
|
273
|
+
no inspectable signature accepting ``(t)`` or ``(t, c0)``.
|
|
274
|
+
RuntimeError: If ``constr_val`` and ``constr_func(X)`` have
|
|
275
|
+
different shapes.
|
|
276
|
+
ValueError: If the batched constraint residual is not two-dimensional.
|
|
277
|
+
|
|
278
|
+
Note:
|
|
279
|
+
``X``, ``masses``, and a tensor constraint target are converted to
|
|
280
|
+
the module-level ``FLOAT_TYPE`` (``BT_FLOAT_TYPE`` environment
|
|
281
|
+
variable, default float32) before use.
|
|
282
|
+
"""
|
|
283
|
+
if not isinstance(compile_jacobian, bool):
|
|
284
|
+
raise TypeError(
|
|
285
|
+
f'`compile_jacobian` must be bool, but got {type(compile_jacobian)}.'
|
|
286
|
+
)
|
|
287
|
+
X = X.to(device=self.device, dtype=FLOAT_TYPE)
|
|
288
|
+
if masses is not None:
|
|
289
|
+
masses = masses.to(device=self.device, dtype=FLOAT_TYPE)
|
|
290
|
+
if masses is None: masses = th.ones_like(X)
|
|
291
|
+
self.sqrtM = th.sqrt(masses) # M^1/2, (n_batch, n_atoms, n_dim)
|
|
292
|
+
self.negsqrtM = 1 / th.sqrt(masses) # M^-1/2
|
|
293
|
+
_y_check = th.vmap(self.constr_func)(X)
|
|
294
|
+
if self._lazy_calc_constr_val:
|
|
295
|
+
self.constr_val_now = _y_check
|
|
296
|
+
elif not self.is_const_constr:
|
|
297
|
+
# Keep the initial target as a detached, immutable run reference;
|
|
298
|
+
# later time updates must not retain the coordinate autograd graph.
|
|
299
|
+
self._constr_val_c0 = _y_check.detach().clone()
|
|
300
|
+
try:
|
|
301
|
+
constr_val_signature = inspect.signature(self.constr_val_func_raw)
|
|
302
|
+
except (TypeError, ValueError) as error:
|
|
303
|
+
raise TypeError(
|
|
304
|
+
'`constr_val` must have an inspectable signature accepting '
|
|
305
|
+
'(t) or (t, c0).'
|
|
306
|
+
) from error
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
constr_val_signature.bind(self.time_now, self._constr_val_c0)
|
|
310
|
+
except TypeError:
|
|
311
|
+
try:
|
|
312
|
+
constr_val_signature.bind(self.time_now)
|
|
313
|
+
except TypeError as unary_error:
|
|
314
|
+
raise TypeError(
|
|
315
|
+
'`constr_val` must accept either (t) or (t, c0).'
|
|
316
|
+
) from unary_error
|
|
317
|
+
self._constr_val_uses_c0 = False
|
|
318
|
+
self._constr_val_func = self.constr_val_func_raw
|
|
319
|
+
else:
|
|
320
|
+
self._constr_val_uses_c0 = True
|
|
321
|
+
self._constr_val_func = lambda t: self.constr_val_func_raw(t, self._constr_val_c0)
|
|
322
|
+
self._update_constr(self.time_now)
|
|
323
|
+
if self.verbose > 0:
|
|
324
|
+
self.logger.info(
|
|
325
|
+
f'Constraint values are now {np.array2string(self.constr_val_now.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}'
|
|
326
|
+
)
|
|
327
|
+
# check constr_val shape
|
|
328
|
+
if _y_check.shape != self.constr_val_now.shape:
|
|
329
|
+
raise RuntimeError(
|
|
330
|
+
f'`constr_val` must have the same shape as what constr_func returned {self.constr_val_now.shape}, but got {_y_check.shape}.'
|
|
331
|
+
)
|
|
332
|
+
self.constr_val_now = self.constr_val_now.to(self.device, dtype=FLOAT_TYPE)
|
|
333
|
+
# A previous initialization may have compiled a different run shape or
|
|
334
|
+
# constraint callable. Perform validation through the eager path, then
|
|
335
|
+
# build a fresh graph for the current inputs below.
|
|
336
|
+
self._compiled_jac = None
|
|
337
|
+
self._proj2_call_number = 0
|
|
338
|
+
self._thres_below = False
|
|
339
|
+
jac, y = self._jacobian(X)
|
|
340
|
+
if y.ndim != 2:
|
|
341
|
+
raise ValueError(f'`constr_func` must return a 2D tensor of shape (n_batch, n_constr), but got {y.shape}.')
|
|
342
|
+
if compile_jacobian:
|
|
343
|
+
self._build_compiled_jacobian(X)
|
|
344
|
+
self._do_qr(jac)
|
|
345
|
+
if self._detect_dynamic_thres != 'None':
|
|
346
|
+
# Numerical rank deficiency is a one-off initialization diagnostic;
|
|
347
|
+
# it is not repeated in the projection loop.
|
|
348
|
+
weighted_jac = th.flatten(jac * self.negsqrtM.unsqueeze(1), -2, -1)
|
|
349
|
+
spectrum = th.linalg.svdvals(weighted_jac)
|
|
350
|
+
eps = th.finfo(X.dtype).eps
|
|
351
|
+
sigma_min = float(spectrum.min())
|
|
352
|
+
sigma_max = float(spectrum.max())
|
|
353
|
+
if sigma_max > 0. and sigma_min <= sigma_max * eps ** 0.5:
|
|
354
|
+
_report_diagnostic(
|
|
355
|
+
'The constraint Jacobian is numerically rank-deficient at '
|
|
356
|
+
f'the working precision (sigma_min={sigma_min:.3e}, '
|
|
357
|
+
f'sigma_max={sigma_max:.3e}, 1/sqrt(eps)={eps ** -0.5:.3e}). '
|
|
358
|
+
'The projection may not converge. Please ensure that your constraints have a full-rank Jacobian '
|
|
359
|
+
'(almost everywhere). If so, try to set `retraction_solver=\'damped\'`.',
|
|
360
|
+
self.verbose,
|
|
361
|
+
self.logger,
|
|
362
|
+
)
|
|
363
|
+
if self._detect_dynamic_thres == 'Once' or isinstance(self._detect_dynamic_thres, int):
|
|
364
|
+
self._apply_thres_estimate(self._detect_thres_inf(X, jac), X)
|
|
365
|
+
self.X_cache = th.empty_like(X)
|
|
366
|
+
|
|
367
|
+
def _constr_func_wrapped(self, X, constr_val_now):
|
|
368
|
+
"""
|
|
369
|
+
Manage the constraint functions.
|
|
370
|
+
Converting constraint functions into s_k(X) = constr_func(X) - constr_val, thereby constraints are s_k(X) = 0.
|
|
371
|
+
Returns:
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
y = self.constr_func(X) - constr_val_now # (n_batch, n_constr, )
|
|
375
|
+
y = th.atleast_1d(y)
|
|
376
|
+
return y, y # repeat the output to use `has_aux` in th.func.jacrev to return the function values
|
|
377
|
+
|
|
378
|
+
def _constr_func_for_hvp(self, X):
|
|
379
|
+
"""
|
|
380
|
+
Manage the constraint functions used for HVP.
|
|
381
|
+
Converting constraint functions into s_k(X) = constr_func(X) - constr_val, thereby constraints are s_k(X) = 0.
|
|
382
|
+
Returns:
|
|
383
|
+
|
|
384
|
+
"""
|
|
385
|
+
y = self.constr_func(X) - self.constr_val_now # (n_batch, n_constr, )
|
|
386
|
+
y = th.atleast_1d(y)
|
|
387
|
+
return y
|
|
388
|
+
|
|
389
|
+
def _constr_val_wrapped(self, t):
|
|
390
|
+
"""
|
|
391
|
+
Manage the time-dependent constraint values.
|
|
392
|
+
Args:
|
|
393
|
+
t:
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
if self.is_const_constr:
|
|
399
|
+
return None
|
|
400
|
+
else:
|
|
401
|
+
y = self._constr_val_func(t)
|
|
402
|
+
if isinstance(y, (Tuple, List)):
|
|
403
|
+
y = th.vstack(y).mT
|
|
404
|
+
else:
|
|
405
|
+
y = y.view(self._constr_val_c0.shape)
|
|
406
|
+
|
|
407
|
+
return y, y
|
|
408
|
+
|
|
409
|
+
def _update_constr(self, t: th.Tensor | float):
|
|
410
|
+
"""Update a time-dependent constraint at an explicit evaluation time.
|
|
411
|
+
|
|
412
|
+
This method does not mutate ``self.time_now``. Integrators therefore
|
|
413
|
+
remain responsible for advancing the committed simulation clock and
|
|
414
|
+
may evaluate constraint stages such as ``time_now + time_step`` or a
|
|
415
|
+
half-step without changing the parent motion-loop state.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
t: Scalar time at which ``constr_val_func_raw`` and its derivative
|
|
419
|
+
are evaluated. Python numeric values are converted to the same
|
|
420
|
+
device and dtype as ``self.time_now``.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
None. For a time-dependent constraint, ``self.constr_val_now`` and
|
|
424
|
+
``self.d_constr`` are replaced by the value and time derivative at
|
|
425
|
+
``t``. Constant constraints are unchanged.
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
ValueError: If ``t`` is not scalar.
|
|
429
|
+
"""
|
|
430
|
+
if not self.is_const_constr:
|
|
431
|
+
t = th.as_tensor(
|
|
432
|
+
t,
|
|
433
|
+
device=self.time_now.device,
|
|
434
|
+
dtype=self.time_now.dtype,
|
|
435
|
+
)
|
|
436
|
+
if t.numel() != 1:
|
|
437
|
+
raise ValueError(f'Constraint evaluation time must be scalar, but got shape {t.shape}.')
|
|
438
|
+
t = t.reshape(())
|
|
439
|
+
self.d_constr, self.constr_val_now = th.func.jacrev(
|
|
440
|
+
self._constr_val_wrapped,
|
|
441
|
+
has_aux=True,
|
|
442
|
+
)(t) # d constr_val(t) / dt
|
|
443
|
+
# ensure the returns from udf function keeping the dtype and device.
|
|
444
|
+
self.d_constr = self.d_constr.to(self.device, dtype=FLOAT_TYPE)
|
|
445
|
+
self.constr_val_now = self.constr_val_now.to(self.device, dtype=FLOAT_TYPE)
|
|
446
|
+
|
|
447
|
+
def _build_compiled_jacobian(self, X_sample: th.Tensor):
|
|
448
|
+
"""
|
|
449
|
+
Pre-compile the batched constraint Jacobian for a fixed run shape.
|
|
450
|
+
|
|
451
|
+
Each structure and its corresponding constraint target are mapped as a
|
|
452
|
+
pair. The resulting function returns the same tensors as the eager
|
|
453
|
+
``vmap(jacrev(...))`` path: a Jacobian with shape
|
|
454
|
+
``(n_batch, n_constr, n_atom, n_dim)`` and residual values with shape
|
|
455
|
+
``(n_batch, n_constr)``. Molecular-dynamics runs keep these dimensions
|
|
456
|
+
fixed, which permits a static full-graph compilation and avoids graph
|
|
457
|
+
breaks inside every projection iteration.
|
|
458
|
+
|
|
459
|
+
Falls back to eager mode if compilation fails.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
X_sample: Representative coordinates with shape
|
|
463
|
+
``(n_batch, n_atom, n_dim)``. Its shape, device, and dtype are
|
|
464
|
+
used to compile and warm up the Jacobian graph.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
None. On success, ``self._compiled_jac`` stores the compiled
|
|
468
|
+
callable. On failure it is reset to ``None`` so ``_jacobian`` uses
|
|
469
|
+
the eager implementation.
|
|
470
|
+
"""
|
|
471
|
+
self._compiled_jac = None
|
|
472
|
+
try:
|
|
473
|
+
def _wrapped(X_single, constr_val):
|
|
474
|
+
y = self.constr_func(X_single) - constr_val
|
|
475
|
+
y = th.atleast_1d(y)
|
|
476
|
+
return y, y
|
|
477
|
+
|
|
478
|
+
jac_fn = th.func.vmap(
|
|
479
|
+
th.func.jacrev(_wrapped, argnums=0, has_aux=True),
|
|
480
|
+
# Pair X[i] with constr_val_now[i]. Broadcasting the complete
|
|
481
|
+
# target tensor here would introduce a second batch dimension.
|
|
482
|
+
in_dims=(0, 0)
|
|
483
|
+
)
|
|
484
|
+
compiled_jac = th.compile(jac_fn, fullgraph=True, dynamic=False)
|
|
485
|
+
|
|
486
|
+
# Warm up now so compilation latency and unsupported operations are
|
|
487
|
+
# handled before entering the MD loop. Shape validation prevents a
|
|
488
|
+
# malformed compiled graph from silently replacing the eager path.
|
|
489
|
+
# Match the gradient mode used by _project2. Without this, a normal
|
|
490
|
+
# non-Fixman MD run warms the graph with gradients enabled and then
|
|
491
|
+
# recompiles it on first use inside the no_grad main loop.
|
|
492
|
+
with th.set_grad_enabled(self.require_fixman):
|
|
493
|
+
jac, y = compiled_jac(X_sample, self.constr_val_now)
|
|
494
|
+
expected_jac_shape = (
|
|
495
|
+
X_sample.shape[0], self.constr_val_now.shape[-1],
|
|
496
|
+
*X_sample.shape[1:]
|
|
497
|
+
)
|
|
498
|
+
expected_y_shape = self.constr_val_now.shape
|
|
499
|
+
if jac.shape != expected_jac_shape or y.shape != expected_y_shape:
|
|
500
|
+
raise RuntimeError(
|
|
501
|
+
'Compiled constraint Jacobian returned invalid shapes: '
|
|
502
|
+
f'expected {expected_jac_shape} and {expected_y_shape}, '
|
|
503
|
+
f'but got {jac.shape} and {y.shape}.'
|
|
504
|
+
)
|
|
505
|
+
self._compiled_jac = compiled_jac
|
|
506
|
+
|
|
507
|
+
if self.verbose:
|
|
508
|
+
self.logger.info('Constraint Jacobian compiled successfully.')
|
|
509
|
+
except Exception as e:
|
|
510
|
+
if self.verbose:
|
|
511
|
+
self.logger.warning(f'Jacobian compilation failed ({type(e).__name__}): {e}. Falling back to eager.')
|
|
512
|
+
self._compiled_jac = None
|
|
513
|
+
|
|
514
|
+
def _jacobian(self, X: th.Tensor) -> Tuple[th.Tensor, th.Tensor]:
|
|
515
|
+
"""
|
|
516
|
+
Compute the Jacobian for the current constraint target.
|
|
517
|
+
|
|
518
|
+
``constr_val_now`` and ``d_constr`` must already correspond to the
|
|
519
|
+
current simulation time. Projection entry points refresh that state
|
|
520
|
+
before calling this method, which avoids differentiating a
|
|
521
|
+
time-dependent target in every nonlinear projection subiteration.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
X: Coordinates with shape ``(n_batch, n_atom, n_dim)``.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
The Jacobian with shape
|
|
528
|
+
``(n_batch, n_constr, n_atom, n_dim)`` and residual values with
|
|
529
|
+
shape ``(n_batch, n_constr)``.
|
|
530
|
+
|
|
531
|
+
"""
|
|
532
|
+
if self._compiled_jac is not None:
|
|
533
|
+
jac, y = self._compiled_jac(X, self.constr_val_now)
|
|
534
|
+
else:
|
|
535
|
+
jac, y = th.vmap(th.func.jacrev(self._constr_func_wrapped, has_aux=True))(X, self.constr_val_now)
|
|
536
|
+
self.jac = jac
|
|
537
|
+
return jac, y
|
|
538
|
+
|
|
539
|
+
def _do_qr(self, jac: th.Tensor):
|
|
540
|
+
"""
|
|
541
|
+
Do QR factorization and save/update self.Q.
|
|
542
|
+
Args:
|
|
543
|
+
jac: Jacobian matrix of s_k at X.
|
|
544
|
+
|
|
545
|
+
Returns: None
|
|
546
|
+
|
|
547
|
+
"""
|
|
548
|
+
# weighted jac: J_w = J @ M^-1/2,
|
|
549
|
+
# (n_batch, n_constr, n_atoms, n_dim) * (n_batch, 1, n_atoms, n_dim) -flatten-> (n_batch, n_constr, n_atoms * n_dim)
|
|
550
|
+
_jac = th.flatten(jac * self.negsqrtM.unsqueeze(1), -2, -1)
|
|
551
|
+
_jacT = _jac.mT.contiguous()
|
|
552
|
+
self.Q, self.R = th.linalg.qr(_jacT) # Q(n_batch, n_atoms * n_dim, n_constr) , R(n_batch, n_constr, n_constr)
|
|
553
|
+
|
|
554
|
+
def _detect_thres_inf(self, X: th.Tensor, jac: th.Tensor | None = None) -> float | None:
|
|
555
|
+
"""
|
|
556
|
+
Estimate the smallest achievable constraint residual for this dtype.
|
|
557
|
+
|
|
558
|
+
The estimate combines the measured round-off of the user constraint
|
|
559
|
+
function with the coordinate round-off amplified by the normal-space
|
|
560
|
+
spectrum. The function is probed at a few deterministic ULP-scale
|
|
561
|
+
perturbations in the working dtype and, when possible, in the next
|
|
562
|
+
precision selected by the local ``_PROBE_DTYPE`` map.
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
X: Reference coordinates with shape ``(n_batch, n_atom, n_dim)``.
|
|
566
|
+
jac: Constraint Jacobian at ``X``. Recomputed when omitted.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
The estimated residual floor, or ``None`` when the constraint
|
|
570
|
+
function cannot be probed in the reference precision.
|
|
571
|
+
"""
|
|
572
|
+
if jac is None:
|
|
573
|
+
jac, _ = self._jacobian(X)
|
|
574
|
+
work_dtype = X.dtype
|
|
575
|
+
reference_dtype = _PROBE_DTYPE.get(work_dtype, work_dtype)
|
|
576
|
+
with th.no_grad():
|
|
577
|
+
weighted_jac = th.flatten(jac * self.negsqrtM.unsqueeze(1), -2, -1)
|
|
578
|
+
sigma_max = float(th.linalg.svdvals(weighted_jac).max())
|
|
579
|
+
eps = th.finfo(work_dtype).eps
|
|
580
|
+
coordinate_scale = max(1., float(X.abs().max())) # bound of input X, clamp to 1 to avoid too small values.
|
|
581
|
+
evaluation_floor = 0.
|
|
582
|
+
if reference_dtype != work_dtype:
|
|
583
|
+
try:
|
|
584
|
+
rng = th.Generator(device=X.device).manual_seed(114514) # the hard-code seed is only for reproduction
|
|
585
|
+
noise = th.randn(X.shape, generator=rng, device=X.device, dtype=X.dtype)
|
|
586
|
+
# 8 ULP of the largest coordinate; clamped to avoid underflow
|
|
587
|
+
step = 8. * eps * X.abs().clamp_min(coordinate_scale)
|
|
588
|
+
probes = (X, X + step * noise, X - step * noise)
|
|
589
|
+
values_work = th.stack([th.vmap(self.constr_func)(p) for p in probes])
|
|
590
|
+
values_reference = th.stack([
|
|
591
|
+
th.vmap(self.constr_func)(p.to(reference_dtype)) for p in probes
|
|
592
|
+
]) # (3, batch, n_constr)
|
|
593
|
+
|
|
594
|
+
if values_reference.dtype != reference_dtype:
|
|
595
|
+
return None
|
|
596
|
+
difference = (values_work.to(reference_dtype) - values_reference).abs()
|
|
597
|
+
if not bool(th.isfinite(difference).all()):
|
|
598
|
+
return None
|
|
599
|
+
evaluation_floor = float(difference.max())
|
|
600
|
+
except Exception:
|
|
601
|
+
return None
|
|
602
|
+
return float(
|
|
603
|
+
2. * evaluation_floor
|
|
604
|
+
+ 2. * sigma_max * eps * coordinate_scale
|
|
605
|
+
) # count both the inherent error from constr_function and the error from Jacobian
|
|
606
|
+
|
|
607
|
+
def _apply_thres_estimate(self, floor: float|None, X: th.Tensor) -> None:
|
|
608
|
+
"""
|
|
609
|
+
Apply or warn about an estimated constraint-residual floor.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
floor: Estimate from ``_detect_thres_inf``; ``None`` when it failed.
|
|
613
|
+
X: Coordinates used for the estimate.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
None. An ``'auto'`` threshold is replaced by three times the floor;
|
|
617
|
+
an explicit threshold is never changed and is only warned about
|
|
618
|
+
when it lies below half of the estimated floor.
|
|
619
|
+
|
|
620
|
+
Raises:
|
|
621
|
+
RuntimeError: If the threshold is ``'auto'`` and the estimate failed.
|
|
622
|
+
"""
|
|
623
|
+
if floor is None:
|
|
624
|
+
if self._is_auto_thres:
|
|
625
|
+
raise RuntimeError(
|
|
626
|
+
'Unable to estimate the constraint threshold floor for the '
|
|
627
|
+
'given constraint function and dtype; set an explicit '
|
|
628
|
+
'`constr_threshold` or make the constraint function '
|
|
629
|
+
'evaluable in the probe precision.'
|
|
630
|
+
)
|
|
631
|
+
_report_diagnostic(
|
|
632
|
+
'Unable to estimate the constraint threshold floor for the '
|
|
633
|
+
'given constraint function; the user-supplied '
|
|
634
|
+
'`constr_threshold` is used unchanged.',
|
|
635
|
+
self.verbose,
|
|
636
|
+
self.logger,
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
if self._is_auto_thres:
|
|
640
|
+
self.constr_thres = max(3. * floor, th.finfo(X.dtype).eps)
|
|
641
|
+
return
|
|
642
|
+
is_below = float(self.constr_thres) < 0.5 * floor
|
|
643
|
+
if is_below and not self._thres_below:
|
|
644
|
+
_report_diagnostic(
|
|
645
|
+
f'`constr_threshold`={self.constr_thres} is below half of the '
|
|
646
|
+
f'estimated achievable floor ({floor:.3e} for dtype '
|
|
647
|
+
f'{X.dtype}); convergence may stagnate.',
|
|
648
|
+
self.verbose,
|
|
649
|
+
self.logger,
|
|
650
|
+
)
|
|
651
|
+
self._thres_below = is_below
|
|
652
|
+
|
|
653
|
+
def _project1(self, V:th.Tensor, X:th.Tensor | None = None, out: th.Tensor | None = None) -> th.Tensor:
|
|
654
|
+
"""
|
|
655
|
+
Project X into the manifold defined by all constrains by QR factorization.
|
|
656
|
+
Usually used for update velocity V.
|
|
657
|
+
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)
|
|
658
|
+
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.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
V: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
662
|
+
X: the point that calculate the projector. If None, stored Q R (maybe last step) will be used.
|
|
663
|
+
out: the output tensor. If None, return a new tensor.
|
|
664
|
+
|
|
665
|
+
Returns: th.Tensor, the projected X.
|
|
666
|
+
|
|
667
|
+
"""
|
|
668
|
+
n_batch, n_atoms, n_dim = V.shape
|
|
669
|
+
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
670
|
+
negsqrtM = self.negsqrtM # M^-1/2
|
|
671
|
+
# Check consistency. For time-dependent constraints, callers must have
|
|
672
|
+
# explicitly selected the target stage through ``_update_constr(t)`` or
|
|
673
|
+
# a preceding ``_project2`` call.
|
|
674
|
+
if X is not None:
|
|
675
|
+
jac, y = self._jacobian(X)
|
|
676
|
+
self._do_qr(jac)
|
|
677
|
+
#self._X_qr_cache = X.clone()
|
|
678
|
+
|
|
679
|
+
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
680
|
+
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
681
|
+
Px = (sqrtM * V).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
682
|
+
Px = (Q @ (Q.mT.contiguous() @ Px)).reshape(n_batch, n_atoms, n_dim) # (n_batch, n_atoms, n_dim)
|
|
683
|
+
Px.mul_(negsqrtM)
|
|
684
|
+
Px = th.sub(V, Px, out=out)
|
|
685
|
+
# manifold veloc. correction
|
|
686
|
+
if not self.is_const_constr: # time-dependent constraints
|
|
687
|
+
th.linalg.solve_triangular(self.R.mT.contiguous(), self.d_constr.unsqueeze(-1), upper=False, out=self.q)
|
|
688
|
+
Px.add_(self.negsqrtM * (self.Q @ self.q).reshape(n_batch, n_atoms, n_dim))
|
|
689
|
+
|
|
690
|
+
return Px
|
|
691
|
+
|
|
692
|
+
def _project_norm(self, V:th.Tensor, X:th.Tensor | None = None) -> th.Tensor:
|
|
693
|
+
"""
|
|
694
|
+
Project V to normal space.
|
|
695
|
+
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)
|
|
696
|
+
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.
|
|
697
|
+
Args:
|
|
698
|
+
V: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
699
|
+
|
|
700
|
+
Returns: th.Tensor, the projected X.
|
|
701
|
+
|
|
702
|
+
"""
|
|
703
|
+
n_batch, n_atoms, n_dim = V.shape
|
|
704
|
+
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
705
|
+
negsqrtM = self.negsqrtM # M^-1/2
|
|
706
|
+
# The normal-space basis depends on the coordinate Jacobian, not on the
|
|
707
|
+
# target value. Do not alter the caller-selected constraint time here.
|
|
708
|
+
if X is not None:
|
|
709
|
+
jac, y = self._jacobian(X)
|
|
710
|
+
self._do_qr(jac)
|
|
711
|
+
|
|
712
|
+
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
713
|
+
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
714
|
+
Px = (sqrtM * V).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
715
|
+
Px = (Q @ (Q.mT.contiguous() @ Px)).reshape(n_batch, n_atoms, n_dim) # (n_batch, n_atoms, n_dim)
|
|
716
|
+
Px.mul_(negsqrtM)
|
|
717
|
+
|
|
718
|
+
return Px
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def _retraction_newton(
|
|
722
|
+
X: th.Tensor,
|
|
723
|
+
X_work: th.Tensor,
|
|
724
|
+
sqrtM_Q: th.Tensor,
|
|
725
|
+
R: th.Tensor,
|
|
726
|
+
aux_multiplier: th.Tensor,
|
|
727
|
+
jacobian_func: Callable[[th.Tensor], Tuple[th.Tensor, th.Tensor]],
|
|
728
|
+
initial_constr_err: th.Tensor,
|
|
729
|
+
constr_threshold: float,
|
|
730
|
+
stagnation_threshold: float,
|
|
731
|
+
max_proj_iter: int,
|
|
732
|
+
time_step: float,
|
|
733
|
+
verbose: int,
|
|
734
|
+
logger: Any,
|
|
735
|
+
) -> Tuple[th.Tensor, th.Tensor, th.Tensor]:
|
|
736
|
+
"""Solve the retraction in the fixed initial normal space.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
X: Coordinates before retraction. Updated in-place with the final
|
|
740
|
+
working coordinates when the solver exits.
|
|
741
|
+
X_work: Coordinate work buffer with the same shape as ``X``.
|
|
742
|
+
sqrtM_Q: Fixed mass-weighted normal basis.
|
|
743
|
+
R: Fixed upper-triangular QR factor for force recovery.
|
|
744
|
+
aux_multiplier: Accumulated multiplier updated in-place.
|
|
745
|
+
jacobian_func: Callable returning the current Jacobian and
|
|
746
|
+
residual.
|
|
747
|
+
initial_constr_err: Initial maximum constraint residual.
|
|
748
|
+
constr_threshold: Absolute constraint convergence threshold.
|
|
749
|
+
stagnation_threshold: Threshold for the existing early-stop
|
|
750
|
+
criterion.
|
|
751
|
+
max_proj_iter: Maximum number of solver iterations.
|
|
752
|
+
time_step: Integration time step used for force conversion.
|
|
753
|
+
verbose: Constraint diagnostic verbosity.
|
|
754
|
+
logger: Logger used for diagnostics.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
``Fc``, the constraint force recovered once from the final
|
|
758
|
+
multiplier; ``dX``, the final coordinate displacement; and a
|
|
759
|
+
scalar Boolean tensor that is true only when the constraint
|
|
760
|
+
residual reached ``constr_threshold``.
|
|
761
|
+
"""
|
|
762
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
763
|
+
dX = th.zeros_like(X)
|
|
764
|
+
X_work.copy_(X)
|
|
765
|
+
constr_err_old = initial_constr_err
|
|
766
|
+
|
|
767
|
+
def _final_calc_Fc():
|
|
768
|
+
"""
|
|
769
|
+
Re-solve the constraints force by triangular solve.
|
|
770
|
+
"""
|
|
771
|
+
fc: th.Tensor = th.linalg.solve_triangular(
|
|
772
|
+
R,
|
|
773
|
+
aux_multiplier.unsqueeze(-1),
|
|
774
|
+
upper=True,
|
|
775
|
+
).squeeze(-1)
|
|
776
|
+
fc *= 207.28617 / (time_step ** 2)
|
|
777
|
+
X.copy_(X_work)
|
|
778
|
+
return fc
|
|
779
|
+
|
|
780
|
+
for i in range(max_proj_iter):
|
|
781
|
+
jac, y = jacobian_func(X_work)
|
|
782
|
+
constr_err = th.max(th.abs(y))
|
|
783
|
+
if verbose > 1:
|
|
784
|
+
logger.info(f'{i: <3d} Constraint errors are now: {constr_err:.4e}')
|
|
785
|
+
|
|
786
|
+
is_converged = constr_err <= constr_threshold
|
|
787
|
+
is_fixed = th.abs(constr_err - constr_err_old) < stagnation_threshold
|
|
788
|
+
if is_converged or is_fixed:
|
|
789
|
+
if is_fixed and not is_converged:
|
|
790
|
+
_report_diagnostic(
|
|
791
|
+
'Constraint errors are not converged yet, but it has '
|
|
792
|
+
'been fixed. This loop will be skipped.',
|
|
793
|
+
verbose,
|
|
794
|
+
logger,
|
|
795
|
+
)
|
|
796
|
+
Fc = _final_calc_Fc()
|
|
797
|
+
return Fc, dX, is_converged
|
|
798
|
+
constr_err_old = constr_err
|
|
799
|
+
|
|
800
|
+
aux = th.bmm(jac.flatten(-2, -1), sqrtM_Q)
|
|
801
|
+
d_multiplier, _ = th.linalg.solve_ex(aux, -y)
|
|
802
|
+
aux_multiplier.add_(d_multiplier)
|
|
803
|
+
dX = th.einsum(
|
|
804
|
+
'bnc, bc -> bn',
|
|
805
|
+
sqrtM_Q, aux_multiplier
|
|
806
|
+
).reshape(n_batch, n_atoms, n_dim)
|
|
807
|
+
th.add(X, dX, out=X_work)
|
|
808
|
+
|
|
809
|
+
_report_diagnostic(
|
|
810
|
+
"Projection of X to the manifold is not converged.", verbose, logger,
|
|
811
|
+
)
|
|
812
|
+
is_converged = th.zeros((), device=X.device, dtype=th.bool)
|
|
813
|
+
Fc = _final_calc_Fc()
|
|
814
|
+
return Fc, dX, is_converged
|
|
815
|
+
|
|
816
|
+
@staticmethod
|
|
817
|
+
def _retraction_damp_newton(
|
|
818
|
+
X: th.Tensor,
|
|
819
|
+
X_work: th.Tensor,
|
|
820
|
+
sqrtM_Q: th.Tensor,
|
|
821
|
+
R: th.Tensor,
|
|
822
|
+
aux_multiplier: th.Tensor,
|
|
823
|
+
jacobian_func: Callable[[th.Tensor], Tuple[th.Tensor, th.Tensor]],
|
|
824
|
+
initial_constr_err: th.Tensor,
|
|
825
|
+
constr_threshold: float,
|
|
826
|
+
stagnation_threshold: float,
|
|
827
|
+
max_proj_iter: int,
|
|
828
|
+
time_step: float,
|
|
829
|
+
verbose: int,
|
|
830
|
+
logger: Any,
|
|
831
|
+
max_backtrack: int = 8,
|
|
832
|
+
step_shrink: float = 0.5,
|
|
833
|
+
) -> Tuple[th.Tensor, th.Tensor, th.Tensor]:
|
|
834
|
+
"""Solve the retraction with a safeguarded Newton step.
|
|
835
|
+
|
|
836
|
+
The Newton direction is always formed in the fixed initial normal
|
|
837
|
+
space. A trial multiplier is accepted only when it lowers the maximum
|
|
838
|
+
absolute constraint residual; otherwise the same direction is reduced
|
|
839
|
+
geometrically. The accumulated multiplier is changed only after an
|
|
840
|
+
accepted trial, so the force and velocity correction retain their
|
|
841
|
+
existing physical meaning.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
X: Coordinates before retraction. Updated in-place on exit.
|
|
845
|
+
X_work: Coordinate work buffer with the same shape as ``X``.
|
|
846
|
+
sqrtM_Q: Fixed mass-weighted normal basis.
|
|
847
|
+
R: Fixed upper-triangular QR factor for force recovery.
|
|
848
|
+
aux_multiplier: Accumulated multiplier updated in-place.
|
|
849
|
+
jacobian_func: Callable returning the current Jacobian and residual.
|
|
850
|
+
initial_constr_err: Initial maximum constraint residual.
|
|
851
|
+
constr_threshold: Absolute constraint convergence threshold.
|
|
852
|
+
stagnation_threshold: Threshold for the existing early-stop test.
|
|
853
|
+
max_proj_iter: Maximum number of solver iterations.
|
|
854
|
+
time_step: Integration time step used for force conversion.
|
|
855
|
+
verbose: Constraint diagnostic verbosity.
|
|
856
|
+
logger: Logger used for diagnostics.
|
|
857
|
+
max_backtrack: Maximum number of step reductions per iteration.
|
|
858
|
+
step_shrink: Multiplicative reduction applied to a rejected step.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
``Fc``, ``dX``, and a scalar Boolean convergence status.
|
|
862
|
+
"""
|
|
863
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
864
|
+
dX = th.zeros_like(X)
|
|
865
|
+
X_work.copy_(X)
|
|
866
|
+
constr_err_old = initial_constr_err
|
|
867
|
+
is_converged = initial_constr_err <= constr_threshold
|
|
868
|
+
|
|
869
|
+
def _final_calc_Fc():
|
|
870
|
+
"""
|
|
871
|
+
Re-solve the constraints force by triangular solve.
|
|
872
|
+
"""
|
|
873
|
+
fc: th.Tensor = th.linalg.solve_triangular(
|
|
874
|
+
R,
|
|
875
|
+
aux_multiplier.unsqueeze(-1),
|
|
876
|
+
upper=True,
|
|
877
|
+
).squeeze(-1)
|
|
878
|
+
fc *= 207.28617 / (time_step ** 2)
|
|
879
|
+
X.copy_(X_work)
|
|
880
|
+
return fc
|
|
881
|
+
|
|
882
|
+
for i in range(max_proj_iter):
|
|
883
|
+
jac, y = jacobian_func(X_work)
|
|
884
|
+
constr_err = th.max(th.abs(y))
|
|
885
|
+
if verbose > 1:
|
|
886
|
+
logger.info(f'{i: <3d} Constraint errors are now: {constr_err:.4e}')
|
|
887
|
+
|
|
888
|
+
aux = th.bmm(jac.flatten(-2, -1), sqrtM_Q)
|
|
889
|
+
aux_q, aux_r = th.linalg.qr(aux.mT)
|
|
890
|
+
aux_diag = th.abs(th.diagonal(aux_r, dim1=-2, dim2=-1))
|
|
891
|
+
aux_scale = th.max(aux_diag)
|
|
892
|
+
is_rank_deficient = aux_scale <= 0
|
|
893
|
+
if not is_rank_deficient:
|
|
894
|
+
is_rank_deficient = th.min(aux_diag) <= 1e-6 * aux_scale
|
|
895
|
+
|
|
896
|
+
is_converged = constr_err <= constr_threshold
|
|
897
|
+
is_fixed = th.abs(constr_err - constr_err_old) < stagnation_threshold
|
|
898
|
+
if is_fixed or is_converged:
|
|
899
|
+
if is_fixed and (not is_converged):
|
|
900
|
+
if is_rank_deficient:
|
|
901
|
+
_report_diagnostic(
|
|
902
|
+
'Constraint errors are not converged yet, but the '
|
|
903
|
+
'current normal-space Jacobian is degenerate. The '
|
|
904
|
+
'remaining iterations will be skipped.',
|
|
905
|
+
verbose,
|
|
906
|
+
logger,
|
|
907
|
+
)
|
|
908
|
+
else:
|
|
909
|
+
_report_diagnostic(
|
|
910
|
+
'Constraint errors are not converged yet, but the '
|
|
911
|
+
'residual improvement is below the stagnation '
|
|
912
|
+
'threshold. The remaining iterations will be skipped.',
|
|
913
|
+
verbose,
|
|
914
|
+
logger,
|
|
915
|
+
)
|
|
916
|
+
Fc = _final_calc_Fc()
|
|
917
|
+
return Fc, dX, is_converged
|
|
918
|
+
constr_err_old = constr_err
|
|
919
|
+
|
|
920
|
+
d_multiplier = th.linalg.solve_triangular(
|
|
921
|
+
aux_r.mT.contiguous(),
|
|
922
|
+
(-y).unsqueeze(-1),
|
|
923
|
+
upper=False,
|
|
924
|
+
)
|
|
925
|
+
d_multiplier = th.bmm(aux_q, d_multiplier).squeeze(-1)
|
|
926
|
+
accepted = False
|
|
927
|
+
step = 1.
|
|
928
|
+
candidate_multiplier = th.empty_like(aux_multiplier)
|
|
929
|
+
candidate_dX = th.empty_like(dX)
|
|
930
|
+
for _ in range(max_backtrack + 1):
|
|
931
|
+
candidate_multiplier.copy_(aux_multiplier).add_(d_multiplier, alpha=step)
|
|
932
|
+
candidate_dX = th.einsum(
|
|
933
|
+
'bnc, bc -> bn',
|
|
934
|
+
sqrtM_Q, candidate_multiplier
|
|
935
|
+
).reshape(n_batch, n_atoms, n_dim)
|
|
936
|
+
th.add(X, candidate_dX, out=X_work)
|
|
937
|
+
_, y_trial = jacobian_func(X_work)
|
|
938
|
+
trial_err = th.max(th.abs(y_trial))
|
|
939
|
+
if trial_err < constr_err:
|
|
940
|
+
accepted = True
|
|
941
|
+
break
|
|
942
|
+
th.add(X, dX, out=X_work)
|
|
943
|
+
step *= step_shrink
|
|
944
|
+
|
|
945
|
+
if not accepted:
|
|
946
|
+
_report_diagnostic(
|
|
947
|
+
'Damped Newton retraction step did not lower the constraint error.',
|
|
948
|
+
verbose,
|
|
949
|
+
logger,
|
|
950
|
+
)
|
|
951
|
+
Fc = _final_calc_Fc()
|
|
952
|
+
return Fc, dX, is_converged
|
|
953
|
+
|
|
954
|
+
aux_multiplier.copy_(candidate_multiplier)
|
|
955
|
+
dX.copy_(candidate_dX)
|
|
956
|
+
|
|
957
|
+
_report_diagnostic(
|
|
958
|
+
"Projection of X to the manifold is not converged.", verbose, logger,
|
|
959
|
+
)
|
|
960
|
+
Fc = _final_calc_Fc()
|
|
961
|
+
return Fc, dX, is_converged
|
|
962
|
+
|
|
963
|
+
def _project2(
|
|
964
|
+
self,
|
|
965
|
+
X: th.Tensor,
|
|
966
|
+
X_orig: th.Tensor | None = None,
|
|
967
|
+
V: th.Tensor | None = None,
|
|
968
|
+
constr_time: th.Tensor | float | None = None,
|
|
969
|
+
) -> Tuple[th.Tensor, th.Tensor|None, th.Tensor|None]:
|
|
970
|
+
"""
|
|
971
|
+
Continuously project the Jacobian of all constrains to the exact manifold.
|
|
972
|
+
** Update X and V (if given) in-place.
|
|
973
|
+
If V is provided, it is updated in-place with the constraint force contribution
|
|
974
|
+
to velocity, using the converged multiplier mu to avoid subtraction cancellation.
|
|
975
|
+
|
|
976
|
+
Args:
|
|
977
|
+
X: (n_batch, n_atom, n_dim) coordinates to be projected. Modified in-place.
|
|
978
|
+
X_orig: reference coordinates for fixed Jacobian basis. Defaults to X.
|
|
979
|
+
V: (n_batch, n_atom, n_dim) velocities. If given, updated in-place by
|
|
980
|
+
dv = M^{-1/2} Q mu / time_step, where mu is the converged multiplier.
|
|
981
|
+
constr_time: Time at which the position constraint is enforced. If
|
|
982
|
+
``None``, use ``self.time_now + self.time_step``, corresponding
|
|
983
|
+
to the end of the current outer MD step. Explicit values allow
|
|
984
|
+
integrators with other splitting schemes to select a half-step
|
|
985
|
+
or another stage without modifying the parent MD clock.
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
Fc: constraint forces (eV/atom)
|
|
989
|
+
G: geometric Fixman force term, or ``None`` when
|
|
990
|
+
``require_fixman=False``.
|
|
991
|
+
w: Fixman/Blue-Moon reweighting factor
|
|
992
|
+
``1 / sqrt(det(J M^-1 J^T))`` for each batch sample, or
|
|
993
|
+
``None`` when ``require_fixman=False``.
|
|
994
|
+
"""
|
|
995
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
996
|
+
self._proj2_call_number += 1
|
|
997
|
+
n_constr = self.R.shape[-1]
|
|
998
|
+
aux_multipler = th.zeros(n_batch, n_constr, device=self.device, dtype=FLOAT_TYPE)
|
|
999
|
+
if (X_orig is None) or (X_orig is X):
|
|
1000
|
+
# A detached reference is required: the retraction rewrites ``X``
|
|
1001
|
+
# in place at the end, which is illegal on a leaf that requires
|
|
1002
|
+
# grad for the Fixman derivative.
|
|
1003
|
+
X_orig = X.detach()
|
|
1004
|
+
time_step = self.time_step
|
|
1005
|
+
|
|
1006
|
+
# The target value and its time derivative are constant throughout one
|
|
1007
|
+
# nonlinear position projection. Refresh them once at the end-of-step
|
|
1008
|
+
# stage by default, before the fixed-basis Jacobian and all subsequent
|
|
1009
|
+
# Newton subiterations. The following velocity projection reuses this
|
|
1010
|
+
# same staged state.
|
|
1011
|
+
if constr_time is None:
|
|
1012
|
+
constr_time = self.time_now + time_step
|
|
1013
|
+
self._update_constr(constr_time)
|
|
1014
|
+
|
|
1015
|
+
with th.set_grad_enabled(self.require_fixman):
|
|
1016
|
+
X_orig.requires_grad_(self.require_fixman)
|
|
1017
|
+
jac, y = self._jacobian(X_orig)
|
|
1018
|
+
check_freq = self._detect_dynamic_thres
|
|
1019
|
+
if isinstance(check_freq, int):
|
|
1020
|
+
if self._proj2_call_number % check_freq == 0:
|
|
1021
|
+
self._apply_thres_estimate(self._detect_thres_inf(X_orig, jac), X_orig)
|
|
1022
|
+
|
|
1023
|
+
constr_thres = self.constr_thres
|
|
1024
|
+
self._do_qr(jac)
|
|
1025
|
+
Q, R = self.Q, self.R
|
|
1026
|
+
# Safeguard against degenerate constraint directions
|
|
1027
|
+
# (e.g., erf soft-CN with vanishing gradients):
|
|
1028
|
+
# mask y for convergence — degenerate dirs ignored
|
|
1029
|
+
# clamp R diagonals — prevents solve_triangular blow-up
|
|
1030
|
+
# Stop only when the residual improvement is small compared with
|
|
1031
|
+
# the requested tolerance. Five percent preserves
|
|
1032
|
+
# the cheap stagnation escape without cutting off useful Newton
|
|
1033
|
+
# progress too early.
|
|
1034
|
+
degenerate_eps = self.retraction_stagnation_factor * constr_thres
|
|
1035
|
+
#degenerate_mask = th.abs(R.diagonal(dim1=-2, dim2=-1)) < degenerate_eps # (n_batch, n_constr)
|
|
1036
|
+
#R.diagonal(dim1=-2, dim2=-1).clamp_min_(degenerate_eps)
|
|
1037
|
+
sqrtM_Q = self.negsqrtM.reshape(n_batch, -1, 1) * self.Q # (b, an, c)
|
|
1038
|
+
if self.require_fixman:
|
|
1039
|
+
# (n_batch, n_constr, n_constr) -> (n_batch, n_constr)
|
|
1040
|
+
abs_diag_R = th.diagonal(R, dim1=-2, dim2=-1).abs()
|
|
1041
|
+
log_sqrt_det_Z = abs_diag_R.log_().sum(dim=-1) # (n_batch), "sqrt" is due to Z=R^T R, R is triangular
|
|
1042
|
+
half_logdet_Z = log_sqrt_det_Z.sum() # scalar
|
|
1043
|
+
# A linear constraint has a coordinate-independent Jacobian,
|
|
1044
|
+
# hence a constant Fixman determinant and an exactly zero
|
|
1045
|
+
# derivative. In that case ``half_logdet_Z`` contains no
|
|
1046
|
+
# autograd graph and calling ``autograd.grad`` would raise even
|
|
1047
|
+
# though the required result is well-defined. ``allow_unused``
|
|
1048
|
+
# also covers CV implementations whose Jacobian graph is
|
|
1049
|
+
# present but independent of some/all coordinate inputs.
|
|
1050
|
+
if half_logdet_Z.requires_grad:
|
|
1051
|
+
half_dlnz_dx = th.autograd.grad(
|
|
1052
|
+
half_logdet_Z,
|
|
1053
|
+
X_orig,
|
|
1054
|
+
allow_unused=True,
|
|
1055
|
+
)[0]
|
|
1056
|
+
if half_dlnz_dx is None:
|
|
1057
|
+
half_dlnz_dx = th.zeros_like(X_orig)
|
|
1058
|
+
else:
|
|
1059
|
+
half_dlnz_dx = th.zeros_like(X_orig)
|
|
1060
|
+
half_dlnz_dx = half_dlnz_dx.reshape(n_batch, -1)
|
|
1061
|
+
RG = th.einsum('bnc, bn -> bc', sqrtM_Q, half_dlnz_dx)
|
|
1062
|
+
G = th.linalg.solve_triangular(R, RG.unsqueeze(-1), upper=True).squeeze(-1)
|
|
1063
|
+
w = log_sqrt_det_Z.neg_().exp_()
|
|
1064
|
+
else:
|
|
1065
|
+
G = None
|
|
1066
|
+
w = None
|
|
1067
|
+
R.detach_()
|
|
1068
|
+
sqrtM_Q.detach_()
|
|
1069
|
+
|
|
1070
|
+
Fc, dX, _is_converged = self._retraction_map_solver(
|
|
1071
|
+
X=X,
|
|
1072
|
+
X_work=self.X_cache,
|
|
1073
|
+
sqrtM_Q=sqrtM_Q,
|
|
1074
|
+
R=R,
|
|
1075
|
+
aux_multiplier=aux_multipler,
|
|
1076
|
+
jacobian_func=self._jacobian,
|
|
1077
|
+
initial_constr_err=th.max(th.abs(y)),
|
|
1078
|
+
constr_threshold=constr_thres,
|
|
1079
|
+
stagnation_threshold=degenerate_eps,
|
|
1080
|
+
max_proj_iter=self.max_proj_iter,
|
|
1081
|
+
time_step=time_step,
|
|
1082
|
+
verbose=self.verbose,
|
|
1083
|
+
logger=self.logger,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
if V is not None:
|
|
1087
|
+
V.add_(dX.div_(time_step))
|
|
1088
|
+
|
|
1089
|
+
return Fc, G, w
|
|
1090
|
+
|
|
1091
|
+
def _jacobian_derivative(self, X, v: th.Tensor) -> th.Tensor:
|
|
1092
|
+
"""
|
|
1093
|
+
Compute `dJ/dt = v^T H` by auto-differentiation.
|
|
1094
|
+
Args:
|
|
1095
|
+
X:
|
|
1096
|
+
v:
|
|
1097
|
+
|
|
1098
|
+
Returns: v^T H with shape (n_batch, n_constr, n_atom, n_dim)
|
|
1099
|
+
|
|
1100
|
+
"""
|
|
1101
|
+
vH: th.Tensor = bhvp(self._constr_func_for_hvp, (X,), (v,), False) # (n_batch, n_constr, n_atom, n_dim)
|
|
1102
|
+
return vH.contiguous()
|
|
1103
|
+
|
|
1104
|
+
def christoffel_solver(
|
|
1105
|
+
self,
|
|
1106
|
+
X: th.Tensor,
|
|
1107
|
+
V: th.Tensor,
|
|
1108
|
+
F_tan: th.Tensor | None = None,
|
|
1109
|
+
max_iter: int = 20,
|
|
1110
|
+
tol: float = 1e-6,
|
|
1111
|
+
omega: float = 0.7
|
|
1112
|
+
):
|
|
1113
|
+
r"""
|
|
1114
|
+
Implicit midpoint integration of the constrained geodesic equation:
|
|
1115
|
+
|
|
1116
|
+
dV/dt = -Gamma(V, V) + M^{-1} F_tan
|
|
1117
|
+
|
|
1118
|
+
where Gamma = M^{-1/2} Q R^{-T} (V^T H) V is the Christoffel symbol
|
|
1119
|
+
capturing the curvature of the constraint manifold, and F_tan is the
|
|
1120
|
+
tangent-space projection of the physical forces.
|
|
1121
|
+
|
|
1122
|
+
Uses the Woodbury identity to reduce the Newton-step matrix inverse
|
|
1123
|
+
from O(N_free^3) to O(k^3), where k << N_free is the number of constraints.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
X: current positions (n_batch, n_atom, n_dim)
|
|
1127
|
+
V: current velocities (n_batch, n_atom, n_dim)
|
|
1128
|
+
F_tan: tangent-projected forces, or None for pure-geodesic flow
|
|
1129
|
+
max_iter: maximum Newton iterations
|
|
1130
|
+
tol: convergence tolerance on the residual norm
|
|
1131
|
+
omega: SOR relaxation factor (< 1 damps, =1 pure Newton)
|
|
1132
|
+
|
|
1133
|
+
Returns:
|
|
1134
|
+
X_new, V_new – updated in-place.
|
|
1135
|
+
"""
|
|
1136
|
+
n_batch, n_atom, n_dim = X.shape
|
|
1137
|
+
n_free = n_atom * n_dim
|
|
1138
|
+
dt = self.time_step
|
|
1139
|
+
|
|
1140
|
+
# Precompute Woodbury blocks (independent of X_new, V_new)
|
|
1141
|
+
B = self.negsqrtM.reshape(n_batch, -1, 1) * self.Q # (B, n_free, k)
|
|
1142
|
+
B_T = B.mT.contiguous() # (B, k, n_free)
|
|
1143
|
+
eye_k = th.eye(self.R.shape[-1], device=X.device, dtype=X.dtype).expand(n_batch, -1, -1)
|
|
1144
|
+
|
|
1145
|
+
V_old = V.clone()
|
|
1146
|
+
X_old = X.clone()
|
|
1147
|
+
# Initial guess: explicit Euler half-step
|
|
1148
|
+
X_new = X.add(V_old, alpha=dt)
|
|
1149
|
+
V_new = V.clone()
|
|
1150
|
+
if F_tan is not None:
|
|
1151
|
+
# Include force: V_new = V_old + dt * acc, acc = M^{-1} F_tan (F_tan is already force)
|
|
1152
|
+
acc = F_tan * self.negsqrtM * self.negsqrtM # M^{-1} = negsqrtM^2
|
|
1153
|
+
V_new.add_(acc, alpha=dt)
|
|
1154
|
+
|
|
1155
|
+
for it in range(max_iter):
|
|
1156
|
+
X_mid = (X_new + X_old) * 0.5
|
|
1157
|
+
V_mid = (V_new + V_old) * 0.5
|
|
1158
|
+
|
|
1159
|
+
# Compute Christoffel symbol: Gamma = B @ R^{-T} @ (J' V_mid)
|
|
1160
|
+
dJ = self._jacobian_derivative(X_mid, V_mid) # (B, k, N, D)
|
|
1161
|
+
dJ_flat = dJ.flatten(-2, -1) # (B, k, n_free)
|
|
1162
|
+
Jp_V = th.einsum('bkn, bn -> bk', dJ_flat, V_mid.flatten(-2, -1)) # J'·V_mid (B, k)
|
|
1163
|
+
_sol = th.linalg.solve_triangular(
|
|
1164
|
+
self.R.mT.contiguous(), Jp_V.unsqueeze(-1), upper=False
|
|
1165
|
+
) # R^{-T} J'·V (B, k, 1)
|
|
1166
|
+
Gamma = (B @ _sol).reshape(n_batch, n_atom, n_dim) # Christoffel (B, N, D)
|
|
1167
|
+
|
|
1168
|
+
# Residuals for the implicit midpoint system
|
|
1169
|
+
# R_X = X_new - X_old - dt * V_mid
|
|
1170
|
+
R_X = X_new - X_old - dt * V_mid
|
|
1171
|
+
# R_V = V_new - V_old - dt * (acc - Gamma)
|
|
1172
|
+
if F_tan is not None:
|
|
1173
|
+
R_V = V_new - V_old - dt * (acc - Gamma)
|
|
1174
|
+
else:
|
|
1175
|
+
R_V = V_new - V_old + dt * Gamma
|
|
1176
|
+
|
|
1177
|
+
# Convergence check
|
|
1178
|
+
norm_RX = th.linalg.norm(R_X.reshape(n_batch, -1), dim=-1)
|
|
1179
|
+
norm_RV = th.linalg.norm(R_V.reshape(n_batch, -1), dim=-1)
|
|
1180
|
+
if th.all((norm_RX < tol) & (norm_RV < tol)):
|
|
1181
|
+
V.copy_(V_new)
|
|
1182
|
+
X.copy_(X_new)
|
|
1183
|
+
return X_new, V_new
|
|
1184
|
+
|
|
1185
|
+
# --- Newton update with Woodbury identity ---
|
|
1186
|
+
# dJ_flat^T = dJ.mT, shape (B, n_free, k)
|
|
1187
|
+
dJ_T = dJ_flat.mT.contiguous() # (B, n_free, k)
|
|
1188
|
+
dJ_T_B = th.bmm(dJ_T, B) # (B, k, k)
|
|
1189
|
+
M_wood = eye_k + dt * dJ_T_B # I + dt * dJ^T B
|
|
1190
|
+
rv_flat = R_V.reshape(n_batch, -1, 1) # (B, n_free, 1)
|
|
1191
|
+
|
|
1192
|
+
dJ_T_rv = th.bmm(dJ_T, rv_flat) # dJ^T @ R_V (B, k, 1)
|
|
1193
|
+
sol_W = th.linalg.solve(M_wood, dJ_T_rv) # (I + dt dJ^T B)^{-1} dJ^T R_V
|
|
1194
|
+
P_RV = rv_flat - dt * th.bmm(B, sol_W) # P @ R_V (B, n_free, 1)
|
|
1195
|
+
|
|
1196
|
+
dV = P_RV.reshape(n_batch, n_atom, n_dim)
|
|
1197
|
+
dX = (R_X + 0.5 * dt * dV)
|
|
1198
|
+
|
|
1199
|
+
# SOR relaxation
|
|
1200
|
+
V_new = V_new - dV
|
|
1201
|
+
V_new = omega * V_new + (1.0 - omega) * V
|
|
1202
|
+
X_new = X_new - dX
|
|
1203
|
+
|
|
1204
|
+
# Did not converge – return best effort
|
|
1205
|
+
V.copy_(V_new)
|
|
1206
|
+
X.copy_(X_new)
|
|
1207
|
+
return X_new, V_new
|