sparse-ir 2.1.2__tar.gz → 2.1.4__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 (38) hide show
  1. {sparse_ir-2.1.2/src/sparse_ir.egg-info → sparse_ir-2.1.4}/PKG-INFO +1 -1
  2. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/pyproject.toml +1 -1
  3. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/__init__.py +2 -2
  4. sparse_ir-2.1.4/src/sparse_ir/_util.py +298 -0
  5. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/augment.py +41 -6
  6. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/basis.py +9 -9
  7. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/dlr.py +103 -30
  8. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/poly.py +85 -116
  9. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/sampling.py +154 -69
  10. {sparse_ir-2.1.2 → sparse_ir-2.1.4/src/sparse_ir.egg-info}/PKG-INFO +1 -1
  11. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/SOURCES.txt +1 -0
  12. sparse_ir-2.1.4/tests/test_advanced_features.py +223 -0
  13. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_augment.py +27 -30
  14. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_dlr.py +34 -0
  15. sparse_ir-2.1.4/tests/test_ffi_boundary.py +564 -0
  16. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_poly.py +1 -117
  17. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_sampling.py +50 -0
  18. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_sampling_advanced.py +6 -14
  19. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_sve_advanced.py +38 -44
  20. sparse_ir-2.1.2/src/sparse_ir/_util.py +0 -154
  21. sparse_ir-2.1.2/tests/test_advanced_features.py +0 -250
  22. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/LICENSE.txt +0 -0
  23. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/MANIFEST.in +0 -0
  24. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/README.rst +0 -0
  25. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/setup.cfg +0 -0
  26. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/_gauss.py +0 -0
  27. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/abstract.py +0 -0
  28. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/basis_set.py +0 -0
  29. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/kernel.py +0 -0
  30. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir/sve.py +0 -0
  31. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/dependency_links.txt +0 -0
  32. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/requires.txt +0 -0
  33. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/top_level.txt +0 -0
  34. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_basis.py +0 -0
  35. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_basis_set.py +0 -0
  36. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_core.py +0 -0
  37. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_kernel.py +0 -0
  38. {sparse_ir-2.1.2 → sparse_ir-2.1.4}/tests/test_sve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sparse-ir
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Python bindings for the libsparseir library, providing efficient sparse intermediate representation for many-body physics calculations
5
5
  Author: SpM-lab
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sparse-ir"
3
- version = "2.1.2"
3
+ version = "2.1.4"
4
4
  description = "Python bindings for the libsparseir library, providing efficient sparse intermediate representation for many-body physics calculations"
5
5
  readme = "README.rst"
6
6
  requires-python = ">=3.10"
@@ -30,12 +30,12 @@ __all__ = [
30
30
  'AbstractBasis', 'FiniteTempBasis', 'finite_temp_bases',
31
31
  'TauSampling', 'MatsubaraSampling', 'FiniteTempBasisSet',
32
32
  'LogisticKernel', 'RegularizedBoseKernel',
33
- 'SVEResult', 'compute',
33
+ 'SVEResult', 'compute', 'compute_sve',
34
34
 
35
35
  # Augmented functionality
36
36
  'AugmentedBasis', 'AugmentedTauFunction', 'AugmentedMatsubaraFunction',
37
37
  'AbstractAugmentation', 'TauConst', 'TauLinear', 'MatsubaraConst',
38
38
 
39
39
  # DLR functionality
40
- 'DiscreteLehmannRepresentation', 'TauPoles', 'MatsubaraPoles',
40
+ 'DiscreteLehmannRepresentation',
41
41
  ]
@@ -0,0 +1,298 @@
1
+ # Copyright (C) 2020-2025 Satoshi Terasaki, Markus Wallerberger, Hiroshi Shinaoka, and others
2
+ # SPDX-License-Identifier: MIT
3
+ import functools
4
+ import numpy as np
5
+
6
+
7
+ def ravel_argument(last_dim=False):
8
+ """Wrap function operating on 1-D numpy array to allow arbitrary shapes.
9
+
10
+ This decorator allows to write functions which only need to operate over
11
+ one-dimensional (ravelled) arrays. This often simplifies the "shape logic"
12
+ of the computation.
13
+ """
14
+ return lambda fn: RavelArgumentDecorator(fn, last_dim)
15
+
16
+
17
+ class RavelArgumentDecorator(object):
18
+ def __init__(self, inner, last_dim=False):
19
+ self.instance = None
20
+ self.inner = inner
21
+ self.last_dim = last_dim
22
+ functools.update_wrapper(self, inner)
23
+
24
+ def __get__(self, instance, _owner=None):
25
+ self.instance = instance
26
+ return self
27
+
28
+ def __call__(self, x):
29
+ x = np.asarray(x)
30
+ if self.instance is None:
31
+ res = self.inner(x.ravel())
32
+ else:
33
+ res = self.inner(self.instance, x.ravel())
34
+ if self.last_dim:
35
+ return res.reshape(res.shape[:-1] + x.shape)
36
+ else:
37
+ return res.reshape(x.shape + res.shape[1:])
38
+
39
+
40
+ # Element-type kinds that can be widened to float64 without losing
41
+ # information about what the caller meant: bool, signed/unsigned integer,
42
+ # and floating point. Complex is deliberately excluded.
43
+ _REAL_KINDS = "biuf"
44
+
45
+
46
+ def check_reduced_matsubara(n, zeta=None):
47
+ """Checks that ``n`` is a reduced Matsubara frequency.
48
+
49
+ Check that the argument is a reduced Matsubara frequency, which is an
50
+ integer obtained by scaling the freqency `w[n]` as follows::
51
+
52
+ beta / np.pi * w[n] == 2 * n + zeta
53
+
54
+ Note that this means that instead of a fermionic frequency (``zeta == 1``),
55
+ we expect an odd integer, while for a bosonic frequency (``zeta == 0``),
56
+ we expect an even one. If ``zeta`` is omitted, any one is fine.
57
+
58
+ Raises:
59
+ TypeError: if ``n`` is complex.
60
+ ValueError: if ``n`` is not integral (naming the offending value) or
61
+ has the wrong parity.
62
+ """
63
+ n = np.asarray(n)
64
+ if n.dtype.kind == 'c':
65
+ raise TypeError(
66
+ f"reduced Matsubara frequency must be real, got dtype {n.dtype}")
67
+ if not np.issubdtype(n.dtype, np.integer):
68
+ if n.dtype.kind not in _REAL_KINDS:
69
+ raise TypeError(
70
+ f"reduced Matsubara frequency must be numeric, "
71
+ f"got dtype {n.dtype}")
72
+ nfloat = np.asarray(n, dtype=np.float64)
73
+ if not np.all(np.isfinite(nfloat)):
74
+ raise ValueError(
75
+ "reduced Matsubara frequency must be finite, got "
76
+ f"{nfloat[~np.isfinite(nfloat)][0]!r}")
77
+ n = np.rint(nfloat).astype(np.int64)
78
+ bad = n != nfloat
79
+ if bad.any():
80
+ offending = np.atleast_1d(nfloat)[np.atleast_1d(bad)][0]
81
+ raise ValueError(
82
+ "reduced Matsubara frequency must be an integer, got "
83
+ f"{offending!r} (no truncation is performed)")
84
+ if zeta is not None:
85
+ parity = np.asarray(n) & 1
86
+ if not (parity == zeta).all():
87
+ expected = "odd" if zeta else "even"
88
+ offending = np.atleast_1d(n)[np.atleast_1d(parity != zeta)][0]
89
+ raise ValueError(
90
+ f"reduced Matsubara frequency must be {expected} for "
91
+ f"zeta={zeta}, got {offending!r}")
92
+ return n
93
+
94
+
95
+ def _check_finite(arr, name):
96
+ if arr.size and not np.all(np.isfinite(arr)):
97
+ pos = tuple(int(i) for i in np.argwhere(~np.isfinite(arr))[0])
98
+ raise ValueError(
99
+ f"{name} must be finite, but contains {arr[pos]!r} at index "
100
+ f"{pos[0] if arr.ndim == 1 else pos}")
101
+ return arr
102
+
103
+
104
+ def as_boundary_real(a, name="array", check_finite=True):
105
+ """Normalize ``a`` into a C-contiguous ``float64`` array for the C boundary.
106
+
107
+ The returned object is the one whose pointer must be handed to C: a
108
+ pointer taken from the *original* array would be a defect if a copy was
109
+ made here (see ``rules/ffi-boundary.md``, Pointer Provenance).
110
+
111
+ Raises:
112
+ TypeError: if ``a`` is complex or of a non-numeric element type.
113
+ ValueError: if ``a`` contains a non-finite value.
114
+ """
115
+ arr = np.asarray(a)
116
+ if arr.dtype.kind == 'c':
117
+ raise TypeError(
118
+ f"{name} must be real-valued, got dtype {arr.dtype}; "
119
+ "the C entry point takes a double pointer")
120
+ if arr.dtype.kind not in _REAL_KINDS:
121
+ raise TypeError(f"{name} has unsupported dtype {arr.dtype}")
122
+ out = np.ascontiguousarray(arr, dtype=np.float64)
123
+ if check_finite:
124
+ _check_finite(out, name)
125
+ return out
126
+
127
+
128
+ def as_boundary_complex(a, name="array", check_finite=True):
129
+ """Normalize ``a`` into a C-contiguous ``complex128`` array.
130
+
131
+ ``complex64`` is *not* ``complex128``: passing its buffer through a
132
+ ``c_double_complex`` pointer would read twice as many bytes per element
133
+ as were allocated, so the conversion here is explicit and the pointer
134
+ must be taken from the returned object.
135
+ """
136
+ arr = np.asarray(a)
137
+ if arr.dtype.kind not in _REAL_KINDS + "c":
138
+ raise TypeError(f"{name} has unsupported dtype {arr.dtype}")
139
+ out = np.ascontiguousarray(arr, dtype=np.complex128)
140
+ if check_finite:
141
+ _check_finite(out, name)
142
+ return out
143
+
144
+
145
+ def as_boundary_matsubara(n, name="Matsubara indices", zeta=None):
146
+ """Normalize reduced Matsubara indices into a C-contiguous ``int64`` array.
147
+
148
+ Validates integrality (and, if ``zeta`` is given, parity) *before* the
149
+ conversion, so a non-integral index raises instead of being truncated.
150
+ """
151
+ checked = check_reduced_matsubara(n, zeta=zeta)
152
+ return np.ascontiguousarray(checked, dtype=np.int64)
153
+
154
+
155
+ def normalize_axis(axis, ndim):
156
+ """Resolve a possibly negative ``axis`` against ``ndim`` and range-check it.
157
+
158
+ The C API takes a non-negative target dimension; a negative Python axis
159
+ must be resolved here rather than handed through.
160
+ """
161
+ axis = int(axis)
162
+ resolved = axis + ndim if axis < 0 else axis
163
+ if not 0 <= resolved < ndim:
164
+ raise IndexError(
165
+ f"axis {axis} is out of bounds for an array of dimension {ndim} "
166
+ f"(valid: {-ndim} .. {ndim - 1})")
167
+ return resolved
168
+
169
+
170
+ def resolve_function_indices(index, size):
171
+ """Resolve a basis-function index, list of indices, or slice.
172
+
173
+ Negative indices are resolved explicitly (Python semantics); an index
174
+ outside ``[-size, size)`` raises :class:`IndexError` naming the requested
175
+ index and the valid range. No modulo wrap-around is performed.
176
+ """
177
+ if isinstance(index, slice):
178
+ return list(range(*index.indices(size)))
179
+
180
+ idx = np.asarray(index)
181
+ if idx.dtype.kind == 'c':
182
+ raise TypeError(
183
+ f"basis-function index must be an integer, got dtype {idx.dtype}")
184
+ if idx.dtype.kind not in _REAL_KINDS:
185
+ raise TypeError(
186
+ f"basis-function index must be an integer, got dtype {idx.dtype}")
187
+ if idx.dtype.kind == 'f':
188
+ rounded = np.rint(idx)
189
+ if not np.array_equal(rounded, idx):
190
+ offending = np.atleast_1d(idx)[np.atleast_1d(rounded != idx)][0]
191
+ raise ValueError(
192
+ f"basis-function index must be an integer, got {offending!r} "
193
+ "(no truncation is performed)")
194
+ idx = rounded.astype(np.int64)
195
+
196
+ flat = np.atleast_1d(idx).ravel()
197
+ resolved = []
198
+ for i in flat.tolist():
199
+ j = i + size if i < 0 else i
200
+ if not 0 <= j < size:
201
+ raise IndexError(
202
+ f"basis-function index {i} is out of range for a function set "
203
+ f"of size {size} (valid: {-size} .. {size - 1})")
204
+ resolved.append(int(j))
205
+ return resolved
206
+
207
+
208
+ def check_range(x, xmin, xmax):
209
+ """Checks each element is in range [xmin, xmax]"""
210
+ x = np.asarray(x)
211
+ if not (x >= xmin).all():
212
+ raise ValueError(f"Some x violate lower bound {xmin}")
213
+ if not (x <= xmax).all():
214
+ raise ValueError(f"Some x violate upper bound {xmax}")
215
+ return x
216
+
217
+
218
+ def normalize_tau(statistics, tau, beta):
219
+ """Normalize τ to [0, β] with statistics-dependent periodicity.
220
+
221
+ Handles boundary conditions based on statistics:
222
+ - Fermions ('F'): Anti-periodic G(τ + β) = -G(τ)
223
+ - Bosons ('B'): Periodic G(τ + β) = G(τ)
224
+
225
+ This function maps τ values from the range [-β, β] to [0, β] with
226
+ appropriate sign factors, following the periodicity rules.
227
+
228
+ Arguments:
229
+ statistics (str):
230
+ 'F' for Fermionic or 'B' for Bosonic statistics.
231
+ tau (array_like):
232
+ Imaginary time value(s) in range [-β, β].
233
+ beta (float):
234
+ Inverse temperature.
235
+
236
+ Returns:
237
+ tuple[ndarray, ndarray]:
238
+ (tau_normalized, sign) where:
239
+ - tau_normalized: τ values mapped to [0, β]
240
+ - sign: Sign factor (±1) for periodicity
241
+
242
+ Raises:
243
+ ValueError: If tau is outside [-β, β] or statistics is invalid.
244
+
245
+ Special cases:
246
+ - Negative zero (τ = -0.0) is treated as τ = β with appropriate sign
247
+ - For τ in [0, β]: returns (τ, +1)
248
+ - For τ in [-β, 0): returns (τ + β, sign) where sign depends on statistics
249
+
250
+ .. versionadded:: 1.2
251
+ """
252
+ tau = np.asarray(tau, dtype=np.float64)
253
+ beta = float(beta)
254
+
255
+ if statistics not in ('F', 'B'):
256
+ raise ValueError("statistics must be 'F' (Fermionic) or 'B' (Bosonic)")
257
+
258
+ if np.any(tau < -beta) or np.any(tau > beta):
259
+ raise ValueError(f"τ must be in [-β, β] = [{-beta}, {beta}]")
260
+
261
+ # Handle negative zero: τ = -0.0 → τ = β
262
+ is_neg_zero = (tau == 0.0) & np.signbit(tau)
263
+
264
+ tau_normalized = np.where(is_neg_zero, beta, tau)
265
+ sign = np.ones_like(tau, dtype=np.float64)
266
+
267
+ if statistics == 'F':
268
+ # Fermionic: anti-periodic
269
+ sign = np.where(is_neg_zero, -1.0, sign)
270
+ else: # statistics == 'B'
271
+ # Bosonic: periodic
272
+ sign = np.where(is_neg_zero, 1.0, sign)
273
+
274
+ # Normalize negative tau to [0, β]
275
+ mask_neg = tau_normalized < 0
276
+ tau_normalized = np.where(mask_neg, tau_normalized + beta, tau_normalized)
277
+
278
+ if statistics == 'F':
279
+ sign = np.where(mask_neg, -sign, sign)
280
+
281
+ return tau_normalized, sign
282
+
283
+
284
+ def check_svd_result(svd_result, matrix_shape=None):
285
+ """Checks that argument is a valid SVD triple (u, s, vH)"""
286
+ u, s, vH = map(np.asarray, svd_result)
287
+ m_u, k_u = u.shape
288
+ k_s, = s.shape
289
+ k_v, n_v = vH.shape
290
+ if k_u != k_s or k_s != k_v:
291
+ raise ValueError("shape mismatch between SVD elements:"
292
+ f"({m_u}, {k_u}) x ({k_s}) x ({k_v}, {n_v})")
293
+ if matrix_shape is not None:
294
+ m, n = matrix_shape
295
+ if m_u != m or n_v != n:
296
+ raise ValueError(f"shape mismatch between SVD ({m_u}, {n_v}) "
297
+ f"and matrix ({m}, {n})")
298
+ return u, s, vH
@@ -77,7 +77,7 @@ class AugmentedBasis(abstract.AbstractBasis):
77
77
  return self._basis.statistics
78
78
 
79
79
  def __getitem__(self, index):
80
- stop = basis._slice_to_size(index)
80
+ stop = _slice_to_size(index, self.size)
81
81
  if stop <= self._naug:
82
82
  raise ValueError("Cannot truncate to only augmentation")
83
83
  return AugmentedBasis(self._basis[:stop - self._naug],
@@ -202,7 +202,7 @@ class _AugmentedFunction:
202
202
  def __getitem__(self, l):
203
203
  # TODO make this more general
204
204
  if isinstance(l, slice):
205
- stop = basis._slice_to_size(l)
205
+ stop = _slice_to_size(l, self.size)
206
206
  if stop <= self._naug:
207
207
  raise NotImplementedError("Don't truncate to only augmentation")
208
208
  return _AugmentedFunction(self._fbasis[:stop-self._naug], self._faug)
@@ -221,13 +221,13 @@ class AugmentedTauFunction(_AugmentedFunction):
221
221
 
222
222
  @property
223
223
  def xmax(self):
224
- return self._fbasis.xmin
224
+ return self._fbasis.xmax
225
225
 
226
226
  def deriv(self, n=1):
227
227
  """Get polynomial for the n'th derivative"""
228
228
  dbasis = self._fbasis.deriv(n)
229
229
  daug = [faug_l.deriv(n) for faug_l in self._faug]
230
- return AugmentedTauFunction(dbasis, *daug)
230
+ return AugmentedTauFunction(dbasis, daug)
231
231
 
232
232
 
233
233
  class AugmentedMatsubaraFunction(_AugmentedFunction):
@@ -291,6 +291,12 @@ class TauConst(AbstractAugmentation):
291
291
  raise ValueError("temperature must be positive")
292
292
  if statistics not in ('F', 'B'):
293
293
  raise ValueError("statistics must be 'F' or 'B'")
294
+ # A fermionic TauConst is not merely ill-conditioned, it is useless:
295
+ # its Fourier transform is sqrt(beta) * (n == 0), and fermionic reduced
296
+ # frequencies are odd, so the augmentation column vanishes identically
297
+ # and the augmented basis is rank-deficient. Refuse instead of
298
+ # silently returning a singular fit.
299
+ _check_bosonic_statistics(statistics)
294
300
  self._beta = beta
295
301
  self._statistics = statistics
296
302
 
@@ -406,6 +412,31 @@ class MatsubaraConst(AbstractAugmentation):
406
412
  return np.broadcast_to(1.0, n.shape)
407
413
 
408
414
 
415
+ def _slice_to_size(index, size):
416
+ """Return the number of basis functions selected by ``index``.
417
+
418
+ Only ``basis[:stop]``-style truncation is supported, mirroring
419
+ :py:meth:`FiniteTempBasis.__getitem__`.
420
+ """
421
+ if not isinstance(index, slice):
422
+ raise TypeError(
423
+ f"only slice truncation is supported, got {index!r}")
424
+ if index.start not in (None, 0):
425
+ raise ValueError(
426
+ f"basis truncation must start at 0, got {index.start!r}")
427
+ if index.step not in (None, 1):
428
+ raise ValueError(
429
+ f"basis truncation must have unit step, got {index.step!r}")
430
+ if index.stop is None:
431
+ return size
432
+ stop = int(index.stop)
433
+ if not 0 < stop <= size:
434
+ raise IndexError(
435
+ f"truncation to {stop} functions is out of range for a basis of "
436
+ f"size {size}")
437
+ return stop
438
+
439
+
409
440
  def _augmentation_factory(basis, *augs):
410
441
  for aug in augs:
411
442
  if isinstance(aug, AbstractAugmentation):
@@ -418,6 +449,10 @@ def _check_bosonic_statistics(statistics):
418
449
  if statistics == 'B':
419
450
  return
420
451
  elif statistics == 'F':
421
- raise ValueError("term only allowed for bosonic basis")
452
+ raise ValueError(
453
+ "TauConst augmentation is only allowed for a bosonic basis: for "
454
+ "fermionic statistics its Fourier transform vanishes at every "
455
+ "(odd) reduced Matsubara frequency, which makes the augmented "
456
+ "basis rank-deficient")
422
457
  else:
423
- raise ValueError("invalid statistics")
458
+ raise ValueError(f"invalid statistics {statistics!r}, expected 'F' or 'B'")
@@ -303,16 +303,16 @@ class FiniteTempBasis(AbstractBasis):
303
303
  temperature. Note that this implies a different UV cutoff ``wmax``,
304
304
  since ``lambda_ == beta * wmax`` stays constant.
305
305
  """
306
- # Calculate new beta and wmax that give the desired lambda
307
- # We keep the ratio beta/wmax constant
308
- ratio = self.beta / self.wmax
309
- new_wmax = np.sqrt(new_lambda / ratio)
310
- new_beta = new_lambda / new_wmax
311
-
312
- # Get epsilon from the current basis accuracy
313
- eps = self.accuracy
306
+ new_beta = float(new_beta)
307
+ if not new_beta > 0:
308
+ raise ValueError(
309
+ f"inverse temperature must be positive, got {new_beta!r}")
314
310
 
315
- return FiniteTempBasis(self.statistics, new_beta, new_wmax, eps)
311
+ # lambda_ == beta * wmax is held fixed, so the SVE (which depends only
312
+ # on lambda_ and eps) can be reused as is.
313
+ new_wmax = self._lambda / new_beta
314
+ return FiniteTempBasis(self.statistics, new_beta, new_wmax, self._eps,
315
+ kernel=self._kernel, sve_result=self._sve)
316
316
 
317
317
 
318
318
  def finite_temp_bases(beta, wmax, eps=None, sve_result=None):
@@ -11,8 +11,22 @@ import ctypes
11
11
  import numpy as np
12
12
  from .abstract import AbstractBasis
13
13
  from pylibsparseir.core import basis_get_default_omega_sampling_points
14
- from pylibsparseir.core import _lib, COMPUTATION_SUCCESS, get_default_blas_backend
14
+ from pylibsparseir.core import (
15
+ _lib,
16
+ COMPUTATION_SUCCESS,
17
+ get_default_blas_backend,
18
+ c_double_complex,
19
+ basis_get_u,
20
+ basis_get_uhat,
21
+ )
15
22
  from pylibsparseir.constants import SPIR_ORDER_ROW_MAJOR
23
+ from . import _util
24
+ from .poly import (
25
+ FunctionSet,
26
+ FunctionSetFT,
27
+ PiecewiseLegendrePolyVector,
28
+ PiecewiseLegendrePolyFTVector,
29
+ )
16
30
 
17
31
  class DiscreteLehmannRepresentation(AbstractBasis):
18
32
  """Discrete Lehmann representation (DLR), with poles being extrema of IR.
@@ -41,18 +55,65 @@ class DiscreteLehmannRepresentation(AbstractBasis):
41
55
  status = ctypes.c_int()
42
56
  if poles is None:
43
57
  poles = basis_get_default_omega_sampling_points(basis._ptr)
58
+ # Normalize first, then take the pointer from the *normalized* object.
59
+ # Taking it from the caller's array instead silently hands C the
60
+ # buffer of a non-contiguous or non-float64 array.
61
+ poles = _util.as_boundary_real(poles, "poles")
62
+ if poles.ndim != 1:
63
+ raise ValueError(
64
+ f"poles must be one-dimensional, got shape {poles.shape}")
65
+ if poles.size == 0:
66
+ raise ValueError("poles must not be empty")
44
67
  self._basis = basis
45
- self._poles = np.ascontiguousarray(poles)
68
+ self._poles = poles
69
+ self._u = None
70
+ self._uhat = None
46
71
  self._backend = get_default_blas_backend()
47
- self._ptr = _lib.spir_dlr_new_with_poles(basis._ptr, len(poles), poles.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), status)
72
+ self._ptr = _lib.spir_dlr_new_with_poles(
73
+ basis._ptr,
74
+ poles.size,
75
+ poles.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
76
+ status,
77
+ )
48
78
  if status.value != COMPUTATION_SUCCESS:
49
79
  raise RuntimeError(f"Failed to create DLR basis: {status.value}")
80
+ if not self._ptr:
81
+ raise RuntimeError("Failed to create DLR basis: null handle")
50
82
 
51
83
  @property
52
- def u(self): return self._basis._u
84
+ def u(self):
85
+ r"""DLR basis functions on the imaginary-time axis.
86
+
87
+ These are the *DLR* basis functions, i.e. ``u[i](tau)`` is the
88
+ imaginary-time kernel evaluated at the ``i``-th pole, so that::
89
+
90
+ gtau == g_dlr @ dlr.u(tau)
91
+
92
+ holds for DLR coefficients ``g_dlr``. They are **not** the basis
93
+ functions of the underlying IR basis.
94
+ """
95
+ if self._u is None:
96
+ beta = self._basis.beta
97
+ self._u = PiecewiseLegendrePolyVector(
98
+ FunctionSet(basis_get_u(self._ptr)),
99
+ -beta, beta, beta, default_overlap_range=(0, beta))
100
+ return self._u
53
101
 
54
102
  @property
55
- def uhat(self): return self._basis._uhat
103
+ def uhat(self):
104
+ r"""DLR basis functions on the reduced Matsubara frequency axis.
105
+
106
+ ``uhat[i](n)`` is the Fourier transform of :py:attr:`u`, so that::
107
+
108
+ giv == g_dlr @ dlr.uhat(n)
109
+
110
+ holds for DLR coefficients ``g_dlr``. They are **not** the Matsubara
111
+ basis functions of the underlying IR basis.
112
+ """
113
+ if self._uhat is None:
114
+ self._uhat = PiecewiseLegendrePolyFTVector(
115
+ FunctionSetFT(basis_get_uhat(self._ptr)))
116
+ return self._uhat
56
117
 
57
118
  @property
58
119
  def statistics(self):
@@ -110,20 +171,26 @@ class DiscreteLehmannRepresentation(AbstractBasis):
110
171
  array_like
111
172
  Expansion coefficients in DLR
112
173
  """
113
- gl = np.ascontiguousarray(gl)
174
+ gl = np.asarray(gl)
175
+ if gl.ndim == 0:
176
+ raise ValueError("IR coefficients must be at least one-dimensional")
177
+ axis = _util.normalize_axis(axis, gl.ndim)
114
178
  if gl.shape[axis] != self.basis.size:
115
- raise ValueError(f"Input array has wrong size along dimension {axis}")
179
+ raise ValueError(
180
+ f"IR coefficients have length {gl.shape[axis]} along axis "
181
+ f"{axis}, expected {self.basis.size}")
116
182
 
117
183
  output_dims = list(gl.shape)
118
184
  output_dims[axis] = self.size
119
- output = np.zeros(output_dims, dtype=gl.dtype)
120
185
 
121
- ndim = len(gl.shape)
122
- input_dims = np.asarray(gl.shape, dtype=np.int32)
186
+ ndim = gl.ndim
187
+ input_dims = np.ascontiguousarray(gl.shape, dtype=np.int32)
123
188
  target_dim = axis
124
189
  order = SPIR_ORDER_ROW_MAJOR
125
190
 
126
- if gl.dtype.kind == 'f':
191
+ if gl.dtype.kind != 'c':
192
+ gl = _util.as_boundary_real(gl, "IR coefficients")
193
+ output = np.zeros(output_dims, dtype=np.float64)
127
194
  ret = _lib.spir_ir2dlr_dd(
128
195
  self._ptr,
129
196
  self._backend,
@@ -134,7 +201,9 @@ class DiscreteLehmannRepresentation(AbstractBasis):
134
201
  gl.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
135
202
  output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
136
203
  )
137
- elif gl.dtype.kind == 'c':
204
+ else:
205
+ gl = _util.as_boundary_complex(gl, "IR coefficients")
206
+ output_c = np.zeros(output_dims, dtype=c_double_complex)
138
207
  ret = _lib.spir_ir2dlr_zz(
139
208
  self._ptr,
140
209
  self._backend,
@@ -142,12 +211,10 @@ class DiscreteLehmannRepresentation(AbstractBasis):
142
211
  ndim,
143
212
  input_dims.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)),
144
213
  target_dim,
145
- # TODO: use complex data
146
- gl.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
147
- output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
214
+ gl.ctypes.data_as(ctypes.POINTER(c_double_complex)),
215
+ output_c.ctypes.data_as(ctypes.POINTER(c_double_complex)),
148
216
  )
149
- else:
150
- raise ValueError(f"Unsupported dtype: {gl.dtype}")
217
+ output = output_c['real'] + 1j * output_c['imag']
151
218
  if ret != COMPUTATION_SUCCESS:
152
219
  raise RuntimeError(f"Failed to convert IR to DLR: {ret}")
153
220
  return output
@@ -169,18 +236,24 @@ class DiscreteLehmannRepresentation(AbstractBasis):
169
236
  array_like
170
237
  Expansion coefficients in IR
171
238
  """
172
- g_dlr = np.ascontiguousarray(g_dlr)
239
+ g_dlr = np.asarray(g_dlr)
240
+ if g_dlr.ndim == 0:
241
+ raise ValueError("DLR coefficients must be at least one-dimensional")
242
+ axis = _util.normalize_axis(axis, g_dlr.ndim)
173
243
  if g_dlr.shape[axis] != self.size:
174
- raise ValueError(f"Input array has wrong size along dimension {axis}")
175
- output_dims = np.asarray(g_dlr.shape, dtype=np.int32)
244
+ raise ValueError(
245
+ f"DLR coefficients have length {g_dlr.shape[axis]} along axis "
246
+ f"{axis}, expected {self.size}")
247
+ output_dims = list(g_dlr.shape)
176
248
  output_dims[axis] = self.basis.size
177
- output = np.zeros(output_dims, dtype=g_dlr.dtype)
178
- ndim = len(g_dlr.shape)
179
- input_dims = np.asarray(g_dlr.shape, dtype=np.int32)
249
+ ndim = g_dlr.ndim
250
+ input_dims = np.ascontiguousarray(g_dlr.shape, dtype=np.int32)
180
251
  target_dim = axis
181
252
  order = SPIR_ORDER_ROW_MAJOR
182
253
 
183
- if g_dlr.dtype.kind == 'f':
254
+ if g_dlr.dtype.kind != 'c':
255
+ g_dlr = _util.as_boundary_real(g_dlr, "DLR coefficients")
256
+ output = np.zeros(output_dims, dtype=np.float64)
184
257
  ret = _lib.spir_dlr2ir_dd(
185
258
  self._ptr,
186
259
  self._backend,
@@ -191,7 +264,9 @@ class DiscreteLehmannRepresentation(AbstractBasis):
191
264
  g_dlr.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
192
265
  output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
193
266
  )
194
- elif g_dlr.dtype.kind == 'c':
267
+ else:
268
+ g_dlr = _util.as_boundary_complex(g_dlr, "DLR coefficients")
269
+ output_c = np.zeros(output_dims, dtype=c_double_complex)
195
270
  ret = _lib.spir_dlr2ir_zz(
196
271
  self._ptr,
197
272
  self._backend,
@@ -199,12 +274,10 @@ class DiscreteLehmannRepresentation(AbstractBasis):
199
274
  ndim,
200
275
  input_dims.ctypes.data_as(ctypes.POINTER(ctypes.c_int)),
201
276
  target_dim,
202
- # TODO: use complex data
203
- g_dlr.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
204
- output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
277
+ g_dlr.ctypes.data_as(ctypes.POINTER(c_double_complex)),
278
+ output_c.ctypes.data_as(ctypes.POINTER(c_double_complex)),
205
279
  )
206
- else:
207
- raise ValueError(f"Unsupported dtype: {g_dlr.dtype}")
280
+ output = output_c['real'] + 1j * output_c['imag']
208
281
  if ret != COMPUTATION_SUCCESS:
209
282
  raise RuntimeError(f"Failed to convert DLR to IR: {ret}")
210
283
  return output