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/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 == ''