diff --git a/kwant/kpm.py b/kwant/kpm.py index 2f4f8ac6607471f8eab8368b0719728eb8e61812..f55b532b9f3cb2923eb94eedd4b5a77cadbf61f2 100644 --- a/kwant/kpm.py +++ b/kwant/kpm.py @@ -7,10 +7,7 @@ # the file AUTHORS.rst at the top-level directory of this distribution and at # http://kwant-project.org/authors. -__all__ = ['SpectralDensity'] - -import warnings - +import math import numpy as np import scipy import scipy.sparse.linalg as sla @@ -20,6 +17,9 @@ from . import system from ._common import ensure_rng from .operator import _LocalOperator +__all__ = ['SpectralDensity'] + + class SpectralDensity: """Calculate the spectral density of an operator. @@ -50,13 +50,14 @@ class SpectralDensity: ``scipy.sparse.matrices``, the densities will be scalars. If no operator is provided, the density of states is calculated with a faster algorithm. - num_vectors : integer, default: 10 + num_vectors : positive int, default: 10 Number of random vectors for the KPM method. - num_moments : integer, default: 100 - Number of moments, order of the KPM expansion. - num_energies : integer, optional - Number of points where the spectral density will be evaluated. - If not provided, ``2*num_moments`` will be used. + num_moments : positive int, default: 100 + Number of moments, order of the KPM expansion. Mutually exclusive + with 'energy_resolution'. + energy_resolution : positive float, optional + The resolution in energy of the KPM approximation to the spectral + density. Mutually exclusive with 'num_moments'. vector_factory : function, optional The user defined function ``f(n)`` generates random vectors of length ``n`` that will be used in the algorithm. @@ -66,7 +67,7 @@ class SpectralDensity: bounds : pair of floats, optional Lower and upper bounds for the eigenvalue spectrum of the system. If not provided, they are computed. - eps : float, default: 0.05 + eps : positive float, default: 0.05 Parameter to ensure that the rescaled spectrum lies in the interval ``(-1, 1)``; required for stability. rng : seed, or random number generator, optional @@ -126,15 +127,33 @@ class SpectralDensity: Attributes ---------- energies : array of floats - Array of sampling points with length ``num_energies`` in + Array of sampling points with length ``2 * num_moments`` in the range of the spectrum. densities : array of floats Spectral density of the ``operator`` evaluated at the energies. """ def __init__(self, hamiltonian, params=None, operator=None, - num_vectors=10, num_moments=100, num_energies=None, + num_vectors=10, num_moments=None, energy_resolution=None, vector_factory=None, bounds=None, eps=0.05, rng=None): + + if num_moments and energy_resolution: + raise TypeError("either 'num_moments' or 'energy_resolution' " + "must be provided.") + + if energy_resolution: + num_moments = math.ceil((1.6 * self._a) / energy_resolution) + elif num_moments is None: + num_moments = 100 + + must_be_positive_int = ['num_vectors', 'num_moments'] + for var in must_be_positive_int: + val = locals()[var] + if val <= 0 or val != int(val): + raise ValueError('{} must be a positive integer'.format(var)) + if eps <= 0: + raise ValueError('eps must be positive') + rng = ensure_rng(rng) # self.eps ensures that the rescaled Hamiltonian has a # spectrum strictly in the interval (-1,1). @@ -164,16 +183,6 @@ class SpectralDensity: raise ValueError('Parameter `operator` has no `.dot` ' 'attribute and is not callable.') - self.num_moments = num_moments - # Default number of sampling points - if num_energies is None: - self.num_energies = 2 * self.num_moments - elif num_energies < self.num_moments: - raise ValueError('The number of sampling points should be larger ' - 'than the number of moments.') - else: - self.num_energies = num_energies - self._vector_factory = vector_factory or \ (lambda n: np.exp(2j * np.pi * rng.random_sample(n))) # store this vector for reproducibility @@ -192,25 +201,15 @@ class SpectralDensity: self._last_two_alphas = [0.] * num_vectors self._moments_list = [0.] * num_vectors - self.num_vectors = 0 # new random vectors will be used + self.num_moments = num_moments + self.num_vectors = 0 # new random vectors will be used self._update_moments_list(self.num_moments, num_vectors) - #update the number of random vectors self.num_vectors = num_vectors - # sum moments of all random vectors - moments = np.sum(np.asarray(self._moments_list).real, axis=0) - # divide by the number of random vectors - moments = moments / self.num_vectors - # divide by the length of the vectors to normalize - moments = moments / self.hamiltonian.shape[0] - # divide by scale factor to reflect the integral rescaling - moments = moments / self._a - - # obtain energies, densities, and gammas of Eq. 83 + moments = self._moments() xk_rescaled, rho, self._gammas = _calc_fft_moments( - moments, self.num_energies) - # energies to normal scale - self.energies = xk_rescaled*self._a + self._b + moments, 2 * self.num_moments) + self.energies = xk_rescaled * self._a + self._b self.densities = rho def __call__(self, energy=None): @@ -239,15 +238,7 @@ class SpectralDensity: g_e = (np.pi * np.sqrt(1 - rescaled_energy) * np.sqrt(1 + rescaled_energy)) - # calculate the coefficients for Chebyshev expansion - # sum moments of all random vectors - moments = np.sum(np.asarray(self._moments_list).real, axis=0) - # divide by the number of random vectors - moments = moments / self.num_vectors - # divide by the length of the vectors to normalize - moments = moments / self.hamiltonian.shape[0] - # divide by scale factor to reflect the integral rescaling - moments = moments / self._a + moments = self._moments() m = np.arange(self.num_moments) @@ -275,7 +266,7 @@ class SpectralDensity: # This factor divides the sum to normalize the Gauss integral # and rescales the integral back with ``self._a`` to normal # scale. - factor = self._a/self.num_energies + factor = self._a / (2 * self.num_moments) if distribution_function is None: rho = self._gammas else: @@ -285,67 +276,79 @@ class SpectralDensity: rho = np.transpose(self._gammas.transpose() * distribution_array) return factor * np.sum(rho, axis=0) - def increase_energy_resolution(self, tol=0.05, increase_num_moments=True): - """Set the minimum energy resolution to ``tol``. + def add_moments(self, num_moments=None, *, energy_resolution=None): + """Increase the number of Chebyshev moments. - The energy resolution is increased by increasing the number of - sampling points. By default the number of moments is also - increased. + Parameters + ---------- + num_moments: positive int + The number of Chebyshev moments to add. Mutually + exclusive with 'energy_resolution'. + energy_resolution: positive float, optional + Features wider than this resolution are visible + in the spectral density. Mutually exclusive with + 'num_moments'. """ - resolution = 2 * self._a / (self.num_energies / 1.6) - if tol > resolution: - warnings.warn('Energy resolution is already smaller than tol.') - else: - num_energies = np.ceil((1.6 * 2*self._a) / tol) - num_energies = num_energies.astype(int) - if increase_num_moments: - num_moments = num_energies // 2 - self.increase_accuracy(num_moments=num_moments, - num_energies=num_energies) - else: - self.increase_accuracy(num_energies=num_energies) + if not ((num_moments is None) ^ (energy_resolution is None)): + raise TypeError("either 'num_moments' or 'energy_resolution' " + "must be provided.") + + if energy_resolution: + if energy_resolution <= 0: + raise ValueError("'energy_resolution' must be positive" + .format(energy_resolution)) + # factor of 1.6 comes from the fact that we use the + # Jackson kernel when calculating the FFT, which has + # maximal slope π/2. Rounding to 1.6 ensures that the + # energy resolution is sufficient. + present_resolution = self._a * 1.6 / self.num_moments + if present_resolution < energy_resolution: + raise ValueError('Energy resolution is already smaller ' + 'than the requested resolution') + num_moments = math.ceil((1.6 * self._a) / energy_resolution) + + if (num_moments is None or num_moments <= 0 + or num_moments != int(num_moments)): + raise ValueError("'num_moments' must be a positive integer") + + self._update_moments_list(self.num_moments + num_moments, + self.num_vectors) + self.num_moments += num_moments + + # recalculate quantities derived from the moments + moments = self._moments() + xk_rescaled, rho, self._gammas = _calc_fft_moments( + moments, 2 * self.num_moments) + self.energies = xk_rescaled * self._a + self._b + self.densities = rho - def increase_accuracy(self, num_moments=None, num_vectors=None, - num_energies=None): - """Increase the number of moments, random vectors, or sampling - points. + def add_vectors(self, num_vectors): + """Increase the number of random vectors. Parameters ---------- - num_moments, num_vectors, num_energies : integer - Number of Chebyshev moments, random vectors used for - sampling, and sampling points for the calculation of - densities. + num_vectors: positive int + The number of random vectors to add. """ - # Calls to update_moments_list should be in this order: - # 1st. update random vectors and recalculate current moments, - # 2nd. update moments with the updated random vectors, - # 3rd. update sampling points and FFT densities. - num_vectors = num_vectors or self.num_vectors - new_rand_vect = num_vectors - self.num_vectors - for r in range(new_rand_vect): + if num_vectors <= 0 or num_vectors != int(num_vectors): + raise ValueError("'num_vectors' must be a positive integer") + for r in range(num_vectors): self._rand_vect_list.append( self._vector_factory(self.hamiltonian.shape[0])) - self._moments_list.extend([0.] * new_rand_vect) - self._last_two_alphas.extend([0.] * new_rand_vect) - self._update_moments_list(self.num_moments, num_vectors) - self.num_vectors = num_vectors - - num_moments = num_moments or self.num_moments - self._update_moments_list(num_moments, self.num_vectors) - self.num_moments = num_moments - - ## cut random vectors and moments when necessary - self._moments_list[:] = [m[:num_moments] - for m in self._moments_list[:num_vectors]] - - num_energies = num_energies or self.num_energies - if num_energies < self.num_moments: - raise ValueError( - 'The number of sampling points should be larger than the ' - 'number of moments.') - self.num_energies = num_energies + self._moments_list.extend([0.] * num_vectors) + self._last_two_alphas.extend([0.] * num_vectors) + self._update_moments_list(self.num_moments, + self.num_vectors + num_vectors) + self.num_vectors += num_vectors + + # recalculate quantities derived from the moments + moments = self._moments() + xk_rescaled, rho, self._gammas = _calc_fft_moments( + moments, 2 * self.num_moments) + self.energies = xk_rescaled * self._a + self._b + self.densities = rho + def _moments(self): # sum moments of all random vectors moments = np.sum(np.asarray(self._moments_list).real, axis=0) # divide by the number of random vectors @@ -353,14 +356,7 @@ class SpectralDensity: # divide by the length of the vectors to normalize moments = moments / self.hamiltonian.shape[0] # divide by scale factor to reflect the integral rescaling - moments = moments / self._a - - xk_rescaled, rho, self._gammas = _calc_fft_moments( - moments, self.num_energies) - - # Sampling points to normal scale - self.energies = xk_rescaled * self._a + self._b - self.densities = rho + return moments / self._a def _update_moments_list(self, n_moments, n_rand): """Calculate the Chebyshev moments of an operator's spectral @@ -385,8 +381,7 @@ class SpectralDensity: r_start = self.num_vectors new_rand_vect = n_rand - self.num_vectors else: - warnings.warn('Decreasing number of random vectors') - return + raise ValueError('Cannot decrease number of random vectors') if n_moments == self.num_moments: m_start = 2 @@ -398,12 +393,11 @@ class SpectralDensity: new_moments = n_moments - self.num_moments m_start = self.num_moments if new_moments < 0: - warnings.warn('Decreasing the number of moments.') - return + raise ValueError('Cannot decrease number of moments') - assert new_rand_vect == 0,\ - ("Only 'num_moments' *or* 'num_vectors' may be updated " - "at a time.") + if new_rand_vect != 0: + raise ValueError("Only 'num_moments' *or* 'num_vectors' " + "may be updated at a time.") for r in range(r_start, n_rand): alpha_zero = self._rand_vect_list[r] diff --git a/kwant/tests/test_kpm.py b/kwant/tests/test_kpm.py index 94b411d50f3304451d930eb3a064cfcad9352d86..5ea3dbf76d508b9c28f415f223b5ce662cc7d421 100644 --- a/kwant/tests/test_kpm.py +++ b/kwant/tests/test_kpm.py @@ -25,7 +25,6 @@ dim = 20 p = SimpleNamespace( num_moments=200, num_vectors=5, - num_energies=400 ) ham = kwant.rmt.gaussian(dim) @@ -51,7 +50,6 @@ def make_spectrum(ham, p, operator=None, vector_factory=None, rng=None, params=N vector_factory=vector_factory, num_moments=p.num_moments, num_vectors=p.num_vectors, - num_energies=p.num_energies, rng=rng, params=params ) @@ -172,42 +170,6 @@ def test_api_operator(): operator=not_an_operator) -def test_api_lower_num_moments(): - spectrum = SpectralDensity(kwant.rmt.gaussian(dim)) - num_moments = spectrum.num_moments - 1 - with pytest.warns(UserWarning): - spectrum.increase_accuracy(num_moments=num_moments) - - -def test_api_lower_sampling_points(): - spectrum = SpectralDensity(kwant.rmt.gaussian(dim)) - num_samplint_points = spectrum.num_moments - 1 - with pytest.raises(ValueError): - spectrum.increase_accuracy(num_energies=num_samplint_points) - - -def test_api_warning_less_sampling_points(): - precise = copy(p) - precise.num_energies = precise.num_moments - 1 - ham = kwant.rmt.gaussian(dim) - with pytest.raises(ValueError): - make_spectrum(ham, precise) - - -def test_api_lower_energy_resolution(): - spectrum = make_spectrum(ham, p) - tol = np.max(np.abs(np.diff(spectrum.energies))) * 2. - with pytest.warns(UserWarning): - spectrum.increase_energy_resolution(tol=tol) - - -def test_api_lower_num_vectors(): - spectrum = SpectralDensity(kwant.rmt.gaussian(dim)) - num_vectors = spectrum.num_vectors - 1 - with pytest.warns(UserWarning): - spectrum.increase_accuracy(num_vectors=num_vectors) - - def test_api_single_eigenvalue_error(): with pytest.raises(ValueError): SpectralDensity(np.identity(dim, dtype=complex)) @@ -248,6 +210,7 @@ def test_bounds(): # different algorithms are used so these arrays are equal up to TOL_SP assert_allclose_sp(sp1.densities, sp2.densities) + def test_operator_user(): """Check operator=None gives the same results as operator=np.identity(), with the same random vectors. @@ -357,7 +320,7 @@ def test_increase_num_moments(): precise.num_moments = 2 * p.num_moments spectrum_raise = make_spectrum(ham, precise, rng=1) - spectrum.increase_accuracy(num_moments=precise.num_moments) + spectrum.add_moments(p.num_moments) # Check bit for bit equality assert np.all(np.array(spectrum_raise._moments_list) == @@ -373,13 +336,11 @@ def test_increase_num_moments(): # test when increasing num_moments by 1 from an even to an odd number spectrum_even = make_spectrum(ham, p, rng=1) - spectrum_even.increase_accuracy(num_moments=p.num_moments + 1) + spectrum_even.add_moments(num_moments=1) assert np.all(np.array(spectrum_even._moments_list) == np.array(spectrum_odd._moments_list)) - - # ### increase num_moments with an operator (different algorithm) def test_increase_num_moments_op(): @@ -389,8 +350,8 @@ def test_increase_num_moments_op(): sp1 = make_spectrum(ham, p, operator=None, rng=1) sp2 = make_spectrum(ham, p, operator=identity, rng=1) - sp1.increase_accuracy(num_moments=2*sp1.num_moments) - sp2.increase_accuracy(num_moments=2*sp2.num_moments) + sp1.add_moments(num_moments=sp1.num_moments) + sp2.add_moments(num_moments=sp2.num_moments) # different algorithms are used so these arrays are equal up to TOL_SP assert_allclose_sp(sp1.densities, sp2.densities) @@ -404,24 +365,41 @@ def test_increase_num_vectors(): spectrum_raise = make_spectrum(ham, precise, rng=1) spectrum = make_spectrum(ham, p, rng=1) - spectrum.increase_accuracy(num_vectors=precise.num_vectors) + spectrum.add_vectors(num_vectors=p.num_vectors) # Check bit for bit equality assert np.all(spectrum_raise.densities == spectrum.densities) -# ### increase num_energies +def test_invalid_input(): -def test_increase_num_energies(): - precise = copy(p) - precise.sampling_points = 2 * p.num_energies + with pytest.raises(TypeError): + SpectralDensity(ham, num_moments=10, energy_resolution=0.1) - spectrum_raise = make_spectrum(ham, precise, rng=1) - spectrum = make_spectrum(ham, p, rng=1) - spectrum.increase_accuracy(num_moments=precise.num_moments) + s = SpectralDensity(ham) + with pytest.raises(TypeError): + s.add_moments(num_moments=20, energy_resolution=0.1) + + for v in (-20, 0, 10.5): + for p in ('num_vectors', 'num_moments'): + with pytest.raises(ValueError): + SpectralDensity(ham, **{p: v}) + s = SpectralDensity(ham) + with pytest.raises(ValueError): + s.add_moments(num_moments=v) + with pytest.raises(ValueError): + s.add_vectors(num_vectors=v) + + for v in (-1, -0.5, 0): + with pytest.raises(ValueError): + SpectralDensity(ham, eps=v) + s = SpectralDensity(ham) + with pytest.raises(ValueError): + s.add_moments(energy_resolution=v) + + with pytest.raises(ValueError): + s.add_moments(energy_resolution=10) - # Check bit for bit equality - assert np.all(spectrum_raise.densities == spectrum.densities) # ### Convergence for higher number of moments ### @@ -434,7 +412,6 @@ def test_check_convergence_decreasing_values(): precise = SimpleNamespace( num_moments=10*dim + 100*i*dim, num_vectors=dim//2, - num_energies=None ) results = [] np.random.seed(1) @@ -480,7 +457,6 @@ def test_convergence_custom_vector_factory(): precise = SimpleNamespace( num_moments=10*dim + 100*i*dim, num_vectors=dim//2, - num_energies=None ) results = [] iterations = 3 @@ -537,28 +513,16 @@ def test_average(): def test_increase_energy_resolution(): spectrum, filter_index = make_spectrum_and_peaks(ham, p) - old_sampling_points = spectrum.num_energies + old_sampling_points = 2 * spectrum.num_moments tol = np.max(np.abs(np.diff(spectrum.energies))) / 2 - spectrum.increase_energy_resolution(tol=tol) - new_sampling_points = spectrum.num_energies + spectrum.add_moments(energy_resolution=tol) + new_sampling_points = 2 * spectrum.num_moments assert old_sampling_points < new_sampling_points assert np.max(np.abs(np.diff(spectrum.energies))) < tol -def test_increase_energy_resolution_no_moments(): - spectrum, filter_index = make_spectrum_and_peaks(ham, p) - - old_sampling_points = spectrum.num_energies - tol = np.max(np.abs(np.diff(spectrum.energies))) / 2 - - spectrum.increase_energy_resolution(tol=tol, increase_num_moments=False) - new_sampling_points = spectrum.num_energies - - assert old_sampling_points < new_sampling_points - assert np.max(np.abs(np.diff(spectrum.energies))) < tol - # ### check _rescale