sparse-ir 1.1.7__py3-none-any.whl → 2.0.0a2__py3-none-any.whl

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.
sparse_ir/kernel.py CHANGED
@@ -1,139 +1,33 @@
1
- # Copyright (C) 2020-2022 Markus Wallerberger, Hiroshi Shinaoka, and others
2
- # SPDX-License-Identifier: MIT
3
- import numpy as np
4
- from typing import Callable
5
-
6
-
7
- class AbstractKernel:
8
- r"""Integral kernel ``K(x, y)``.
9
-
10
- Abstract base class for an integral kernel, i.e., a real binary function
11
- ``K(x, y)`` used in a Fredhold integral equation of the first kind:
12
-
13
- .. math:: u(x) = \int K(x, y) v(y) dy
14
-
15
- where ``x ∈ [xmin, xmax]`` and ``y ∈ [ymin, ymax]``. For its SVE to exist,
16
- the kernel must be square-integrable, for its singular values to decay
17
- exponentially, it must be smooth.
18
-
19
- In general, the kernel is applied to a scaled spectral function ρ'(y) as:
20
-
21
- .. math:: \int K(x, y) \rho'(y) dy,
22
-
23
- where ρ'(y) = w(y) ρ(y).
24
- """
25
- def __call__(self, x, y, x_plus=None, x_minus=None):
26
- """Evaluate kernel at point (x, y)
27
-
28
- For given ``x, y``, return the value of ``K(x, y)``. The arguments may
29
- be numpy arrays, in which case the function shall be evaluated over
30
- the broadcast arrays.
31
-
32
- The parameters ``x_plus`` and ``x_minus``, if given, shall contain the
33
- values of ``x - xmin`` and ``xmax - x``, respectively. This is useful
34
- if either difference is to be formed and cancellation expected.
35
- """
36
- raise NotImplementedError()
37
-
38
- def sve_hints(self, eps):
39
- """Provide discretisation hints for the SVE routines.
40
-
41
- Advises the SVE routines of discretisation parameters suitable in
42
- tranforming the (infinite) SVE into an (finite) SVD problem.
43
-
44
- See: :class:``AbstractSVEHints``.
45
- """
46
- raise NotImplementedError()
47
-
48
- @property
49
- def xrange(self):
50
- """Tuple ``(xmin, xmax)`` delimiting the range of allowed x values"""
51
- return -1, 1
1
+ """
2
+ Kernel classes for SparseIR.
52
3
 
53
- @property
54
- def yrange(self):
55
- """Tuple ``(ymin, ymax)`` delimiting the range of allowed y values"""
56
- return -1, 1
57
-
58
- @property
59
- def is_centrosymmetric(self):
60
- """Kernel is centrosymmetric.
61
-
62
- Returns true if and only if ``K(x, y) == K(-x, -y)`` for all values of
63
- ``x`` and ``y``. This allows the kernel to be block-diagonalized,
64
- speeding up the singular value expansion by a factor of 4. Defaults
65
- to false.
66
- """
67
- return False
68
-
69
- def get_symmetrized(self, sign):
70
- """Return symmetrized kernel ``K(x, y) + sign * K(x, -y)``.
4
+ This module provides Python wrappers for kernel objects from the C library.
5
+ """
71
6
 
72
- By default, this returns a simple wrapper over the current instance
73
- which naively performs the sum. You may want to override this to
74
- avoid cancellation.
75
- """
76
- return ReducedKernel(self, sign)
77
-
78
- @property
79
- def ypower(self):
80
- """Power with which the y coordinate scales."""
81
- return 0
82
-
83
- @property
84
- def conv_radius(self):
85
- """Convergence radius of the Matsubara basis asymptotic model.
86
-
87
- For improved relative numerical accuracy, the IR basis functions on the
88
- Matsubara axis ``basis.uhat(n)`` can be evaluated from an asymptotic
89
- expression for ``abs(n) > conv_radius``. If ``conv_radius`` is
90
- ``None``, then the asymptotics are unused (the default).
91
- """
92
- return None
93
-
94
- def weight_func(self, statistics: str) -> Callable[[np.ndarray], np.ndarray]:
95
- """Return the weight function for given statistics"""
96
- if statistics not in 'FB':
97
- raise ValueError("statistics must be 'F' for fermions or 'B' for bosons")
98
- return lambda x: np.ones_like(x)
99
-
100
-
101
- class AbstractSVEHints:
102
- """Discretization hints for singular value expansion of a given kernel."""
103
- @property
104
- def segments_x(self):
105
- """Segments for piecewise polynomials on the ``x`` axis.
106
-
107
- List of segments on the ``x`` axis for the associated piecewise
108
- polynomial. Should reflect the approximate position of roots of a
109
- high-order singular function in ``x``.
110
- """
111
- raise NotImplementedError()
112
-
113
- @property
114
- def segments_y(self):
115
- """Segments for piecewise polynomials on the ``y`` axis.
7
+ import ctypes
8
+ from ctypes import c_int, c_double, byref
9
+ import numpy as np
116
10
 
117
- List of segments on the ``y`` axis for the associated piecewise
118
- polynomial. Should reflect the approximate position of roots of a
119
- high-order singular function in ``y``.
120
- """
121
- raise NotImplementedError()
11
+ from pylibsparseir.core import _lib
12
+ from pylibsparseir.core import logistic_kernel_new, reg_bose_kernel_new
13
+ from pylibsparseir.constants import COMPUTATION_SUCCESS
14
+ from .abstract import AbstractKernel
122
15
 
123
- @property
124
- def ngauss(self):
125
- """Gauss-Legendre order to use to guarantee accuracy"""
126
- raise NotImplementedError()
127
16
 
128
- @property
129
- def nsvals(self):
130
- """Upper bound for number of singular values
17
+ def kernel_domain(kernel: AbstractKernel):
18
+ """Get the domain boundaries of a kernel."""
19
+ xmin = c_double()
20
+ xmax = c_double()
21
+ ymin = c_double()
22
+ ymax = c_double()
131
23
 
132
- Upper bound on the number of singular values above the given
133
- threshold, i.e., where ``s[l] >= eps * s[0]``.
134
- """
135
- raise NotImplementedError()
24
+ status = _lib.spir_kernel_domain(
25
+ kernel._ptr, byref(xmin), byref(xmax), byref(ymin), byref(ymax)
26
+ )
27
+ if status != COMPUTATION_SUCCESS:
28
+ raise RuntimeError(f"Failed to get kernel domain: {status}")
136
29
 
30
+ return xmin.value, xmax.value, ymin.value, ymax.value
137
31
 
138
32
  class LogisticKernel(AbstractKernel):
139
33
  r"""Fermionic/bosonic analytical continuation kernel.
@@ -155,97 +49,27 @@ class LogisticKernel(AbstractKernel):
155
49
  i.e., a rescaling of the spectral function with the weight function:
156
50
 
157
51
  .. math:: w(y) = \frac1{\tanh(\Lambda y/2)}.
158
- """
159
- def __init__(self, lambda_):
160
- self.lambda_ = lambda_
161
-
162
- def __call__(self, x, y, x_plus=None, x_minus=None):
163
- x, y = _check_domain(self, x, y)
164
- u_plus, u_minus, v = _compute_uv(self.lambda_, x, y, x_plus, x_minus)
165
- return self._compute(u_plus, u_minus, v)
166
-
167
- def sve_hints(self, eps):
168
- return _SVEHintsLogistic(self, eps)
169
-
170
- def _compute(self, u_plus, u_minus, v):
171
- # By introducing u_\pm = (1 \pm x)/2 and v = lambda * y, we can write
172
- # the kernel in the following two ways:
173
- #
174
- # k = exp(-u_+ * v) / (exp(-v) + 1)
175
- # = exp(-u_- * -v) / (exp(v) + 1)
176
- #
177
- # We need to use the upper equation for v >= 0 and the lower one for
178
- # v < 0 to avoid overflowing both numerator and denominator
179
- abs_v = np.abs(v)
180
- enum = np.exp(-abs_v * np.where(v > 0, u_plus, u_minus))
181
- denom = 1 + np.exp(-abs_v)
182
- return enum / denom
183
-
184
- @property
185
- def is_centrosymmetric(self):
186
- return True
187
-
188
- def get_symmetrized(self, sign):
189
- if sign == -1:
190
- return _LogisticKernelOdd(self, sign)
191
- return super().get_symmetrized(sign)
192
-
193
- @property
194
- def conv_radius(self): return 40 * self.lambda_
195
-
196
- def weight_func(self, statistics: str) -> Callable[[np.ndarray], np.ndarray]:
197
- """
198
- Return the weight function for given statistics.
199
-
200
- - Fermion: `w(x) == 1`
201
- - Boson: `w(y) == 1/tanh(Λ*y/2)`
202
- """
203
- if statistics not in "FB":
204
- raise ValueError("invalid value of statistics argument")
205
- if statistics == "F":
206
- return lambda y: np.ones_like(y)
207
- else:
208
- return lambda y: 1/np.tanh(0.5*self.lambda_*y)
209
-
210
-
211
- class _SVEHintsLogistic(AbstractSVEHints):
212
- def __init__(self, kernel, eps):
213
- self.kernel = kernel
214
- self.eps = eps
215
52
 
216
- @property
217
- def ngauss(self): return 10 if self.eps >= 1e-8 else 16
53
+ Parameters
54
+ ----------
55
+ lambda_ : float
56
+ Kernel cutoff Λ = β * ωmax
57
+ """
218
58
 
219
- @property
220
- def segments_x(self):
221
- nzeros = max(int(np.round(15 * np.log10(self.kernel.lambda_))), 1)
222
- diffs = 1./np.cosh(.143 * np.arange(nzeros))
223
- zeros_pos = diffs.cumsum()
224
- zeros_pos /= zeros_pos[-1]
225
- return np.concatenate((-zeros_pos[::-1], [0], zeros_pos))
59
+ def __init__(self, lambda_):
60
+ """Initialize logistic kernel with cutoff lambda."""
61
+ self._lambda = float(lambda_)
62
+ self._ptr = logistic_kernel_new(self._lambda)
226
63
 
227
64
  @property
228
- def segments_y(self):
229
- # Zeros around -1 and 1 are distributed asymptotically identical
230
- leading_diffs = np.array([
231
- 0.01523, 0.03314, 0.04848, 0.05987, 0.06703, 0.07028, 0.07030,
232
- 0.06791, 0.06391, 0.05896, 0.05358, 0.04814, 0.04288, 0.03795,
233
- 0.03342, 0.02932, 0.02565, 0.02239, 0.01951, 0.01699])
65
+ def lambda_(self):
66
+ """Kernel cutoff."""
67
+ return self._lambda
234
68
 
235
- nzeros = max(int(np.round(20 * np.log10(self.kernel.lambda_))), 2)
236
- if nzeros < 20:
237
- leading_diffs = leading_diffs[:nzeros]
238
- diffs = .25 / np.exp(.141 * np.arange(nzeros))
239
- diffs[:leading_diffs.size] = leading_diffs
240
- zeros = diffs.cumsum()
241
- zeros = zeros[:-1] / zeros[-1]
242
- zeros -= 1
243
- return np.concatenate(([-1], zeros, [0], -zeros[::-1], [1]))
244
-
245
- @property
246
- def nsvals(self):
247
- log10_lambda = max(1, np.log10(self.kernel.lambda_))
248
- return int(np.round((25 + log10_lambda) * log10_lambda))
69
+ def __del__(self):
70
+ """Clean up kernel resources."""
71
+ if hasattr(self, '_ptr') and self._ptr:
72
+ _lib.spir_kernel_release(self._ptr)
249
73
 
250
74
 
251
75
  class RegularizedBoseKernel(AbstractKernel):
@@ -259,271 +83,24 @@ class RegularizedBoseKernel(AbstractKernel):
259
83
  K(x, y) = \frac{y \exp(-\Lambda y(x + 1)/2)}{\exp(-\Lambda y) - 1}
260
84
 
261
85
  Care has to be taken in evaluating this expression around ``y == 0``.
262
- """
263
- def __init__(self, lambda_):
264
- self.lambda_ = lambda_
265
86
 
266
- def __call__(self, x, y, x_plus=None, x_minus=None):
267
- x, y = _check_domain(self, x, y)
268
- u_plus, u_minus, v = _compute_uv(self.lambda_, x, y, x_plus, x_minus)
269
- return self._compute(u_plus, u_minus, v)
270
-
271
- def _compute(self, u_plus, u_minus, v):
272
- # With "reduced variables" u, v we have:
273
- #
274
- # K = -1/lambda * exp(-u_+ * v) * v / (exp(-v) - 1)
275
- # = -1/lambda * exp(-u_- * -v) * (-v) / (exp(v) - 1)
276
- #
277
- # where we again need to use the upper equation for v >= 0 and the
278
- # lower one for v < 0 to avoid overflow.
279
- abs_v = np.abs(v)
280
- enum = np.exp(-abs_v * np.where(v >= 0, u_plus, u_minus))
281
- dtype = v.dtype
282
-
283
- # The expression ``v / (exp(v) - 1)`` is tricky to evaluate: firstly,
284
- # it has a singularity at v=0, which can be cured by treating that case
285
- # separately. Secondly, the denominator loses precision around 0 since
286
- # exp(v) = 1 + v + ..., which can be avoided using expm1(...)
287
- not_tiny = abs_v >= 1e-200
288
- denom = -np.ones_like(abs_v)
289
- np.divide(abs_v, np.expm1(-abs_v, where=not_tiny),
290
- out=denom, where=not_tiny)
291
- return -1/dtype.type(self.lambda_) * enum * denom
292
-
293
- def sve_hints(self, eps):
294
- return _SVEHintsRegularizedBose(self, eps)
295
-
296
- @property
297
- def is_centrosymmetric(self):
298
- return True
299
-
300
- def get_symmetrized(self, sign):
301
- if sign == -1:
302
- return _RegularizedBoseKernelOdd(self, sign)
303
- return super().get_symmetrized(sign)
304
-
305
- @property
306
- def ypower(self): return 1
307
-
308
- @property
309
- def conv_radius(self): return 40 * self.lambda_
310
-
311
- def weight_func(self, statistics: str) -> Callable[[np.ndarray], np.ndarray]:
312
- """ Return the weight function for given statistics """
313
- if statistics != "B":
314
- raise ValueError("Kernel is designed for bosonic functions")
315
- return lambda y: 1/y
316
-
317
-
318
- class _SVEHintsRegularizedBose(AbstractSVEHints):
319
- def __init__(self, kernel, eps):
320
- self.kernel = kernel
321
- self.eps = eps
322
-
323
- @property
324
- def ngauss(self): return 10 if self.eps >= 1e-8 else 16
325
-
326
- @property
327
- def segments_x(self):
328
- # Somewhat less accurate ...
329
- nzeros = max(int(np.round(15 * np.log10(self.kernel.lambda_))), 15)
330
- diffs = 1./np.cosh(.18 * np.arange(nzeros))
331
- zeros_pos = diffs.cumsum()
332
- zeros_pos /= zeros_pos[-1]
333
- return np.concatenate((-zeros_pos[::-1], [0], zeros_pos))
334
-
335
- @property
336
- def segments_y(self):
337
- # Zeros around -1 and 1 are distributed asymptotically identical
338
- leading_diffs = np.array([
339
- 0.01363, 0.02984, 0.04408, 0.05514, 0.06268, 0.06679, 0.06793,
340
- 0.06669, 0.06373, 0.05963, 0.05488, 0.04987, 0.04487, 0.04005,
341
- 0.03553, 0.03137, 0.02758, 0.02418, 0.02115, 0.01846])
342
-
343
- nzeros = max(int(np.round(20 * np.log10(self.kernel.lambda_))), 20)
344
- i = np.arange(nzeros)
345
- diffs = .12/np.exp(.0337 * i * np.log(i+1))
346
- #diffs[:leading_diffs.size] = leading_diffs
347
- zeros = diffs.cumsum()
348
- zeros = zeros[:-1] / zeros[-1]
349
- zeros -= 1
350
- return np.concatenate(([-1], zeros, [0], -zeros[::-1], [1]))
351
-
352
- @property
353
- def nsvals(self):
354
- log10_lambda = max(1, np.log10(self.kernel.lambda_))
355
- return int(28 * log10_lambda)
356
-
357
-
358
- class ReducedKernel(AbstractKernel):
359
- """Restriction of centrosymmetric kernel to positive interval.
360
-
361
- For a kernel ``K`` on ``[-1, 1] x [-1, 1]`` that is centrosymmetric, i.e.
362
- ``K(x, y) == K(-x, -y)``, it is straight-forward to show that the left/right
363
- singular vectors can be chosen as either odd or even functions.
364
-
365
- Consequentially, they are singular functions of a reduced kernel ``K_red``
366
- on ``[0, 1] x [0, 1]`` that is given as either::
367
-
368
- K_red(x, y) == K(x, y) + sign * K(x, -y)
369
-
370
- This kernel is what this class represents. The full singular functions can
371
- be reconstructed by (anti-)symmetrically continuing them to the negative
372
- axis.
87
+ Parameters
88
+ ----------
89
+ lambda_ : float
90
+ Kernel cutoff Λ = β * ωmax
373
91
  """
374
- def __init__(self, inner, sign=1):
375
- if not inner.is_centrosymmetric:
376
- raise ValueError("inner kernel must be centrosymmetric")
377
- if np.abs(sign) != 1:
378
- raise ValueError("sign must square to one")
379
-
380
- self.inner = inner
381
- self.sign = sign
382
-
383
- def __call__(self, x, y, x_plus=None, x_minus=None):
384
- x, y = _check_domain(self, x, y)
385
-
386
- # The reduced kernel is defined only over the interval [0, 1], which
387
- # means we must add one to get the x_plus for the inner kernels. We
388
- # can compute this as 1 + x, since we are away from -1.
389
- x_plus = 1 + x_plus
390
-
391
- K_plus = self.inner(x, y, x_plus, x_minus)
392
- K_minus = self.inner(x, -y, x_plus, x_minus)
393
- return K_plus + K_minus if self.sign == 1 else K_plus - K_minus
394
-
395
- @property
396
- def xrange(self):
397
- _, xmax = self.inner.xrange
398
- return 0, xmax
399
-
400
- @property
401
- def yrange(self):
402
- _, ymax = self.inner.yrange
403
- return 0, ymax
404
-
405
- def sve_hints(self, eps):
406
- return _SVEHintsReduced(self.inner.sve_hints(eps))
407
-
408
- @property
409
- def is_centrosymmetric(self):
410
- """True iff K(x,y) = K(-x, -y)"""
411
- return False
412
-
413
- def get_symmetrized(self, sign):
414
- raise RuntimeError("cannot symmetrize twice")
415
-
416
- @property
417
- def ypower(self): return self.inner.ypower
418
-
419
- @property
420
- def conv_radius(self): return self.inner.conv_radius
421
-
422
-
423
- class _SVEHintsReduced(AbstractSVEHints):
424
- def __init__(self, inner_hints):
425
- self.inner_hints = inner_hints
426
-
427
- @property
428
- def ngauss(self): return self.inner_hints.ngauss
429
-
430
- @property
431
- def segments_x(self): return _symm_segments(self.inner_hints.segments_x)
432
92
 
433
- @property
434
- def segments_y(self): return _symm_segments(self.inner_hints.segments_y)
93
+ def __init__(self, lambda_):
94
+ """Initialize regularized bosonic kernel with cutoff lambda."""
95
+ self._lambda = float(lambda_)
96
+ self._ptr = reg_bose_kernel_new(self._lambda)
435
97
 
436
98
  @property
437
- def nsvals(self): return (self.inner_hints.nsvals + 1) // 2
438
-
439
-
440
- class _LogisticKernelOdd(ReducedKernel):
441
- """Fermionic analytical continuation kernel, odd.
442
-
443
- In dimensionless variables ``x = 2*τ/β - 1``, ``y = β*ω/Λ``, the fermionic
444
- integral kernel is a function on ``[-1, 1] x [-1, 1]``::
445
-
446
- K(x, y) == -sinh(Λ/2 * x * y) / cosh(Λ/2 * y)
447
- """
448
- def __call__(self, x, y, x_plus=None, x_minus=None):
449
- result = super().__call__(x, y, x_plus, x_minus)
450
-
451
- # For x * y around 0, antisymmetrization introduces cancellation, which
452
- # reduces the relative precision. To combat this, we replace the
453
- # values with the explicit form
454
- v_half = self.inner.lambda_/2 * y
455
- xy_small = x * v_half < 1
456
- cosh_finite = v_half < 85
457
- np.divide(-np.sinh(v_half * x, where=xy_small),
458
- np.cosh(v_half, where=cosh_finite),
459
- out=result, where=np.logical_and(xy_small, cosh_finite))
460
- return result
461
-
462
-
463
- class _RegularizedBoseKernelOdd(ReducedKernel):
464
- """Bosonic analytical continuation kernel, odd.
465
-
466
- In dimensionless variables ``x = 2*τ/β - 1``, ``y = β*ω/Λ``, the fermionic
467
- integral kernel is a function on ``[-1, 1] x [-1, 1]``::
468
-
469
- K(x, y) = -y * sinh(Λ/2 * x * y) / sinh(Λ/2 * y)
470
- """
471
- def __call__(self, x, y, x_plus=None, x_minus=None):
472
- result = super().__call__(x, y, x_plus, x_minus)
473
-
474
- # For x * y around 0, antisymmetrization introduces cancellation, which
475
- # reduces the relative precision. To combat this, we replace the
476
- # values with the explicit form
477
- v_half = self.inner.lambda_/2 * y
478
- xv_half = x * v_half
479
- xy_small = xv_half < 1
480
- sinh_range = np.logical_and(v_half > 1e-200, v_half < 85)
481
- np.divide(
482
- np.multiply(-y, np.sinh(xv_half, where=xy_small), where=xy_small),
483
- np.sinh(v_half, where=sinh_range),
484
- out=result, where=np.logical_and(xy_small, sinh_range))
485
- return result
486
-
487
-
488
- def matrix_from_gauss(kernel, gauss_x, gauss_y):
489
- """Compute matrix for kernel from Gauss rule"""
490
- # (1 +- x) is problematic around x = -1 and x = 1, where the quadrature
491
- # nodes are clustered most tightly. Thus we have the need for the
492
- # matrix method.
493
- return kernel(gauss_x.x[:,None], gauss_y.x[None,:],
494
- gauss_x.x_forward[:,None], gauss_x.x_backward[:,None])
495
-
496
-
497
- def _check_domain(kernel, x, y):
498
- """Check that arguments lie within the correct domain"""
499
- x = np.asarray(x)
500
- xmin, xmax = kernel.xrange
501
- if not (x >= xmin).all() or not (x <= xmax).all():
502
- raise ValueError("x values not in range [{:g},{:g}]".format(xmin, xmax))
503
-
504
- y = np.asarray(y)
505
- ymin, ymax = kernel.yrange
506
- if not (y >= ymin).all() or not (y <= ymax).all():
507
- raise ValueError("y values not in range [{:g},{:g}]".format(ymin, ymax))
508
- return x, y
509
-
510
-
511
- def _symm_segments(x):
512
- x = np.asarray(x)
513
- if not np.allclose(x, -x[::-1]):
514
- raise ValueError("segments must be symmetric")
515
- xpos = x[x.size // 2:]
516
- if xpos[0] != 0:
517
- xpos = np.hstack([0, xpos])
518
- return xpos
519
-
99
+ def lambda_(self):
100
+ """Kernel cutoff."""
101
+ return self._lambda
520
102
 
521
- def _compute_uv(lambda_, x, y, x_plus=None, x_minus=None):
522
- if x_plus is None:
523
- x_plus = 1 + x
524
- if x_minus is None:
525
- x_minus = 1 - x
526
- u_plus = .5 * x_plus
527
- u_minus = .5 * x_minus
528
- v = lambda_ * y
529
- return u_plus, u_minus, v
103
+ def __del__(self):
104
+ """Clean up kernel resources."""
105
+ if hasattr(self, '_ptr') and self._ptr:
106
+ _lib.spir_kernel_release(self._ptr)