Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright 2011-2019 Kwant authors.
2#
3# This file is part of Kwant. It is subject to the license terms in the file
4# LICENSE.rst found in the top-level directory of this distribution and at
5# https://kwant-project.org/license. A list of Kwant authors can be found in
6# the file AUTHORS.rst at the top-level directory of this distribution and at
7# https://kwant-project.org/authors.
9from keyword import iskeyword
10import functools
11import operator
12import collections
14from math import sqrt
15import tinyarray as ta
16import numpy as np
17import sympy
19import kwant.lattice
20import kwant.builder
21import kwant.system
23import kwant.continuum
24import kwant.continuum._common
25import kwant.continuum.discretizer
26from kwant.continuum import momentum_operators, position_operators
28__all__ = ["to_landau_basis", "discretize_landau"]
30coordinate_vectors = dict(zip("xyz", np.eye(3)))
32ladder_lower, ladder_raise = sympy.symbols(r"a a^\dagger", commutative=False)
35def to_landau_basis(hamiltonian, momenta=None):
36 r"""Replace two momenta by Landau level ladder operators.
38 Replaces:
40 k_0 -> sqrt(B/2) * (a + a^\dagger)
41 k_1 -> 1j * sqrt(B/2) * (a - a^\dagger)
43 Parameters
44 ----------
45 hamiltonian : str or sympy expression
46 The Hamiltonian to which to apply the Landau level transformation.
47 momenta : sequence of str (optional)
48 The momenta to replace with Landau level ladder operators. If not
49 provided, 'k_x' and 'k_y' are used
51 Returns
52 -------
53 hamiltonian : sympy expression
54 momenta : sequence of sympy atoms
55 The momentum operators that have been replaced by ladder operators.
56 normal_coordinate : sympy atom
57 The remaining position coordinate. May or may not be present
58 in 'hamiltonian'.
59 """
60 hamiltonian = kwant.continuum.sympify(hamiltonian)
61 momenta = _normalize_momenta(momenta)
62 normal_coordinate = _find_normal_coordinate(hamiltonian, momenta)
64 # Substitute ladder operators for Landau level momenta
65 B = sympy.symbols("B")
66 hamiltonian = hamiltonian.subs(
67 {
68 momenta[0]: sympy.sqrt(abs(B) / 2) * (ladder_raise + ladder_lower),
69 momenta[1]: sympy.I
70 * sympy.sqrt(abs(B) / 2)
71 * (ladder_lower - ladder_raise),
72 }
73 )
75 return hamiltonian, momenta, normal_coordinate
78def discretize_landau(hamiltonian, N, momenta=None, grid_spacing=1):
79 """Discretize a Hamiltonian in a basis of Landau levels.
81 Parameters
82 ----------
83 hamiltonian : str or sympy expression
84 N : positive integer
85 The number of Landau levels in the basis.
86 momenta : sequence of str (optional)
87 The momenta defining the plane perpendicular to the magnetic field.
88 If not provided, "k_x" and "k_y" are used.
89 grid_spacing : float, default: 1
90 The grid spacing to use when discretizing the normal direction
91 (parallel to the magnetic field).
93 Returns
94 -------
95 builder : `~kwant.builder.Builder`
96 'hamiltonian' discretized in a basis of Landau levels in the plane
97 defined by 'momenta'. If a third coordinate is present in 'hamiltonian',
98 then this also has a translational symmetry in that coordinate direction.
99 The builder has a parameter 'B' in addition to any other parameters
100 present in the provided 'hamiltonian'.
102 Notes
103 -----
104 The units of magnetic field are :math:`ϕ₀ / 2 π a²` with :math:`ϕ₀ = h/e`
105 the magnetic flux quantum and :math:`a` the unit length.
106 """
108 if N <= 0:
109 raise ValueError("N must be positive")
111 hamiltonian, momenta, normal_coordinate = to_landau_basis(hamiltonian, momenta)
113 # Discretize in normal direction and split terms for onsites/hoppings into
114 # monomials in ladder operators.
115 tb_hamiltonian, _ = kwant.continuum.discretize_symbolic(
116 hamiltonian, coords=[normal_coordinate.name]
117 )
118 tb_hamiltonian = {
119 key: kwant.continuum._common.monomials(value, gens=(ladder_lower, ladder_raise))
120 for key, value in tb_hamiltonian.items()
121 }
123 # Replace ladder operator monomials by tuple of integers:
124 # e.g. a^\dagger a^2 -> (+1, -2).
125 tb_hamiltonian = {
126 outer_key: {
127 _ladder_term(inner_key): inner_value
128 for inner_key, inner_value in outer_value.items()
129 }
130 for outer_key, outer_value in tb_hamiltonian.items()
131 }
133 # Construct map from LandauLattice HoppingKinds to a sequence of pairs
134 # that encode the ladder operators and multiplying expression.
135 tb_hoppings = collections.defaultdict(list)
136 for outer_key, outer_value in tb_hamiltonian.items():
137 for inner_key, inner_value in outer_value.items():
138 tb_hoppings[(*outer_key, sum(inner_key))] += [(inner_key, inner_value)]
139 # Extract the number of orbitals on each site/hopping
140 random_element = next(iter(tb_hoppings.values()))[0][1]
141 norbs = 1 if isinstance(random_element, sympy.Expr) else random_element.shape[0]
142 tb_onsite = tb_hoppings.pop((0, 0), None)
144 # Construct Builder
145 if _has_coordinate(normal_coordinate, hamiltonian):
146 sym = kwant.lattice.TranslationalSymmetry([grid_spacing, 0])
147 else:
148 sym = kwant.system.NoSymmetry()
149 lat = LandauLattice(grid_spacing, norbs=norbs)
150 syst = kwant.Builder(sym)
152 # Add onsites
153 landau_sites = (lat(0, j) for j in range(N))
154 if tb_onsite is None:
155 syst[landau_sites] = ta.zeros((norbs, norbs))
156 else:
157 syst[landau_sites] = _builder_value(
158 tb_onsite, normal_coordinate.name, grid_spacing, is_onsite=True
159 )
161 # Add zero hoppings between adjacent Landau levels.
162 # Necessary to be able to use the Landau level builder
163 # to populate another builder using builder.fill().
164 syst[kwant.builder.HoppingKind((0, 1), lat)] = ta.zeros((norbs, norbs))
166 # Add the hoppings from the Hamiltonian
167 for hopping, parts in tb_hoppings.items():
168 syst[kwant.builder.HoppingKind(hopping, lat)] = _builder_value(
169 parts, normal_coordinate.name, grid_spacing, is_onsite=False
170 )
172 return syst
175# This has to subclass lattice so that it will work with TranslationalSymmetry.
176class LandauLattice(kwant.lattice.Monatomic):
177 """
178 A `~kwant.lattice.Monatomic` lattice with a Landau level index per site.
180 Site tags (see `~kwant.system.SiteFamily`) are pairs of integers, where
181 the first integer describes the real space position and the second the
182 Landau level index.
184 The real space Bravais lattice is one dimensional, oriented parallel
185 to the magnetic field. The Landau level index represents the harmonic
186 oscillator basis states used for the Landau quantization in the plane
187 normal to the field.
189 Parameters
190 ----------
191 grid_spacing : float
192 Real space lattice spacing (parallel to the magnetic field).
193 offset : float (optional)
194 Displacement of the lattice origin from the real space
195 coordinates origin.
196 """
198 def __init__(self, grid_spacing, offset=None, name="", norbs=None):
199 if offset is not None: 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true
200 offset = [offset, 0]
201 # The second vector and second coordinate do not matter (they are
202 # not used in pos())
203 super().__init__([[grid_spacing, 0], [0, 1]], offset, name, norbs)
205 def pos(self, tag):
206 return ta.array((self.prim_vecs[0, 0] * tag[0] + self.offset[0],))
208 def landau_index(self, tag):
209 return tag[-1]
212def _builder_value(terms, normal_coordinate, grid_spacing, is_onsite):
213 """Construct an onsite/hopping value function from a list of terms
215 Parameters
216 ----------
217 terms : list
218 Each element is a pair (ladder_term, sympy expression/matrix).
219 ladder_term is a tuple of integers that encodes a string of
220 Landau raising/lowering operators and the sympy expression
221 is the rest
222 normal_coordinate : str
223 grid_spacing : float
224 The grid spacing in the normal direction
225 is_onsite : bool
226 True if we are constructing an onsite value function
227 """
228 ladder_term_symbols = [sympy.Symbol(_ladder_term_name(lt)) for lt, _ in terms]
229 # Construct a single expression from the terms, multiplying each part
230 # by the placeholder that represents the prefactor from the ladder operator term.
231 (ladder, (_, part)), *rest = zip(ladder_term_symbols, terms)
232 expr = ladder * part
233 for ladder, (_, part) in rest:
234 expr += ladder * part
235 expr = expr.subs(
236 {kwant.continuum.discretizer._displacements[normal_coordinate]: grid_spacing}
237 )
238 # Construct the return string and temporary variable names
239 # for function calls.
240 return_string, map_func_calls, const_symbols, _cache = kwant.continuum.discretizer._return_string(
241 expr, coords=[normal_coordinate]
242 )
244 # Remove the ladder term placeholders, as these are not parameters
245 const_symbols = set(const_symbols)
246 for ladder_term in ladder_term_symbols:
247 const_symbols.discard(ladder_term)
249 # Construct the argument part of signature. Arguments
250 # consist of any constants and functions in the return string.
251 arg_names = set.union(
252 {s.name for s in const_symbols}, {str(k.func) for k in map_func_calls}
253 )
254 arg_names.discard("Abs") # Abs function is not a parameter
255 for arg_name in arg_names:
256 if not (arg_name.isidentifier() and not iskeyword(arg_name)): 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true
257 raise ValueError(
258 "Invalid name in used symbols: {}\n"
259 "Names of symbols used in Hamiltonian "
260 "must be valid Python identifiers and "
261 "may not be keywords".format(arg_name)
262 )
263 arg_names = ", ".join(sorted(arg_names))
264 # Construct site part of the function signature
265 site_string = "from_site" if is_onsite else "to_site, from_site"
266 # Construct function signature
267 if arg_names:
268 function_header = "def _({}, {}):".format(site_string, arg_names)
269 else:
270 function_header = "def _({}):".format(site_string)
271 # Construct function body
272 function_body = []
273 if "B" not in arg_names:
274 # B is not a parameter for terms with no ladder operators but we still
275 # need something to pass to _evaluate_ladder_term
276 function_body.append("B = +1")
277 function_body.extend(
278 [
279 "{}, = from_site.pos".format(normal_coordinate),
280 "_ll_index = from_site.family.landau_index(from_site.tag)",
281 ]
282 )
283 # To get the correct hopping if B < 0, we need to set the Hermitian
284 # conjugate of the ladder operator matrix element, which swaps the
285 # from_site and to_site Landau level indices.
286 if not is_onsite:
287 function_body.extend(
288 ["if B < 0:",
289 " _ll_index = to_site.family.landau_index(to_site.tag)"
290 ]
291 )
292 function_body.extend(
293 "{} = _evaluate_ladder_term({}, _ll_index, B)".format(symb.name, lt)
294 for symb, (lt, _) in zip(ladder_term_symbols, terms)
295 )
296 function_body.extend(
297 "{} = {}".format(v, kwant.continuum.discretizer._print_sympy(k))
298 for k, v in map_func_calls.items()
299 )
300 function_body.append(return_string)
301 func_code = "\n ".join([function_header] + function_body)
302 # Add "I" to namespace just in case sympy again would miss to replace it
303 # with Python's 1j as it was the case with SymPy 1.2 when "I" was argument
304 # of some function.
305 namespace = {
306 "pi": np.pi,
307 "I": 1j,
308 "_evaluate_ladder_term": _evaluate_ladder_term,
309 "Abs": abs,
310 }
311 namespace.update(_cache)
312 # Construct full source, including cached arrays
313 source = []
314 for k, v in _cache.items():
315 source.append("{} = (\n{})\n".format(k, repr(np.array(v))))
316 source.append(func_code)
317 exec(func_code, namespace)
318 f = namespace["_"]
319 f._source = "".join(source)
321 return f
324def _ladder_term(operator_string):
325 r"""Return a tuple of integers representing a string of ladder operators
327 Parameters
328 ----------
329 operator_string : Sympy expression
330 Monomial in ladder operators, e.g. a^\dagger a^2 a^\dagger.
332 Returns
333 -------
334 ladder_term : tuple of int
335 e.g. a^\dagger a^2 -> (+1, -2)
336 """
337 ret = []
338 for factor in operator_string.as_ordered_factors():
339 ladder_op, exponent = factor.as_base_exp()
340 if ladder_op == ladder_lower:
341 sign = -1
342 elif ladder_op == ladder_raise:
343 sign = +1
344 else:
345 sign = 0
346 ret.append(sign * int(exponent))
347 return tuple(ret)
350def _ladder_term_name(ladder_term):
351 """
352 Parameters
353 ----------
354 ladder_term : tuple of int
356 Returns
357 -------
358 ladder_term_name : str
359 """
361 def ns(i):
362 if i >= 0:
363 return str(i)
364 else:
365 return "_" + str(-i)
367 return "_ladder_{}".format("_".join(map(ns, ladder_term)))
370def _evaluate_ladder_term(ladder_term, n, B):
371 r"""Evaluates the prefactor for a ladder operator on a landau level.
373 Example: a^\dagger a^2 -> (n - 1) * sqrt(n)
375 Parameters
376 ----------
377 ladder_term : tuple of int
378 Represents a string of ladder operators. Positive
379 integers represent powers of the raising operator,
380 negative integers powers of the lowering operator.
381 n : non-negative int
382 Landau level index on which to act with ladder_term.
383 B : float
384 Magnetic field with sign
386 Returns
387 -------
388 ladder_term_prefactor : float
389 """
390 assert n >= 0
391 # For negative B we swap a and a^\dagger.
392 if B < 0:
393 ladder_term = tuple(-i for i in ladder_term)
394 ret = 1
395 for m in reversed(ladder_term):
396 if m > 0:
397 factors = range(n + 1, n + m + 1)
398 elif m < 0:
399 factors = range(n + m + 1, n + 1)
400 if n == 0:
401 return 0 # a|0> = 0
402 else:
403 factors = (1,)
404 ret *= sqrt(functools.reduce(operator.mul, factors))
405 n += m
406 return ret
409def _normalize_momenta(momenta=None):
410 """Return Landau level momenta as Sympy atoms
412 Parameters
413 ----------
414 momenta : None or pair of int or pair of str
415 The momenta to choose. If None then 'k_x' and 'k_y'
416 are chosen. If integers, then these are the indices
417 of the momenta: 0 → k_x, 1 → k_y, 2 → k_z. If strings,
418 then these name the momenta.
420 Returns
421 -------
422 The specified momenta as sympy atoms.
423 """
425 # Specify which momenta to substitute for the Landau level basis.
426 if momenta is None:
427 # Use k_x and k_y by default
428 momenta = momentum_operators[:2]
429 else:
430 if len(momenta) != 2: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true
431 raise ValueError("Two momenta must be specified.")
433 k_names = [k.name for k in momentum_operators]
435 if all([type(i) is int for i in momenta]) and all( 435 ↛ 438line 435 didn't jump to line 438, because the condition on line 435 was never true
436 [i >= 0 and i < 3 for i in momenta]
437 ):
438 momenta = [momentum_operators[i] for i in momenta]
439 elif all([isinstance(momentum, str) for momentum in momenta]) and all( 439 ↛ 446line 439 didn't jump to line 446, because the condition on line 439 was never false
440 [momentum in k_names for momentum in momenta]
441 ):
442 momenta = [
443 momentum_operators[k_names.index(momentum)] for momentum in momenta
444 ]
445 else:
446 raise ValueError("Momenta must all be integers or strings.")
448 return tuple(momenta)
451def _find_normal_coordinate(hamiltonian, momenta):
452 discrete_momentum = next( 452 ↛ exitline 452 didn't finish the generator expression on line 452
453 momentum for momentum in momentum_operators if momentum not in momenta
454 )
455 normal_coordinate = position_operators[momentum_operators.index(discrete_momentum)]
456 return normal_coordinate
459def _has_coordinate(coord, expr):
460 momentum = momentum_operators[position_operators.index(coord)]
461 atoms = set(expr.atoms())
462 return coord in atoms or momentum in atoms