flashmd 0.2.8__tar.gz → 0.3.0__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 (30) hide show
  1. {flashmd-0.2.8 → flashmd-0.3.0}/PKG-INFO +18 -9
  2. {flashmd-0.2.8 → flashmd-0.3.0}/README.md +10 -3
  3. flashmd-0.3.0/pyproject.toml +105 -0
  4. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/ase/__init__.py +1 -1
  5. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/ase/bussi.py +17 -3
  6. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/ase/langevin.py +10 -2
  7. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/ase/velocity_verlet.py +45 -20
  8. flashmd-0.3.0/src/flashmd/fpi.py +86 -0
  9. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/ipi.py +122 -23
  10. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/models.py +55 -3
  11. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/stepper.py +24 -15
  12. flashmd-0.3.0/src/flashmd/symplectic_stepper.py +189 -0
  13. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd.egg-info/PKG-INFO +18 -9
  14. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd.egg-info/SOURCES.txt +6 -1
  15. flashmd-0.3.0/src/flashmd.egg-info/requires.txt +11 -0
  16. {flashmd-0.2.8 → flashmd-0.3.0}/tests/test_edge_cases.py +5 -5
  17. flashmd-0.3.0/tests/test_ipi.py +111 -0
  18. flashmd-0.3.0/tests/test_random_rotation.py +26 -0
  19. flashmd-0.3.0/tests/test_symplectic.py +66 -0
  20. flashmd-0.2.8/pyproject.toml +0 -82
  21. flashmd-0.2.8/src/flashmd.egg-info/requires.txt +0 -8
  22. {flashmd-0.2.8 → flashmd-0.3.0}/LICENSE +0 -0
  23. {flashmd-0.2.8 → flashmd-0.3.0}/setup.cfg +0 -0
  24. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/__init__.py +0 -0
  25. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd/constraints.py +0 -0
  26. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd.egg-info/dependency_links.txt +0 -0
  27. {flashmd-0.2.8 → flashmd-0.3.0}/src/flashmd.egg-info/top_level.txt +0 -0
  28. {flashmd-0.2.8 → flashmd-0.3.0}/tests/test_energy.py +0 -0
  29. {flashmd-0.2.8 → flashmd-0.3.0}/tests/test_models.py +0 -0
  30. {flashmd-0.2.8 → flashmd-0.3.0}/tests/test_readme.py +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashmd
3
- Version: 0.2.8
3
+ Version: 0.3.0
4
4
  Summary: Accelerated molecular dynamics with large-time-step predictions
5
5
  Author: flashmd developers
6
- License: Apache-2.0
6
+ License-Expression: Apache-2.0
7
7
  Keywords: machine learning,molecular modeling,molecular dynamics
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: Apache Software License
11
10
  Classifier: Operating System :: POSIX
12
11
  Classifier: Operating System :: MacOS :: MacOS X
13
12
  Classifier: Operating System :: Microsoft :: Windows
@@ -19,12 +18,15 @@ Classifier: Topic :: Scientific/Engineering :: Chemistry
19
18
  Classifier: Topic :: Scientific/Engineering :: Physics
20
19
  Classifier: Topic :: Software Development :: Libraries
21
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Requires-Python: >=3.10
21
+ Requires-Python: >=3.11
23
22
  Description-Content-Type: text/markdown
24
23
  License-File: LICENSE
25
- Requires-Dist: metatrain==2026.2
26
- Requires-Dist: ase
24
+ Requires-Dist: metatrain==2026.3.1
25
+ Requires-Dist: metatomic-ase
26
+ Requires-Dist: vesin>=0.5.8
27
+ Requires-Dist: ase>=3.29.0
27
28
  Requires-Dist: huggingface_hub
29
+ Requires-Dist: scipy
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: pytest; extra == "dev"
30
32
  Requires-Dist: ruff; extra == "dev"
@@ -56,7 +58,7 @@ After installation, you can run accelerated molecular dynamics as follows:
56
58
  import ase.build
57
59
  import ase.units
58
60
  import torch
59
- from ase.md.velocitydistribution import MaxwellBoltzmannDistribution
61
+ from ase.md.velocitydistribution import thermalize_momenta
60
62
 
61
63
  from flashmd import get_pretrained
62
64
  from flashmd.ase import EnergyCalculator
@@ -68,7 +70,7 @@ time_step = 64 # 64 fs; also available: 1, 2, 4, 8, 16, 32, 128 fs
68
70
 
69
71
  # Create a structure and initialize velocities
70
72
  atoms = ase.build.bulk("Al", "fcc", cubic=True)
71
- MaxwellBoltzmannDistribution(atoms, temperature_K=300)
73
+ thermalize_momenta(atoms, temperature_K=300)
72
74
  atoms.set_velocities( # it is generally a good idea to remove any net velocity
73
75
  atoms.get_velocities() - atoms.get_momenta().sum(axis=0) / atoms.get_masses().sum()
74
76
  )
@@ -128,7 +130,7 @@ Using FlashMD in i-PI
128
130
  ---------------------
129
131
 
130
132
  You can see
131
- [this cookbook recipe](https://atomistic-cookbook.org/examples/flashmd/flashmd-demo.html)
133
+ [this cookbook recipe](https://atomistic-cookbook.org/examples/flashmd/flashmd-demo.html)
132
134
  for usage examples. i-PI is our most mature interface, and the one that was used to
133
135
  generate all our published results.
134
136
 
@@ -191,3 +193,10 @@ Note that the results were obtained through the i-PI interface.
191
193
 
192
194
  Instructions and material to reproduce the results in the paper are available on
193
195
  Materials Cloud at https://doi.org/10.24435/materialscloud:b7-xq.
196
+
197
+ Maintainers
198
+ -----------
199
+
200
+ This project is [maintained](https://github.com/lab-cosmo/.github/blob/main/Maintainers.md) by [@johannes-spies](https://github.com/johannes-spies) and [@zyxwwxyz](https://github.com/zyxwwxyz), who will reply to issues and pull requests opened on this repository as soon as possible. You can mention them directly if you haven't received an answer after a couple of days.
201
+
202
+ It was previously maintained by [@frostedoyster](https://github.com/frostedoyster) (2025 to 2026), many thanks to them for creating FlashMD!
@@ -23,7 +23,7 @@ After installation, you can run accelerated molecular dynamics as follows:
23
23
  import ase.build
24
24
  import ase.units
25
25
  import torch
26
- from ase.md.velocitydistribution import MaxwellBoltzmannDistribution
26
+ from ase.md.velocitydistribution import thermalize_momenta
27
27
 
28
28
  from flashmd import get_pretrained
29
29
  from flashmd.ase import EnergyCalculator
@@ -35,7 +35,7 @@ time_step = 64 # 64 fs; also available: 1, 2, 4, 8, 16, 32, 128 fs
35
35
 
36
36
  # Create a structure and initialize velocities
37
37
  atoms = ase.build.bulk("Al", "fcc", cubic=True)
38
- MaxwellBoltzmannDistribution(atoms, temperature_K=300)
38
+ thermalize_momenta(atoms, temperature_K=300)
39
39
  atoms.set_velocities( # it is generally a good idea to remove any net velocity
40
40
  atoms.get_velocities() - atoms.get_momenta().sum(axis=0) / atoms.get_masses().sum()
41
41
  )
@@ -95,7 +95,7 @@ Using FlashMD in i-PI
95
95
  ---------------------
96
96
 
97
97
  You can see
98
- [this cookbook recipe](https://atomistic-cookbook.org/examples/flashmd/flashmd-demo.html)
98
+ [this cookbook recipe](https://atomistic-cookbook.org/examples/flashmd/flashmd-demo.html)
99
99
  for usage examples. i-PI is our most mature interface, and the one that was used to
100
100
  generate all our published results.
101
101
 
@@ -158,3 +158,10 @@ Note that the results were obtained through the i-PI interface.
158
158
 
159
159
  Instructions and material to reproduce the results in the paper are available on
160
160
  Materials Cloud at https://doi.org/10.24435/materialscloud:b7-xq.
161
+
162
+ Maintainers
163
+ -----------
164
+
165
+ This project is [maintained](https://github.com/lab-cosmo/.github/blob/main/Maintainers.md) by [@johannes-spies](https://github.com/johannes-spies) and [@zyxwwxyz](https://github.com/zyxwwxyz), who will reply to issues and pull requests opened on this repository as soon as possible. You can mention them directly if you haven't received an answer after a couple of days.
166
+
167
+ It was previously maintained by [@frostedoyster](https://github.com/frostedoyster) (2025 to 2026), many thanks to them for creating FlashMD!
@@ -0,0 +1,105 @@
1
+ [project]
2
+ name = "flashmd"
3
+ version = "0.3.0"
4
+ requires-python = ">=3.11"
5
+
6
+ readme = "README.md"
7
+ license = "Apache-2.0"
8
+ description = "Accelerated molecular dynamics with large-time-step predictions"
9
+ authors = [{name = "flashmd developers"}]
10
+
11
+ dependencies = [
12
+ "metatrain==2026.3.1",
13
+ "metatomic-ase",
14
+ "vesin>=0.5.8",
15
+ "ase>=3.29.0", # thermalize_momenta replaces MaxwellBoltzmannDistribution
16
+ "huggingface_hub",
17
+ "scipy",
18
+ ]
19
+
20
+ keywords = ["machine learning", "molecular modeling", "molecular dynamics"]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Science/Research",
24
+ "Operating System :: POSIX",
25
+ "Operating System :: MacOS :: MacOS X",
26
+ "Operating System :: Microsoft :: Windows",
27
+ "Programming Language :: Python",
28
+ "Programming Language :: Python :: 3",
29
+ "Topic :: Scientific/Engineering",
30
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
31
+ "Topic :: Scientific/Engineering :: Chemistry",
32
+ "Topic :: Scientific/Engineering :: Physics",
33
+ "Topic :: Software Development :: Libraries",
34
+ "Topic :: Software Development :: Libraries :: Python Modules",
35
+ ]
36
+
37
+ # TODO: add project URLs
38
+
39
+ [build-system]
40
+ requires = [
41
+ "setuptools",
42
+ "wheel",
43
+ ]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "pytest",
52
+ "ruff",
53
+ "mypy",
54
+ ]
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "B", "I"]
61
+ ignore = ["B018", "B904", "E501"]
62
+
63
+ [tool.ruff.lint.per-file-ignores]
64
+ "**/{tests}/*" = ["D"]
65
+
66
+ [tool.ruff.lint.isort]
67
+ lines-after-imports = 2
68
+ known-first-party = ["flashmd"]
69
+
70
+ [tool.ruff.format]
71
+ docstring-code-format = true
72
+
73
+ [tool.mypy]
74
+ follow_imports = 'skip'
75
+ ignore_missing_imports = true
76
+ disable_error_code = ["union-attr"]
77
+
78
+ [tool.pytest.ini_options]
79
+ filterwarnings = [
80
+ "error",
81
+ "ignore:custom data:UserWarning",
82
+ "ignore:`torch.jit.script` is deprecated:DeprecationWarning",
83
+ "ignore:`torch.jit.load` is deprecated:DeprecationWarning",
84
+ "ignore:`torch.jit.save` is deprecated:DeprecationWarning",
85
+ "ignore:`torch.jit.save` is not supported:DeprecationWarning",
86
+ "ignore:`compute_requested_neighbors_from_options` is deprecated and will be removed in a future version:UserWarning",
87
+ "ignore:hf_xet\\.download_files\\(\\) is deprecated:DeprecationWarning",
88
+ "ignore:Due to '_pack_', the '.*' Structure will use memory layout compatible with MSVC:DeprecationWarning",
89
+ "ignore:warp.config.quiet is deprecated:DeprecationWarning",
90
+ "ignore:`torch.jit.script` is not supported:DeprecationWarning",
91
+ "ignore:`torch.jit.script_method` is not supported:DeprecationWarning",
92
+ "ignore:`torch.jit.script_method` is deprecated:DeprecationWarning",
93
+ "ignore:`torch.jit.load` is not supported:DeprecationWarning",
94
+ "ignore:The 'nvalchemiops.neighborlist' module has been renamed to 'nvalchemiops.neighbors':DeprecationWarning",
95
+ "ignore:From version 0.3.0 onwards, PyTorch is now an optional dependency:DeprecationWarning",
96
+ "ignore:`torch.jit.interface` is deprecated:DeprecationWarning",
97
+ "ignore:`torch.jit.set_fusion_strategy` is deprecated:DeprecationWarning",
98
+ "ignore:the '.*' output name is deprecated:UserWarning",
99
+ "ignore:the '.*' quantity is deprecated:UserWarning",
100
+ "ignore:the '.*' quantity is deprecated:DeprecationWarning",
101
+ "ignore:ModelOutput.quantity is deprecated:DeprecationWarning",
102
+ "ignore:Setting the shape on a NumPy array has been deprecated in NumPy 2.5:DeprecationWarning",
103
+ "ignore:currentThread\\(\\) is deprecated:DeprecationWarning",
104
+ "ignore:unclosed file:ResourceWarning", # i-PI's initializer leaks its handle
105
+ ]
@@ -1,4 +1,4 @@
1
- from metatomic.torch.ase_calculator import MetatomicCalculator
1
+ from metatomic_ase import MetatomicCalculator
2
2
 
3
3
 
4
4
  class EnergyCalculator(MetatomicCalculator):
@@ -18,15 +18,29 @@ class Bussi(VelocityVerlet):
18
18
  device: str | torch.device = "auto",
19
19
  rescale_energy: bool = False,
20
20
  random_rotation: bool = False,
21
+ rng: np.random.Generator | None = None,
21
22
  **kwargs,
22
23
  ):
23
24
  super().__init__(
24
- atoms, timestep, model, device, rescale_energy, random_rotation, **kwargs
25
+ atoms,
26
+ timestep,
27
+ model,
28
+ device,
29
+ rescale_energy,
30
+ random_rotation,
31
+ rng,
32
+ **kwargs,
25
33
  )
26
34
 
27
35
  self.temperature_K = temperature_K
28
36
  self.time_constant = time_constant
29
37
 
38
+ if np.isclose(self.atoms.get_kinetic_energy(), 0.0, rtol=0, atol=1e-12):
39
+ raise ValueError(
40
+ "Initial kinetic energy is zero. "
41
+ "Please set the initial velocities before running Bussi NVT."
42
+ )
43
+
30
44
  def step(self):
31
45
  self.apply_bussi_half_step()
32
46
  super().step()
@@ -34,7 +48,7 @@ class Bussi(VelocityVerlet):
34
48
 
35
49
  def apply_bussi_half_step(self):
36
50
  old_kinetic_energy = self.atoms.get_kinetic_energy()
37
- n_degrees_of_freedom = 3 * len(self.atoms)
51
+ n_degrees_of_freedom = self.atoms.get_number_of_degrees_of_freedom()
38
52
  target_kinetic_energy = (
39
53
  0.5 * ase.units.kB * self.temperature_K * n_degrees_of_freedom
40
54
  )
@@ -46,7 +60,7 @@ class Bussi(VelocityVerlet):
46
60
  / old_kinetic_energy
47
61
  / n_degrees_of_freedom
48
62
  )
49
- r = np.random.randn(n_degrees_of_freedom)
63
+ r = self.rng.standard_normal(n_degrees_of_freedom)
50
64
  alpha_sq = (
51
65
  exp_term
52
66
  + energy_scaling_term * np.sum(r**2)
@@ -19,10 +19,18 @@ class Langevin(VelocityVerlet):
19
19
  device: str | torch.device = "auto",
20
20
  rescale_energy: bool = False,
21
21
  random_rotation: bool = False,
22
+ rng: np.random.Generator | None = None,
22
23
  **kwargs,
23
24
  ):
24
25
  super().__init__(
25
- atoms, timestep, model, device, rescale_energy, random_rotation, **kwargs
26
+ atoms,
27
+ timestep,
28
+ model,
29
+ device,
30
+ rescale_energy,
31
+ random_rotation,
32
+ rng,
33
+ **kwargs,
26
34
  )
27
35
 
28
36
  self.temperature_K = temperature_K
@@ -45,7 +53,7 @@ class Langevin(VelocityVerlet):
45
53
  1.0 - np.exp(-self.friction * self.dt)
46
54
  ) * np.sqrt(
47
55
  ase.units.kB * self.temperature_K * self.atoms.get_masses()[:, None]
48
- ) * np.random.randn(*old_momenta.shape)
56
+ ) * self.rng.standard_normal(old_momenta.shape)
49
57
  self.atoms.set_momenta(new_momenta)
50
58
  if self.fixcm:
51
59
  self.atoms.set_velocities(
@@ -1,3 +1,5 @@
1
+ from types import ModuleType
2
+
1
3
  import ase
2
4
  import ase.units
3
5
  import numpy as np
@@ -5,10 +7,11 @@ import torch
5
7
  from ase.md.md import MolecularDynamics
6
8
  from metatensor.torch import Labels, TensorBlock, TensorMap
7
9
  from metatomic.torch import AtomisticModel, System
8
- from metatomic.torch.ase_calculator import _ase_to_torch_data
10
+ from metatomic_ase._calculator import _ase_to_torch_data
9
11
  from scipy.spatial.transform import Rotation
10
12
 
11
13
  from ..stepper import FlashMDStepper
14
+ from ..symplectic_stepper import SymplecticStepper
12
15
 
13
16
 
14
17
  class VelocityVerlet(MolecularDynamics):
@@ -20,13 +23,25 @@ class VelocityVerlet(MolecularDynamics):
20
23
  device: str | torch.device = "auto",
21
24
  rescale_energy: bool = True,
22
25
  random_rotation: bool = False,
26
+ rng: np.random.Generator | None = None,
23
27
  **kwargs,
24
28
  ):
25
29
  super().__init__(atoms, timestep, **kwargs)
26
30
 
27
- capabilities = model.capabilities()
31
+ self.rng = np.random if rng is None else rng
32
+
33
+ if isinstance(model, tuple):
34
+ flashmd_model, symplectic_part = model
35
+ if isinstance(symplectic_part, tuple):
36
+ symplectic_model, symplectic_config = symplectic_part
37
+ else:
38
+ symplectic_model, symplectic_config = symplectic_part, None
39
+ else:
40
+ flashmd_model, symplectic_model, symplectic_config = model, None, None
41
+
42
+ capabilities = flashmd_model.capabilities()
28
43
 
29
- model_timestep = float(model.module.timestep)
44
+ model_timestep = float(flashmd_model.module.timestep)
30
45
  if not np.allclose(model_timestep, self.dt / ase.units.fs):
31
46
  raise ValueError(
32
47
  f"Mismatch between timestep ({self.dt / ase.units.fs} fs) "
@@ -40,7 +55,14 @@ class VelocityVerlet(MolecularDynamics):
40
55
  self.device = torch.device(device)
41
56
  self.dtype = getattr(torch, capabilities.dtype)
42
57
 
43
- self.stepper = FlashMDStepper(model, self.device)
58
+ flashmd_stepper = FlashMDStepper(flashmd_model, self.device)
59
+ self.stepper: FlashMDStepper | SymplecticStepper
60
+ if symplectic_model is not None:
61
+ self.stepper = SymplecticStepper(
62
+ flashmd_stepper, symplectic_model, symplectic_config
63
+ )
64
+ else:
65
+ self.stepper = flashmd_stepper
44
66
  self.rescale_energy = rescale_energy
45
67
  self.random_rotation = random_rotation
46
68
 
@@ -55,7 +77,7 @@ class VelocityVerlet(MolecularDynamics):
55
77
  if self.random_rotation:
56
78
  # generate a random rotation matrix with SciPy
57
79
  R = torch.tensor(
58
- _get_random_rotation(),
80
+ _get_random_rotation(self.rng),
59
81
  device=system.positions.device,
60
82
  dtype=system.positions.dtype,
61
83
  )
@@ -132,6 +154,14 @@ def _convert_atoms_to_system(
132
154
  ) -> System:
133
155
  system_data = _ase_to_torch_data(atoms, dtype=dtype, device=device)
134
156
  system = System(*system_data)
157
+ n = len(atoms)
158
+ atom_samples = Labels(
159
+ names=["system", "atom"],
160
+ values=torch.vstack(
161
+ [torch.full((n,), 0, device=device), torch.arange(n, device=device)]
162
+ ).T,
163
+ assume_unique=True,
164
+ )
135
165
  system.add_data(
136
166
  "momenta",
137
167
  TensorMap(
@@ -141,12 +171,7 @@ def _convert_atoms_to_system(
141
171
  values=torch.tensor(
142
172
  atoms.get_momenta(), dtype=dtype, device=device
143
173
  ).unsqueeze(-1),
144
- samples=Labels(
145
- names=["system", "atom"],
146
- values=torch.tensor(
147
- [[0, j] for j in range(len(atoms))], device=device
148
- ),
149
- ),
174
+ samples=atom_samples,
150
175
  components=[
151
176
  Labels(
152
177
  names="xyz",
@@ -167,12 +192,7 @@ def _convert_atoms_to_system(
167
192
  values=torch.tensor(
168
193
  atoms.get_masses(), dtype=dtype, device=device
169
194
  ).unsqueeze(-1),
170
- samples=Labels(
171
- names=["system", "atom"],
172
- values=torch.tensor(
173
- [[0, j] for j in range(len(atoms))], device=device
174
- ),
175
- ),
195
+ samples=atom_samples,
176
196
  components=[],
177
197
  properties=Labels.single().to(device),
178
198
  )
@@ -182,8 +202,13 @@ def _convert_atoms_to_system(
182
202
  return system
183
203
 
184
204
 
185
- def _get_random_rotation():
186
- R = Rotation.random().as_matrix()
187
- if np.random.rand() < 0.5:
205
+ def _get_random_rotation(rng: np.random.Generator | ModuleType = np.random):
206
+ # Haar-uniform rotation from a normalised Gaussian quaternion. This only
207
+ # needs standard_normal/random, so it works with any RNG following the
208
+ # ASE convention (the np.random module, a Generator, ...), unlike
209
+ # Rotation.random(rng=...), which requires an actual Generator.
210
+ quaternion = rng.standard_normal(4)
211
+ R = Rotation.from_quat(quaternion / np.linalg.norm(quaternion)).as_matrix()
212
+ if rng.random() < 0.5:
188
213
  R *= -1 # allow improper rotations
189
214
  return R
@@ -0,0 +1,86 @@
1
+ from typing import Callable
2
+
3
+ import torch
4
+
5
+
6
+ def anderson_solver(
7
+ f: Callable[[torch.Tensor], torch.Tensor],
8
+ x0: torch.Tensor,
9
+ m: int = 5,
10
+ max_iter: int = 50,
11
+ tol: float = 1e-5,
12
+ beta: float = 1.0,
13
+ lambda_reg: float = 1e-4,
14
+ return_residual_norms: bool = False,
15
+ ) -> torch.Tensor | tuple[torch.Tensor, list[float]]:
16
+ """
17
+ Solve fixed-point problem x = f(x) using Anderson acceleration.
18
+
19
+ Args:
20
+ f: Fixed-point mapping.
21
+ x0: Initial guess.
22
+ m: Number of previous iterates to use for acceleration.
23
+ max_iter: Maximum number of iterations.
24
+ tol: Convergence tolerance based on residual norm.
25
+ beta: Mixing parameter for the fixed-point step.
26
+ lambda_reg: Regularization parameter for least-squares solve.
27
+ return_residual_norms: If True, also return list of residual norms.
28
+
29
+ Returns:
30
+ Approximate solution x, and optionally list of residual norms.
31
+ """
32
+ # history buffers
33
+ delta_xs: list[torch.Tensor] = []
34
+ delta_gs: list[torch.Tensor] = []
35
+ residual_norms = []
36
+
37
+ # run fixed-pointer iteration
38
+ x = x0
39
+ fx = f(x)
40
+ g = fx - x
41
+ x_prev, g_prev = None, None
42
+ for k in range(max_iter):
43
+ # evaluate residual and compute convergence
44
+ res_norm = torch.norm(g).item()
45
+ residual_norms.append(res_norm)
46
+ if res_norm < tol:
47
+ break
48
+
49
+ # update history
50
+ if k > 0:
51
+ assert x_prev is not None and g_prev is not None
52
+ delta_xs.append(x - x_prev)
53
+ delta_gs.append(g - g_prev)
54
+
55
+ # truncate history to hold at most m elements
56
+ if len(delta_xs) > m:
57
+ delta_xs.pop(0)
58
+ delta_gs.pop(0)
59
+ x_prev, g_prev = x, g
60
+
61
+ # compute Anderson acceleration step
62
+ if len(delta_xs) > 0:
63
+ # create matrices from history of shape (features, history_length)
64
+ X = torch.stack(delta_xs, dim=1) # (n, k)
65
+ G = torch.stack(delta_gs, dim=1) # (n, k)
66
+
67
+ # solve regularized least-squares problem
68
+ A = G.T @ G + lambda_reg * torch.eye(G.shape[1], device=G.device)
69
+ b = G.T @ g
70
+ try:
71
+ coeffs = torch.linalg.solve(A, b)
72
+ # update iterate with momentum + Anderson step
73
+ x = x + beta * g - (X + beta * G) @ coeffs
74
+ except RuntimeError:
75
+ x = x + beta * g # fallback to fixed-point step if matrix is singular
76
+ else:
77
+ x = x + beta * g # fixed-point step if there is no history
78
+
79
+ # update iterate and residual
80
+ fx = f(x)
81
+ g = fx - x
82
+
83
+ if return_residual_norms:
84
+ return x, residual_norms
85
+ else:
86
+ return x