Commit 9ad36a5c authored by Joseph Weston's avatar Joseph Weston

add magnetic gauge fixing for finite systems

Co-authored-by: Pablo Piskunow's avatarPablo Piskunow <pablo.perez.piskunow@gmail.com>
Co-authored-by: Dániel Varjas's avatarDaniel Varjas <dvarjas@gmail.com>
parent 77b2153f
This diff is collapsed.
# Copyright 2011-2013 Kwant authors.
#
# Copyright 2011-2018 Kwant authors.
# This file is part of Kwant. It is subject to the license terms in the file
# LICENSE.rst 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
......@@ -10,7 +9,7 @@
# Merge the public interface of all submodules.
__all__ = []
for module in ['leads', 'dispersion', 'noise', 'symmetry']:
for module in ['leads', 'dispersion', 'noise', 'symmetry', 'gauge']:
exec('from . import {0}'.format(module))
exec('from .{0} import *'.format(module))
exec('__all__.extend({0}.__all__)'.format(module))
This diff is collapsed.
from collections import namedtuple, Counter
from math import sqrt
import numpy as np
import pytest
from ... import lattice
from ...builder import HoppingKind, Builder, NoSymmetry, Site
from .. import gauge
## Utilities
# TODO: remove in favour of 'scipy.stats.special_ortho_group' once
# we depend on scipy 0.18
class special_ortho_group_gen:
def rvs(self, dim):
H = np.eye(dim)
D = np.empty((dim,))
for n in range(dim-1):
x = np.random.normal(size=(dim-n,))
D[n] = np.sign(x[0]) if x[0] != 0 else 1
x[0] += D[n]*np.sqrt((x*x).sum())
# Householder transformation
Hx = (np.eye(dim-n)
- 2.*np.outer(x, x)/(x*x).sum())
mat = np.eye(dim)
mat[n:, n:] = Hx
H = np.dot(H, mat)
D[-1] = (-1)**(dim-1)*D[:-1].prod()
# Equivalent to np.dot(np.diag(D), H) but faster, apparently
H = (D*H.T).T
return H
special_ortho_group = special_ortho_group_gen()
square_lattice = lattice.square(norbs=1, name='square')
honeycomb_lattice = lattice.honeycomb(norbs=1, name='honeycomb')
cubic_lattice = lattice.cubic(norbs=1, name='cubic')
def rectangle(W, L):
return (
lambda s: 0 <= s.pos[0] < L and 0 <= s.pos[1] < W,
(L/2, W/2)
)
def ring(r_inner, r_outer):
return (
lambda s: r_inner <= np.linalg.norm(s.pos) <= r_outer,
((r_inner + r_outer) / 2, 0)
)
def wedge(W):
return (
lambda s: (0 <= s.pos[0] < W) and (0 <= s.pos[1] <= s.pos[0]),
(0, 0)
)
def half_ring(r_inner, r_outer):
in_ring, _ = ring(r_inner, r_outer)
return (
lambda s: s.pos[0] <= 0 and in_ring(s),
(-(r_inner + r_outer) / 2, 0)
)
def cuboid(a, b, c):
return (
lambda s: 0 <= s.pos[0] < a and 0 <= s.pos[1] < b and 0 <= s.pos[2] < c,
(a/2, b/2, c/2)
)
def hypercube(dim, W):
return (
lambda s: all(0 <= x < W for x in s.pos),
(W / 2,) * dim
)
def circle(r):
return (
lambda s: np.linalg.norm(s.pos) < r,
(0, 0)
)
def ball(dim, r):
return (
lambda s: np.linalg.norm(s.pos) < r,
(0,) * dim
)
def model(lat, neighbors):
syst = Builder(lattice.TranslationalSymmetry(*lat.prim_vecs))
if hasattr(lat, 'sublattices'):
for l in lat.sublattices:
zv = (0,) * len(l.prim_vecs)
syst[l(*zv)] = None
else:
zv = (0,) * len(l.prim_vecs)
syst[lat(*zv)] = None
for r in range(neighbors):
syst[lat.neighbors(r + 1)] = None
return syst
def check_loop_kind(loop_kind):
(_, first_fam_a, prev_fam_b), *rest = loop_kind
for (_, fam_a, fam_b) in rest:
if prev_fam_b != fam_a:
raise ValueError('Invalid loop kind: does not close')
prev_fam_b = fam_b
# loop closes
net_delta = np.sum([hk.delta for hk in loop_kind])
if first_fam_a != fam_b or np.any(net_delta != 0):
raise ValueError('Invalid loop kind: does not close')
def available_loops(syst, loop_kind):
def maybe_loop(site):
loop = [site]
a = site
for delta, family_a, family_b in loop_kind:
b = Site(family_b, a.tag + delta, True)
if family_a != a.family or (a, b) not in syst:
return None
loop.append(b)
a = b
return loop
check_loop_kind(loop_kind)
return list(filter(None, map(maybe_loop, syst.sites())))
def loop_to_links(loop):
return list(zip(loop, loop[1:]))
def no_symmetry(lat, neighbors):
return NoSymmetry()
def translational_symmetry(lat, neighbors):
return lattice.TranslationalSymmetry(int((neighbors + 1)/2) * lat.prim_vecs[0])
## Tests
# Tests that phase around a loop is equal to the flux through the loop.
# First we define the loops that we want to test, for various latticeutils.
# If a system does not support a particular kind of loop, they will simply
# not be generated.
Loop = namedtuple('Loop', ('path', 'flux'))
square_loops = [([HoppingKind(d, square_lattice) for d in l.path], l.flux)
for l in [
# 1st nearest neighbors
Loop(path=[(1, 0), (0, 1), (-1, 0), (0, -1)], flux=1),
# 2nd nearest neighbors
Loop(path=[(1, 0), (0, 1), (-1, -1)], flux=0.5),
Loop(path=[(1, 0), (-1, 1), (0, -1)], flux=0.5),
# 3rd nearest neighbors
Loop(path=[(2, 0), (0, 1), (-2, 0), (0, -1)], flux=2),
Loop(path=[(2, 0), (-1, 1), (-1, 0), (0, -1)], flux=1.5),
]]
a, b = honeycomb_lattice.sublattices
honeycomb_loops = [([HoppingKind(d, a, b) for *d, a, b in l.path], l.flux)
for l in [
# 1st nearest neighbors
Loop(path=[(0, 0, a, b), (-1, 1, b, a), (0, -1, a, b), (0, 0, b, a),
(1, -1, a, b), (0, 1, b, a)],
flux=sqrt(3)/2),
# 2nd nearest neighbors
Loop(path=[(-1, 1, a, a), (0, -1, a, a), (1, 0, a, a)],
flux=sqrt(3)/4),
Loop(path=[(-1, 0, b, b), (1, -1, b, b), (0, 1, b, b)],
flux=sqrt(3)/4),
]]
cubic_loops = [([HoppingKind(d, cubic_lattice) for d in l.path], l.flux)
for l in [
# 1st nearest neighbors
Loop(path=[(1, 0, 0), (0, 1, 0), (-1, 0, 0), (0, -1, 0)], flux=1),
Loop(path=[(0, 1, 0), (0, 0, 1), (0, -1, 0), (0, 0, -1)], flux=0),
Loop(path=[(1, 0, 0), (0, 0, 1), (-1, 0, 0), (0, 0, -1)], flux=0),
# 2nd nearest neighbors
Loop(path=[(1, 0, 0), (-1, 1, 0), (0, -1, 0)], flux=0.5),
Loop(path=[(1, 0, 0), (0, 1, 0), (-1, -1, 0)], flux=0.5),
Loop(path=[(1, 0, 0), (-1, 0, 1), (0, 0, -1)], flux=0),
Loop(path=[(1, 0, 0), (0, 0, 1), (-1, 0, -1)], flux=0),
Loop(path=[(0, 1, 0), (0, -1, 1), (0, 0, -1)], flux=0),
Loop(path=[(0, 1, 0), (0, 0, 1), (0, -1, -1)], flux=0),
# 3rd nearest neighbors
Loop(path=[(1, 1, 1), (0, 0, -1), (-1, -1, 0)], flux=0),
Loop(path=[(1, 1, 1), (-1, 0, -1), (0, -1, 0)], flux=0.5),
]]
square = (square_lattice, square_loops)
honeycomb = (honeycomb_lattice, honeycomb_loops)
cubic = (cubic_lattice, cubic_loops)
def _test_phase_loops(syst, phases, loops):
for loop_kind, loop_flux in loops:
for loop in available_loops(syst, loop_kind):
loop_phase = sum(phases(a, b) for a, b in loop_to_links(loop))
assert np.isclose(loop_phase, loop_flux)
@pytest.mark.parametrize("neighbors", [1, 2, 3])
@pytest.mark.parametrize("symmetry", [no_symmetry],
ids=['finite'])
@pytest.mark.parametrize("lattice, loops", [square, honeycomb, cubic],
ids=['square', 'honeycomb', 'cubic'])
def test_phases(lattice, neighbors, symmetry, loops):
"""Check that the phases around common loops are equal to the flux, for
finite and infinite systems with uniform magnetic field.
"""
W = 4
dim = len(lattice.prim_vecs)
field = np.array([0, 0, 1]) if dim == 3 else 1
syst = Builder(symmetry(lattice, neighbors))
syst.fill(model(lattice, neighbors), *hypercube(dim, W))
this_gauge = gauge.magnetic_gauge(syst.finalized())
phases = this_gauge(field)
_test_phase_loops(syst, phases, loops)
# Test internal parts of magnetic_gauge
@pytest.mark.parametrize("shape",
[rectangle(5, 5), circle(4),
half_ring(5, 10)],
ids=['rectangle', 'circle', 'half-ring']
)
@pytest.mark.parametrize("lattice", [square_lattice, honeycomb_lattice],
ids=['square', 'honeycomb'])
@pytest.mark.parametrize("neighbors", [1, 2, 3])
def test_minimal_cycle_basis(lattice, neighbors, shape):
"""Check that for lattice models on genus 0 shapes, nearly
all loops have the same (minimal) length. This is not an
equality, as there may be weird loops on the edges.
"""
syst = Builder()
syst.fill(model(lattice, neighbors), *shape)
syst = syst.finalized()
loops = gauge.loops_in_finite(syst)
loop_counts = Counter(map(len, loops))
min_loop = min(loop_counts)
# arbitrarily allow 1% of slightly longer loops;
# we don't make stronger guarantees about the quality
# of our loop basis
assert loop_counts[min_loop] / len(loops) > 0.99, loop_counts
def random_loop(n, max_radius=10, planar=False):
"""Return a loop of 'n' points.
The loop is in the x-y plane if 'planar is False', otherwise
each point is given a random perturbation in the z direction
"""
theta = np.sort(2 * np.pi * np.random.rand(n))
r = max_radius * np.random.rand(n)
if planar:
z = np.zeros((n,))
else:
z = 2 * (max_radius / 5) * (np.random.rand(n) - 1)
return np.array([r * np.cos(theta), r * np.sin(theta), z]).transpose()
def test_constant_surface_integral():
field_direction = np.random.rand(3)
field_direction /= np.linalg.norm(field_direction)
loop = random_loop(7)
integral = gauge.surface_integral
I = integral(lambda r: field_direction, loop)
assert np.isclose(I, integral(field_direction, loop))
assert np.isclose(I, integral(lambda r: field_direction, loop, average=True))
def circular_field(r_vec):
return np.array([r_vec[1], -r_vec[0], 0])
def test_invariant_surface_integral():
"""Surface integral should be identical if we apply a random
rotation to loop and vector field.
"""
integral = gauge.surface_integral
# loop with random orientation
orig_loop = loop = random_loop(7)
I = integral(circular_field, loop)
for _ in range(4):
rot = special_ortho_group.rvs(3)
loop = orig_loop @ rot.transpose()
assert np.isclose(I, integral(lambda r: rot @ circular_field(rot.transpose() @ r), loop))
......@@ -532,6 +532,8 @@ def main():
dict(sources=['kwant/graph/core.pyx'],
depends=['kwant/graph/core.pxd', 'kwant/graph/defs.h',
'kwant/graph/defs.pxd'])),
('kwant.graph.dijkstra',
dict(sources=['kwant/graph/dijkstra.pyx'])),
('kwant.linalg.lapack',
dict(sources=['kwant/linalg/lapack.pyx'])),
('kwant.linalg._mumps',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment