Coverage for gpaw/core/atom_centered_functions.py: 92%
100 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-20 00:19 +0000
1from __future__ import annotations
3from typing import TYPE_CHECKING, Callable
5import numpy as np
6from gpaw.core.atom_arrays import (AtomArrays, AtomArraysLayout,
7 AtomDistribution)
8from gpaw.kpt_descriptor import KPointDescriptor
9from gpaw.lfc import LocalizedFunctionsCollection as LFC
10from gpaw.mpi import MPIComm, serial_comm
11from gpaw.new import zips, trace
12from gpaw.spline import Spline
13from gpaw.typing import Array1D, ArrayLike2D
14from gpaw.gpu import XP
16if TYPE_CHECKING:
17 from gpaw.core.uniform_grid import UGArray
20def to_spline(l: int,
21 rcut: float,
22 f: Callable[[Array1D], Array1D]) -> Spline:
23 """Convert to GPAW's Spline object."""
24 r = np.linspace(0, rcut, 100)
25 return Spline.from_data(l, rcut, f(r))
28class AtomCenteredFunctions(XP):
29 def __init__(self,
30 functions,
31 relpos_ac: ArrayLike2D,
32 atomdist: AtomDistribution | None = None,
33 xp=None):
34 XP.__init__(self, xp or np)
35 self.functions = [[to_spline(*f) if isinstance(f, tuple) else f
36 for f in funcs]
37 for funcs in functions]
38 self.relpos_ac = np.array(relpos_ac)
39 self._atomdist = atomdist
41 self._layout = None
42 self._lfc = None
44 def __repr__(self):
45 funcs = [['spdfgh'[f.l] for f in ff] for ff in self.functions[:4]]
46 if len(self.functions) > 4:
47 funcs.append(...)
48 return (f'{self.__class__.__name__}'
49 f'(functions={funcs}, atomdist={self.atomdist})')
51 def new(self, desc, atomdist):
52 raise NotImplementedError
54 @property
55 def layout(self):
56 self._lazy_init()
57 return self._layout
59 @property
60 def atomdist(self):
61 self._lazy_init()
62 return self._atomdist
64 def _lazy_init(self):
65 raise NotImplementedError
67 def empty(self,
68 dims: int | tuple[int, ...] = (),
69 comm: MPIComm = serial_comm) -> AtomArrays:
70 """Create AtomsArray for coefficients."""
71 return self.layout.empty(dims, comm)
73 def move(self,
74 relpos_ac: np.ndarray,
75 atomdist: AtomDistribution) -> AtomDistribution:
76 """Move atoms to new positions."""
77 self.relpos_ac = np.array(relpos_ac)
78 self._atomdist = atomdist
79 if self._lfc is not None:
80 self._layout = self._layout.new(atomdist=atomdist)
81 migration = self._lfc.set_positions(relpos_ac, atomdist)
82 if migration:
83 atomdist = AtomDistribution(
84 [sphere.rank for sphere in self._lfc.sphere_a],
85 atomdist.comm)
86 return atomdist
88 def add_to(self, functions, coefs=1.0):
89 """Add atom-centered functions multiplied by *coefs* to *functions*."""
90 self._lazy_init()
91 if isinstance(coefs, float):
92 self._lfc.add(functions.data, coefs)
93 else:
94 self._lfc.add(functions.data, coefs, q=0)
96 @trace
97 def integrate(self, functions, out=None, add_to=False):
98 """Calculate integrals of atom-centered functions multiplied by
99 *functions*.
100 """
101 self._lazy_init()
102 if out is None:
103 assert not add_to
104 out = self.layout.empty(functions.dims, functions.comm)
105 self._lfc.integrate(functions.data, out, q=0, add_to=add_to)
106 return out
108 def derivative(self, functions, out=None):
109 """Calculate derivatives of integrals with respect to atom
110 positions.
111 """
112 self._lazy_init()
113 if out is None:
114 out = self.layout.empty(functions.dims + (3,), functions.comm)
115 coef_axiv = {a: self.xp.moveaxis(array_xvi, -2, -1)
116 for a, array_xvi in out._arrays.items()}
117 self._lfc.derivative(functions.data, coef_axiv, q=0)
118 return out
120 def stress_contribution(self, a, c=1.0):
121 self._lazy_init()
122 return self._lfc.stress_tensor_contribution(a.data, c)
125class UGAtomCenteredFunctions(AtomCenteredFunctions):
126 def __init__(self,
127 functions,
128 relpos_ac,
129 grid,
130 *,
131 atomdist=None,
132 integrals=None,
133 cut=False,
134 xp=np):
135 AtomCenteredFunctions.__init__(self,
136 functions,
137 relpos_ac,
138 atomdist, xp=xp)
139 self.grid = grid
140 self.integrals = integrals
141 self.cut = cut
143 def new(self, grid, atomdist):
144 return UGAtomCenteredFunctions(
145 self.functions,
146 self.relpos_ac,
147 grid,
148 atomdist=atomdist,
149 integrals=self.integrals,
150 cut=self.cut,
151 xp=self.xp)
153 def _lazy_init(self):
154 if self._lfc is not None:
155 return
156 gd = self.grid._gd
157 kd = KPointDescriptor(np.array([self.grid.kpt]))
158 self._lfc = LFC(gd, self.functions, kd,
159 dtype=self.grid.dtype,
160 integral=self.integrals,
161 forces=True,
162 cut=self.cut,
163 xp=self.xp)
164 self._lfc.set_positions(self.relpos_ac)
166 if self._atomdist is None:
167 self._atomdist = AtomDistribution(
168 ranks=np.array([sphere.rank for sphere in self._lfc.sphere_a]),
169 comm=self.grid.comm)
170 else:
171 for sphere, rank in zips(self._lfc.sphere_a,
172 self._atomdist.rank_a):
173 assert sphere.rank == rank
174 assert self.grid.comm is self._atomdist.comm
176 self._layout = AtomArraysLayout([sum(2 * f.l + 1 for f in funcs)
177 for funcs in self.functions],
178 self._atomdist,
179 self.grid.dtype, xp=self.xp)
181 def to_uniform_grid(self,
182 out: UGArray,
183 scale: float = 1.0) -> UGArray:
184 out.data[:] = 0.0
185 self.add_to(out, scale)
186 return out