diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7af2fa4c7f71374c9bc77861617e0a0b78b9d0ce..c6e2f7921e43b6bb5997f11e2889f91b46927071 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -62,6 +62,7 @@ build documentation: run tests: stage: test script: + - pip3 install sympy - py.test --cov=kwant --cov-report term --cov-report html --flakes kwant artifacts: paths: diff --git a/discretizer/__init__.py b/discretizer/__init__.py deleted file mode 100644 index 441fa0c75e4af5d2ad29e0f5fb5cf60f8f05d8a3..0000000000000000000000000000000000000000 --- a/discretizer/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -__all__ = ['algorithms'] - - -from.discretizer import Discretizer -import sympy - -__version__ = '0.4.3' - -momentum_operators = sympy.symbols('k_x k_y k_z', commutative=False) -coordinates = sympy.symbols('x y z', commutative=False) diff --git a/discretizer/algorithms.py b/discretizer/algorithms.py deleted file mode 100644 index 6ad0e047092edefc8803b2e1b789f5f2954e0f38..0000000000000000000000000000000000000000 --- a/discretizer/algorithms.py +++ /dev/null @@ -1,416 +0,0 @@ -from __future__ import print_function, division - -import itertools -import sympy -import numpy as np -from collections import defaultdict - -from .postprocessing import make_kwant_functions -from .postprocessing import offset_to_direction - -from .interpolation import interpolate_tb_hamiltonian - -try: - # normal situation - from kwant.lattice import Monatomic -except ImportError: - # probably run on gitlab-ci - pass - -# ************************** Some globals ********************************* -wavefunction_name = 'Psi' - - -# **************** Operation on sympy expressions ************************** -def read_coordinates(expression): - """Read coordinates used in expression. - - This function is used if ``discrete_coordinates`` are not provided by user. - - Parameters: - ----------- - expression : sympy.Expr or sympy.Matrix instance - - Returns: - -------- - discrete_coordinates : set of strings - """ - discrete_coordinates = set() - for a in expression.atoms(sympy.Symbol): - if a.name in ['k_x', 'k_y', 'k_z']: - discrete_coordinates.add(a.name.split('_')[1]) - - if a.name in ['x', 'y', 'z']: - discrete_coordinates.add(a.name) - - return discrete_coordinates - -def split_factors(expression, discrete_coordinates): - """ Split symbolic `expression` for a discretization step. - - Parameters: - ----------- - expression : sympy.Expr instance - The expression to be split. It should represents single summand. - - Output: - ------- - lhs : sympy.Expr instance - Part of expression standing to the left from operators - that acts in current discretization step. - - operators: sympy.Expr instance - Operator that perform discretization in current step. - - rhs : sympy.Expr instance - Part of expression that is derivated in current step. - - Raises: - ------- - AssertionError - if input `expression` is of type ``sympy.Add`` - """ - assert not isinstance(expression, sympy.Add), \ - 'Input expression must not be sympy.Add. It should be a single summand.' - - momentum_names = ['k_{}'.format(s) for s in discrete_coordinates] - momentum_operators = sympy.symbols(momentum_names, commutative=False) - momentum_operators += sympy.symbols(momentum_names, commutative=True) - - output = {'rhs': [1], 'operator': [1], 'lhs': [1]} - - if isinstance(expression, sympy.Pow): - base, exponent = expression.args - if base in momentum_operators: - output['operator'].append(base) - output['lhs'].append(sympy.Pow(base, exponent-1)) - else: - output['rhs'].append(expression) - - - elif isinstance(expression, (int, float, sympy.Integer, sympy.Float)): - output['rhs'].append(expression) - - elif isinstance(expression, (sympy.Symbol, sympy.Function)): - if expression in momentum_operators: - output['operator'].append(expression) - else: - output['rhs'].append(expression) - - elif isinstance(expression, sympy.Mul): - iterator = iter(expression.args[::-1]) - for factor in iterator: - if factor in momentum_operators: - output['operator'].append(factor) - break - elif factor.func == sympy.Pow and factor.args[0] in momentum_operators: - base, exponent = factor.args - output['operator'].append(base) - output['lhs'].append(sympy.Pow(base, exponent-1)) - break - else: - output['rhs'].append(factor) - - for factor in iterator: - output['lhs'].append(factor) - - output = tuple(sympy.Mul(*output[key][::-1]) - for key in ['lhs', 'operator', 'rhs']) - return output - - -def derivate(expression, operator): - """ Calculate derivate of expression for given momentum operator: - - Parameters: - ----------- - expression : sympy.Expr instance - Sympy expression containing functions to to be derivated. - operator : sympy.Symbol - Sympy symbol representing momentum operator. - - Returns: - -------- - output : sympy.Expr instance - Derivated input expression. - """ - if not isinstance(operator, sympy.Symbol): - raise TypeError("Input operator '{}' is not type sympy.Symbol.") - - if operator.name not in ['k_x', 'k_y', 'k_z']: - raise ValueError("Input operator '{}' unkown.".format(operator)) - - if isinstance(expression, (int, float, sympy.Symbol)): - return 0 - else: - coordinate_name = operator.name.split('_')[1] - ct = sympy.Symbol(coordinate_name, commutative=True) - cf = sympy.Symbol(coordinate_name, commutative=False) - h = sympy.Symbol('a_'+coordinate_name) - - expr1 = expression.subs({ct: ct + h, cf: cf + h}) - expr2 = expression.subs({ct: ct - h, cf: cf - h}) - output = (expr1 - expr2) / 2 / h - return -sympy.I * sympy.expand(output) - - -def _discretize_summand(summand, discrete_coordinates): - """ Discretize one summand. """ - assert not isinstance(summand, sympy.Add), "Input should be one summand." - - def do_stuff(expr): - """ Derivate expr recursively. """ - expr = sympy.expand(expr) - - if isinstance(expr, sympy.Add): - return do_stuff(expr.args[-1]) + do_stuff(sympy.Add(*expr.args[:-1])) - - lhs, operator, rhs = split_factors(expr, discrete_coordinates) - if rhs == 1 and operator != 1: - return 0 - elif operator == 1: - return lhs*rhs - elif lhs == 1: - return derivate(rhs, operator) - else: - return do_stuff(lhs*derivate(rhs, operator)) - - return do_stuff(summand) - - -def _discretize_expression(expression, discrete_coordinates): - """ Discretize continous `expression` into discrete tb representation. - - Parameters: - ----------- - expression : sympy.Expr instance - The expression to be discretized. - - Returns: - -------- - discrete_expression: dict - dict in which key is offset of hopping ((0, 0, 0) for onsite) - and value is corresponding symbolic hopping (onsite). - - Note: - ----- - Recursive derivation implemented in _discretize_summand is applied - on every summand. Shortening is applied before return on output. - """ - if isinstance(expression, (int, float, sympy.Integer, sympy.Float)): - n = len(discrete_coordinates) - return {(tuple(0 for i in range(n))): expression} - - if not isinstance(expression, sympy.Expr): - raise TypeError('Input expression should be a valid sympy expression.') - - coordinates_names = sorted(list(discrete_coordinates)) - coordinates = [sympy.Symbol(c, commutative=False) for c in coordinates_names] - wf = sympy.Function(wavefunction_name)(*coordinates) - - if wf in expression.atoms(sympy.Function): - raise ValueError("Input expression must not contain {}.".format(wf)) - - expression = sympy.expand(expression*wf) - - # make sure we have list of summands - summands = expression.args if expression.func == sympy.Add else [expression] - - # discretize every summand - outputs = [] - for summand in summands: - out = _discretize_summand(summand, discrete_coordinates) - out = extract_hoppings(out, discrete_coordinates) - outputs.append(out) - - # gather together - discrete_expression = defaultdict(int) - for summand in outputs: - for k, v in summand.items(): - discrete_expression[k] += v - - return dict(discrete_expression) - - -def discretize(hamiltonian, discrete_coordinates): - """ Discretize continous `expression` into discrete tb representation. - - Parameters: - ----------- - hamiltonian : sympy.Expr or sympy.Matrix instance - The expression for the Hamiltonian. - - Returns: - -------- - discrete_hamiltonian: dict - dict in which key is offset of hopping ((0, 0, 0) for onsite) - and value is corresponding symbolic hopping (onsite). - - Note: - ----- - Recursive derivation implemented in _discretize_summand is applied - on every summand. Shortening is applied before return on output. - """ - if not isinstance(hamiltonian, sympy.matrices.MatrixBase): - onsite_zeros = (0,)*len(discrete_coordinates) - discrete_hamiltonian = {onsite_zeros: sympy.Integer(0)} - hoppings = _discretize_expression(hamiltonian, discrete_coordinates) - discrete_hamiltonian.update(hoppings) - return discrete_hamiltonian - - shape = hamiltonian.shape - - discrete_hamiltonian = defaultdict(lambda: sympy.zeros(*shape)) - for i,j in itertools.product(range(shape[0]), range(shape[1])): - expression = hamiltonian[i, j] - hoppings = _discretize_expression(expression, discrete_coordinates) - - for offset, hop in hoppings.items(): - discrete_hamiltonian[offset][i,j] += hop - return discrete_hamiltonian - - -# ****** extracting hoppings *********** -def read_hopping_from_wf(wf): - """Read offset of a wave function in respect to (x,y,z). - - Parameters: - ---------- - wf : sympy.function.AppliedUndef instance - Function representing correct wave function used in discretizer. - Should be created using global `wavefunction_name`. - - Returns: - -------- - offset : tuple - tuple of integers or floats that represent offset in respect to (x,y,z). - - Raises: - ------- - ValueError - If arguments of wf are repeated / do not stand for valid coordinates or - lattice constants / order of dimensions is not lexical. - TypeError: - If wf is not of type sympy.function.AppliedUndef or its name does not - corresponds to global 'wavefunction_name'. - """ - if not isinstance(wf, sympy.function.AppliedUndef): - raise TypeError('Input should be of type sympy.function.AppliedUndef.') - - if not wf.func.__name__ == wavefunction_name: - msg = 'Input should be function that represents wavefunction in module.' - raise TypeError(msg) - - # Check if input is consistent and in lexical order. - # These are more checks for internal usage. - coordinates_names = ['x', 'y', 'z'] - lattice_const_names = ['a_x', 'a_y', 'a_z'] - arg_coords = [] - for arg in wf.args: - names = [s.name for s in arg.atoms(sympy.Symbol)] - ind = -1 - for s in names: - if not any(s in coordinates_names for s in names): - raise ValueError("Wave function argument '{}' is incorrect.".format(s)) - if s not in coordinates_names and s not in lattice_const_names: - raise ValueError("Wave function argument '{}' is incorrect.".format(s)) - if s in lattice_const_names: - s = s.split('_')[1] - tmp = coordinates_names.index(s) - if tmp in arg_coords: - msg = "Wave function '{}' arguments are inconsistent." - raise ValueError(msg.format(wf)) - if ind != -1: - if tmp != ind: - msg = "Wave function '{}' arguments are inconsistent." - raise ValueError(msg.format(wf)) - else: - ind = tmp - arg_coords.append(ind) - if arg_coords != sorted(arg_coords): - msg = "Coordinates of wave function '{}' are not in lexical order." - raise ValueError(msg.format(wf)) - - # function real body - offset = [] - for argument in wf.args: - temp = sympy.expand(argument) - if temp in sympy.symbols('x y z', commutative = False): - offset.append(0) - elif temp.func == sympy.Add: - for arg_summands in temp.args: - if arg_summands.func == sympy.Mul: - if len(arg_summands.args) > 2: - print('More than two factors in an argument of wf') - if not arg_summands.args[0] in sympy.symbols('a_x a_y a_z'): - offset.append(arg_summands.args[0]) - else: - offset.append(arg_summands.args[1]) - elif arg_summands in sympy.symbols('a_x a_y a_z'): - offset.append(1) - else: - print('Argument of \wf is neither a sum nor a single space variable.') - return tuple(offset) - - -def extract_hoppings(expression, discrete_coordinates): - """Extract hopping and perform shortening operation. """ - - # make sure we have list of summands - expression = sympy.expand(expression) - summands = expression.args if expression.func == sympy.Add else [expression] - - hoppings = defaultdict(int) - for summand in summands: - if summand.func.__name__ == wavefunction_name: - hoppings[read_hopping_from_wf(summand)] += 1 - else: - for i in range(len(summand.args)): - if summand.args[i].func.__name__ == wavefunction_name: - index = i - if index < len(summand.args) - 1: - print('Psi is not in the very end of the term. Output will be wrong!') - hoppings[read_hopping_from_wf(summand.args[-1])] += sympy.Mul(*summand.args[:-1]) - - # START: shortenig - discrete_coordinates = sorted(list(discrete_coordinates)) - tmps = ['a_{}'.format(s) for s in discrete_coordinates] - lattice_constants = sympy.symbols(tmps) - a = sympy.Symbol('a') - - # make a list of all hopping kinds we have to consider during the shortening - hops_kinds = np.array(list(hoppings)) - # find the longest hopping range in each direction - longest_ranges = [np.max(hops_kinds[:,i]) for i in range(len(hops_kinds[0,:]))] - # define an array in which we are going to store by which factor we - # can shorten the hoppings in each direction - shortening_factors = np.ones_like(longest_ranges) - # Loop over the direction and each potential shortening factor. - # Inside the loop test whether the hopping distances are actually - # multiples of the potential shortening factor. - for dim in np.arange(len(longest_ranges)): - for factor in np.arange(longest_ranges[dim])+1: - modulos = np.mod(hops_kinds[:, dim], factor) - if np.sum(modulos) < 0.1: - shortening_factors[dim] = factor - # Apply the shortening factors on the hopping. - short_hopping = {} - for hopping_kind in hoppings.keys(): - short_hopping_kind = tuple(np.array(hopping_kind) / shortening_factors) - - for i in short_hopping_kind: - if isinstance(i, float): - assert i.is_integer() - short_hopping_kind = tuple(int(i) for i in short_hopping_kind) - - short_hopping[short_hopping_kind] = hoppings[hopping_kind] - for lat_const, factor in zip(lattice_constants, shortening_factors): - factor = int(factor) - subs = {lat_const: lat_const/factor} - short_hopping[short_hopping_kind] = short_hopping[short_hopping_kind].subs(subs) - - # We don't need separate a_x, a_y and a_z anymore. - for key, val in short_hopping.items(): - short_hopping[key] = val.subs({i: a for i in sympy.symbols('a_x a_y a_z')}) - - return short_hopping diff --git a/discretizer/discretizer.py b/discretizer/discretizer.py deleted file mode 100644 index 70b2a9dac8368a0f00413e018051b2c4527fa594..0000000000000000000000000000000000000000 --- a/discretizer/discretizer.py +++ /dev/null @@ -1,160 +0,0 @@ -from __future__ import print_function, division - -import warnings -import numpy as np -import sympy - -from .algorithms import read_coordinates -from .algorithms import discretize - -from .postprocessing import offset_to_direction -from .postprocessing import make_kwant_functions -from .postprocessing import offset_to_direction - -from .interpolation import interpolate_tb_hamiltonian - -try: - # normal situation - from kwant import Builder - from kwant import TranslationalSymmetry - from kwant import HoppingKind - from kwant.lattice import Monatomic -except ImportError: - # probably run on gitlab-ci - pass - -class Discretizer(object): - """Discretize continous Hamiltonian into its tight binding representation. - - This class provides easy and nice interface for passing models to Kwant. - - Parameters: - ----------- - hamiltonian : sympy.Expr or sympy.Matrix instance - Symbolic representation of a continous Hamiltonian. Momentum operators - should be taken from ``discretizer.momentum_operators``. - discrete_coordinates : set of strings - Set of coordinates for which momentum operators will be treated as - differential operators. For example ``discrete_coordinates={'x', 'y'}``. - If left as a None they will be obtained from the input hamiltonian by - reading present coordinates and momentum operators. - interpolate : bool - If True all space dependent parameters in onsite and hopping will be - interpolated to depenend only on the values at site positions. - Default is False. - both_hoppings_directions : bool - If True all hoppings will be returned. For example, if set to True, both - hoppings into (1, 0) and (-1, 0) will be returned. Default is False. - verbose : bool - If True additional information will be printed. Default is False. - - Attributes: - ----------- - symbolic_hamiltonian : dictionary - Dictionary containing symbolic result of discretization. Key is the - direction of the hopping (zeros for onsite) - lattice : kwant.lattice.Monatomic instance - Lattice to create kwant system. Lattice constant is set to - lattice_constant value. - onsite : function - The value of the onsite Hamiltonian. - hoppings : dict - A dictionary with keys being tuples of the lattice hopping, and values - the corresponding value functions. - discrete_coordinates : set of strings - As in input. - input_hamiltonian : sympy.Expr or sympy.Matrix instance - The input hamiltonian after preprocessing (substitution of functions). - """ - def __init__(self, hamiltonian, discrete_coordinates=None, - lattice_constant=1, interpolate=False, - both_hoppings_directions=False, verbose=False): - - self.input_hamiltonian = hamiltonian - - if discrete_coordinates is None: - self.discrete_coordinates = read_coordinates(hamiltonian) - else: - self.discrete_coordinates = discrete_coordinates - - if verbose: - print('Discrete coordinates set to: ', - sorted(self.discrete_coordinates), end='\n\n') - - # discretization - if self.discrete_coordinates: - tb_ham = discretize(hamiltonian, self.discrete_coordinates) - tb_ham = offset_to_direction(tb_ham, self.discrete_coordinates) - else: - tb_ham = {(0,0,0): hamiltonian} - self.discrete_coordinates = {'x', 'y', 'z'} - - if interpolate: - tb_ham = interpolate_tb_hamiltonian(tb_ham) - - if not both_hoppings_directions: - keys = list(tb_ham) - tb_ham = {k: v for k, v in tb_ham.items() - if k in sorted(keys)[len(keys)//2:]} - - self.symbolic_hamiltonian = tb_ham.copy() - - for key, val in tb_ham.items(): - tb_ham[key] = val.subs(sympy.Symbol('a'), lattice_constant) - - # making kwant lattice - dim = len(self.discrete_coordinates) - - self.lattice = Monatomic(lattice_constant*np.eye(dim).reshape(dim, dim)) - self.lattice_constant = lattice_constant - - # making kwant functions - tb = make_kwant_functions(tb_ham, self.discrete_coordinates, verbose) - self.onsite = tb.pop((0,)*len(self.discrete_coordinates)) - self.hoppings = {HoppingKind(d, self.lattice): val - for d, val in tb.items()} - - def build(self, shape, start, symmetry=None, periods=None): - """Build Kwant's system. - - Convienient functions that simplifies building of a Kwant's system. - - Parameters: - ----------- - shape : function - A function of real space coordinates that returns a truth value: - true for coordinates inside the shape, and false otherwise. - start : 1d array-like - The real-space origin for the flood-fill algorithm. - symmetry : 1d array-like - Deprecated. Please use ```periods=[symmetry]`` instead. - periods : list of tuples - If periods are provided a translational invariant system will be - built. Periods corresponds basically to a translational symmetry - defined in real space. This vector will be scalled by a lattice - constant before passing it to ``kwant.TranslationalSymmetry``. - Examples: ``periods=[(1,0,0)]`` or ``periods=[(1,0), (0,1)]``. - In second case one will need https://gitlab.kwant-project.org/cwg/wraparound - in order to finalize system. - - Returns: - -------- - system : kwant.Builder instance - """ - if symmetry is not None: - warnings.warn("\nSymmetry argument is deprecated. " + - "Please use ```periods=[symmetry]`` instead.", - DeprecationWarning) - periods = [symmetry] - - if periods is None: - sys = Builder() - else: - vecs = [self.lattice.vec(p) for p in periods] - sys = Builder(TranslationalSymmetry(*vecs)) - - sys[self.lattice.shape(shape, start)] = self.onsite - for hop, val in self.hoppings.items(): - sys[hop] = val - - return sys diff --git a/discretizer/interpolation.py b/discretizer/interpolation.py deleted file mode 100644 index da83f0ace11cfada05db549c161f4ec05dd0e759..0000000000000000000000000000000000000000 --- a/discretizer/interpolation.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import print_function, division - -import itertools -import numpy as np -import sympy - - -def _follow_path(expr, path): - res = expr - for i in np.arange(len(path)): - res = res.args[path[i]] - return res - -def _interchange(expr, sub, path): - res = sub - for i in np.arange(len(path)): - temp = _follow_path(expr, path[:-(i+1)]) - args = list(temp.args) - args[path[len(path)-i-1]] = res - res = temp.func(*tuple(args)) - return res - -def _interpolate_Function(expr): - path = 'None' - factor = 'None' - change = False - summand_0 = 'None' - summand_1 = 'None' - res = expr - for i in np.arange(len(expr.args)): - argument = sympy.expand(expr.args[i]) - if argument.func == sympy.Add: - for j in np.arange(len(argument.args)): - summand = argument.args[j] - if summand.func == sympy.Mul: - for k in np.arange(len(summand.args)): - temp = 0 - if summand.args[k] == sympy.Symbol('a'): - temp = sympy.Mul(sympy.Mul(*summand.args[:k]), - sympy.Mul(*summand.args[k+1:])) - #print(temp) - if not temp == int(temp): - #print('found one') - factor = (temp) - path = np.array([i, j, k]) - if not factor == 'None': - change = True - - sign = np.sign(factor) - offsets = np.array([int(factor), sign * (int(sign * factor) + 1)]) - weights = 1/np.abs(offsets - factor) - weights = weights/np.sum(weights) - - res = ( weights[0] * _interchange(expr, offsets[0] * sympy.Symbol('a'), path[:-1]) - + weights[1] * _interchange(expr, offsets[1] * sympy.Symbol('a'), path[:-1])) - - return sympy.expand(res), change - - -def _interpolate_expression(expr): - change = False - expr = sympy.expand(expr) - res = expr - - if isinstance(expr, sympy.Function):# and not change: - path = np.array([]) - temp, change = _interpolate_Function(_follow_path(expr, path)) - res = _interchange(expr, temp, path) - - for i in np.arange(len(expr.args)): - path = np.array([i]) - if isinstance(_follow_path(expr, path), sympy.Function) and not change: - temp, change = _interpolate_Function(_follow_path(expr, path)) - res = _interchange(expr, temp, path) - - for j in np.arange(len(expr.args[i].args)): - path = np.array([i, j]) - if isinstance(_follow_path(expr, path), sympy.Function) and not change: - temp, change = _interpolate_Function(_follow_path(expr, path)) - res = _interchange(expr, temp, path) - - if change: - res = _interpolate_expression(res) - - return sympy.expand(res) - - -def _interpolate(expression): - if not isinstance(expression, sympy.Matrix): - return _interpolate_expression(expression) - - shape = expression.shape - interpolated = sympy.zeros(*shape) - for i,j in itertools.product(range(shape[0]), repeat=2): - interpolated[i,j] = _interpolate_expression(expression[i, j]) - - return interpolated - - -def interpolate_tb_hamiltonian(tb_hamiltonian): - """Interpolate tight binding hamiltonian. - - This function perform linear interpolation to provide onsite and hoppings - depending only on parameters values at sites positions. - """ - interpolated = {} - for key, val in tb_hamiltonian.items(): - interpolated[key] = _interpolate(val) - return interpolated diff --git a/discretizer/postprocessing.py b/discretizer/postprocessing.py deleted file mode 100644 index cd2fe61a0e5e9209852375345d9fd261350cf815..0000000000000000000000000000000000000000 --- a/discretizer/postprocessing.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import print_function, division - -import sympy -from sympy.utilities.lambdify import lambdastr -from sympy.printing.lambdarepr import LambdaPrinter -from sympy.core.function import AppliedUndef - - -class NumericPrinter(LambdaPrinter): - def _print_ImaginaryUnit(self, expr): - return "1.j" - - -def offset_to_direction(discrete_hamiltonian, discrete_coordinates): - """Translate hopping keys from offsets to directions. - - Parameters: - ----------- - discrete_hamiltonian: dict - Discretized hamiltonian, key should be an offset of a hopping and value - corresponds to symbolic hopping. - discrete_coordinates: set - Set of discrete coordinates. - - Returns: - -------- - discrete_hamiltonian: dict - Discretized hamiltonian, key is a direction of a hopping and value - corresponds to symbolic hopping. - - Note: - ----- - Coordinates (x,y,z) in output stands for a position of a source of the - hopping. - """ - coordinates = sorted(list(discrete_coordinates)) - coordinates = [sympy.Symbol(s, commutative=False) for s in coordinates] - a = sympy.Symbol('a') - - onsite_zeros = (0,)*len(discrete_coordinates) - output = {onsite_zeros: discrete_hamiltonian.pop(onsite_zeros)} - for offset, hopping in discrete_hamiltonian.items(): - direction = tuple(-c for c in offset) - subs = {c: c + d*a for c, d in zip(coordinates, direction)} - output[direction] = hopping.subs(subs) - - return output - -# ************ Making kwant functions *********** -def make_return_string(expr): - """Process a sympy expression into an evaluatable Python return statement. - - Parameters: - ----------- - expr : sympy.Expr instance - - Returns: - -------- - output : string - A return string that can be used to assemble a Kwant value function. - func_symbols : set of sympy.Symbol instances - All space dependent functions that appear in the expression. - const_symbols : set of sympy.Symbol instances - All constants that appear in the expression. - """ - func_symbols = {sympy.Symbol(i.func.__name__) for i in - expr.atoms(AppliedUndef)} - - free_symbols = {i for i in expr.free_symbols if i.name not in ['x', 'y', 'z']} - const_symbols = free_symbols - func_symbols - - expr = expr.subs(sympy.I, sympy.Symbol('1.j')) # quick hack - output = lambdastr((), expr, printer=NumericPrinter)[len('lambda : '):] - output = output.replace('MutableDenseMatrix', 'np.array') - output = output.replace('ImmutableMatrix', 'np.array') - - return 'return {}'.format(output), func_symbols, const_symbols - - -def assign_symbols(func_symbols, const_symbols, discrete_coordinates, - onsite=True): - """Generate a series of assingments defining a set of symbols. - - Parameters: - ----------- - func_symbols : set of sympy.Symbol instances - All space dependent functions that appear in the expression. - const_symbols : set of sympy.Symbol instances - All constants that appear in the expression. - - Returns: - -------- - assignments : list of strings - List of lines used for including in a function. - - Notes: - where A, B, C are all the free symbols plus the symbols that appear on the - ------ - The resulting lines begin with a coordinates assignment of form - `x,y,z = site.pos` when onsite=True, or - `x,y,z = site2.pos` when onsite=False - - followed by two lines of form - `A, B, C = p.A, p.B, p.C` - `f, g, h = p.f, p.g, p.h` - where A, B, C are symbols representing constants and f, g, h are symbols - representing functions. Separation of constant and func symbols is probably - not necessary but I leave it for now, just in case. - """ - lines = [] - func_names = [i.name for i in func_symbols] - const_names = [i.name for i in const_symbols] - - if func_names: - lines.insert(0, ', '.join(func_names) + ' = p.' + - ', p.'.join(func_names)) - - if const_names: - lines.insert(0, ', '.join(const_names) + ' = p.' + - ', p.'.join(const_names)) - - if onsite: - site = 'site' - else: - site = 'site2' - - names = sorted(list(discrete_coordinates)) - lines.insert(0, '({}, ) = {}.pos'.format(', '.join(names), site)) - - return lines - - -def value_function(content, name='_anonymous_func', onsite=True, verbose=False): - """Generate a Kwant value function from a list of lines containing its body. - - Parameters: - ----------- - content : list of lines - Lines forming the body of the function. - name : string - Function name (not important). - onsite : bool - If True, the function call signature will be `f(site, p)`, otherwise - `f(site1, site2, p)`. - verbose : bool - Whether the function bodies should be printed. - - Returns: - -------- - f : function - The function defined in a separated namespace. - """ - if not content[-1].startswith('return'): - raise ValueError('The function does not end with a return statement') - - separator = '\n' + 4 * ' ' - site_string = 'site' if onsite else 'site1, site2' - header = 'def {0}({1}, p):'.format(name, site_string) - func_code = separator.join([header] + list(content)) - - namespace = {} - if verbose: - print(func_code) - exec("from __future__ import division", namespace) - exec("import numpy as np", namespace) - exec("from numpy import *", namespace) - exec(func_code, namespace) - return namespace[name] - - -def make_kwant_functions(discrete_hamiltonian, discrete_coordinates, - verbose=False): - """Transform discrete hamiltonian into valid kwant functions. - - Parameters: - ----------- - discrete_hamiltonian: dict - dict in which key is offset of hopping ((0, 0, 0) for onsite) - and value is corresponding symbolic hopping (onsite). - - verbose : bool - Whether the function bodies should be printed. - - discrete_coordinates : tuple/list - List of discrete coordinates. Must corresponds to offsets in - discrete_hamiltonian keys. - - Note: - ----- - - """ - dim = len(discrete_coordinates) - if not all(len(i)==dim for i in list(discrete_hamiltonian.keys())): - raise ValueError("Dimension of offsets and discrete_coordinates" + - "do not match.") - - functions = {} - for offset, hopping in discrete_hamiltonian.items(): - onsite = True if all(i == 0 for i in offset) else False - return_string, func_symbols, const_symbols = make_return_string(hopping) - lines = assign_symbols(func_symbols, const_symbols, onsite=onsite, - discrete_coordinates=discrete_coordinates) - lines.append(return_string) - - if verbose: - print("Function generated for {}:".format(offset)) - f = value_function(lines, verbose=verbose, onsite=onsite) - print() - else: - f = value_function(lines, verbose=verbose, onsite=onsite) - - functions[offset] = f - - return functions diff --git a/discretizer/tests/test_algorithms.py b/discretizer/tests/test_algorithms.py deleted file mode 100644 index cd253183cee947de67e1ef724e7ec6db7f1c7e7b..0000000000000000000000000000000000000000 --- a/discretizer/tests/test_algorithms.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import print_function, division - -import sympy -import discretizer -from discretizer.algorithms import read_coordinates -from discretizer.algorithms import split_factors -from discretizer.algorithms import wavefunction_name -from discretizer.algorithms import derivate -from discretizer.algorithms import _discretize_summand - -from nose.tools import raises -from nose.tools import assert_raises -import numpy as np - -kx, ky, kz = sympy.symbols('k_x k_y k_z', commutative=False) -x, y, z = sympy.symbols('x y z', commutative=False) -ax, ay, az = sympy.symbols('a_x a_y a_z') - -wf = sympy.Function(wavefunction_name) -Psi = sympy.Function(wavefunction_name)(x, y, z) -A, B = sympy.symbols('A B', commutative=False) - -ns = {'A': A, 'B': B, 'a_x': ax, 'a_y': ay, 'az': az, 'x': x, 'y': y, 'z': z} - -def test_read_coordinates(): - test = { - kx**2 : {'x'}, - kx**2 + ky**2 : {'x', 'y'}, - kx**2 + ky**2 + kz**2 : {'x', 'y', 'z'}, - ky**2 + kz**2 : {'y', 'z'}, - kz**2 : {'z'}, - kx * A(x,y) * kx : {'x', 'y'}, - kx**2 + kz * B(y) : {'x', 'y', 'z'}, - } - for inp, out in test.items(): - got = read_coordinates(inp) - assert got == out,\ - "Should be: split_factors({})=={}. Not {}".format(inp, out, got) - -def test_split_factors_1(): - test = { - kz * Psi : (1, kz, Psi), - A * kx**2 * Psi : (A * kx, kx, Psi), - A * kx**2 * ky * Psi : (A * kx**2, ky, Psi), - ky * A * kx * B * Psi : (ky * A, kx, B * Psi), - kx : (1, kx, 1), - kx**2 : (kx, kx, 1), - A : (1, 1, A), - A**2 : (1, 1, A**2), - kx*A**2 : (1, kx, A**2), - kx**2*A**2 : (kx, kx, A**2), - A(x, y, z) : (1, 1, A(x, y, z)), - Psi : (1, 1, Psi), - np.int(5) : (1, 1, np.int(5)), - np.float(5) : (1, 1, np.float(5)), - sympy.Integer(5) : (1, 1, sympy.Integer(5)), - sympy.Float(5) : (1, 1, sympy.Float(5)), - 1 : (1, 1, 1), - 1.0 : (1, 1, 1.0), - 5 : (1, 1, 5), - 5.0 : (1, 1, 5.0), - } - - for inp, out in test.items(): - got = split_factors(inp, discrete_coordinates={'x', 'y', 'z'}) - assert got == out,\ - "Should be: split_factors({})=={}. Not {}".format(inp, out, got) - - -@raises(AssertionError) -def test_split_factors_2(): - split_factors(A+B, discrete_coordinates={'x', 'y', 'z'}) - - -def test_derivate_1(): - test = { - (A(x), kx): '-I*(-A(-a_x + x)/(2*a_x) + A(a_x + x)/(2*a_x))', - (A(x), ky): '0', - (A(x)*B, kx): '-I*(-A(-a_x + x)*B/(2*a_x) + A(a_x + x)*B/(2*a_x))', - (A(x) + B(x), kx): '-I*(-A(-a_x + x)/(2*a_x) + A(a_x + x)/(2*a_x) - B(-a_x + x)/(2*a_x) + B(a_x + x)/(2*a_x))', - (A, kx): '0', - (5, kx): '0', - (A(x) * B(x), kx): '-I*(-A(-a_x + x)*B(-a_x + x)/(2*a_x) + A(a_x + x)*B(a_x + x)/(2*a_x))', - (Psi, ky): '-I*(-Psi(x, -a_y + y, z)/(2*a_y) + Psi(x, a_y + y, z)/(2*a_y))', - } - - for inp, out in test.items(): - got = (derivate(*inp)) - out = sympy.sympify(out, locals=ns) - assert sympy.simplify(sympy.expand(got - out)) == 0,\ - "Should be: derivate({0[0]}, {0[1]})=={1}. Not {2}".format(inp, out, got) - - -@raises(TypeError) -def test_derivate_2(): - derivate(A(x), kx**2) - - -@raises(ValueError) -def test_derivate_3(): - derivate(A(x), sympy.Symbol('A')) - - -def test_discretize_summand_1(): - test = { - kx * A(x): '-I*(-A(-a_x + x)/(2*a_x) + A(a_x + x)/(2*a_x))', - kx * Psi: '-I*(-Psi(-a_x + x, y, z)/(2*a_x) + Psi(a_x + x, y, z)/(2*a_x))', - kx**2 * Psi: 'Psi(x, y, z)/(2*a_x**2) - Psi(-2*a_x + x, y, z)/(4*a_x**2) - Psi(2*a_x + x, y, z)/(4*a_x**2)', - kx * A(x) * kx * Psi: 'A(-a_x + x)*Psi(x, y, z)/(4*a_x**2) - A(-a_x + x)*Psi(-2*a_x + x, y, z)/(4*a_x**2) + A(a_x + x)*Psi(x, y, z)/(4*a_x**2) - A(a_x + x)*Psi(2*a_x + x, y, z)/(4*a_x**2)', - } - - for inp, out in test.items(): - got = _discretize_summand(inp, discrete_coordinates={'x', 'y', 'z'}) - out = sympy.sympify(out, locals=ns) - assert sympy.simplify(sympy.expand(got - out)) == 0,\ - "Should be: _discretize_summand({})=={}. Not {}".format(inp, out, got) - -@raises(AssertionError) -def test_discretize_summand_2(): - _discretize_summand(kx*A(x)+ B(x), discrete_coordinates={'x', 'y', 'z'}) diff --git a/discretizer/tests/test_hoppings.py b/discretizer/tests/test_hoppings.py deleted file mode 100644 index 21075c70ad724294a5c2672a2ff8f290cd5dde30..0000000000000000000000000000000000000000 --- a/discretizer/tests/test_hoppings.py +++ /dev/null @@ -1,129 +0,0 @@ -from __future__ import print_function, division - -import sympy -import discretizer -from discretizer.algorithms import wavefunction_name -from discretizer.algorithms import read_hopping_from_wf -from discretizer.algorithms import extract_hoppings - -from nose.tools import raises -from nose.tools import assert_raises -import numpy as np - - -kx, ky, kz = sympy.symbols('k_x k_y k_z', commutative=False) -x, y, z = sympy.symbols('x y z', commutative=False) -ax, ay, az = sympy.symbols('a_x a_y a_z') - -wf = sympy.Function(wavefunction_name) -Psi = sympy.Function(wavefunction_name)(x, y, z) -A, B = sympy.symbols('A B', commutative=False) - -ns = {'A': A, 'B': B, 'a_x': ax, 'a_y': ay, 'az': az, 'x': x, 'y': y, 'z': z} - - -def test_read_hoppings_from_wf_1(): - offsets = [(0,0,0), (1,0,0), (0,1,0), (0,0,1), (1,1,1), (1,2,3)] - test = {} - - for offset in offsets: - nx, ny, nz = offset - key = Psi.subs({x: x + nx*ax, y: y + ny * ay, z: z + nz * az}) - test[key] = offset - - for inp, out in test.items(): - got = read_hopping_from_wf(inp) - assert got == out,\ - "Should be: read_hopping_from_wf({}) == {}. Not {}".format(inp, out, got) - - -def test_read_hoppings_from_wf_2(): - test = { - wf(x, y, z): (0,0,0), - wf(x, y): (0, 0), - wf(x, z): (0, 0), - wf(x+ax, y-2*ay): (1, -2), - wf(x, z+3*az): (0, 3), - wf(y, z): (0, 0), - wf(y, z+az): (0, 1), - } - - for inp, out in test.items(): - got = read_hopping_from_wf(inp) - assert got == out,\ - "Should be: read_hopping_from_wf({}) == {}. Not {}".format(inp, out, got) - - -def test_test_read_hoppings_from_wf_ValueError(): - tests = { - wf(x+ay, ay), - wf(y, x), - wf(x, x), - wf(x, ax), - wf(y, A), - } - for inp in tests: - assert_raises(ValueError, read_hopping_from_wf, inp) - - -def test_test_read_hoppings_from_wf_TypeError(): - tests = { - wf(x,y,z) + A, - A(x,y), - 5*Psi, - Psi+2, - A, - } - for inp in tests: - assert_raises(TypeError, read_hopping_from_wf, inp) - - - -def test_extract_hoppings(): - discrete_coordinates = {'x', 'y', 'z'} - tests = [ - { - 'test_inp': '-I*(-Psi(-a_x + x, y)/(2*a_x) + Psi(a_x + x, y)/(2*a_x))', - 'test_out': { - (1, 0): '-I/(2*a)', - (-1, 0): 'I/(2*a)', - }, - }, - { - 'test_inp': 'Psi(x, y)/(2*a_x**2) - Psi(-2*a_x + x, y)/(4*a_x**2) - Psi(2*a_x + x, y)/(4*a_x**2)', - 'test_out': { - (1, 0): '-1/a**2', - (0, 0): '2/a**2', - (-1, 0): '-1/a**2', - }, - }, - { - 'test_inp': 'A(-a_x + x)*Psi(x, y)/(4*a_x**2) - A(-a_x + x)*Psi(-2*a_x + x, y)/(4*a_x**2) + A(a_x + x)*Psi(x, y)/(4*a_x**2) - A(a_x + x)*Psi(2*a_x + x, y)/(4*a_x**2)', - 'test_out': { - (1, 0): '-A(a/2 + x)/a**2', - (0, 0): 'A(-a/2 + x)/a**2 + A(a/2 + x)/a**2', - (-1, 0): '-A(-a/2 + x)/a**2', - }, - }, - { - 'test_inp': 'I*A(x, y)*Psi(x, -a_y + y, z)/(4*a_x**2*a_y) - I*A(x, y)*Psi(x, a_y + y, z)/(4*a_x**2*a_y) - I*A(-2*a_x + x, y)*Psi(-2*a_x + x, -a_y + y, z)/(8*a_x**2*a_y) + I*A(-2*a_x + x, y)*Psi(-2*a_x + x, a_y + y, z)/(8*a_x**2*a_y) - I*A(2*a_x + x, y)*Psi(2*a_x + x, -a_y + y, z)/(8*a_x**2*a_y) + I*A(2*a_x + x, y)*Psi(2*a_x + x, a_y + y, z)/(8*a_x**2*a_y)', - 'test_out': { - (-1, 1, 0): 'I*A(-a + x, y)/(2*a**3)', - (1, 1, 0): 'I*A(a + x, y)/(2*a**3)', - (0, -1, 0): 'I*A(x, y)/a**3', - (0, 1, 0): '-I*A(x, y)/a**3', - (1, -1, 0): '-I*A(a + x, y)/(2*a**3)', - (-1, -1, 0): '-I*A(-a + x, y)/(2*a**3)', - }, - }, - ] - - for test in tests: - test_inp = test['test_inp'] - test_out = test['test_out'] - inp = sympy.sympify(test_inp, locals=ns) - result = extract_hoppings(inp, discrete_coordinates) - for key, got in result.items(): - out = sympy.sympify(test_out[key], locals=ns) - assert sympy.simplify(sympy.expand(got - out)) == 0, \ - "Should be: extract_hoppings({})=={}. Not {}".format(inp, test_out, result) diff --git a/kwant/__init__.py b/kwant/__init__.py index 6dcc99eec2062e2befca40811efefa408e161c8e..a3c430d765b53497f8527c2b1485ca90e4bdbf65 100644 --- a/kwant/__init__.py +++ b/kwant/__init__.py @@ -9,6 +9,7 @@ __all__ = [] import numpy # Needed by C. Gohlke's Windows package. +import warnings try: from . import _system @@ -62,3 +63,12 @@ def test(verbose=True): "-s"] + (['-v'] if verbose else [])) test.__test__ = False + + +# Exposing discretizer +try: + from .continuum import discretize + __all__.extend(['discretize']) +except ImportError: + warnings.warn('Discretizer module not available. Is sympy installed?', + RuntimeWarning) diff --git a/kwant/continuum/__init__.py b/kwant/continuum/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bcc0b6d17d732799617f44c0f399592e4b10860a --- /dev/null +++ b/kwant/continuum/__init__.py @@ -0,0 +1,7 @@ +__all__ = ['discretize', 'discretize_symbolic', 'build_discretized', + 'momentum_operators', 'position_operators'] + + +from .discretizer import discretize, discretize_symbolic, build_discretized +from ._common import momentum_operators, position_operators +from ._common import sympify, lambdify, make_commutative diff --git a/kwant/continuum/_common.py b/kwant/continuum/_common.py new file mode 100644 index 0000000000000000000000000000000000000000..b107d59f855d57373fc7bec027dee2c254eacbce --- /dev/null +++ b/kwant/continuum/_common.py @@ -0,0 +1,217 @@ +import numpy as np + +import sympy +from sympy.core.function import AppliedUndef +from sympy.core.sympify import converter +from sympy.abc import _clash + +import functools +import inspect + +from collections import defaultdict +from operator import mul + +from sympy.physics.matrices import msigma as _msigma +from sympy.physics.quantum import TensorProduct + + +momentum_operators = sympy.symbols('k_x k_y k_z', commutative=False) +position_operators = sympy.symbols('x y z', commutative=False) + +msigma = lambda i: sympy.eye(2) if i == 0 else _msigma(i) +pauli = (msigma(0), msigma(1), msigma(2), msigma(3)) + +_clash = _clash.copy() +_clash.update({s.name: s for s in momentum_operators}) +_clash.update({s.name: s for s in position_operators}) +_clash.update({'kron': TensorProduct, 'eye': sympy.eye}) +_clash.update({'sigma_{}'.format(c): p for c, p in zip('0xyz', pauli)}) + + +# workaroud for https://github.com/sympy/sympy/issues/12060 +del _clash['I'] +del _clash['pi'] + + +################ Various helpers to handle sympy + + +def lambdify(hamiltonian, *, substitutions=None): + """Return a callable object for computing continuum Hamiltonian. + + Parameters + ---------- + hamiltonian : sympy.Expr or sympy.Matrix, or string + Symbolic representation of a continous Hamiltonian. When providing + a sympy expression, it is recommended to use operators + defined in `~kwant.continuum.momentum_operators` in order to provide + proper commutation properties. If a string is provided it will be + converted to a sympy expression using `kwant.continuum.sympify`. + substitutions : dict, defaults to empty + A namespace to be passed to ``kwant.continuum.sympify`` when + ``hamiltonian`` is a string. Could be used to simplify matrix input: + ``sympify('k_x**2 * s_z', substitutions={'s_z': [[1, 0], [0, -1]]})``. + """ + expr = hamiltonian + if not isinstance(expr, (sympy.Expr, sympy.matrices.MatrixBase)): + expr = sympify(expr, substitutions) + + args = [s.name for s in expr.atoms(sympy.Symbol)] + args += [str(f.func) for f in expr.atoms(AppliedUndef, sympy.Function)] + + f = sympy.lambdify(sorted(args), expr) + + sig = inspect.signature(f) + pars = list(sig.parameters.values()) + pars = [p.replace(kind=inspect.Parameter.KEYWORD_ONLY) for p in pars] + f.__signature__ = inspect.Signature(pars) + return f + + +def sympify(string, substitutions=None, **kwargs): + """Return sympified object with respect to kwant-specific rules. + + This is a modification of ``sympy.sympify`` to apply kwant-specific rules, + which includes preservation of proper commutation relations between + position and momentum operators, and array to matrix casting. + + Parameters + ---------- + string : string + String representation of a Hamiltonian. Momenta must be defined as + ``k_i`` where ``i`` stands for ``x``, ``y``, or ``z``. All present + momenta and coordinates will be interpreted as non commutative. + substitutions : dict, defaults to empty + A namespace to be passed internally to ``sympy.sympify``. + Could be used to simplify matrix input: + ``sympify('k_x**2 * s_z', substitutions={'s_z': [[1, 0], [0, -1]]})``. + **kwargs + Additional arguments that will be passed to ``sympy.sympify``. + + Example + ------- + >>> from kwant.continuum import sympify + >>> hamiltonian = sympify('k_x * A(x) * k_x + V(x)') + """ + stored_value = None + + if substitutions is None: + substitutions = {} + + substitutions.update(_clash) + + try: + stored_value = converter.pop(list, None) + converter[list] = lambda x: sympy.Matrix(x) + substitutions = {k: (sympy.sympify(v, locals=substitutions, **kwargs) + if isinstance(v, (list, str)) else v) + for k, v in substitutions.items()} + hamiltonian = sympy.sympify(string, locals=substitutions, **kwargs) + hamiltonian = sympy.sympify(hamiltonian, **kwargs) + finally: + if stored_value is not None: + converter[list] = stored_value + else: + del converter[list] + return sympy.expand(hamiltonian) + + +def make_commutative(expr, *symbols): + """Make sure that specified symbols are defined as commutative. + + Parameters + ---------- + expr: sympy.Expr or sympy.Matrix + symbols: sequace of symbols + Set of symbols that are requiered to be commutative. It doesn't matter + of symbol is provided as commutative or not. + + Returns + ------- + input expression with all specified symbols changed to commutative. + """ + symbols = [sympy.Symbol(s.name, commutative=False) for s in symbols] + subs = {s: sympy.Symbol(s.name) for s in symbols} + expr = expr.subs(subs) + return expr + + +def expression_monomials(expression, *gens): + """Parse ``expression`` into monomials in the symbols in ``gens``. + + Example + ------- + >>> expr = A * (x**2 + y) + B * x + C + >>> expression_monomials(expr, x, y) + {1: C, x**2: A, y: A, x: B} + """ + if expression.atoms(AppliedUndef): + raise NotImplementedError('Getting monomials of expressions containing ' + 'functions is not implemented.') + + expression = make_commutative(expression, *gens) + gens = [make_commutative(g, g) for g in gens] + + expression = sympy.expand(expression) + summands = expression.as_ordered_terms() + + output = defaultdict(int) + for summand in summands: + key = [sympy.Integer(1)] + if summand in gens: + key.append(summand) + + elif isinstance(summand, sympy.Pow): + if summand.args[0] in gens: + key.append(summand) + + else: + for arg in summand.args: + if arg in gens: + key.append(arg) + if isinstance(arg, sympy.Pow): + if arg.args[0] in gens: + key.append(arg) + + key = functools.reduce(mul, key) + val = summand.xreplace({g: 1 for g in gens}) + + ### to not create key + if val != 0: + output[key] += val + + new_expression = sum(k * v for k, v in output.items()) + assert sympy.expand(expression) == sympy.expand(new_expression) + + return dict(output) + + +def matrix_monomials(matrix, *gens): + output = defaultdict(lambda: sympy.zeros(*matrix.shape)) + for (i, j), expression in np.ndenumerate(matrix): + summands = expression_monomials(expression, *gens) + for key, val in summands.items(): + output[key][i, j] += val + + return dict(output) + + +################ general help functions + +def gcd(*args): + if len(args) == 1: + return args[0] + + L = list(args) + + while len(L) > 1: + a = L[len(L) - 2] + b = L[len(L) - 1] + L = L[:len(L) - 2] + + while a: + a, b = b%a, a + + L.append(b) + + return abs(b) diff --git a/kwant/continuum/discretizer.py b/kwant/continuum/discretizer.py new file mode 100644 index 0000000000000000000000000000000000000000..af7cd1c8831920e22a5ee10bb62d708484705086 --- /dev/null +++ b/kwant/continuum/discretizer.py @@ -0,0 +1,555 @@ +from collections import defaultdict + +import numpy as np +import tinyarray as ta +import sympy + +from sympy.utilities.lambdify import lambdastr +from sympy.printing.lambdarepr import LambdaPrinter +from sympy.printing.precedence import precedence + +from sympy.core.function import AppliedUndef + +from ..builder import Builder, HoppingKind +from ..lattice import Monatomic, TranslationalSymmetry + +from ._common import sympify, gcd +from ._common import position_operators, momentum_operators +from ._common import matrix_monomials + + +################ Globals variables and definitions + +_wf = sympy.Function('_internal_unique_name', commutative=False) +_position_operators = {s.name: s for s in position_operators} +_displacements = {s: sympy.Symbol('_internal_a_{}'.format(s)) for s in 'xyz'} + + +################ Interface functions + +def discretize(hamiltonian, discrete_coordinates=None, lattice_constant=1, + substitutions=None, verbose=False): + """Construct a tight-binding model from a continuum Hamiltonian. + + Parameters + ---------- + hamiltonian : sympy.Expr or sympy.Matrix, or string + Symbolic representation of a continous Hamiltonian. When providing + a sympy expression, it is recommended to use operators + defined in `~kwant.continuum.momentum_operators` in order to provide + proper commutation properties. If a string is provided it will be + converted to a sympy expression using `kwant.continuum.sympify`. + discrete_coordinates : sequence of strings, or ``None`` (default) + Set of coordinates for which momentum operators will be treated as + differential operators. For example ``discrete_coordinates=('x', 'y')``. + If not provided they will be obtained from the input hamiltonian by + reading present coordinates and momentum operators. Order of discrete + coordinates is always lexical, even if provided otherwise. + lattice_constant : int or float, default: 1 + Lattice constant for the template Builder. + substitutions : dict, defaults to empty + A namespace to be passed to `kwant.continuum.sympify` when + ``hamiltonian`` is a string. Could be used to simplify matrix input: + ``sympify('k_x**2 * s_z', substitutions={'s_z': [[1, 0], [0, -1]]})``. + verbose : bool, default: False + If ``True`` additional information will be printed. + + Returns + ------- + `~kwant.builder.Builder` with translational symmetry, + which can be used as a template. + """ + + args = hamiltonian, discrete_coordinates, substitutions, verbose + tb, coords = discretize_symbolic(*args) + return build_discretized(tb, coords, lattice_constant, substitutions, verbose) + + +def discretize_symbolic(hamiltonian, discrete_coordinates=None, substitutions=None, + verbose=False): + """Discretize a continuous Hamiltonian into a tight-binding representation. + + Parameters + ---------- + hamiltonian : sympy.Expr or sympy.Matrix, or string + Symbolic representation of a continous Hamiltonian. When providing + a sympy expression, it is recommended to use operators + defined in `~kwant.continuum.momentum_operators` in order to provide + proper commutation properties. If a string is provided it will be + converted to a sympy expression using `kwant.continuum.sympify`. + discrete_coordinates : sequence of strings, or ``None`` (default) + Set of coordinates for which momentum operators will be treated as + differential operators. For example ``discrete_coordinates=('x', 'y')``. + If not provided they will be obtained from the input hamiltonian by + reading present coordinates and momentum operators. Order of discrete + coordinates is always lexical, even if provided otherwise. + substitutions : dict, defaults to empty + A namespace to be passed to `kwant.continuum.sympify` when + ``hamiltonian`` is a string. Could be used to simplify matrix input: + ``sympify('k_x**2 * s_z', substitutions={'s_z': [[1, 0], [0, -1]]})``. + verbose : bool, default: False + If ``True`` additional information will be printed. + + Returns + ------- + (discrete_hamiltonian, discrete_coordinates) + discrete_hamiltonian: dict + Keys are tuples of integers; the offsets of the hoppings + ((0, 0, 0) for the onsite). Values are symbolic expressions + for the hoppings/onsite. + discrete_coordinates : sequence of strings + The coordinates that have been discretized. + """ + if not isinstance(hamiltonian, (sympy.Expr, sympy.matrices.MatrixBase)): + hamiltonian = sympify(hamiltonian, substitutions) + + atoms = hamiltonian.atoms(sympy.Symbol) + if not all('a' != s.name for s in atoms): + raise ValueError("'a' is a symbol used internally to represent " + "lattice spacing; please use a different symbol.") + + hamiltonian = sympy.expand(hamiltonian) + if discrete_coordinates is None: + used_momenta = set(momentum_operators) & set(atoms) + discrete_coordinates = {k.name[-1] for k in used_momenta} + else: + discrete_coordinates = set(discrete_coordinates) + if not discrete_coordinates <= set('xyz'): + msg = "Discrete coordinates must only contain 'x', 'y', or 'z'." + raise ValueError(msg) + + discrete_coordinates = sorted(discrete_coordinates) + + if len(discrete_coordinates) == 0: + raise ValueError("Failed to read any discrete coordinates. This is " + "probably due to a lack of momentum operators in " + "your input. You can use the 'discrete_coordinates'" + "parameter to provide them.") + + if verbose: + print('Discrete coordinates set to: ', + discrete_coordinates, end='\n\n') + + onsite_zeros = (0,) * len(discrete_coordinates) + + if not isinstance(hamiltonian, sympy.matrices.MatrixBase): + hamiltonian = sympy.Matrix([hamiltonian]) + _input_format = 'expression' + else: + _input_format = 'matrix' + + shape = hamiltonian.shape + tb = defaultdict(lambda: sympy.zeros(*shape)) + tb[onsite_zeros] = sympy.zeros(*shape) + + for (i, j), expression in np.ndenumerate(hamiltonian): + hoppings = _discretize_expression(expression, discrete_coordinates) + + for offset, hop in hoppings.items(): + tb[offset][i, j] += hop + + # do not include Hermitian conjugates of hoppings + wanted_hoppings = sorted(list(tb))[len(list(tb)) // 2:] + tb = {k: v for k, v in tb.items() if k in wanted_hoppings} + + if _input_format == 'expression': + tb = {k: v[0, 0] for k, v in tb.items()} + + return tb, discrete_coordinates + + +def build_discretized(tb_hamiltonian, discrete_coordinates, + lattice_constant=1, substitutions=None, verbose=False): + """Create a template Builder from a symbolic tight-binding Hamiltonian. + + Parameters + ---------- + tb_hamiltonian : dict + Keys are tuples of integers: the offsets of the hoppings + ((0, 0, 0) for the onsite). Values are symbolic expressions + for the hoppings/onsite or expressions that can by sympified using + `kwant.continuum.sympify`. + discrete_coordinates : sequence of strings + Set of coordinates for which momentum operators will be treated as + differential operators. For example ``discrete_coordinates=('x', 'y')``. + lattice_constant : int or float, default: 1 + Lattice constant for the template Builder. + substitutions : dict, defaults to empty + A namespace to be passed to `kwant.continuum.sympify` when + ``tb_hamiltonian`` is a string. Could be used to simplify matrix input: + ``sympify('k_x**2 * s_z', substitutions={'s_z': [[1, 0], [0, -1]]})``. + verbose : bool, default: False + If ``True`` additional information will be printed. + + Returns + ------- + `~kwant.builder.Builder` with translational symmetry, + which can be used as a template. + """ + if len(discrete_coordinates) == 0: + raise ValueError('Discrete coordinates cannot be empty.') + + for k, v in tb_hamiltonian.items(): + if not isinstance(v, (sympy.Expr, sympy.matrices.MatrixBase)): + tb_hamiltonian[k] = sympify(v, substitutions) + + discrete_coordinates = sorted(discrete_coordinates) + + tb = {} + for n, (offset, hopping) in enumerate(tb_hamiltonian.items()): + if verbose: + print("Function generated for {}:".format(offset)) + + onsite = all(i == 0 for i in offset) + + if onsite: + name = 'onsite' + else: + name = 'hopping_{}'.format(n) + + tb[offset] = _value_function(hopping, discrete_coordinates, + lattice_constant, onsite, name, + verbose=verbose) + + dim = len(discrete_coordinates) + onsite_zeros = (0,) * dim + + prim_vecs = lattice_constant * np.eye(dim) + random_element = next(iter(tb_hamiltonian.values())) + norbs = (1 if isinstance(random_element, sympy.Expr) + else random_element.shape[0]) + lattice = Monatomic(prim_vecs, norbs=norbs) + + onsite = tb.pop(onsite_zeros) + # 'delta' parameter to HoppingKind is the negative of the 'hopping offset' + hoppings = {HoppingKind(tuple(-i for i in d), lattice): val + for d, val in tb.items()} + + syst = Builder(TranslationalSymmetry(*prim_vecs)) + syst[lattice(*onsite_zeros)] = onsite + for hop, val in hoppings.items(): + syst[hop] = val + + return syst + + +def _differentiate(expression, coordinate_name): + """Calculate derivative of an expression for given coordinate. + + Parameters: + ----------- + expression : sympy.Expr + Sympy expression containing function to be derivated. + coordinate_name : string + Coordinate over which derivative is calculated. + + Returns + ------- + sympy.Expr + """ + assert coordinate_name in 'xyz' + coordinate = _position_operators[coordinate_name] + h = _displacements[coordinate_name] + + expr1 = expression.subs(coordinate, coordinate + h) + expr2 = expression.subs(coordinate, coordinate - h) + + return (expr1 - expr2) / (2 * h) + + +def _discretize_summand(summand, discrete_coordinates): + """Discretize a product of factors. + + Parameters + ---------- + summand : sympy.Expr + discrete_coordinates : sequence of strings + Must be a subset of ``{'x', 'y', 'z'}``. + + Returns + ------- + sympy.Expr + """ + assert not isinstance(summand, sympy.Add), "Input should be one summand." + momenta = ['k_{}'.format(s) for s in discrete_coordinates] + + factors = reversed(summand.as_ordered_factors()) + result = 1 + for factor in factors: + symbol, exponent = factor.as_base_exp() + if isinstance(symbol, sympy.Symbol) and symbol.name in momenta: + for i in range(exponent): + coordinate = symbol.name[-1] + # apply momentum as differential operator '-i d/dx' + result = -sympy.I * _differentiate(result, coordinate) + else: + result = factor * result + + return sympy.expand(result) + + +def _discretize_expression(expression, discrete_coordinates): + """Discretize an expression into a discrete (tight-binding) representation. + + Parameters + ---------- + expression : sympy.Expr + discrete_coordinates : sequence of strings + Must be a subset of ``{'x', 'y', 'z'}``. + + Returns + ------- + dict + Keys are tuples of integers; the offsets of the hoppings + ((0, 0, 0) for the onsite). Values are symbolic expressions + for the hoppings/onsite. + """ + def _read_offset(wf): + # e.g. wf(x + h, y, z + h) -> (1, 0, 1) + assert wf.func == _wf + + offset = [] + for c, arg in zip(discrete_coordinates, wf.args): + coefficients = arg.as_coefficients_dict() + assert coefficients[_position_operators[c]] == 1 + + ai = _displacements[c] + offset.append(coefficients.pop(ai, 0)) + return tuple(offset) + + def _extract_hoppings(expr): + """Read hoppings and perform shortening operation.""" + expr = sympy.expand(expr) + summands = expr.args if expr.func == sympy.Add else [expr] + + offset = [_read_offset(s.args[-1]) for s in summands] + coeffs = [sympy.Mul(*s.args[:-1]) for s in summands] + offset = np.array(offset, dtype=int) + # rescale the offsets for each coordinate by their greatest + # common divisor across the summands. e.g: + # wf(x+2h) + wf(x+4h) --> wf(x+h) + wf(x+2h) and a_x //= 2 + subs = {} + for i, xi in enumerate(discrete_coordinates): + factor = int(gcd(*offset[:, i])) + if factor < 1: + continue + offset[:, i] //= factor + subs[_displacements[xi]] = sympy.symbols('a') / factor + # apply the rescaling to the hoppings + output = defaultdict(lambda: sympy.Integer(0)) + for n, c in enumerate(coeffs): + output[tuple(offset[n].tolist())] += c.subs(subs) + return dict(output) + + # main function body starts here + if (isinstance(expression, (int, float, sympy.Integer, sympy.Float)) + or not expression.atoms(sympy.Symbol) & set(momentum_operators)): + n = len(discrete_coordinates) + return {(0,) * n: expression} + + assert isinstance(expression, sympy.Expr) + + # make sure we have list of summands + summands = expression.as_ordered_terms() + + # discretize every summand + coordinates = tuple(_position_operators[s] for s in discrete_coordinates) + wf = _wf(*coordinates) + + discrete_expression = defaultdict(int) + for summand in summands: + summand = _discretize_summand(summand * wf, discrete_coordinates) + hops = _extract_hoppings(summand) + for k, v in hops.items(): + discrete_expression[k] += v + + return dict(discrete_expression) + + +################ string processing + +class _NumericPrinter(LambdaPrinter): + + def _print_ImaginaryUnit(self, expr): + # prevent sympy from printing 'I' for imaginary unit + return "1j" + + def _print_Pow(self, expr): + # copied from sympy's StrPrinter with the code paths + # to print 'sqrt' removed. + PREC = precedence(expr) + + if expr.is_commutative and expr.exp is -sympy.S.One: + return '1/%s' % self.parenthesize(expr.base, PREC) + + e = self.parenthesize(expr.exp, PREC) + if (self.printmethod == '_sympyrepr' and + expr.exp.is_Rational and expr.exp.q != 1): + if e.startswith('(Rational'): + return '%s**%s' % (self.parenthesize(expr.base, PREC), e[1:-1]) + return '%s**%s' % (self.parenthesize(expr.base, PREC), e) + + +def _print_sympy(expr): + return lambdastr((), expr, printer=_NumericPrinter)[len('lambda : '):] + + +def _return_string(expr, discrete_coordinates): + """Process a sympy expression into an evaluatable Python return statement. + + Parameters + ---------- + expr : sympy.Expr + + Returns + ------- + output : string + A return string that can be used to assemble a Kwant value function. + map_function_calls : dict + mapping of function calls to assigned constants. + const_symbols : sequance of sympy.Symbol + All constants that appear in the expression. + _cache: dict + mapping of cache symbols to cached matrices. + """ + _cache = {} + def cache(x): + s = sympy.symbols('_cache_{}'.format(len(_cache))) + _cache[str(s)] = ta.array(x.tolist(), complex) + return s + + blacklisted = set(discrete_coordinates) | {'site', 'site1', 'site2'} + const_symbols = {s for s in expr.atoms(sympy.Symbol) + if s.name not in blacklisted} + + # functions will be evaluated within the function body and the + # result assigned to a symbol '_const_<n>', so we replace all + # function calls by these symbols in the return statement. + map_function_calls = expr.atoms(AppliedUndef, sympy.Function) + map_function_calls = {s: sympy.symbols('_const_{}'.format(n)) + for n, s in enumerate(map_function_calls)} + + expr = expr.subs(map_function_calls) + + if isinstance(expr, sympy.matrices.MatrixBase): + # express matrix return values in terms of sums of known matrices, + # which will be assigned to '_cache_n' in the function body. + mons = matrix_monomials(expr, *expr.atoms(sympy.Symbol)) + mons = {k: cache(v) for k, v in mons.items()} + mons = ["{} * {}".format(_print_sympy(k), _print_sympy(v)) + for k, v in mons.items()] + output = " + ".join(mons) + else: + output = _print_sympy(expr) + + return 'return {}'.format(output), map_function_calls, const_symbols, _cache + + +def _assign_symbols(map_function_calls, lattice_constant, + discrete_coordinates, onsite): + """Generate a series of assignments. + + Parameters + ---------- + map_function_calls : dict + mapping of function calls to assigned constants. + lattice_constant : int or float + Used to get site.pos from site.tag + discrete_coordinates : sequence of strings + If left as None coordinates will not be read from a site. + onsite : bool + True if function is called for onsite, false for hoppings + + Returns + ------- + assignments : list of strings + List of lines used for including in a function. + """ + lines = [] + + if discrete_coordinates: + site = 'site' if onsite else 'site1' + args = ', '.join(discrete_coordinates), str(lattice_constant), site + lines.append('({}, ) = {} * {}.tag'.format(*args)) + + for k, v in map_function_calls.items(): + lines.append("{} = {}".format(v, _print_sympy(k))) + + return lines + + +def _value_function(expr, discrete_coordinates, lattice_constant, onsite, + name='_anonymous_func', verbose=False): + """Generate a numeric function from a sympy expression. + + Parameters + ---------- + expr : sympy.Expr or sympy.matrix + Expr that from which value function will be generated. + discrete_coordinates : sequence of strings + List of coodinates present in the system. + lattice_constant : int or float + Lattice spacing of the system + verbose : bool, default: False + If True, the function body is printed. + + Returns + ------- + numerical function that can be used with Kwant. + """ + + expr = expr.subs({sympy.Symbol('a'): lattice_constant}) + return_string, map_function_calls, const_symbols, _cache = \ + _return_string(expr, discrete_coordinates=discrete_coordinates) + + # first check if value function needs to read coordinates + if not ({_position_operators[s] for s in discrete_coordinates} & + expr.atoms(sympy.Symbol)): + discrete_coordinates = None + + # constants and functions in the sympy input will be passed + # as keyword-only arguments to the value function + required_kwargs = set.union({s.name for s in const_symbols}, + {str(k.func) for k in map_function_calls}) + required_kwargs = ', '.join(sorted(required_kwargs)) + + if (not required_kwargs) and (discrete_coordinates is None): + # we can just use a constant value instead of a value function + if isinstance(expr, sympy.MatrixBase): + output = ta.array(expr.tolist(), complex) + else: + output = complex(expr) + + if verbose: + print("\n{}\n\n".format(output)) + + return output + + lines = _assign_symbols(map_function_calls, onsite=onsite, + lattice_constant=lattice_constant, + discrete_coordinates=discrete_coordinates) + + lines.append(return_string) + + separator = '\n' + 4 * ' ' + # 'site_string' is tightly coupled to the symbols used in '_assign_symbol' + site_string = 'site' if onsite else 'site1, site2' + if required_kwargs: + header_str = 'def {}({}, *, {}):' + header = header_str.format(name, site_string, required_kwargs) + else: + header = 'def {}({}):'.format(name, site_string) + func_code = separator.join([header] + list(lines)) + + namespace = {'pi': np.pi} + namespace.update(_cache) + + if verbose: + for k, v in _cache.items(): + print("\n{} = (\n{})".format(k, repr(np.array(v)))) + print('\n' + func_code + '\n\n') + + exec(func_code, namespace) + f = namespace[name] + + return f diff --git a/kwant/continuum/tests/__init__.py b/kwant/continuum/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/kwant/continuum/tests/test_common.py b/kwant/continuum/tests/test_common.py new file mode 100644 index 0000000000000000000000000000000000000000..fa562c1679bfbdbde09e00f7dc916805b7f513c3 --- /dev/null +++ b/kwant/continuum/tests/test_common.py @@ -0,0 +1,146 @@ +from kwant.continuum._common import position_operators, momentum_operators +from kwant.continuum._common import make_commutative, sympify +from kwant.continuum._common import expression_monomials, matrix_monomials +from kwant.continuum._common import lambdify + +import tinyarray as ta +from sympy.physics.matrices import msigma +import sympy +from functools import reduce +from operator import mul +import pytest + + +def test_sympify(): + A, B, C = sympy.symbols('A B C') + x, y, z = position_operators + kx, ky, kz = momentum_operators + + # basics + assert sympify('k_x * A(x) * k_x') == kx * A(x) * kx + assert sympify('[[k_x * A(x) * k_x]]') == sympy.Matrix([kx * A(x) * kx]) + + # using substitutions + symbolic_pauli = {'sigma_x': msigma(1), 'sigma_y': msigma(2), 'sigma_z': msigma(3)} + got = sympify('k_x * sigma_y + k_y * sigma_x', substitutions=symbolic_pauli) + assert got == kx * symbolic_pauli['sigma_y'] + ky * symbolic_pauli['sigma_x'] + + got = sympify("sigma_y", substitutions={'sigma_y': "[[0, -1j], [1j, 0]]"}) + assert got == symbolic_pauli['sigma_y'] + + got = sympify("sigma_y", substitutions={'sigma_y': [[0, -sympy.I], [sympy.I, 0]]}) + assert got == symbolic_pauli['sigma_y'] + + got = sympify('[[k_x*A(x)*k_x, B(x, y)*k_x], [k_x*B(x, y), C*k_y**2]]') + assert got == sympy.Matrix([[kx*A(x)*kx, B(x, y)*kx], [kx*B(x, y), C*ky**2]]) + + + + +A, B, non_x = sympy.symbols('A B x', commutative=False) +x = sympy.Symbol('x') + +expr1 = non_x*A*non_x + x**2 * A * x + B*non_x**2 + +matr = sympy.Matrix([[expr1, expr1+A*non_x], [0, -expr1]]) +res_mat = sympy.Matrix([[x**3*A + x**2*A + x**2*B, x**3*A + x**2*A + x**2*B + x*A], + [0, -x**3*A - x**2*A - x**2*B]]) + +def test_make_commutative(): + assert make_commutative(expr1, x) == make_commutative(expr1, non_x) + assert make_commutative(expr1, x) == x**3*A + x**2*A + x**2*B + assert make_commutative(matr, x) == res_mat + + +expr2 = non_x*A*non_x + x**2 * A*2 * x + B*non_x/2 + non_x*B/2 + x + A + non_x + x/A + +def test_expression_monomials(): + assert expression_monomials(expr2, x) == {x**3: 2*A, 1: A, x: 2 + A**(-1) + B, x**2: A} + assert expression_monomials(expr1, x) == {x**2: A + B, x**3: A} + assert expression_monomials(x, x) == {x: 1} + assert expression_monomials(x**2, x) == {x**2: 1} + assert expression_monomials(x**2 + x, x) == {x: 1, x**2: 1} + + expr = 1 + x + A*x + 2*x + x**2 + A*x**2 + non_x*A*non_x + out = {1: 1, x: 3 + A, x**2: 2 * A + 1} + assert expression_monomials(expr, x) == out + + expr = 1 + x * (3 + A) + x**2 * (1 + A) + out = {1: 1, x: 3 + A, x**2: 1 * A + 1} + assert expression_monomials(expr, x) == out + + +def legacy_expression_monomials(expr, *gens): + """ This was my first implementation. Unfortunately it is very slow. + + It is used to test correctness of new matrix_monomials function. + """ + expr = make_commutative(expr, x) + R = sympy.ring(gens, sympy.EX, sympy.lex)[0] + expr = R(expr) + + output = {} + for power, coeff in zip(expr.monoms(), expr.coeffs()): + key = reduce(mul, [sympy.Symbol(k.name)**n for k, n in zip(gens, power)]) + output[key] = sympy.expand(coeff.as_expr()) + return output + + +def test_expression_monomials_with_reference_function(): + assert legacy_expression_monomials(expr2, x) == expression_monomials(expr2, x) + + + +def test_matrix_monomials(): + out = { + x**2: sympy.Matrix([[A + B, A + B],[0, -A - B]]), + x: sympy.Matrix([[0, A], [0, 0]]), + x**3: sympy.Matrix([[A, A], [0, -A]])} + mons = matrix_monomials(matr, x) + assert mons == out + + + +@pytest.mark.parametrize("e, should_be, kwargs", [ + ("x+y", lambda x, y: x+y, dict(x=1, y=2)), + ("1", lambda: 1, dict()), + ("f(x)", lambda f, x: f(x), dict(f=lambda x: x, x=2)), + (sympify("f(x)"), lambda f, x: f(x), dict(f=lambda x: x, x=2)), + ("[[f(x)]]", lambda f, x: ta.array(f(x)), dict(f=lambda x: x, x=2)) +]) +def test_lambdify(e, should_be, kwargs): + e = lambdify(e) + assert e(**kwargs) == should_be(**kwargs) + + + + + +# dispersion_string = ('A * k_x**2 * eye(2) + B * k_y**2 * eye(2)' +# '+ alpha * k_x * sigma_y') + +# def dispersion_function(kx, ky, A, B, alpha, **kwargs): +# h_k = ((A * kx**2 + B * ky**2) * sigma_0 + +# alpha * kx * sigma_y) +# return np.linalg.eigvalsh(h_k) + +# dispersion_kwargs = {'A': 1, 'B': 2, 'alpha': 0.5, 'M': 2} +# sigma_0 = np.eye(2) +# sigma_y = np.array([[0, -1j], [1j, 0]]) + + +# @pytest.mark.parametrize("expr, should_be, kwargs", [ +# (dispersion_string, dispersion_function, dispersion_kwargs), +# (sympify(dispersion_string), dispersion_function, dispersion_kwargs), +# ]) +# def test_lambdify(expr, should_be, kwargs): +# N = 5 + +# x = y = np.linspace(-N, N+1) +# xy = list(itertools.product(x, y)) + +# h_k = lambdify(expr) +# energies = [la.eigvalsh(h_k(k_x=kx, k_y=ky, **kwargs)) +# for kx, ky in xy] +# energies_should_be = [should_be(kx, ky, **kwargs) for kx, ky in xy] +# assert np.allclose(energies, energies_should_be) diff --git a/kwant/continuum/tests/test_discretizer.py b/kwant/continuum/tests/test_discretizer.py new file mode 100644 index 0000000000000000000000000000000000000000..014c37b32f606057b7990dad58787b4ee6096a39 --- /dev/null +++ b/kwant/continuum/tests/test_discretizer.py @@ -0,0 +1,480 @@ +import sympy +import numpy as np + +from ..discretizer import discretize +from ..discretizer import discretize_symbolic +from ..discretizer import build_discretized +from ..discretizer import _wf + +import inspect +from functools import wraps + + +def swallows_extra_kwargs(f): + sig = inspect.signature(f) + pars = sig.parameters + if any(i.kind is inspect.Parameter.VAR_KEYWORD for i in pars.values()): + return f + + names = {name for name, value in pars.items() if + value.kind not in (inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.POSITIONAL_ONLY)} + + @wraps(f) + def wrapped(*args, **kwargs): + bound = sig.bind(*args, **{name: value for name, value + in kwargs.items() if name in names}) + return f(*bound.args, **bound.kwargs) + + return wrapped + + +I = sympy.I +kx, ky, kz = sympy.symbols('k_x k_y k_z', commutative=False) +x, y, z = sympy.symbols('x y z', commutative=False) +ax, ay, az = sympy.symbols('a_x a_y a_z') +a = sympy.symbols('a') + +wf = _wf +Psi = wf(x, y, z) +A, B = sympy.symbols('A B', commutative=False) + +ns = {'A': A, 'B': B, 'a_x': ax, 'a_y': ay, 'az': az, 'x': x, 'y': y, 'z': z} + + +def test_reading_coordinates(): + test = { + kx**2 : {'x'}, + kx**2 + ky**2 : {'x', 'y'}, + kx**2 + ky**2 + kz**2 : {'x', 'y', 'z'}, + ky**2 + kz**2 : {'y', 'z'}, + kz**2 : {'z'}, + kx * A(x,y) * kx : {'x'}, + kx**2 + kz * B(y) : {'x', 'z'}, + } + for inp, out in test.items(): + ham, got = discretize_symbolic(inp) + assert all(d in out for d in got),\ + "Should be: _split_factors({})=={}. Not {}".format(inp, out, got) + + +def test_reading_coordinates_matrix(): + test = [ + (sympy.Matrix([kx**2]) , {'x'}), + (sympy.Matrix([kx**2 + ky**2]) , {'x', 'y'}), + (sympy.Matrix([kx**2 + ky**2 + kz**2]) , {'x', 'y', 'z'}), + (sympy.Matrix([ky**2 + kz**2]) , {'y', 'z'}), + (sympy.Matrix([kz**2]) , {'z'}), + (sympy.Matrix([kx * A(x,y) * kx]) , {'x'}), + (sympy.Matrix([kx**2 + kz * B(y)]) , {'x', 'z'}), + ] + for inp, out in test: + ham, got = discretize_symbolic(inp) + assert all(d in out for d in got),\ + "Should be: _split_factors({})=={}. Not {}".format(inp, out, got) + + +def test_reading_different_matrix_types(): + test = [ + (sympy.MutableMatrix([kx**2]) , {'x'}), + (sympy.ImmutableMatrix([kx**2]) , {'x'}), + (sympy.MutableDenseMatrix([kx**2]) , {'x'}), + (sympy.ImmutableDenseMatrix([kx**2]) , {'x'}), + ] + for inp, out in test: + ham, got = discretize_symbolic(inp) + assert all(d in out for d in got),\ + "Should be: _split_factors({})=={}. Not {}".format(inp, out, got) + + +def test_simple_derivations(): + test = { + kx**2 : {(0,): 2/a**2, (1,): -1/a**2}, + kx**2 + ky**2 : {(0, 1): -1/a**2, (0, 0): 4/a**2, + (1, 0): -1/a**2}, + kx**2 + ky**2 + kz**2 : {(1, 0, 0): -1/a**2, (0, 0, 1): -1/a**2, + (0, 0, 0): 6/a**2, (0, 1, 0): -1/a**2}, + ky**2 + kz**2 : {(0, 1): -1/a**2, (0, 0): 4/a**2, + (1, 0): -1/a**2}, + kz**2 : {(0,): 2/a**2, (1,): -1/a**2}, + kx * A(x,y) * kx : {(1, ): -A(a/2 + x, y)/a**2, + (0, ): A(-a/2 + x, y)/a**2 + A(a/2 + x, y)/a**2}, + kx**2 + kz * B(y) : {(1, 0): -1/a**2, (0, 1): -I*B(y)/(2*a), + (0, 0): 2/a**2}, + kx * A(x) : {(0,): 0, (1,): -I*A(a + x)/(2*a)}, + ky * A(x) : {(1,): -I*A(x)/(2*a), (0,): 0}, + kx * A(x) * B : {(0,): 0, (1,): -I*A(a + x)*B/(2*a)}, + 5 * kx : {(0,): 0, (1,): -5*I/(2*a)}, + kx * (A(x) + B(x)) : {(0,): 0, + (1,): -I*A(a + x)/(2*a) - I*B(a + x)/(2*a)}, + } + for inp, out in test.items(): + got, _ = discretize_symbolic(inp) + assert got == out + + for inp, out in test.items(): + got, _ = discretize_symbolic(str(inp), substitutions=ns) + assert got == out + + +def test_simple_derivations_matrix(): + test = { + kx**2 : {(0,): 2/a**2, (1,): -1/a**2}, + kx**2 + ky**2 : {(0, 1): -1/a**2, (0, 0): 4/a**2, + (1, 0): -1/a**2}, + kx**2 + ky**2 + kz**2 : {(1, 0, 0): -1/a**2, (0, 0, 1): -1/a**2, + (0, 0, 0): 6/a**2, (0, 1, 0): -1/a**2}, + ky**2 + kz**2 : {(0, 1): -1/a**2, (0, 0): 4/a**2, + (1, 0): -1/a**2}, + kz**2 : {(0,): 2/a**2, (1,): -1/a**2}, + kx * A(x,y) * kx : {(1, ): -A(a/2 + x, y)/a**2, + (0, ): A(-a/2 + x, y)/a**2 + A(a/2 + x, y)/a**2}, + kx**2 + kz * B(y) : {(1, 0): -1/a**2, (0, 1): -I*B(y)/(2*a), + (0, 0): 2/a**2}, + kx * A(x) : {(0,): 0, (1,): -I*A(a + x)/(2*a)}, + ky * A(x) : {(1,): -I*A(x)/(2*a), (0,): 0}, + kx * A(x) * B : {(0,): 0, (1,): -I*A(a + x)*B/(2*a)}, + 5 * kx : {(0,): 0, (1,): -5*I/(2*a)}, + kx * (A(x) + B(x)) : {(0,): 0, + (1,): -I*A(a + x)/(2*a) - I*B(a + x)/(2*a)}, + } + + new_test = [] + for inp, out in test.items(): + new_out = {} + for k, v in out.items(): + new_out[k] = sympy.Matrix([v]) + new_test.append((sympy.Matrix([inp]), new_out)) + + for inp, out in new_test: + got, _ = discretize_symbolic(inp) + assert got == out + + for inp, out in new_test: + got, _ = discretize_symbolic(str(inp), substitutions=ns) + assert got == out + + for inp, out in new_test: + got, _ = discretize_symbolic(str(inp).replace('Matrix', ''), substitutions=ns) + assert got == out + + + +def test_integer_float_input(): + test = { + 0 : {(0,0,0): 0}, + 1 : {(0,0,0): 1}, + 5 : {(0,0,0): 5}, + } + + for inp, out in test.items(): + got, _ = discretize_symbolic(int(inp), {'x', 'y', 'z'}) + assert got == out + + got, _ = discretize_symbolic(float(inp), {'x', 'y', 'z'}) + assert got == out + + # let's test in matrix version too + new_test = [] + for inp, out in test.items(): + new_out = {} + for k, v in out.items(): + new_out[k] = sympy.Matrix([v]) + new_test.append((inp, new_out)) + + for inp, out in new_test: + got, _ = discretize_symbolic(sympy.Matrix([int(inp)]), {'x', 'y', 'z'}) + assert got == out + + got, _ = discretize_symbolic(sympy.Matrix([float(inp)]), {'x', 'y', 'z'}) + assert got == out + + +def test_different_discrete_coordinates(): + test = [ + ( + {'x', 'y', 'z'}, { + (1, 0, 0): -1/a**2, (0, 0, 1): -1/a**2, + (0, 0, 0): 6/a**2, (0, 1, 0): -1/a**2 + } + ), + ( + {'x', 'y'}, { + (0, 1): -1/a**2, + (1, 0): -1/a**2, + (0, 0): kz**2 + 4/a**2 + } + ), + ( + {'x', 'z'}, { + (0, 1): -1/a**2, + (1, 0): -1/a**2, + (0, 0): ky**2 + 4/a**2 + } + ), + ( + {'y', 'z'}, { + (0, 1): -1/a**2, + (1, 0): -1/a**2, + (0, 0): kx**2 + 4/a**2 + } + ), + ( + {'x'}, { + (0,): ky**2 + kz**2 + 2/a**2, (1,): -1/a**2 + } + ), + ( + {'y'}, { + (0,): kx**2 + kz**2 + 2/a**2, (1,): -1/a**2 + } + ), + ( + {'z'}, { + (0,): ky**2 + kx**2 + 2/a**2, (1,): -1/a**2 + } + ) , + ] + for inp, out in test: + got, _ = discretize_symbolic(kx**2 + ky**2 + kz**2, inp) + assert got == out + + # let's test in matrix version too + new_test = [] + for inp, out in test: + new_out = {} + for k, v in out.items(): + new_out[k] = sympy.Matrix([v]) + new_test.append((inp, new_out)) + + for inp, out in new_test: + got, _ = discretize_symbolic(sympy.Matrix([kx**2 + ky**2 + kz**2]), inp) + assert got == out + + +def test_non_expended_input(): + symbolic, coords = discretize_symbolic(kx * (kx + A(x))) + desired = { + (0,): 2/a**2, + (1,): -I*A(a + x)/(2*a) - 1/a**2 + } + assert symbolic == desired + + + +def test_matrix_with_zeros(): + Matrix = sympy.Matrix + symbolic, _ = discretize_symbolic("[[k_x*A(x)*k_x, 0], [0, k_x*A(x)*k_x]]") + output = { + (0,) : Matrix([[A(-a/2 + x)/a**2 + A(a/2 + x)/a**2, 0], [0, A(-a/2 + x)/a**2 + A(a/2 + x)/a**2]]), + (1,) : Matrix([[-A(a/2 + x)/a**2, 0], [0, -A(a/2 + x)/a**2]]), + } + assert symbolic == output + + +def test_numeric_functions_basic_symbolic(): + for i in [0, 1, 3, 5]: + builder = discretize(i, {'x'}) + lat = next(iter(builder.sites()))[0] + assert builder[lat(0)] == i + + p = dict(t=i) + + tb = {(0,): sympy.sympify("2*t"), (1,): sympy.sympify('-t')} + builder = build_discretized(tb, {'x'}, lattice_constant=1) + lat = next(iter(builder.sites()))[0] + assert 2*p['t'] == builder[lat(0)](None, **p) + assert -p['t'] == builder[lat(1), lat(0)](None, None, **p) + + tb = {(0,): sympy.sympify("0"), (1,): sympy.sympify('-1j * t')} + builder = build_discretized(tb, {'x'}, lattice_constant=1) + lat = next(iter(builder.sites()))[0] + assert -1j * p['t'] == builder[lat(0), lat(1)](None, None, **p) + assert +1j * p['t'] == builder[lat(1), lat(0)](None, None, **p) + + +def test_numeric_functions_not_discrete_coords(): + builder = discretize('k_y + y', 'x') + lat = next(iter(builder.sites()))[0] + onsite = builder[lat(0)] + + assert onsite(None, k_y=2, y=1) == 2 + 1 + + +def test_numeric_functions_with_pi(): + # Two cases because once it is casted + # to complex, one there is a function created + + builder = discretize('A + pi', 'x') + lat = next(iter(builder.sites()))[0] + onsite = builder[lat(0)] + assert onsite(None, A=1) == 1 + np.pi + + + builder = discretize('pi', 'x') + lat = next(iter(builder.sites()))[0] + onsite = builder[lat(0)] + assert onsite == np.pi + + +def test_numeric_functions_basic_string(): + for i in [0, 1, 3, 5]: + builder = discretize(i, {'x'}) + lat = next(iter(builder.sites()))[0] + assert builder[lat(0)] == i + + p = dict(t=i) + + tb = {(0,): "2*t", (1,): "-t"} + builder = build_discretized(tb, {'x'}, lattice_constant=1) + lat = next(iter(builder.sites()))[0] + assert 2*p['t'] == builder[lat(0)](None, **p) + assert -p['t'] == builder[lat(1), lat(0)](None, None, **p) + + tb = {(0,): "0", (1,): "-1j * t"} + builder = build_discretized(tb, {'x'}, lattice_constant=1) + lat = next(iter(builder.sites()))[0] + assert -1j * p['t'] == builder[lat(0), lat(1)](None, None, **p) + assert +1j * p['t'] == builder[lat(1), lat(0)](None, None, **p) + + tb = {(0,): "0", (-1,): "+1j * t"} + builder = build_discretized(tb, {'x'}, lattice_constant=1) + lat = next(iter(builder.sites()))[0] + assert -1j * p['t'] == builder[lat(0), lat(1)](None, None, **p) + assert +1j * p['t'] == builder[lat(1), lat(0)](None, None, **p) + + +def test_numeric_functions_advance(): + hams = [ + kx**2, + kx**2 + x, + A(x), + kx*A(x)*kx, + sympy.Matrix([[kx * A(x) * kx, A(x)*kx], [kx*A(x), A(x)+B]]), + kx**2 + B * x, + 'k_x**2 + sin(x)', + B ** 0.5 * kx**2, + B ** (1/2) * kx**2, + sympy.sqrt(B) * kx**2, + + ] + for hamiltonian in hams: + for a in [1, 2, 5]: + for fA in [lambda x: x, lambda x: x**2, lambda x: x**3]: + symbolic, coords = discretize_symbolic(hamiltonian, {'x'}) + builder = build_discretized(symbolic, coords, lattice_constant=a) + lat = next(iter(builder.sites()))[0] + + p = dict(A=fA, B=5, sin=np.sin) + + # test onsite + v = symbolic.pop((0,)).subs({sympy.symbols('a'): a, B: p['B']}) + f_sym = sympy.lambdify(['A', 'x'], v) + f_num = builder[lat(0)] + + if callable(f_num): + f_num = swallows_extra_kwargs(f_num) + for n in range(-100, 100, 10): + assert np.allclose(f_sym(fA, a*n), f_num(lat(n), **p)) + else: + for n in range(-100, 100, 10): + assert np.allclose(f_sym(fA, a*n), f_num) + + + # test hoppings + for k, v in symbolic.items(): + v = v.subs({sympy.symbols('a'): a, B: p['B']}) + f_sym = sympy.lambdify(['A', 'x'], v) + f_num = builder[lat(0), lat(k[0])] + + if callable(f_num): + f_num = swallows_extra_kwargs(f_num) + for n in range(10): + lhs = f_sym(fA, a * n) + rhs = f_num(lat(n), lat(n+k[0]), **p) + assert np.allclose(lhs, rhs) + else: + for n in range(10): + lhs = f_sym(fA, a * n) + rhs = f_num + assert np.allclose(lhs, rhs) + + +def test_numeric_functions_with_parameter(): + hams = [ + kx**2 + A(B, x) + ] + for hamiltonian in hams: + for a in [1, 2, 5]: + for fA in [lambda c, x: x+c, lambda c, x: x**2 + c]: + symbolic, coords = discretize_symbolic(hamiltonian, {'x'}) + builder = build_discretized(symbolic, coords, lattice_constant=a) + lat = next(iter(builder.sites()))[0] + + p = dict(A=fA, B=5) + + # test onsite + v = symbolic.pop((0,)).subs({sympy.symbols('a'): a, B: p['B']}) + f_sym = sympy.lambdify(['A', 'x'], v) + + f_num = builder[lat(0)] + if callable(f_num): + f_num = swallows_extra_kwargs(f_num) + + + for n in range(10): + s = lat(n) + xi = a * n + if callable(f_num): + assert np.allclose(f_sym(fA, xi), f_num(s, **p)) + else: + assert np.allclose(f_sym(fA, xi), f_num) + + + # test hoppings + for k, v in symbolic.items(): + v = v.subs({sympy.symbols('a'): a, B: p['B']}) + f_sym = sympy.lambdify(['A', 'x'], v) + f_num = builder[lat(0), lat(k[0])] + + if callable(f_num): + f_num = swallows_extra_kwargs(f_num) + + for n in range(10): + s = lat(n) + xi = a * n + + lhs = f_sym(fA, xi) + if callable(f_num): + rhs = f_num(lat(n), lat(n+k[0]), **p) + else: + rhs = f_num + + assert np.allclose(lhs, rhs) + + +def test_basic_verbose(capsys): # or use "capfd" for fd-level + discretize('k_x * A(x) * k_x', verbose=True) + out, err = capsys.readouterr() + assert "Discrete coordinates set to" in out + assert "Function generated for (0,)" in out + + +def test_that_verbose_covers_all_hoppings(capsys): + discretize('k_x**2 + k_y**2 + k_x*k_y', verbose=True) + out, err = capsys.readouterr() + + for tag in [(0, 1), (0, 0), (1, -1), (1, 1)]: + assert "Function generated for {}".format(tag) in out + + +def test_verbose_cache(capsys): + discretize('[[k_x * A(x) * k_x]]', verbose=True) + out, err = capsys.readouterr() + assert '_cache_0' in out + + +def test_no_output_when_verbose_false(capsys): + discretize('[[k_x * A(x) * k_x]]', verbose=False) + out, err = capsys.readouterr() + assert out == ''