Coverage for gpaw/utilities/folder.py: 93%
159 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-08 00:17 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-08 00:17 +0000
1import numpy as np
3from gpaw.gauss import Gauss
6def x_y_xl(x, y, xl=None):
7 X = np.array(x)
8 assert len(X.shape) == 1
9 Y = np.array(y)
10 assert X.shape[0] == Y.shape[0]
12 if xl is None:
13 Xl = np.unique(X)
14 else:
15 Xl = np.array(xl)
16 assert len(Xl.shape) == 1
17 return X, Y, Xl
20class Lorentz:
21 """Normalized Lorentzian distribution"""
22 def __init__(self, width=0.08):
23 self.dtype = float
24 self.set_width(width)
26 def get(self, x, x0=0):
27 return self.norm / ((x - x0)**2 + self.width2)
29 def set_width(self, width=0.08):
30 self.norm = width / np.pi
31 self.width2 = width**2
32 self._fwhm = 2. * width
34 @property
35 def fwhm(self):
36 return self._fwhm
38 @fwhm.setter
39 def fwhm(self, value):
40 self.set_width(value / 2.)
43class Voigt:
44 """Voigt profile.
46 See https://reference.wolfram.com/language/ref/VoigtDistribution.html
47 """
48 def __init__(self, width=0.08):
49 self.dtype = float
50 self.set_width(width)
52 def get(self, x, x0=0):
53 from scipy.special import erfc
54 argm = (-1j * (x - x0) + self.delta) * self.argpre
55 argp = (1j * (x - x0) + self.delta) * self.argpre
56 res = np.exp(argm**2) * erfc(argm)
57 res += np.exp(argp**2) * erfc(argp)
58 return res.real * self.prefactor
60 def set_width(self, width=0.08):
61 """Width is interpreted as [delta, sigma]"""
62 if not hasattr(width, '__iter__'):
63 width = [width, width]
64 self.delta, self.sigma = width
65 self.width = np.linalg.norm(np.array(width))
66 self.prefactor = 1. / 2 / np.sqrt(2 * np.pi) / self.sigma
67 self.argpre = 1. / np.sqrt(2) / self.sigma
69 """Full width at half maximum approximation after
70 Olivero, J. J.; R. L. Longbothum
71 "Empirical fits to the Voigt line width: A brief review"
72 J. Quantitative Spectroscopy and Radiative Transfer. 17 (1977) 233-236
73 """
74 fl = 2 * self.delta
75 fg = np.sqrt(8 * np.log(2)) * self.sigma
76 self._fwhm = 0.5346 * fl + np.sqrt(0.2166 * fl**2 + fg**2)
78 @property
79 def fwhm(self):
80 return self._fwhm
82 @fwhm.setter
83 def fwhm(self, value):
84 "setting fwhm is not uniquely defined"
85 raise NotImplementedError
88class ComplexLorentz:
89 def __init__(self, width=0.08):
90 self.dtype = complex
91 self.set_width(width)
93 def get(self, x, x0):
94 return 0.5 / x0 * (((x + x0) / ((x + x0)**2 + self.width2) -
95 (x - x0) / ((x - x0)**2 + self.width2)) -
96 1.0j * self.width *
97 (1 / ((x + x0)**2 + self.width2) -
98 1 / ((x - x0)**2 + self.width2))
99 )
101 def set_width(self, width=0.08):
102 self.width = width
103 self.width2 = width**2
106class ComplexGauss:
107 def __init__(self, width=0.08):
108 self.dtype = complex
109 self.set_width(width)
111 def get(self, x, x0):
112 from scipy.special import dawsn
113 return 0.5 / x0 * (2 * self.wm1 *
114 (dawsn((x + x0) * self.wm1) -
115 dawsn((x - x0) * self.wm1)) -
116 1.0j * self.norm *
117 (np.exp(-((x + x0) * self.wm1)**2) -
118 np.exp(-((x - x0) * self.wm1)**2))
119 )
121 def set_width(self, width=0.08):
122 self.norm = 1. / width * np.sqrt(np.pi / 2)
123 self.wm1 = np.sqrt(.5) / width
126class LorentzPole:
127 """Pole of a damped harmonic oscillator.
129 See: e.g. Frederick Wooten, Optical properties of solids,
130 Academic Press Inc. (1972)"""
131 def __init__(self, width=0.08, imag=True):
132 self.dtype = float
133 if imag:
134 self.get = self.get_imaginary
135 else:
136 self.get = self.get_real
137 self.set_width(width)
139 def get_real(self, x, x0):
140 return (x0**2 - x**2) / (
141 (x0**2 - x**2)**2 + self.width**2 * x**2)
143 def get_imaginary(self, x, x0):
144 return self.width * x / (
145 (x0**2 - x**2)**2 + self.width**2 * x**2)
147 def set_width(self, width=0.08):
148 self.width = width
151class Folder:
152 """Fold a function with normalised Gaussians or Lorentzians
154 Example: fold a function y(x) by Lorentzians of 0.2 width
156 >>> xlist = np.linspace(-1.0, 1.0, 101) # list of x-values
157 >>> ylist = np.zeros_like(xlist) # corresponding list of y(x)-values
158 >>> ylist[50] = 1.0
159 >>> from gpaw.utilities.folder import Folder
160 >>> fxlist, fylist = Folder(width=0.2, folding='Lorentz').fold(
161 ... xlist, ylist, dx=0.1, xmin=-1, xmax=1)
162 """
163 def __init__(self, width,
164 folding='Gauss'):
165 self.width = width
166 if folding == 'Gauss':
167 self.func = Gauss(width)
168 elif folding == 'Lorentz':
169 self.func = Lorentz(width)
170 elif folding == 'ComplexGauss':
171 self.func = ComplexGauss(width)
172 elif folding == 'ComplexLorentz':
173 self.func = ComplexLorentz(width)
174 elif folding == 'RealLorentzPole':
175 self.func = LorentzPole(width, imag=False)
176 elif folding == 'ImaginaryLorentzPole':
177 self.func = LorentzPole(width, imag=True)
178 elif folding == 'Voigt':
179 self.func = Voigt(width)
180 else:
181 raise RuntimeError('unknown folding "' + folding + '"')
183 def x_lim(self, x, dx=None, xmin=None, xmax=None):
184 try:
185 w = self.func.width
186 except AttributeError:
187 w = self.width
189 if xmin is None:
190 xmin = np.min(x) - 4 * w
191 if xmax is None:
192 xmax = np.max(x) + 4 * w
193 if dx is None:
194 try:
195 dx = self.func.width / 4.
196 except AttributeError:
197 dx = self.width / 4.
199 return dx, xmin, xmax
201 def fold(self, x, y, dx=None, xmin=None, xmax=None):
202 X = np.array(x)
203 assert len(X.shape) == 1
204 Y = np.array(y)
205 assert X.shape[0] == Y.shape[0]
207 dx, xmin, xmax = self.x_lim(X, dx, xmin, xmax)
209 xl = np.arange(xmin, xmax + 0.5 * dx, dx)
211 return self.fold_values(x, y, xl)
213 def fold_values(self, x, y, xl=None):
214 X, Y, Xl = x_y_xl(x, y, xl)
216 # weight matrix
217 weightm = np.empty((Xl.shape[0], X.shape[0]),
218 dtype=self.func.dtype)
219 for i, x in enumerate(X):
220 weightm[:, i] = self.func.get(Xl, x)
222 yl = np.tensordot(weightm, Y, axes=(1, 0))
224 return Xl, yl
226 def varing_fold(self, x, y, width2, x1, x2,
227 dx=None, xmin=None, xmax=None):
229 X = np.array(x)
230 assert len(X.shape) == 1
231 Y = np.array(y)
232 assert X.shape[0] == Y.shape[0]
234 dx, xmin, xmax = self.x_lim(X, dx, xmin, xmax)
236 xl = np.arange(xmin, xmax + 0.5 * dx, dx)
237 return self.varing_fold_values(x, y, xl, width2, x1, x2)
239 def varing_fold_values(self, x, y, xl, width2, x1, x2,):
241 X, Y, Xl = x_y_xl(x, y, xl)
243 width1 = self.width
245 weightm = np.empty((Xl.shape[0], X.shape[0]),
246 dtype=self.func.dtype)
248 for i, x in enumerate(X):
249 if x < x1:
250 width_eff = width1
251 elif x < x2:
252 width_eff = (width1 + (x - x1) *
253 (width2 - width1) / (x2 - x1))
254 elif x >= x2:
255 width_eff = width2
256 self.func.set_width(width_eff)
258 weightm[:, i] = self.func.get(Xl, x)
260 yl = np.tensordot(weightm, Y, axes=(1, 0))
262 return Xl, yl