diff --git a/doc/source/pre/whatsnew/1.4.rst b/doc/source/pre/whatsnew/1.4.rst index 6c8ee05475cbc944cfa8a9cf62aee92ef06c5a37..c3c4168d536eaf1c02dc572a2b03ac46bdab74a4 100644 --- a/doc/source/pre/whatsnew/1.4.rst +++ b/doc/source/pre/whatsnew/1.4.rst @@ -18,16 +18,26 @@ which calculates the Peierls phases for you:: import numpy as np import kwant - def hopping(a, b, t, phi): - return -t * np.exp(-1j * phi(a, b)) + def hopping(a, b, t, peierls): + return -t * peierls(a, b) + + syst = make_system(hopping) + lead = make_lead(hopping) + lead.substitute(peierls='peierls_lead') + syst.attach_lead(lead) + syst = syst.finalized() - syst = make_system(hopping).finalized() gauge = kwant.physics.magnetic_gauge(syst) - def B(pos): - return np.exp(-np.sum(pos * pos)) + def B_syst(pos): + return np.exp(-np.sum(pos * pos)) + + # B_syst in scattering region, 0 in lead. + # Ensure that the fields match at the system/lead interface! + peierls_syst, peierls_lead = gauge(B_syst, 0) - kwant.hamiltonian_submatrix(syst, params=dict(t=1, phi=gauge(B)) + params = dict(t=1, peierls=peierls_syst, peierls_lead=peierls_lead) + kwant.hamiltonian_submatrix(syst, params=params) Note that the API for this functionality is provisional, and may be revised in a future version of Kwant. diff --git a/kwant/physics/gauge.py b/kwant/physics/gauge.py index 097112373cfe18c75d5edd43b23590b714b7e4eb..0a0561f7f565548d9aa61a20e6180d918579794e 100644 --- a/kwant/physics/gauge.py +++ b/kwant/physics/gauge.py @@ -13,11 +13,14 @@ Backwards incompatible changes (up to and including removal of the package) may occur if deemed necessary by the core developers. """ -import functools as ft +import bisect +from functools import partial +from itertools import permutations import numpy as np import scipy from scipy.integrate import dblquad +from scipy.sparse import csgraph from .. import system, builder @@ -28,7 +31,7 @@ __all__ = ['magnetic_gauge'] ### Integation -# Integrate vector field over triangle, for internal use by 'surface_integral' +# Integrate vector field over triangle, for internal use by '_surface_integral' # Triangle is (origin, origin + v1, origin + v2), 'n' is np.cross(v1, v2) def _quad_triangle(f, origin, v1, v2, n, tol): @@ -50,7 +53,7 @@ def _average_triangle(f, origin, v1, v2, n, tol): return np.dot(n, f(origin + 1/3 * (v1 + v2))) / 2 -def surface_integral(f, loop, tol=1e-8, average=False): +def _surface_integral(f, loop, tol=1e-8, average=False): """Calculate the surface integral of 'f' over a surface enclosed by 'loop'. This function only works for *divergence free* vector fields, where the @@ -90,7 +93,7 @@ def surface_integral(f, loop, tol=1e-8, average=False): ### Loop finding graph algorithm -def find_loops(graph, subgraph): +def _find_loops(graph, subgraph): """ Parameters ---------- @@ -114,13 +117,13 @@ def find_loops(graph, subgraph): # matrix rather than convert to LIL and back every iteration. subgraph = subgraph.tocsr() graph = graph.tocsr() - assert same_sparsity_structure(subgraph, graph) + assert _same_sparsity_structure(subgraph, graph) # Links in graph, but not in subgraph. links_to_find = scipy.sparse.triu(graph - subgraph).tocoo() links_to_find = np.vstack((links_to_find.row, links_to_find.col)).transpose() - links_to_find, min_length = order_links(subgraph, links_to_find) + links_to_find, min_length = _order_links(subgraph, links_to_find) # Find shortest path between each link in turn, updating the subgraph with # the links as we go. @@ -137,18 +140,18 @@ def find_loops(graph, subgraph): # The "little bit" is needed so we don't needlessly re-order the links # on amorphous lattices. if path_length > min_length * 1.1: - links_to_find, min_length = order_links(subgraph, links_to_find) + links_to_find, min_length = _order_links(subgraph, links_to_find) else: # Assumes that 'graph' and 'subgraph' have the same sparsity structure. - assign_csr(subgraph, graph, (frm, to)) - assign_csr(subgraph, graph, (to, frm)) + _assign_csr(subgraph, graph, (frm, to)) + _assign_csr(subgraph, graph, (to, frm)) loops.append(path) links_to_find = links_to_find[1:] return loops -def order_links(subgraph, links_to_find): +def _order_links(subgraph, links_to_find): if len(links_to_find) == 0: return [], None # Order 'links_to_find' by length of shortest path between the nodes of the link @@ -162,7 +165,7 @@ def order_links(subgraph, links_to_find): ### Generic sparse matrix utilities -def assign_csr(a, b, element): +def _assign_csr(a, b, element): """Assign a single element from a CSR matrix to another. Parameters @@ -187,14 +190,14 @@ def assign_csr(a, b, element): a.data[j] = b -def same_sparsity_structure(a, b): +def _same_sparsity_structure(a, b): a = a.tocsr().sorted_indices() b = b.tocsr().sorted_indices() return (np.array_equal(a.indices, b.indices) and np.array_equal(a.indptr, b.indptr)) -def add_coo_matrices(*mats, shape): +def _add_coo_matrices(*mats, shape): """Add a sequence of COO matrices by appending their constituent arrays.""" values = np.hstack([mat.data for mat in mats]) rows = np.hstack([mat.row for mat in mats]) @@ -202,14 +205,14 @@ def add_coo_matrices(*mats, shape): return scipy.sparse.coo_matrix((values, (rows, cols)), shape=shape) -def shift_diagonally(mat, shift, shape): +def _shift_diagonally(mat, shift, shape): """Shift the row/column indices of a COO matrix.""" return scipy.sparse.coo_matrix( (mat.data, (mat.row + shift, mat.col + shift)), shape=shape) -def distance_matrix(links, pos, shape): +def _distance_matrix(links, pos, shape): """Return the distances between the provided links as a COO matrix. Parameters @@ -244,7 +247,7 @@ def distance_matrix(links, pos, shape): # link is the one to be determined. -def loops_in_finite(syst): +def _loops_in_finite(syst): """Find the loops in a finite system with no leads. The site indices in the returned loops are those of the system, @@ -254,13 +257,13 @@ def loops_in_finite(syst): nsites = len(syst.sites) # Fix the gauge across the minimum spanning tree of the system graph. - graph = distance_matrix(list(syst.graph), - pos=syst.pos, shape=(nsites, nsites)) - spanning_tree = shortest_distance_forest(graph) - return find_loops(graph, spanning_tree) + graph = _distance_matrix(list(syst.graph), + pos=syst.pos, shape=(nsites, nsites)) + spanning_tree = _shortest_distance_forest(graph) + return _find_loops(graph, spanning_tree) -def shortest_distance_forest(graph): +def _shortest_distance_forest(graph): # Grow a forest of minimum distance trees for all connected components of the graph graph = graph.tocsr() tree = graph.copy() @@ -282,14 +285,482 @@ def shortest_distance_forest(graph): # or it was not reached. if p != -1: unvisited.remove(i) - assign_csr(tree, graph, (i, p)) - assign_csr(tree, graph, (p, i)) + _assign_csr(tree, graph, (i, p)) + _assign_csr(tree, graph, (p, i)) return tree +def _loops_in_infinite(syst): + """Find the loops in an infinite system. + + Returns + ------- + loops : sequence of sequences of integers + The sites in the returned loops belong to two adjacent unit + cells. The first 'syst.cell_size' sites are in the first + unit cell, and the next 'sys.cell_size' are in the next + (in the direction of the translational symmetry). + extended_sites : callable : int -> Site + Given a site index in the extended system consisting of + two unit cells, returns the associated high-level + `kwant.builder.Site`. + """ + assert isinstance(syst, system.InfiniteSystem) + _check_infinite_syst(syst) + + cell_size = syst.cell_size + + unit_cell_links = [(i, j) for i, j in syst.graph + if i < cell_size and j < cell_size] + unit_cell_graph = _distance_matrix(unit_cell_links, + pos=syst.pos, + shape=(cell_size, cell_size)) + + # Loops in the interior of the unit cell + spanning_tree = _shortest_distance_forest(unit_cell_graph) + loops = _find_loops(unit_cell_graph, spanning_tree) + + # Construct an extended graph consisting of 2 unit cells connected + # by the inter-cell links. + extended_shape = (2 * cell_size, 2 * cell_size) + uc1 = _shift_diagonally(unit_cell_graph, 0, shape=extended_shape) + uc2 = _shift_diagonally(unit_cell_graph, cell_size, shape=extended_shape) + hop_links = [(i, j) for i, j in syst.graph if j >= cell_size] + hop = _distance_matrix(hop_links, + pos=syst.pos, + shape=extended_shape) + graph = _add_coo_matrices(uc1, uc2, hop, hop.T, + shape=extended_shape) + + # Construct a subgraph where only the shortest link between the + # 2 unit cells is added. The other links are added with infinite + # values, so that the subgraph has the same sparsity structure + # as 'graph'. + idx = np.argmin(hop.data) + data = np.full_like(hop.data, np.inf) + data[idx] = hop.data[idx] + smallest_edge = scipy.sparse.coo_matrix( + (data, (hop.row, hop.col)), + shape=extended_shape) + subgraph = _add_coo_matrices(uc1, uc2, smallest_edge, smallest_edge.T, + shape=extended_shape) + + # Use these two graphs to find the loops between unit cells. + loops.extend(_find_loops(graph, subgraph)) + + def extended_sites(i): + unit_cell = np.array([i // cell_size]) + site = syst.sites[i % cell_size] + return syst.symmetry.act(-unit_cell, site) + + return loops, extended_sites + + +def _loops_in_composite(syst): + """Find the loops in finite system with leads. + + Parameters + ---------- + syst : kwant.builder.FiniteSystem + + Returns + ------- + loops : sequence of sequences of integers + The sites in each loop belong to the extended scattering region + (see notes). The first and last site in each loop are guaranteed + to be in 'syst'. + which_patch : callable : int -> int + Given a site index in the extended scattering region (see notes), + returns the lead patch (see notes) to which the site belongs. Returns + -1 if the site is part of the reduced scattering region (see notes). + extended_sites : callable : int -> Site + Given a site index in the extended scattering region (see notes), + returns the associated high-level `kwant.builder.Site`. + + Notes + ----- + extended scattering region + The scattering region with a single lead unit cell attached at + each interface. This unit cell is added so that we can "see" any + loops formed with sites in the lead (see 'check_infinite_syst' + for details). The sites for each lead are added in the same + order as the leads, and within a given added unit cell the sites + are ordered in the same way as the associated lead. + lead patch + Sites in the extended scattering region that belong to the added + unit cell for a given lead, or the lead padding for a given lead + are said to be in the "lead patch" for that lead. + reduced scattering region + The sites of the extended scattering region that are not + in a lead patch. + """ + # Check that we can consistently fix the gauge in the scattering region, + # given that we have independently fixed gauges in the leads. + _check_composite_system(syst) + + # Get distance matrix for the extended scattering region, + # a function that maps sites to their lead patches (-1 for sites + # in the reduced scattering region), and a function that maps sites + # to high-level 'kwant.builder.Site' objects. + distance_matrix, which_patch, extended_sites =\ + _extended_scattering_region(syst) + + spanning_tree = _spanning_tree_composite(distance_matrix, which_patch).tocsr() + + # Fill in all links with at least 1 site in a lead patch; + # their gauge is fixed by the lead gauge. + for i, j, v in zip(distance_matrix.row, distance_matrix.col, + distance_matrix.data): + if which_patch(i) > -1 or which_patch(j) > -1: + _assign_csr(spanning_tree, v, (i, j)) + _assign_csr(spanning_tree, v, (j, i)) + + loops = _find_loops(distance_matrix, spanning_tree) + + return loops, which_patch, extended_sites + + +def _extended_scattering_region(syst): + """Return the distance matrix of a finite system with 1 unit cell + added to each lead interface. + + Parameters + ---------- + syst : kwant.builder.FiniteSystem + + Returns + ------- + extended_scattering_region: COO matrix + Distance matrix between connected sites in the extended + scattering region. + which_patch : callable : int -> int + Given a site index in the extended scattering region, returns + the lead patch to which the site belongs. Returns + -1 if the site is part of the reduced scattering region. + extended_sites : callable : int -> Site + Given a site index in the extended scattering region, returns + the associated high-level `kwant.builder.Site`. + + Notes + ----- + Definitions of the terms 'extended scatteringr region', + 'lead patch' and 'reduced scattering region' are given + in the notes for `kwant.physics.gauge._loops_in_composite`. + """ + extended_size = (syst.graph.num_nodes + + sum(l.cell_size for l in syst.leads)) + extended_shape = (extended_size, extended_size) + + added_unit_cells = [] + first_lead_site = syst.graph.num_nodes + for lead, interface in zip(syst.leads, syst.lead_interfaces): + # Here we assume that the distance between sites in the added + # unit cell and sites in the interface is the same as between sites + # in neighboring unit cells. + uc = _distance_matrix(list(lead.graph), + pos=lead.pos, shape=extended_shape) + # Map unit cell lead sites to their indices in the extended scattering, + # region and sites in next unit cell to their interface sites. + hop_from_syst = uc.row >= lead.cell_size + uc.row[~hop_from_syst] = uc.row[~hop_from_syst] + first_lead_site + uc.row[hop_from_syst] = interface[uc.row[hop_from_syst] - lead.cell_size] + # Same for columns + hop_to_syst = uc.col >= lead.cell_size + uc.col[~hop_to_syst] = uc.col[~hop_to_syst] + first_lead_site + uc.col[hop_to_syst] = interface[uc.col[hop_to_syst] - lead.cell_size] + + added_unit_cells.append(uc) + first_lead_site += lead.cell_size + + scattering_region = _distance_matrix(list(syst.graph), + pos=syst.pos, shape=extended_shape) + + extended_scattering_region = _add_coo_matrices(scattering_region, + *added_unit_cells, + shape=extended_shape) + + lead_starts = np.cumsum([syst.graph.num_nodes, + *[lead.cell_size for lead in syst.leads]]) + # Frozenset to quickly check 'is this site in the lead padding?' + extra_sites = [frozenset(sites) for sites in syst.lead_paddings] + + + def which_patch(i): + if i < len(syst.sites): + # In scattering region + for patch_num, sites in enumerate(extra_sites): + if i in sites: + return patch_num + # If not in 'extra_sites' it is in the reduced scattering region. + return -1 + else: + # Otherwise it's in an attached lead cell + which_lead = bisect.bisect(lead_starts, i) - 1 + assert which_lead > -1 + return which_lead + + + # Here we use the fact that all the sites in a lead interface belong + # to the same symmetry domain. + interface_domains = [lead.symmetry.which(syst.sites[interface[0]]) + for lead, interface in + zip(syst.leads, syst.lead_interfaces)] + + def extended_sites(i): + if i < len(syst.sites): + # In scattering region + return syst.sites[i] + else: + # Otherwise it's in an attached lead cell + which_lead = bisect.bisect(lead_starts, i) - 1 + assert which_lead > -1 + lead = syst.leads[which_lead] + domain = interface_domains[which_lead] + 1 + # Map extended scattering region site index to site index in lead. + i = i - lead_starts[which_lead] + return lead.symmetry.act(domain, lead.sites[i]) + + return extended_scattering_region, which_patch, extended_sites + + +def _interior_links(distance_matrix, which_patch): + """Return the indices of the links in 'distance_matrix' that + connect interface sites of the scattering region to other + sites (interface and non-interface) in the scattering region. + """ + + def _is_in_lead(i): + return which_patch(i) > -1 + + # Sites that connect to/from sites in a lead patch + interface_sites = { + (i if not _is_in_lead(i) else j) + for i, j in zip(distance_matrix.row, distance_matrix.col) + if _is_in_lead(i) ^ _is_in_lead(j) + } + + def _we_want(i, j): + return i in interface_sites and not _is_in_lead(j) + + # Links that connect interface sites to the rest of the scattering region. + return np.array([ + k + for k, (i, j) in enumerate(zip(distance_matrix.row, distance_matrix.col)) + if _we_want(i, j) or _we_want(j, i) + ]) + + +def _make_metatree(graph, links_to_delete): + """Make a tree of the components of 'graph' that are + disconnected by deleting 'links'. The values of + the returned tree are indices of edges in 'graph' + that connect components. + """ + # Partition the graph into disconnected components + dl = partial(np.delete, obj=links_to_delete) + partitioned_graph = scipy.sparse.coo_matrix( + (dl(graph.data), (dl(graph.row), dl(graph.col))) + ) + # Construct the "metagraph", where each component is reduced to + # a single node, and a representative (smallest) edge is chosen + # among the edges that connected the components in the original graph. + ncc, labels = csgraph.connected_components(partitioned_graph) + metagraph = scipy.sparse.dok_matrix((ncc, ncc), int) + for k in links_to_delete: + i, j = labels[graph.row[k]], labels[graph.col[k]] + if i == j: + continue # Discard loop edges + # Add a representative (smallest) edge from each graph component. + if graph.data[k] < metagraph.get((i, j), np.inf): + metagraph[i, j] = k + metagraph[j, i] = k + + return csgraph.minimum_spanning_tree(metagraph).astype(int) + + +def _spanning_tree_composite(distance_matrix, which_patch): + """Find a spanning tree for a composite system. + + We cannot use a simple minimum-distance spanning tree because + we have the additional constraint that all links with at least + one end in a lead patch have their gauge fixed. See the notes + for details. + + Parameters + ---------- + distance_matrix : COO matrix + Distance matrix between connected sites in the extended + scattering region. + which_patch : callable : int -> int + Given a site index in the extended scattering region (see notes), + returns the lead patch (see notes) to which the site belongs. Returns + -1 if the site is part of the reduced scattering region (see notes). + Returns + ------- + spanning_tree : CSR matrix + A spanning tree with the same sparsity structure as 'distance_matrix', + where missing links are denoted with infinite weights. + + Notes + ----- + Definitions of the terms 'extended scattering region', 'lead patch' + and 'reduced scattering region' are given in the notes for + `kwant.physics.gauge._loops_in_composite`. + + We cannot use a simple minimum-distance spanning tree because + we have the additional constraint that all links with at least + one end in a lead patch have their gauge fixed. + Consider the following case using a minimum-distance tree + where 'x' are sites in the lead patch:: + + o-o-x o-o-x + | | | --> | | + o-o-x o-o x + + The removed link on the lower right comes from the lead, and hence + is gauge-fixed, however the vertical link in the center is not in + the lead, but *is* in the tree, which means that we will fix its + gauge to 0. The loop on the right would thus not have the correct + gauge on all links. + + Instead we first cut all links between *interface* sites and + sites in the scattering region (including other interface sites). + We then construct a minimum distance forest for these disconnected + graphs. Finally we add back links from the ones that were cut, + ensuring that we do not form any loops; we do this by contructing + a tree of representative links from the "metagraph" of components + that were disconnected by the link cutting. + """ + # Links that connect interface sites to other sites in the + # scattering region (including other interface sites) + links_to_delete = _interior_links(distance_matrix, which_patch) + # Make a shortest distance tree for each of the components + # obtained by cutting the links. + cut_syst = distance_matrix.copy() + cut_syst.data[links_to_delete] = np.inf + forest = _shortest_distance_forest(cut_syst) + # Connect the forest back up with representative links until + # we have a single tree (if the original system was not connected, + # we get a forest). + metatree = _make_metatree(distance_matrix, links_to_delete) + for k in np.unique(metatree.data): + value = distance_matrix.data[k] + i, j = distance_matrix.row[k], distance_matrix.col[k] + _assign_csr(forest, value, (i, j)) + _assign_csr(forest, value, (j, i)) + + return forest + + +def _check_infinite_syst(syst): + r"""Check that the unit cell is a connected graph. + + If the unit cell is not connected then we cannot be sure whether + there are loops or not just by inspecting the unit cell graph + (this may be a solved problem, but we could not find an algorithm + to do this). + + To illustrate this, consider the following unit cell consisting + of 3 sites and 4 hoppings:: + + o- + \ + o + \ + o- + + None of the sites are connected within the unit cell, however if we repeat + a few unit cells:: + + o-o-o-o + \ \ \ + o o o o + \ \ \ + o-o-o-o + + we see that there is a loop crossing 4 unit cells. A connected unit cell + is a sufficient condition that all the loops can be found by inspecting + the graph consisting of two unit cells glued together. + """ + assert isinstance(syst, system.InfiniteSystem) + n = syst.cell_size + rows, cols = np.array([(i, j) for i, j in syst.graph + if i < n and j < n]).transpose() + data = np.ones(len(rows)) + graph = scipy.sparse.coo_matrix((data, (rows, cols)), shape=(n, n)) + if csgraph.connected_components(graph, return_labels=False) > 1: + raise ValueError( + 'Infinite system unit cell is not connected: we cannot determine ' + 'if there are any loops in the system\n\n' + 'If there are, then you must define your unit cell so that it is ' + 'connected. If there are not, then you must add zero-magnitude ' + 'hoppings to your system.' + ) + + +def _check_composite_system(syst): + """Check that we can consistently fix the gauge in a system with leads. + + If not, raise an exception with an informative error message. + """ + assert isinstance(syst, system.FiniteSystem) and syst.leads + # Frozenset to quickly check 'is this site in the lead padding?' + extras = [frozenset(sites) for sites in syst.lead_paddings] + interfaces = [set(iface) for iface in syst.lead_interfaces] + # Make interfaces between lead patches and the reduced scattering region. + for interface, extra in zip(interfaces, extras): + extra_interface = set() + if extra: + extra_interface = set() + for i, j in syst.graph: + if i in extra and j not in extra: + extra_interface.add(j) + interface -= extra + interface |= extra_interface + assert not extra.intersection(interface) + + pre_msg = ( + 'Attaching leads results in gauge-fixed loops in the extended ' + 'scattering region (scattering region plus one lead unit cell ' + 'from every lead). This does not allow consistent gauge-fixing.\n\n' + ) + solution_msg = ( + 'To avoid this error, attach leads further away from each other.\n\n' + 'Note: calling `attach_lead()` with `add_cells > 0` will not fix ' + 'this problem, as the added sites inherit the gauge from the lead. ' + 'To extend the scattering region, you must manually add sites ' + 'making sure that they use the scattering region gauge.' + ) + + # Check that there is at most one overlapping site between + # reduced interface of one lead and extra sites of another + num_leads = len(syst.leads) + metagraph = scipy.sparse.lil_matrix((num_leads, num_leads)) + for i, j in permutations(range(num_leads), 2): + intersection = len(interfaces[i] & (interfaces[j] | extras[j])) + if intersection > 1: + raise ValueError( + pre_msg + + ('There is at least one gauge-fixed loop in the overlap ' + 'of leads {} and {}.\n\n'.format(i, j)) + + solution_msg + ) + elif intersection == 1: + metagraph[i, j] = 1 + # Check that there is no loop formed by gauge-fixed bonds of multiple leads. + num_components = scipy.sparse.csgraph.connected_components(metagraph, return_labels=False) + if metagraph.nnz // 2 + num_components != num_leads: + raise ValueError( + pre_msg + + ('There is at least one gauge-fixed loop formed by more than 2 leads. ' + ' The connectivity matrix of the leads is:\n\n' + '{}\n\n'.format(metagraph.A)) + + solution_msg + ) + ### Phase calculation -def calculate_phases(loops, pos, previous_phase, flux): +def _calculate_phases(loops, pos, previous_phase, flux): """Calculate the phase across the terminal links of a set of loops Parameters @@ -302,8 +773,8 @@ def calculate_phases(loops, pos, previous_phase, flux): A map from site (integer) to realspace position. previous_phase : callable Takes a dict that maps from links to phases, and a loop and returns - the sum of the phases across each link in the loop, *except* the link - between the first and last site in the loop. + the product of the phases across each link in the loop, + *except* the link between the first and last site in the loop. flux : callable Takes a sequence of positions and returns the magnetic flux through the surface defined by the provided loop. @@ -317,21 +788,58 @@ def calculate_phases(loops, pos, previous_phase, flux): for loop in loops: tail, head = loop[-1], loop[0] integral = flux([pos(p) for p in loop]) - phases[tail, head] = integral - previous_phase(phases, loop) + phase = np.exp(1j * np.pi * integral) + phases[tail, head] = phase / previous_phase(phases, loop) return phases -# These functions are to be used with 'calculate_phases'. +# These functions are to be used with '_calculate_phases'. # 'phases' always stores *either* the phase across (i, j) *or* # (j, i), and never both. If a phase is not present it is assumed to # be zero. def _previous_phase_finite(phases, path): - previous_phase = 0 + previous_phase = 1 + for i, j in zip(path, path[1:]): + previous_phase *= phases.get((i, j), 1) + previous_phase /= phases.get((j, i), 1) + return previous_phase + + +def _previous_phase_infinite(cell_size, phases, path): + previous_phase = 1 + for i, j in zip(path, path[1:]): + # i and j are only in the fundamental unit cell (0 <= i < cell_size) + # or the next one (cell_size <= i < 2 * cell_size). + if i >= cell_size and j >= cell_size: + assert i // cell_size == j // cell_size + i = i % cell_size + j = j % cell_size + previous_phase *= phases.get((i, j), 1) + previous_phase /= phases.get((j, i), 1) + return previous_phase + + +def _previous_phase_composite(which_patch, extended_sites, lead_phases, + phases, path): + previous_phase = 1 for i, j in zip(path, path[1:]): - previous_phase += phases.get((i, j), 0) - previous_phase -= phases.get((j, i), 0) + patch_i = which_patch(i) + patch_j = which_patch(j) + if patch_i == -1 and patch_j == -1: + # Both sites in reduced scattering region. + previous_phase *= phases.get((i, j), 1) + previous_phase /= phases.get((j, i), 1) + else: + # At least one site in a lead patch; use the phase from the + # associated lead. Check that if both are in a patch, they + # are in the same patch. + assert patch_i * patch_j <= 0 or patch_i == patch_j + patch = max(patch_i, patch_j) + a, b = extended_sites(i), extended_sites(j) + previous_phase *= lead_phases[patch](a, b) + return previous_phase @@ -344,32 +852,91 @@ def _finite_wrapper(syst, phases, a, b): i = syst.id_by_site[a] j = syst.id_by_site[b] # We only store *either* (i, j) *or* (j, i). If not present - # then the phase is zero by definition. - return phases.get((i, j), -phases.get((j, i), 0)) - - -def _gauge_finite(syst): - loops = loops_in_finite(syst) - - def _gauge(syst_field, tol=1E-8, average=False): - integrate = ft.partial(surface_integral, syst_field, - tol=tol, average=average) - phases = calculate_phases( - loops, - syst.pos, - _previous_phase_finite, - integrate, - ) - return ft.partial(_finite_wrapper, syst, phases) - - return _gauge + # then the phase is unity by definition. + if (i, j) in phases: + return phases[i, j] + elif (j, i) in phases: + return phases[j, i].conjugate() + else: + return 1 -def magnetic_gauge(syst): +def _infinite_wrapper(syst, phases, a, b): + sym = syst.symmetry + # Bring link to fundamental domain consistently with how + # we store the phases. + t = max(sym.which(a), sym.which(b)) + a, b = sym.act(-t, a, b) + i = syst.id_by_site[a] + j = syst.id_by_site[b] + # We only store *either* (i, j) *or* (j, i). If not present + # then the phase is unity by definition. + if (i, j) in phases: + return phases[i, j] + elif (j, i) in phases: + return phases[j, i].conjugate() + else: + return 1 + + +def _peierls_finite(syst, loops, syst_field, tol, average): + integrate = partial(_surface_integral, syst_field, + tol=tol, average=average) + phases = _calculate_phases( + loops, + syst.pos, + _previous_phase_finite, + integrate, + ) + return partial(_finite_wrapper, syst, phases) + + +def _peierls_infinite(syst, loops, extended_sites, syst_field, tol, average): + integrate = partial(_surface_integral, syst_field, + tol=tol, average=average) + phases = _calculate_phases( + loops, + lambda i: extended_sites(i).pos, + partial(_previous_phase_infinite, syst.cell_size), + integrate, + ) + return partial(_infinite_wrapper, syst, phases) + + +def _peierls_composite(syst, loops, which_patch, extended_sites, lead_gauges, + syst_field, *lead_fields, tol, average): + if len(lead_fields) != len(syst.leads): + raise ValueError('Magnetic fields must be provided for all leads.') + + lead_phases = [gauge(B, tol=tol, average=average) + for gauge, B in zip(lead_gauges, lead_fields)] + + flux = partial(_surface_integral, syst_field, tol=tol, average=average) + + # NOTE: uses the scattering region magnetic field to set the phase + # of the inteface hoppings this choice is somewhat arbitrary, + # but it is consistent with the position defined in the scattering + # region coordinate system. the integrate functions for the leads + # may be defined far from the interface. + phases = _calculate_phases( + loops, + lambda i: extended_sites(i).pos, + partial(_previous_phase_composite, + which_patch, extended_sites, lead_phases), + flux, + ) + + return (partial(_finite_wrapper, syst, phases), *lead_phases) + + +# This class is essentially a closure, but documenting closures is a pain. +# To emphasise the lack of manipulatable or inspectable state, we name the +# class as we would for a function. + +class magnetic_gauge: """Fix the magnetic gauge for a finalized system. - Fix the magnetic gauge for a Kwant system. This can - be used to later calculate the Peierls phases that + This can be used to later calculate the Peierls phases that should be applied to each hopping, given a magnetic field. This API is currently provisional. Refer to the documentation @@ -377,41 +944,90 @@ def magnetic_gauge(syst): Parameters ---------- - syst : kwant.builder.FiniteSystem - May not have leads attached (this restriction will - be lifted in the future). - - Returns - ------- - gauge : callable - When called with a magnetic field as argument, returns - another callable 'phase' that returns the Peierls phase to - apply to a given hopping. + syst : kwant.builder.FiniteSystem or kwant.builder.InfiniteSystem Examples -------- - The following illustrates basic usage: + The following illustrates basic usage for a scattering region with + a single lead attached: >>> import numpy as np >>> import kwant >>> - >>> def hopping(a, b, t, phi): - >>> return -t * np.exp(-1j * phi(a, b)) + >>> def hopping(a, b, t, peierls): + >>> return -t * peierls(a, b) + >>> + >>> syst = make_system(hopping) + >>> lead = make_lead(hopping) + >>> lead.substitute(peierls='peierls_lead') + >>> syst.attach_lead(lead) + >>> syst = syst.finalized() >>> - >>> syst = make_system(hopping).finalized() >>> gauge = kwant.physics.magnetic_gauge(syst) >>> - >>> def B(pos): + >>> def B_syst(pos): >>> return np.exp(-np.sum(pos * pos)) >>> - >>> kwant.hamiltonian_submatrix(syst, params=dict(t=1, phi=gauge(B)) + >>> peierls_syst, peierls_lead = gauge(B_syst, 0) + >>> + >>> params = dict(t=1, peierls=peierls_syst, peierls_lead=peierls_lead) + >>> kwant.hamiltonian_submatrix(syst, params=params) """ - if isinstance(syst, builder.FiniteSystem): - if syst.leads: - raise ValueError('Can only fix magnetic gauge for finite systems ' - 'without leads') + + def __init__(self, syst): + if isinstance(syst, builder.FiniteSystem): + if syst.leads: + loops, which_patch, extended_sites = _loops_in_composite(syst) + lead_gauges = [magnetic_gauge(lead) for lead in syst.leads] + self._peierls = partial(_peierls_composite, syst, + loops, which_patch, + extended_sites, lead_gauges) + else: + loops = _loops_in_finite(syst) + self._peierls = partial(_peierls_finite, syst, loops) + elif isinstance(syst, builder.InfiniteSystem): + loops, extended_sites = _loops_in_infinite(syst) + self._peierls = partial(_peierls_infinite, syst, + loops, extended_sites) else: - return _gauge_finite(syst) - else: - raise TypeError('Can only fix magnetic gauge for finite systems ' - 'without leads') + raise TypeError('Expected a finalized Builder') + + def __call__(self, syst_field, *lead_fields, tol=1E-8, average=False): + """Return the Peierls phase for a magnetic field configuration. + + Parameters + ---------- + syst_field : scalar, vector or callable + The magnetic field to apply to the scattering region. + If callable, takes a position and returns the + magnetic field at that position. Can be a scalar if + the system is 1D or 2D, otherwise must be a vector. + Magnetic field is expressed in units :math:`φ₀ / l²`, + where :math:`φ₀` is the magnetic flux quantum and + :math:`l` is the unit of length. + *lead_fields : scalar, vector or callable + The magnetic fields to apply to each of the leads, in + the same format as 'syst_field'. In addition, if a callable + is provided, then the magnetic field must have the symmetry + of the associated lead. + tol : float, default: 1E-8 + The tolerance to which to calculate the flux through each + hopping loop in the system. + average : bool, default: False + If True, estimate the magnetic flux through each hopping loop + in the system by evaluating the magnetic field at a single + position inside the loop and multiplying it by the area of the + loop. If False, then `~scipy.integrate.quad` is used to integrate + the magnetic field. This parameter is only used when 'syst_field' + or 'lead_fields' are callable. + + Returns + ------- + phases : callable, or sequence of callables + The first callable computes the Peierls phase in the scattering + region and the remaining callables compute the Peierls phases + in each of the leads. Each callable takes a pair of + `~kwant.builder.Site`s 'a, b' and returns a unit complex + number (Peierls phase) that multiplies that hopping. + """ + return self._peierls(syst_field, *lead_fields, tol=tol, average=False) diff --git a/kwant/physics/tests/test_gauge.py b/kwant/physics/tests/test_gauge.py index 762c4a0c3010538201cf32344c24bb829d3526f4..ae79c6f4110b2130faa7abe714948eaee143a3a6 100644 --- a/kwant/physics/tests/test_gauge.py +++ b/kwant/physics/tests/test_gauge.py @@ -1,12 +1,15 @@ from collections import namedtuple, Counter +import warnings from math import sqrt import numpy as np import pytest +import kwant 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 @@ -151,7 +154,7 @@ def translational_symmetry(lat, neighbors): ## 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. +# First we define the loops that we want to test, for various lattices. # If a system does not support a particular kind of loop, they will simply # not be generated. @@ -209,13 +212,14 @@ 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) + loop_phase = np.prod([phases(a, b) for a, b in loop_to_links(loop)]) + expected_loop_phase = np.exp(1j * np.pi * loop_flux) + assert np.isclose(loop_phase, expected_loop_phase) @pytest.mark.parametrize("neighbors", [1, 2, 3]) -@pytest.mark.parametrize("symmetry", [no_symmetry], - ids=['finite']) +@pytest.mark.parametrize("symmetry", [no_symmetry, translational_symmetry], + ids=['finite', 'infinite']) @pytest.mark.parametrize("lattice, loops", [square, honeycomb, cubic], ids=['square', 'honeycomb', 'cubic']) def test_phases(lattice, neighbors, symmetry, loops): @@ -235,6 +239,176 @@ def test_phases(lattice, neighbors, symmetry, loops): _test_phase_loops(syst, phases, loops) +@pytest.mark.parametrize("neighbors", [1, 2, 3]) +@pytest.mark.parametrize("lat, loops", [square, honeycomb], + ids=['square', 'honeycomb']) +def test_phases_composite(neighbors, lat, loops): + """Check that the phases around common loops are equal to the flux, for + composite systems with uniform magnetic field. + """ + W = 4 + dim = len(lat.prim_vecs) + field = np.array([0, 0, 1]) if dim == 3 else 1 + + lead = Builder(lattice.TranslationalSymmetry(-lat.prim_vecs[0])) + lead.fill(model(lat, neighbors), *hypercube(dim, W)) + + # Case where extra sites are added and fields are same in + # scattering region and lead. + syst = Builder() + syst.fill(model(lat, neighbors), *ball(dim, W + 1)) + extra_sites = syst.attach_lead(lead) + assert extra_sites # make sure we're testing the edge case with added sites + + this_gauge = gauge.magnetic_gauge(syst.finalized()) + # same field in scattering region and lead + phases, lead_phases = this_gauge(field, field) + + # When extra sites are added to the central region we need to select + # the correct phase function. + def combined_phases(a, b): + if a in extra_sites or b in extra_sites: + return lead_phases(a, b) + else: + return phases(a, b) + + _test_phase_loops(syst, combined_phases, loops) + _test_phase_loops(lead, lead_phases, loops) + + +@pytest.mark.parametrize("neighbors", [1, 2]) +def test_overlapping_interfaces(neighbors): + """Test composite systems with overlapping lead interfaces.""" + + lat = square_lattice + + def _make_syst(edge, W=5): + + syst = Builder() + syst.fill(model(lat, neighbors), *rectangle(W, W)) + + leadx = Builder(lattice.TranslationalSymmetry((-1, 0))) + leadx[(lat(0, j) for j in range(edge, W - edge))] = None + for n in range(1, neighbors + 1): + leadx[lat.neighbors(n)] = None + + leady = Builder(lattice.TranslationalSymmetry((0, -1))) + leady[(lat(i, 0) for i in range(edge, W - edge))] = None + for n in range(1, neighbors + 1): + leady[lat.neighbors(n)] = None + + assert not syst.attach_lead(leadx) # sanity check; no sites added + assert not syst.attach_lead(leady) # sanity check; no sites added + + return syst, leadx, leady + + # edge == 0: lead interfaces overlap + # edge == 1: lead interfaces partition scattering region + # into 2 disconnected components + for edge in (0, 1): + syst, leadx, leady = _make_syst(edge) + this_gauge = gauge.magnetic_gauge(syst.finalized()) + phases, leadx_phases, leady_phases = this_gauge(1, 1, 1) + _test_phase_loops(syst, phases, square_loops) + _test_phase_loops(leadx, leadx_phases, square_loops) + _test_phase_loops(leady, leady_phases, square_loops) + + +def _make_square_syst(sym, neighbors=1): + lat = square_lattice + syst = Builder(sym) + syst[(lat(i, j) for i in (0, 1) for j in (0, 1))] = None + for n in range(1, neighbors + 1): + syst[lat.neighbors(n)] = None + return syst + + +def test_unfixable_gauge(): + """Check error is raised when we cannot fix the gauge.""" + + leadx = _make_square_syst(lattice.TranslationalSymmetry((-1, 0))) + leady = _make_square_syst(lattice.TranslationalSymmetry((0, -1))) + + # 1x2 line with 2 leads + syst = _make_square_syst(NoSymmetry()) + del syst[[square_lattice(1, 0), square_lattice(1, 1)]] + syst.attach_lead(leadx) + syst.attach_lead(leadx.reversed()) + with pytest.raises(ValueError): + gauge.magnetic_gauge(syst.finalized()) + + # 2x2 square with leads attached from all 4 sides, + # and nearest neighbor hoppings + syst = _make_square_syst(NoSymmetry()) + # Until we add the last lead we have enough gauge freedom + # to specify independent fields in the scattering region + # and each of the leads. We check that no extra sites are + # added as a sanity check. + assert not syst.attach_lead(leadx) + gauge.magnetic_gauge(syst.finalized()) + assert not syst.attach_lead(leady) + gauge.magnetic_gauge(syst.finalized()) + assert not syst.attach_lead(leadx.reversed()) + gauge.magnetic_gauge(syst.finalized()) + # Adding the last lead removes our gauge freedom. + assert not syst.attach_lead(leady.reversed()) + with pytest.raises(ValueError): + gauge.magnetic_gauge(syst.finalized()) + + # 2x2 square with 2 leads, but 4rd nearest neighbor hoppings + syst = _make_square_syst(NoSymmetry()) + del syst[(square_lattice(1, 0), square_lattice(1, 1))] + leadx = _make_square_syst(lattice.TranslationalSymmetry((-1, 0))) + leadx[square_lattice.neighbors(4)] = None + for lead in (leadx, leadx.reversed()): + syst.attach_lead(lead) + + with pytest.raises(ValueError): + gauge.magnetic_gauge(syst.finalized()) + + +def _test_disconnected(syst): + with pytest.raises(ValueError) as excinfo: + gauge.magnetic_gauge(syst.finalized()) + assert 'unit cell not connected' in str(excinfo.value) + +def test_invalid_lead(): + """Check error is raised when a lead unit cell is not connected + within the unit cell itself. + + In order for the algorithm to work we need to be able to close + loops within the lead. However we only add a single lead unit + cell, so not all paths can be closed, even if the lead is + connected. + """ + lat = square_lattice + + lead = _make_square_syst(lattice.TranslationalSymmetry((-1, 0)), + neighbors=0) + # Truly disconnected system + # Ignore warnings to suppress Kwant's complaint about disconnected lead + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + _test_disconnected(lead) + + # 2 disconnected chains (diagonal) + lead[(lat(0, 0), lat(1, 1))] = None + lead[(lat(0, 1), lat(1, 0))] = None + _test_disconnected(lead) + + lead = _make_square_syst(lattice.TranslationalSymmetry((-1, 0)), + neighbors=0) + # 2 disconnected chains (horizontal) + lead[(lat(0, 0), lat(1, 0))] = None + lead[(lat(0, 1), lat(1, 1))] = None + _test_disconnected(lead) + + # System has loops, but need 3 unit cells + # to express them. + lead[(lat(0, 0), lat(1, 1))] = None + lead[(lat(0, 1), lat(1, 0))] = None + _test_disconnected(lead) + # Test internal parts of magnetic_gauge @pytest.mark.parametrize("shape", @@ -254,7 +428,7 @@ def test_minimal_cycle_basis(lattice, neighbors, shape): syst.fill(model(lattice, neighbors), *shape) syst = syst.finalized() - loops = gauge.loops_in_finite(syst) + loops = gauge._loops_in_finite(syst) loop_counts = Counter(map(len, loops)) min_loop = min(loop_counts) # arbitrarily allow 1% of slightly longer loops; @@ -283,7 +457,7 @@ def test_constant_surface_integral(): field_direction /= np.linalg.norm(field_direction) loop = random_loop(7) - integral = gauge.surface_integral + integral = gauge._surface_integral I = integral(lambda r: field_direction, loop) assert np.isclose(I, integral(field_direction, loop)) @@ -298,7 +472,7 @@ 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 + integral = gauge._surface_integral # loop with random orientation orig_loop = loop = random_loop(7) I = integral(circular_field, loop) @@ -306,3 +480,71 @@ def test_invariant_surface_integral(): 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)) + + +@pytest.fixture +def system_and_gauge(): + def hopping(a, b, peierls): + return -1 * peierls(a, b) + + syst = Builder() + syst[(square_lattice(i, j) for i in range(3) for j in range(10))] = 4 + syst[square_lattice.neighbors()] = hopping + + lead = Builder(lattice.TranslationalSymmetry((-1, 0))) + lead[(square_lattice(0, j) for j in range(10))] = 4 + lead[square_lattice.neighbors()] = hopping + + syst.attach_lead(lead.substituted(peierls='peierls_left')) + syst.attach_lead(lead.reversed().substituted(peierls='peierls_right')) + + syst = syst.finalized() + + magnetic_gauge = gauge.magnetic_gauge(syst) + + return syst, magnetic_gauge + + +@pytest.mark.parametrize('B',[0, 0.1, lambda r: 0.1 * np.exp(-r[1]**2)]) +def test_uniform_magnetic_field(system_and_gauge, B): + syst, gauge = system_and_gauge + + peierls, peierls_left, peierls_right = gauge(B, B, B) + + params = dict(peierls=peierls, peierls_left=peierls_left, + peierls_right=peierls_right) + + s = kwant.smatrix(syst, energy=0.6, params=params) + t = s.submatrix(1, 0) + + b = kwant.physics.Bands(syst.leads[0], params=params) + print(b(0)) + + assert t.shape > (0, 0) # sanity check + assert np.allclose(np.abs(t)**2, np.eye(*t.shape)) + + +def test_phase_sign(system_and_gauge): + syst, gauge = system_and_gauge + + peierls, peierls_left, peierls_right = gauge(0.1, 0.1, 0.1) + + params = dict(peierls=peierls, peierls_left=peierls_left, + peierls_right=peierls_right) + + cut = [(square_lattice(1, j), square_lattice(0, j)) + for j in range(10)] + J = kwant.operator.Current(syst, where=cut) + J = J.bind(params=params) + + psi = kwant.wave_function(syst, energy=0.6, params=params)(0)[0] + + # Electrons incident from the left travel along the *top* + # edge of the Hall bar in the presence of a magnetic field + # out of the plane + j = J(psi) + j_bottom = sum(j[0:5]) + j_top = sum(j[5:10]) + + assert np.isclose(j_top + j_bottom, 1) # sanity check + assert j_top > 0.9