diff --git a/kwant/contrib/__init__.py b/kwant/contrib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c52331e99965033fa443a80c3ab4cec0427ab84c --- /dev/null +++ b/kwant/contrib/__init__.py @@ -0,0 +1,10 @@ +# Copyright 2011-2013 kwant authors. +# +# This file is part of kwant. It is subject to the license terms in the +# LICENSE file found in the top-level directory of this distribution and at +# http://kwant-project.org/license. A list of kwant authors can be found in +# the AUTHORS file at the top-level directory of this distribution and at +# http://kwant-project.org/authors. + +__all__ = ['rmt'] +from . import rmt diff --git a/kwant/contrib/rmt.py b/kwant/contrib/rmt.py new file mode 100644 index 0000000000000000000000000000000000000000..67b8b9c83f4fe1bde1f8478105367f2e7bdbc865 --- /dev/null +++ b/kwant/contrib/rmt.py @@ -0,0 +1,277 @@ +# Copyright 2011-2013 kwant authors. +# +# This file is part of kwant. It is subject to the license terms in the +# LICENSE file found in the top-level directory of this distribution and at +# http://kwant-project.org/license. A list of kwant authors can be found in +# the AUTHORS file at the top-level directory of this distribution and at +# http://kwant-project.org/authors. + +from __future__ import division + +__all__ = ['gaussian', 'circular'] + +import numpy as np + +qr = np.linalg.qr +randn = np.random.randn +sym_list = 'A', 'AI', 'AII', 'AIII', 'BDI', 'CII', 'D', 'DIII', 'C', 'CI' + + +def t(sym): + """Return the value of time-reversal symmetry squared (1, 0, or -1)""" + if sym not in sym_list: + raise ValueError('Non-existent symmetry class.') + if sym in ('CI', 'AI', 'BDI'): + return 1 + elif sym in ('CII', 'AII', 'DIII'): + return -1 + else: + return 0 + + +def p(sym): + """Return the value of particle-hole symmetry squared (1, 0, or -1)""" + if sym not in sym_list: + raise ValueError('Non-existent symmetry class.') + if sym in ('D', 'DIII', 'BDI'): + return 1 + elif sym in ('C', 'CI', 'CII'): + return -1 + else: + return 0 + + +def c(sym): + """Return 1 if the system has chiral symmetry, and 0 otherwise.""" + if (t(sym) and p(sym)) or sym == 'AIII': + return 1 + else: + return 0 + + +h_t_matrix = {'AI': [[1]], 'CI': [[0, 1], [1, 0]], 'BDI': [[1, 0], [0, -1]], + 'AII': [[0, 1j], [-1j, 0]], + 'CII': [[0, 0, 1j, 0], [0, 0, 0, 1j], + [-1j, 0, 0, 0], [0, -1j, 0, 0]], + 'DIII': [[0, 1j], [-1j, 0]]} +h_p_matrix = {'C': [[0, 1j], [-1j, 0]], 'CI': [[0, 1j], [-1j, 0]], + 'CII': [[0, 0, 1j, 0], [0, 0, 0, -1j], + [-1j, 0, 0, 0], [0, 1j, 0, 0]], + 'D': [[1]], 'DIII': [[0, 1], [1, 0]], 'BDI': [[1]]} + + +def gaussian(n, sym='A', v=1.): + """Make a n * n random Gaussian Hamiltonian. + + Parameters + ---------- + n : int + Size of the Hamiltonian. It should be even for all the classes + except A, D, and AI, and in class CII it should be a multiple of 4. + sym : one of 'A', 'AI', 'AII', 'AIII', 'BDI', 'CII', 'D', 'DIII', 'C', 'CI' + Altland-Zirnbauer symmetry class of the Hamiltonian. + v : float + Variance every degree of freedom of the Hamiltonian. The probaility + distribution of the Hamiltonian is `P(H) = exp(-Tr(H^2) / 2 v^2)`. + + Returns + ------- + h : numpy.ndarray + A numpy array drawn from a corresponding Gaussian ensemble. + + Notes + ----- + The representations of symmetry operators are chosen according to + Phys. Rev. B 85, 165409. + + Matrix indices are grouped first according to orbital number, + then sigma-index, then tau-index. + + Chiral (sublattice) symmetry C always reads: + H = -tau_z H tau_z. + Time reversal symmetry T reads: + AI: H = H^*. + BDI: H = tau_z H^* tau_z. + CI: H = tau_x H^* tau_x. + AII, CII: H = sigma_y H^* sigma_y. + DIII: H = tau_y H^* tau_y. + Particle-hole symmetry reads: + C, CI: H = -tau_y H^* tau_y. + CII: H = -tau_z sigma_y H^* tau_z sigma_y. + D, BDI: H = -H^*. + DIII: H = -tau_x H^* tau_x. + + This implementation should be sufficiently efficient for large matrices, + since it avoids any matrix multiplication. + """ + if sym not in sym_list: + raise ValueError('Unknown symmetry type.') + if (c(sym) or t(sym) == -1 or p(sym) == -1) and n % 2: + raise ValueError('Matrix dimension should be even in chosen symmetry' + 'class.') + else: + tau_z = np.array((n // 2) * [1, -1]) + idx_x = np.arange(n) + tau_z + if sym == 'CII' and n % 4: + raise ValueError('Matrix dimension should be a multiple of 4 in' + 'symmetry class CII.') + factor = v / np.sqrt(2) + + # Generate a Gaussian matrix of appropriate dtype. + if sym == 'AI': + h = randn(n, n) + elif sym in ('D', 'BDI'): + h = 1j * randn(n, n) + else: + h = randn(n, n) + 1j * randn(n, n) + + # Ensure Hermiticity. + h += h.T.conj() + + # Ensure Chiral symmetry. + if c(sym): + h -= tau_z.reshape(-1, 1) * h * tau_z + factor *= 0.5 + + # Ensure the necessary anti-unitary symmetry. + if sym in ('AII', 'DIII'): + h += tau_z.reshape(-1, 1) * h[idx_x][:, idx_x].conj() * tau_z + factor /= np.sqrt(2) + elif sym in ('C', 'CI'): + h -= tau_z.reshape(-1, 1) * h[idx_x][:, idx_x].conj() * tau_z + factor /= np.sqrt(2) + elif sym == 'CII': + sigma_z = np.array((n // 4) * [1, 1, -1, -1]) + idx_sigma_x = np.arange(n) + 2 * sigma_z + h += sigma_z.reshape(-1, 1) * h[idx_sigma_x][:, idx_sigma_x].conj() * \ + sigma_z + factor /= np.sqrt(2) + + h *= factor + + return h + + +def circular(n, sym='A', charge=None): + """Make a n * n matrix belonging to a symmetric circular ensemble. + + Parameters + ---------- + n : int + Size of the matrix. It should be even for the classes C, CI, CII, + AII, DIII (either T^2 = -1 or P^2 = -1). + sym : one of 'A', 'AI', 'AII', 'AIII', 'BDI', 'CII', 'D', 'DIII', 'C', 'CI' + Altland-Zirnbauer symmetry class of the matrix. + charge : int or None + Topological invariant of the matrix. Should be one of 1, -1 in symmetry + classes D and DIII, should be from 0 to n in classes AIII and BDI, + and should be from 0 to n / 2 in class CII. If charge is None, + it is drawn from a binomial distribution with p = 1 / 2. + + Returns + ------- + s : numpy.ndarray + A numpy array drawn from a corresponding circular ensemble. + + Notes + ----- + The representations of symmetry operators are chosen according to + Phys. Rev. B 85, 165409, except class D. + + Matrix indices are grouped first according to channel number, + then sigma-index. + + Chiral (sublattice) symmetry C always reads: + s = s^+. + Time reversal symmetry T reads: + AI, BDI: r = r^T. + CI: r = -sigma_y r^T sigma_y. + AII, DIII: r = -r^T. + CII: r = sigma_y r^T sigma_y. + Particle-hole symmetry reads: + CI: r = -sigma_y r^* sigma_y + C, CII: r = sigma_y r^* sigma_y + D, BDI: r = r^*. + DIII: -r = r^*. + + This function uses QR decomposition to probe symmetric compact groups, + as detailed in arXiv:math-ph/0609050. For a reason as yet unknown, scipy + implementation of QR decomposition also works for symplectic matrices. + """ + + if sym not in sym_list: + raise ValueError('Unknown symmetry type.') + if (t(sym) == -1 or p(sym) == -1) and n % 2: + raise ValueError('n must be even in chosen symmetry class.') + + # Prepare a real, complex or symplectic Gaussian matrix + # Real case. + if p(sym) == 1: + h = randn(n, n) + # Complex case. + elif p(sym) == 0: + h = randn(n, n) + 1j * randn(n, n) + # Symplectic case. + else: # p(sym) == -1 + h = randn(n, n) + 1j * randn(n, n) + tau_z = np.array((n // 2) * [1, -1]) + idx_x = np.arange(n) + tau_z + h -= tau_z.reshape(-1, 1) * h[idx_x][:, idx_x].conj() * tau_z + h *= 1j + + # Generate a random matrix with proper electron-hole symmetry. + # This matrix is a random element of groups O(N), U(N), or USp(N). + s, h = qr(h) + if p(sym) == -1 and not np.allclose(np.diag(h, 1)[::2], 0, atol=1e-8): + raise RuntimeError('QR decomposition symmetry failure.') + h = np.diag(h) + h /= np.abs(h) + s *= h + + # Ensure proper topological invariant in classes D and DIII. + if sym in ('D', 'DIII') and charge is not None: + if charge not in (-1, 1): + raise ValueError('Impossible value of topological invariant.') + det = np.linalg.det(s) + if sym == 'DIII': + det *= (-1) ** (n // 2) + if (charge > 0) != (det > 0): + idx = np.arange(n) + idx[-1] -= 1 + idx[-2] += 1 + s = s[idx] + + # Add the proper time-reversal symmetry: + if sym in ('AI', 'CI'): + s = np.dot(s.T, s) + if sym == 'CI': + tau_z = np.array((n // 2) * [1, -1]) + idx_x = np.arange(n) + tau_z + s = 1j * tau_z * s[:, idx_x] + elif sym == 'AII' or sym == 'DIII': + tau_z = np.array((n // 2) * [1, -1]) + idx_x = np.arange(n) + tau_z + s = 1j * np.dot(s.T * tau_z, s[idx_x]) + + # Add the chiral symmetry: + elif sym in ('AIII', 'BDI', 'CII'): + if sym != 'CII': + if charge is None: + diag = 2 * np.random.randint(2, size=(n,)) - 1 + elif (0 <= charge <= n) and int(charge) == charge: + diag = np.array(charge * [-1] + (n - charge) * [1]) + else: + raise ValueError('Impossible value of topological invariant.') + else: + if charge is None: + diag = 2 * np.random.randint(2, size=(n // 2,)) - 1 + diag = np.resize(diag, (2, n // 2)).T.flatten() + elif (0 <= charge <= n // 2) and int(charge) == charge: + charge *= 2 + diag = np.array(charge * [-1] + (n - charge) * [1]) + else: + raise ValueError('Impossible value of topological invariant.') + + s = np.dot(diag * s.T.conj(), s) + + return s diff --git a/kwant/contrib/tests/test_rmt.py b/kwant/contrib/tests/test_rmt.py new file mode 100644 index 0000000000000000000000000000000000000000..5bd749357159ff1747cfc5adb072684cd71bbf39 --- /dev/null +++ b/kwant/contrib/tests/test_rmt.py @@ -0,0 +1,90 @@ +# Copyright 2011-2013 kwant authors. +# +# This file is part of kwant. It is subject to the license terms in the +# LICENSE file found in the top-level directory of this distribution and at +# http://kwant-project.org/license. A list of kwant authors can be found in +# the AUTHORS file at the top-level directory of this distribution and at +# http://kwant-project.org/authors. + +import numpy as np +from scipy import stats +from nose.tools import assert_raises +from kwant.contrib import rmt +assert_allclose = np.testing.assert_allclose + + +def test_gaussian(): + np.random.seed(10) + n = 8 + for sym in rmt.sym_list: + if sym not in ('A', 'D', 'AI'): + assert_raises(ValueError, rmt.gaussian, 5, sym) + h = rmt.gaussian(n, sym) + if rmt.t(sym): + t_mat = np.array(rmt.h_t_matrix[sym]) + t_mat = np.kron(np.identity(n / len(t_mat)), t_mat) + assert_allclose(h, np.dot(t_mat, np.dot(h.conj(), t_mat)), + err_msg='TRS broken in ' + sym) + if rmt.p(sym): + p_mat = np.array(rmt.h_p_matrix[sym]) + p_mat = np.kron(np.identity(n / len(p_mat)), p_mat) + assert_allclose(h, -np.dot(p_mat, np.dot(h.conj(), p_mat)), + err_msg='PHS broken in ' + sym) + if rmt.c(sym): + sz = np.kron(np.identity(n / 2), np.diag([1, -1])) + assert_allclose(h, -np.dot(sz, np.dot(h, sz)), + err_msg='SLS broken in ' + sym) + + for sym in rmt.sym_list: + matrices = np.array([rmt.gaussian(n, sym)[-1, 0] for i in range(3000)]) + matrices = matrices.imag if sym in ('D', 'BDI') else matrices.real + ks = stats.kstest(matrices, 'norm') + assert (ks[1] > 0.1), (sym, ks) + + +def test_circular(): + np.random.seed(10) + n = 6 + sy = np.kron(np.identity(n / 2), [[0, 1j], [-1j, 0]]) + for sym in rmt.sym_list: + if rmt.t(sym) == -1 or rmt.p(sym) == -1: + assert_raises(ValueError, rmt.circular, 5, sym) + s = rmt.circular(n, sym) + assert_allclose(np.dot(s, s.T.conj()), np.identity(n), atol=1e-9, + err_msg='Unitarity broken in ' + sym) + if rmt.t(sym): + s1 = np.copy(s.T if rmt.p(sym) != -1 + else np.dot(sy, np.dot(s.T, sy))) + s1 *= rmt.t(sym) * (-1 if rmt.p(sym) == -1 else 1) + assert_allclose(s, s1, atol=1e-9, err_msg='TRS broken in ' + sym) + if rmt.p(sym): + s1 = np.copy(s.conj() if rmt.p(sym) != -1 + else np.dot(sy, np.dot(s.conj(), sy))) + if sym in ('DIII', 'CI'): + s1 *= -1 + assert_allclose(s, s1, atol=1e-9, err_msg='PHS broken in ' + sym) + if rmt.c(sym): + assert_allclose(s, s.T.conj(), atol=1e-9, + err_msg='SLS broken in ' + sym) + + # Check for distribution properties if the ensemble is a symmetric group. + f = lambda x: x[0] / np.linalg.norm(x) + for sym in ('A', 'C'): + sample_distr = np.apply_along_axis(f, 0, np.random.randn(2 * n, 1000)) + s_sample = np.array([rmt.circular(n, sym) + for i in range(1000)]) + assert stats.ks_2samp(sample_distr, s_sample[:, 0, 0].real)[1] > 0.1, \ + 'Noncircular distribution in ' + sym + assert stats.ks_2samp(sample_distr, s_sample[:, 3, 2].real)[1] > 0.1, \ + 'Noncircular distribution in ' + sym + assert stats.ks_2samp(sample_distr, s_sample[:, 1, 1].imag)[1] > 0.1, \ + 'Noncircular distribution in ' + sym + assert stats.ks_2samp(sample_distr, s_sample[:, 2, 3].imag)[1] > 0.1, \ + 'Noncircular distribution in ' + sym + + sample_distr = np.apply_along_axis(f, 0, np.random.randn(n, 500)) + s_sample = np.array([rmt.circular(n, 'D') for i in range(500)]) + ks = stats.ks_2samp(sample_distr, s_sample[:, 0, 0]) + assert ks[1] > 0.1, 'Noncircular distribution in D ' + str(ks) + ks = stats.ks_2samp(sample_distr, s_sample[:, 3, 2]) + assert ks[1] > 0.1, 'Noncircular distribution in D ' + str(ks)