sparse-ir 2.1.3__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.3/src/sparse_ir.egg-info → sparse_ir-2.1.4}/PKG-INFO +1 -1
  2. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/pyproject.toml +1 -1
  3. {sparse_ir-2.1.3 → 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.3 → sparse_ir-2.1.4}/src/sparse_ir/augment.py +41 -6
  6. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/basis.py +9 -9
  7. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/dlr.py +87 -23
  8. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/poly.py +85 -116
  9. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/sampling.py +154 -78
  10. {sparse_ir-2.1.3 → sparse_ir-2.1.4/src/sparse_ir.egg-info}/PKG-INFO +1 -1
  11. {sparse_ir-2.1.3 → 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.3 → sparse_ir-2.1.4}/tests/test_augment.py +27 -30
  14. sparse_ir-2.1.4/tests/test_ffi_boundary.py +564 -0
  15. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_poly.py +1 -117
  16. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_sampling_advanced.py +6 -14
  17. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_sve_advanced.py +38 -44
  18. sparse_ir-2.1.3/src/sparse_ir/_util.py +0 -154
  19. sparse_ir-2.1.3/tests/test_advanced_features.py +0 -250
  20. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/LICENSE.txt +0 -0
  21. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/MANIFEST.in +0 -0
  22. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/README.rst +0 -0
  23. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/setup.cfg +0 -0
  24. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/_gauss.py +0 -0
  25. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/abstract.py +0 -0
  26. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/basis_set.py +0 -0
  27. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/kernel.py +0 -0
  28. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir/sve.py +0 -0
  29. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/dependency_links.txt +0 -0
  30. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/requires.txt +0 -0
  31. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/src/sparse_ir.egg-info/top_level.txt +0 -0
  32. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_basis.py +0 -0
  33. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_basis_set.py +0 -0
  34. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_core.py +0 -0
  35. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_dlr.py +0 -0
  36. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_kernel.py +0 -0
  37. {sparse_ir-2.1.3 → sparse_ir-2.1.4}/tests/test_sampling.py +0 -0
  38. {sparse_ir-2.1.3 → 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.3
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.3"
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):
@@ -16,8 +16,17 @@ from pylibsparseir.core import (
16
16
  COMPUTATION_SUCCESS,
17
17
  get_default_blas_backend,
18
18
  c_double_complex,
19
+ basis_get_u,
20
+ basis_get_uhat,
19
21
  )
20
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
+ )
21
30
 
22
31
  class DiscreteLehmannRepresentation(AbstractBasis):
23
32
  """Discrete Lehmann representation (DLR), with poles being extrema of IR.
@@ -46,18 +55,65 @@ class DiscreteLehmannRepresentation(AbstractBasis):
46
55
  status = ctypes.c_int()
47
56
  if poles is None:
48
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")
49
67
  self._basis = basis
50
- self._poles = np.ascontiguousarray(poles)
68
+ self._poles = poles
69
+ self._u = None
70
+ self._uhat = None
51
71
  self._backend = get_default_blas_backend()
52
- 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
+ )
53
78
  if status.value != COMPUTATION_SUCCESS:
54
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")
55
82
 
56
83
  @property
57
- 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
58
101
 
59
102
  @property
60
- 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
61
117
 
62
118
  @property
63
119
  def statistics(self):
@@ -115,19 +171,25 @@ class DiscreteLehmannRepresentation(AbstractBasis):
115
171
  array_like
116
172
  Expansion coefficients in DLR
117
173
  """
118
- 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)
119
178
  if gl.shape[axis] != self.basis.size:
120
- 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}")
121
182
 
122
183
  output_dims = list(gl.shape)
123
184
  output_dims[axis] = self.size
124
185
 
125
- ndim = len(gl.shape)
126
- input_dims = np.asarray(gl.shape, dtype=np.int32)
186
+ ndim = gl.ndim
187
+ input_dims = np.ascontiguousarray(gl.shape, dtype=np.int32)
127
188
  target_dim = axis
128
189
  order = SPIR_ORDER_ROW_MAJOR
129
190
 
130
- if gl.dtype.kind == 'f':
191
+ if gl.dtype.kind != 'c':
192
+ gl = _util.as_boundary_real(gl, "IR coefficients")
131
193
  output = np.zeros(output_dims, dtype=np.float64)
132
194
  ret = _lib.spir_ir2dlr_dd(
133
195
  self._ptr,
@@ -139,8 +201,8 @@ class DiscreteLehmannRepresentation(AbstractBasis):
139
201
  gl.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
140
202
  output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
141
203
  )
142
- elif gl.dtype.kind == 'c':
143
- gl = np.ascontiguousarray(gl, dtype=np.complex128)
204
+ else:
205
+ gl = _util.as_boundary_complex(gl, "IR coefficients")
144
206
  output_c = np.zeros(output_dims, dtype=c_double_complex)
145
207
  ret = _lib.spir_ir2dlr_zz(
146
208
  self._ptr,
@@ -153,8 +215,6 @@ class DiscreteLehmannRepresentation(AbstractBasis):
153
215
  output_c.ctypes.data_as(ctypes.POINTER(c_double_complex)),
154
216
  )
155
217
  output = output_c['real'] + 1j * output_c['imag']
156
- else:
157
- raise ValueError(f"Unsupported dtype: {gl.dtype}")
158
218
  if ret != COMPUTATION_SUCCESS:
159
219
  raise RuntimeError(f"Failed to convert IR to DLR: {ret}")
160
220
  return output
@@ -176,17 +236,23 @@ class DiscreteLehmannRepresentation(AbstractBasis):
176
236
  array_like
177
237
  Expansion coefficients in IR
178
238
  """
179
- 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)
180
243
  if g_dlr.shape[axis] != self.size:
181
- raise ValueError(f"Input array has wrong size along dimension {axis}")
182
- 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)
183
248
  output_dims[axis] = self.basis.size
184
- ndim = len(g_dlr.shape)
185
- 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)
186
251
  target_dim = axis
187
252
  order = SPIR_ORDER_ROW_MAJOR
188
253
 
189
- if g_dlr.dtype.kind == 'f':
254
+ if g_dlr.dtype.kind != 'c':
255
+ g_dlr = _util.as_boundary_real(g_dlr, "DLR coefficients")
190
256
  output = np.zeros(output_dims, dtype=np.float64)
191
257
  ret = _lib.spir_dlr2ir_dd(
192
258
  self._ptr,
@@ -198,8 +264,8 @@ class DiscreteLehmannRepresentation(AbstractBasis):
198
264
  g_dlr.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
199
265
  output.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
200
266
  )
201
- elif g_dlr.dtype.kind == 'c':
202
- g_dlr = np.ascontiguousarray(g_dlr, dtype=np.complex128)
267
+ else:
268
+ g_dlr = _util.as_boundary_complex(g_dlr, "DLR coefficients")
203
269
  output_c = np.zeros(output_dims, dtype=c_double_complex)
204
270
  ret = _lib.spir_dlr2ir_zz(
205
271
  self._ptr,
@@ -212,8 +278,6 @@ class DiscreteLehmannRepresentation(AbstractBasis):
212
278
  output_c.ctypes.data_as(ctypes.POINTER(c_double_complex)),
213
279
  )
214
280
  output = output_c['real'] + 1j * output_c['imag']
215
- else:
216
- raise ValueError(f"Unsupported dtype: {g_dlr.dtype}")
217
281
  if ret != COMPUTATION_SUCCESS:
218
282
  raise RuntimeError(f"Failed to convert DLR to IR: {ret}")
219
283
  return output