diff --git a/AUTHORS.rst b/AUTHORS.rst index 43f37b31613b1670f262012847c73ad3acb426fd..3a39755ad20e78b5eaa7daffb2ad1aeb0856bc74 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,6 +15,7 @@ The principal developers of Kwant are Contributors to Kwant include * Jörg Behrmann (FU Berlin) +* Paul Clisson (CEA Grenoble) * Mathieu Istas (CEA Grenoble) * Daniel Jaschke (CEA Grenoble) * Bas Nijholt (TU Delft) @@ -25,6 +26,7 @@ Contributors to Kwant include * Sebastian Rubbert (TU Delft) * Rafał Skolasiński (TU Delft) * Adrien Sorgniard (CEA Grenoble) +* Dániel Varjas (TU Delft) We thank Christoph Gohlke for the creation of installers for Microsoft Windows. diff --git a/doc/source/code/figure/closed_system.py.diff b/doc/source/code/figure/closed_system.py.diff index 7f1de4715780c3c04f7ccf668301027617fff545..21f979f983eae19bf11dd35eab3e7d3d02a594bf 100644 --- a/doc/source/code/figure/closed_system.py.diff +++ b/doc/source/code/figure/closed_system.py.diff @@ -1,4 +1,4 @@ -@@ -1,140 +1,157 @@ +@@ -1,144 +1,162 @@ # Tutorial 2.4.2. Closed systems # ============================== # @@ -94,14 +94,18 @@ + fig.savefig("closed_system_result." + extension, dpi=_defs.dpi) #HIDDEN_END_yvri + def sorted_eigs(ev): + evals, evecs = ev + evals, evecs = map(np.array, zip(*sorted(zip(evals, evecs.transpose())))) + return evals, evecs.transpose() #HIDDEN_BEGIN_wave - def plot_wave_function(syst): + def plot_wave_function(syst, B=0.001): + size = (_defs.figwidth_in, _defs.figwidth_in) + # Calculate the wave functions in the system. - ham_mat = syst.hamiltonian_submatrix(sparse=True) - evecs = sla.eigsh(ham_mat, k=20, which='SM')[1] + ham_mat = syst.hamiltonian_submatrix(sparse=True, args=[B]) + evals, evecs = sorted_eigs(sla.eigsh(ham_mat, k=20, which='SM')) # Plot the probability density of the 10th eigenmode. - kwant.plotter.map(syst, np.abs(evecs[:, 9])**2, @@ -115,16 +119,16 @@ #HIDDEN_BEGIN_current - def plot_current(syst): + def plot_current(syst, B=0.001): + size = (_defs.figwidth_in, _defs.figwidth_in) + # Calculate the wave functions in the system. - ham_mat = syst.hamiltonian_submatrix(sparse=True) - evecs = sla.eigsh(ham_mat, k=20, which='SM')[1] + ham_mat = syst.hamiltonian_submatrix(sparse=True, args=[B]) + evals, evecs = sorted_eigs(sla.eigsh(ham_mat, k=20, which='SM')) # Calculate and plot the local current of the 10th eigenmode. J = kwant.operator.Current(syst) - current = J(evecs[:, 9]) + current = J(evecs[:, 9], args=[B]) - kwant.plotter.current(syst, current, colorbar=False) + for extension in ('pdf', 'png'): + kwant.plotter.current( diff --git a/doc/source/code/figure/spin_orbit.py.diff b/doc/source/code/figure/spin_orbit.py.diff index ba32e81221f498d1b7057502b566632fbdf202df..794c1b5b755e22ea1a175c40a9078bd3e5b3e12b 100644 --- a/doc/source/code/figure/spin_orbit.py.diff +++ b/doc/source/code/figure/spin_orbit.py.diff @@ -34,10 +34,10 @@ #HIDDEN_END_hwbt - def make_system(a=1, t=1.0, alpha=0.5, e_z=0.08, W=10, L=30): + def make_system(t=1.0, alpha=0.5, e_z=0.08, W=10, L=30): # Start with an empty tight-binding system and a single square lattice. # `a` is the lattice constant (by default set to 1 for simplicity). - lat = kwant.lattice.square(a) + lat = kwant.lattice.square() syst = kwant.Builder() @@ -47,23 +47,23 @@ 4 * t * sigma_0 + e_z * sigma_z # hoppings in x-direction syst[kwant.builder.HoppingKind((1, 0), lat, lat)] = \ - -t * sigma_0 - 1j * alpha * sigma_y + -t * sigma_0 + 1j * alpha * sigma_y / 2 # hoppings in y-directions syst[kwant.builder.HoppingKind((0, 1), lat, lat)] = \ - -t * sigma_0 + 1j * alpha * sigma_x + -t * sigma_0 - 1j * alpha * sigma_x / 2 #HIDDEN_END_uxrm #### Define the left lead. #### - lead = kwant.Builder(kwant.TranslationalSymmetry((-a, 0))) + lead = kwant.Builder(kwant.TranslationalSymmetry((-1, 0))) #HIDDEN_BEGIN_yliu lead[(lat(0, j) for j in range(W))] = 4 * t * sigma_0 + e_z * sigma_z # hoppings in x-direction lead[kwant.builder.HoppingKind((1, 0), lat, lat)] = \ - -t * sigma_0 - 1j * alpha * sigma_y + -t * sigma_0 + 1j * alpha * sigma_y / 2 # hoppings in y-directions lead[kwant.builder.HoppingKind((0, 1), lat, lat)] = \ - -t * sigma_0 + 1j * alpha * sigma_x + -t * sigma_0 - 1j * alpha * sigma_x / 2 #HIDDEN_END_yliu #### Attach the leads and return the finalized system. #### diff --git a/doc/source/tutorial/faq.rst b/doc/source/tutorial/faq.rst index 3833f4743bdd9450f051a426c3ad745ee9005fa1..1022bf03982fa419a95faba812013ee02f2771d4 100644 --- a/doc/source/tutorial/faq.rst +++ b/doc/source/tutorial/faq.rst @@ -434,7 +434,12 @@ Scattering states calculated using `~kwant.solvers.default.wave_function` are re same order as the "incoming" modes of `~kwant.system.InfiniteSystem.modes`. Kwant considers that the translational symmetry of a lead points "towards infinity" (*not* towards the system) which means that the incoming modes are -those that have *negative* velocities: +those that have *negative* velocities. + +This means that for a lead attached on the left of a scattering region (with +symmetry vector $(-1, 0)$, for example), the +positive $k$ direction (when inspecting the lead's band structure) actually +corresponds to the *negative* $x$ direction. How does Kwant order components of an individual wavefunction? diff --git a/doc/source/tutorial/first_steps.rst b/doc/source/tutorial/first_steps.rst index 9a74435f0ef9fb3784e6b638893c1e7dc2545900..1ddce048c4da222856413471aeb452390ed3e7c7 100644 --- a/doc/source/tutorial/first_steps.rst +++ b/doc/source/tutorial/first_steps.rst @@ -315,12 +315,12 @@ subbands that increases with energy. - Instead of plotting to the screen (which is standard) `~kwant.plotter.plot` can also write to a file specified by the argument - ``file``. For the plotting to the screen to work the module - ``matplotlib.pyplot`` has to be imported. (An informative error message - will remind you if you forget.) The reason for this is pretty technical: - matplotlib's "backend" can only be chosen before ``matplotlib.pyplot`` has - been imported. Would Kwant import that module by itself, it would deprive - you of the possibility to choose a non-default backend later. + ``file``. + + - Due to matplotlib's limitations, Kwant's plotting routines have the + side effect of fixing matplotlib's "backend". If you would like to choose + a different backend than the standard one, you must do so before asking + Kwant to plot anything. .. rubric:: Footnotes diff --git a/kwant/_common.py b/kwant/_common.py index 3af56a12b2618f2cb7f7ba550e0adf13223a6ced..76e952cf6c0d2ffc437d42ca5c093a5825aac0e9 100644 --- a/kwant/_common.py +++ b/kwant/_common.py @@ -9,6 +9,8 @@ import numpy as np import numbers import inspect +import warnings +from contextlib import contextmanager __all__ = ['KwantDeprecationWarning', 'UserCodeError'] @@ -87,6 +89,14 @@ def ensure_rng(rng=None): "numpy.random.RandomState interface.") +@contextmanager +def reraise_warnings(level=3): + with warnings.catch_warnings(record=True) as caught_warnings: + yield + for warning in caught_warnings: + warnings.warn(warning.message, stacklevel=level) + + def get_parameters(func): """Get the names of the parameters to 'func' and whether it takes kwargs. diff --git a/kwant/builder.py b/kwant/builder.py index d2af7e47dcdee9c5b641ef7ec78ff1f742eacb44..1418b40ae86678d91f64034dd58a7669725c24d1 100644 --- a/kwant/builder.py +++ b/kwant/builder.py @@ -20,7 +20,7 @@ from . import system, graph, KwantDeprecationWarning, UserCodeError from .linalg import lll from .operator import Density from .physics import DiscreteSymmetry -from ._common import ensure_isinstance, get_parameters +from ._common import ensure_isinstance, get_parameters, reraise_warnings __all__ = ['Builder', 'Site', 'SiteFamily', 'SimpleSiteFamily', 'Symmetry', @@ -393,6 +393,8 @@ class NoSymmetry(Symmetry): class HoppingKind(tuple): """A pattern for matching hoppings. + An alias exists for this common name: ``kwant.HoppingKind``. + A hopping ``(a, b)`` matches precisely when the site family of ``a`` equals `family_a` and that of ``b`` equals `family_b` and ``(a.tag - b.tag)`` is equal to `delta`. In other words, the matching hoppings have the form: @@ -442,6 +444,17 @@ class HoppingKind(tuple): else: ensure_isinstance(family_b, SiteFamily) family_b = family_b + + try: + Site(family_b, family_a.normalize_tag(delta) - delta) + except Exception as e: + same_fams = family_b is family_a + msg = (str(family_a), + 'and {} are'.format(family_b) if not same_fams else ' is', + 'not compatible with delta={}'.format(delta), + ) + raise ValueError(' '.join(msg)) from e + return tuple.__new__(cls, (delta, family_a, family_b)) def __call__(self, builder): @@ -700,6 +713,8 @@ def _site_ranges(sites): class Builder: """A tight binding system defined on a graph. + An alias exists for this common name: ``kwant.Builder``. + This is one of the central types in Kwant. It is used to construct tight binding systems in a flexible way. @@ -1514,8 +1529,9 @@ class Builder: if hop_range > 1: # Automatically increase the period, potentially warn the user. new_lead = Builder(sym.subgroup((hop_range,))) - new_lead.fill(lead_builder, lambda site: True, - lead_builder.sites(), max_sites=float('inf')) + with reraise_warnings(): + new_lead.fill(lead_builder, lambda site: True, + lead_builder.sites(), max_sites=float('inf')) lead_builder = new_lead sym = lead_builder.symmetry H = lead_builder.H @@ -1565,8 +1581,9 @@ class Builder: # system (this one is guaranteed to contain a complete unit cell of the # lead). After flood-fill we remove that domain. start = {sym.act((max_dom + 1,), site) for site in H} - all_added = self.fill(lead_builder, shape, start, - max_sites=float('inf')) + with reraise_warnings(): + all_added = self.fill(lead_builder, shape, start, + max_sites=float('inf')) all_added = [site for site in all_added if site not in start] del self[start] @@ -1614,6 +1631,18 @@ class Builder: raise ValueError('Currently, only builders without or with a 1D ' 'translational symmetry can be finalized.') + # Protect novice users from confusing error messages if they + # forget to finalize their Builder. + + @staticmethod + def _require_system(*args, **kwargs): + """You need a finalized system; Use Builder.finalized() first.""" + raise TypeError('You need a finalized system; ' + 'use Builder.finalized() first.') + + hamiltonian = hamiltonian_submatrix = modes = selfenergy = \ + inter_cell_hopping = cell_hamiltonian = precalculated = \ + _require_system ################ Finalized systems diff --git a/kwant/continuum/_common.py b/kwant/continuum/_common.py index cb0effb8d12f634f1899cde20a8eb3a846e68b8d..ee9285bc99e83a87dddda983b4daedfd9a6c149e 100644 --- a/kwant/continuum/_common.py +++ b/kwant/continuum/_common.py @@ -21,6 +21,11 @@ from sympy.physics.matrices import msigma as _msigma import warnings +from .._common import reraise_warnings + +# TODO: remove when sympy correctly includes MutableDenseMatrix (lol). +sympy_classes = set(sympy_classes) | {sympy.MutableDenseMatrix} + momentum_operators = sympy.symbols('k_x k_y k_z', commutative=False) position_operators = sympy.symbols('x y z', commutative=False) @@ -71,7 +76,8 @@ def lambdify(expr, locals=None): [ 0. , 0. ]]) """ - expr = sympify(expr, locals) + with reraise_warnings(level=4): + expr = sympify(expr, locals) args = [s.name for s in expr.atoms(sympy.Symbol)] args += [str(f.func) for f in expr.atoms(AppliedUndef, sympy.Function)] @@ -149,8 +155,10 @@ def sympify(expr, locals=None): # if ``expr`` is already a ``sympy`` object we may terminate a code path if isinstance(expr, tuple(sympy_classes)): if locals: - warnings.warn('Input expression is already SymPy object: ' + - '"locals" will not be used.', RuntimeWarning) + warnings.warn('Input expression is already SymPy object: ' + '"locals" will not be used.', + RuntimeWarning, + stacklevel=2) return expr # if ``expr`` is not a "sympy" then we proceed with sympifying process diff --git a/kwant/continuum/discretizer.py b/kwant/continuum/discretizer.py index 31a0c29b1318aa02dcd82d45e76513f48c628a26..2560063cf1656a30bccea464966b2711a78a9586 100644 --- a/kwant/continuum/discretizer.py +++ b/kwant/continuum/discretizer.py @@ -6,6 +6,7 @@ # the file AUTHORS.rst at the top-level directory of this distribution and at # http://kwant-project.org/authors. +from keyword import iskeyword from collections import defaultdict import itertools import warnings @@ -22,6 +23,7 @@ from sympy.core.function import AppliedUndef from .. import builder, lattice from .. import KwantDeprecationWarning +from .._common import reraise_warnings from ._common import (sympify, gcd, position_operators, momentum_operators, monomials) @@ -67,7 +69,7 @@ class _DiscretizedBuilder(builder.Builder): else: a, b = key assert a is site - result.extend(["# Hopping in direction ", + result.extend(["# Hopping from ", str(tuple(b.tag)), ":\n"]) result.append(val._source if callable(val) else repr(val)) @@ -190,10 +192,11 @@ def discretize_symbolic(hamiltonian, coords=None, *, locals=None): The coordinates that have been discretized. """ - hamiltonian = sympify(hamiltonian, locals) + with reraise_warnings(): + hamiltonian = sympify(hamiltonian, locals) atoms_names = [s.name for s in hamiltonian.atoms(sympy.Symbol)] - if any( s == 'a' for s in atoms_names): + if any(s == 'a' for s in atoms_names): raise TypeError("'a' is a symbol used internally to represent " "grid spacing; please use a different symbol.") @@ -311,8 +314,9 @@ def build_discretized(tb_hamiltonian, coords, *, grid=None, locals=None, raise ValueError("The argument 'coords' must be sorted.") # run sympifcation on hamiltonian values - for k, v in tb_hamiltonian.items(): - tb_hamiltonian[k] = sympify(v, locals) + with reraise_warnings(): + for k, v in tb_hamiltonian.items(): + tb_hamiltonian[k] = sympify(v, locals) # generate grid if required, check constraints if provided random_element = next(iter(tb_hamiltonian.values())) @@ -644,7 +648,16 @@ def _builder_value(expr, coords, grid_spacing, onsite, # constants and functions in the sympy input will be passed # as arguments to the value function arg_names = set.union({s.name for s in const_symbols}, - {str(k.func) for k in map_func_calls}) + {str(k.func) for k in map_func_calls}) + + # check if all argument names are valid python identifiers + for name in arg_names: + if not (name.isidentifier() and not iskeyword(name)): + raise ValueError("Invalid name in used symbols: {}\n" + "Names of symbols used in Hamiltonian " + "must be valid Python identifiers and " + "may not be keywords".format(name)) + arg_names = ', '.join(sorted(arg_names)) if (not arg_names) and (coords is None): diff --git a/kwant/continuum/tests/test_discretizer.py b/kwant/continuum/tests/test_discretizer.py index 7511e5a132daea58cd86365714ff4dd31dcae8dd..56ff8242da1d9932573167cc8158d04f5e18ae1f 100644 --- a/kwant/continuum/tests/test_discretizer.py +++ b/kwant/continuum/tests/test_discretizer.py @@ -570,3 +570,9 @@ def test_grid_input(ham, grid_offset, offset, norbs): def test_grid_constraints(ham, coords, grid): with pytest.raises(ValueError): discretize(ham, coords, grid=grid) + + +@pytest.mark.parametrize('name', ['1', '1a', '-a', '+a', 'while', 'for']) +def test_check_symbol_names(name): + with pytest.raises(ValueError): + discretize(sympy.Symbol(name), 'x') diff --git a/kwant/lattice.py b/kwant/lattice.py index 8cf0d6a20065339ee389de2eb7cc2cc23cad074e..e050552c76e1a7503d4058d08c3c0f32cc660c02 100644 --- a/kwant/lattice.py +++ b/kwant/lattice.py @@ -504,8 +504,9 @@ class Monatomic(builder.SiteFamily, Polyatomic): # point precision issues. class TranslationalSymmetry(builder.Symmetry): - """ - A translational symmetry defined in real space. + """A translational symmetry defined in real space. + + An alias exists for this common name: ``kwant.TranslationalSymmetry``. Group elements of this symmetry are integer tuples of appropriate length. diff --git a/kwant/linalg/mumps.py b/kwant/linalg/mumps.py index 397b4cc963bc9f9d593bce12fcd2c41904bc5912..465ef1aa359ab3c88adbf5a4a4ccf125c73094dd 100644 --- a/kwant/linalg/mumps.py +++ b/kwant/linalg/mumps.py @@ -299,7 +299,9 @@ class MUMPSContext: if reuse_analysis: if self.mumps_instance is None: warnings.warn("Missing analysis although reuse_analysis=True. " - "New analysis is performed.", RuntimeWarning) + "New analysis is performed.", + RuntimeWarning, + stacklevel=2) self.analyze(a, ordering=ordering, overwrite_a=overwrite_a) else: dtype, row, col, data = _make_assembled_from_coo(a, diff --git a/kwant/operator.pyx b/kwant/operator.pyx index 8fc66e81c5e856b274e2e422f7466e38fbe61952..2787b28171504499c718df51cc7f0f4b106bbce5 100644 --- a/kwant/operator.pyx +++ b/kwant/operator.pyx @@ -428,7 +428,9 @@ cdef class _LocalOperator: If True, checks that ``onsite``, as well as any relevant parts of the Hamiltonian are hermitian. sum : bool, default: False - If True, then calling this operator will return a single scalar. + If True, then calling this operator will return a single scalar, + otherwise a vector will be returned (see + `~kwant.operator._LocalOperator.__call__` for details). """ cdef public int check_hermiticity, sum @@ -468,14 +470,27 @@ cdef class _LocalOperator: >>> A(phi, psi) - to compute the matrix element :math:`\bra{φ} A \ket{ψ}`. Note that - these quantities may be vectors (e.g. *local* charge or current - density). - - For an operator :math:`Q_{iαβ}`, ``bra`` :math:`φ_α` and - ``ket`` :math:`ψ_β` this computes :math:`q_i = ∑_{αβ} φ^*_α Q_{iαβ} - ψ_β` if ``self.sum`` is False, otherwise computes :math:`q = ∑_{iαβ} - φ^*_α Q_{iαβ} ψ_β`. + to compute the matrix element :math:`\bra{φ} A \ket{ψ}`. + + If ``sum=True`` was provided when constructing the operator, then + a scalar is returned. If ``sum=False``, then a vector is returned. + The vector is defined over the sites of the system if the operator + is a `~kwant.operator.Density`, or over the hoppings if it is a + `~kwant.operator.Current` or `~kwant.operator.Source`. By default, + the returned vector is ordered in the same way as the sites + (for `~kwant.operator.Density`) or hoppings in the graph of the + system (for `~kwant.operator.Current` or `~kwant.operator.Density`). + If the keyword parameter ``where`` was provided when constructing + the operator, then the returned vector is instead defined only over + the sites or hoppings specified, and is ordered in the same way + as ``where``. + + Alternatively stated, for an operator :math:`Q_{iαβ}`, ``bra`` + :math:`φ_α` and ``ket`` :math:`ψ_β` this computes + :math:`q_i = ∑_{αβ} φ^*_α Q_{iαβ} ψ_β` if ``self.sum`` is False, + otherwise computes :math:`q = ∑_{iαβ} φ^*_α Q_{iαβ} ψ_β`. where + :math:`i` runs over all sites or hoppings, and + :math:`α` and :math:`β` run over all the degrees of freedom. Parameters ---------- @@ -681,8 +696,8 @@ cdef class Density(_LocalOperator): """An operator for calculating general densities. An instance of this class can be called like a function to evaluate the - expectation value with a wavefunction. See the documentation of the - ``__call__`` method for more details. + expectation value with a wavefunction. See + `~kwant.operator.Density.__call__` for details. Parameters ---------- @@ -703,7 +718,9 @@ cdef class Density(_LocalOperator): Hermitian, then an error will be raised when the operator is evaluated. sum : bool, default: False - If True, then calling this operator will return a single scalar. + If True, then calling this operator will return a single scalar, + otherwise a vector will be returned (see + `~kwant.operator.Density.__call__` for details). Notes ----- @@ -818,8 +835,8 @@ cdef class Current(_LocalOperator): r"""An operator for calculating general currents. An instance of this class can be called like a function to evaluate the - expectation value with a wavefunction. See the documentation of the - ``__call__`` method for more details. + expectation value with a wavefunction. See + `~kwant.operator.Current.__call__` for details. Parameters ---------- @@ -842,7 +859,9 @@ cdef class Current(_LocalOperator): is not Hermitian, then an error will be raised when the operator is evaluated. sum : bool, default: False - If True, then calling this operator will return a single scalar. + If True, then calling this operator will return a single scalar, + otherwise a vector will be returned (see + `~kwant.operator.Current.__call__` for details). Notes ----- @@ -945,8 +964,8 @@ cdef class Source(_LocalOperator): """An operator for calculating general sources. An instance of this class can be called like a function to evaluate the - expectation value with a wavefunction. See the documentation of the - ``__call__`` method for more details. + expectation value with a wavefunction. See + `~kwant.operator.Source.__call__` for details. Parameters ---------- @@ -968,7 +987,9 @@ cdef class Source(_LocalOperator): Hermitian, then an error will be raised when the operator is evaluated. sum : bool, default: False - If True, then calling this operator will return a single scalar. + If True, then calling this operator will return a single scalar, + otherwise a vector will be returned (see + `~kwant.operator.Source.__call__` for details). Notes ----- diff --git a/kwant/physics/leads.py b/kwant/physics/leads.py index 4fcdd73ba925a8ff27cf0071cd4fc48ee4d4728c..b156f0292c3feafbc0a718d4ca80b73e97786533 100644 --- a/kwant/physics/leads.py +++ b/kwant/physics/leads.py @@ -121,6 +121,9 @@ class PropagatingModes: velocity, `k` is momentum and `conserved_quantity` is the conservation law eigenvalue. + In the above, the positive velocity and momentum directions are defined + with respect to the translational symmetry direction of the system. + The first dimension of `wave_functions` corresponds to the orbitals of all the sites in a unit cell, the second one to the number of the mode. Each mode is normalized to carry unit current. If several modes have the same @@ -490,7 +493,7 @@ def phs_symmetrization(wfs, particle_hole): ---------- wfs : numpy array A matrix of propagating wave functions at a TRIM that all have the same - velocity. The wave functions form the columns of this matrix. + velocity. The orthonormal wave functions form the columns of this matrix. particle_hole : numpy array The matrix representation of the unitary part of the particle-hole operator, expressed in the tight binding basis. @@ -508,26 +511,44 @@ def phs_symmetrization(wfs, particle_hole): """Apply the particle-hole operator to an array. """ return particle_hole.dot(mat.conj()) - # P always squares to 1 or -1. - P_squared = np.sign(particle_hole[0,:].dot(particle_hole[:,0].conj())) - # np.sign returns the same data type as its argument. Make sure - # that the comparison with integers is okay. - assert P_squared in (-1, 1) + # Take P in the subspace of W = wfs: U = W^+ @ P @ W^*. + U = wfs.T.conj().dot(Pdot(wfs)) + # Check that wfs are orthonormal and the space spanned + # by them is closed under ph, meaning U is unitary. + if not np.allclose(U.dot(U.T.conj()), np.eye(U.shape[0])): + raise ValueError('wfs are not orthonormal or not closed under particle_hole.') + P_squared = U.dot(U.conj()) + if np.allclose(P_squared, np.eye(U.shape[0])): + P_squared = 1 + elif np.allclose(P_squared, -np.eye(U.shape[0])): + P_squared = -1 + else: + raise ValueError('particle_hole must square to +1 or -1. P_squared = {}'.format(P_squared)) if P_squared == 1: - # Make particle hole eigenstates. - # Phase factor ensures they are not numerically close. - phases = np.diag([np.exp(1j*np.angle(wf.T.conj().dot( - Pdot(wf)))*0.5) for wf in wfs.T]) - new_wfs = wfs.dot(phases) + Pdot(wfs.dot(phases)) - # Orthonormalize the modes using QR on the matrix of eigenstates of P. - # So long as the matrix of coefficients R is purely real, any linear - # combination of these modes remains an eigenstate of P. From the way - # we construct eigenstates of P, the coefficients of R are real. - new_wfs, r = la.qr(new_wfs, mode='economic', pivoting=True)[:2] - if not np.allclose(r.imag, np.zeros(r.shape)): # skip coverage - raise RuntimeError("Numerical instability in finding particle-hole \ - symmetric modes.") + # Use the matrix square root method from + # Applied Mathematics and Computation 234 (2014) 380-384. + assert np.allclose(U, U.T) + # Schur decomposition guarantees that vecs are orthonormal. + vals, vecs = la.schur(U) + # U should be normal, so vals is diagonal. + assert np.allclose(np.diag(np.diag(vals)), vals) + vals = np.diag(vals) + # Need to take safe square root of U, the branch cut should not go + # through any eigenvalues. Otherwise the square root may not be symmetric. + # Find largest gap between eigenvalues + phases = np.sort(np.angle(vals)) + dph = np.append(np.diff(phases), phases[0] + 2*np.pi - phases[-1]) + i = np.argmax(dph) + shift = -np.pi - (phases[i] + dph[i]/2) + # Take matrix square root with branch cut in largest gap + vals = np.sqrt(vals * np.exp(1j * shift)) * np.exp(-0.5j * shift) + sqrtU = vecs.dot(np.diag(vals)).dot(vecs.T.conj()) + # For symmetric U sqrt(U) is also symmetric. + assert np.allclose(sqrtU, sqrtU.T) + # We want a new basis W_new such that W_new^+ @ P @ W_new^* = 1. + # This is achieved by W_new = W @ sqrt(U). + new_wfs = wfs.dot(sqrtU) # If P^2 = 1, there is no need to sort the modes further. TRIM_sort = np.zeros((wfs.shape[1],), dtype=int) else: @@ -540,7 +561,7 @@ def phs_symmetrization(wfs, particle_hole): # If there are only two modes in this subspace, they are orthogonal # so we replace the second one with the P applied to the first one. if N_modes == 2: - wf = wfs[:,0] + wf = wfs[:, 0] # Store psi_n and P psi_n. new_wfs.append(wf) new_wfs.append(Pdot(wf)) @@ -553,22 +574,22 @@ def phs_symmetrization(wfs, particle_hole): for i in iterations: # Take a mode psi_n from the basis - the first column # of the matrix of remaining modes. - wf = wfs[:,0] + wf = wfs[:, 0] # Store psi_n and P psi_n. new_wfs.append(wf) P_wf = Pdot(wf) new_wfs.append(P_wf) # Remove psi_n and P psi_n from the basis matrix of modes. # First remove psi_n. - wfs = wfs[:,1:] + wfs = wfs[:, 1:] # Now we project the remaining modes onto the orthogonal - # complement of P psi_n. Projector: - Projector = wfs.dot(wfs.T.conj()) - \ + # complement of P psi_n. projector: + projector = wfs.dot(wfs.T.conj()) - \ np.outer(P_wf, P_wf.T.conj()) # After the projection, the mode matrix is rank deficient - # the span of the column space has dimension one less than # the number of columns. - wfs = Projector.dot(wfs) + wfs = projector.dot(wfs) wfs = la.qr(wfs, mode='economic', pivoting=True)[0] # Remove the redundant column. wfs = wfs[:, :-1] @@ -577,13 +598,12 @@ def phs_symmetrization(wfs, particle_hole): # the projection. if i == iterations[-1]: assert wfs.shape[1] == 2 - wf = wfs[:,0] + wf = wfs[:, 0] # Store psi_n and P psi_n. new_wfs.append(wf) - P_wf = Pdot(wf) - new_wfs.append(P_wf) + new_wfs.append(Pdot(wf)) assert np.allclose(wfs.T.conj().dot(wfs), - np.eye(wfs.shape[1])) + np.eye(wfs.shape[1])) new_wfs = np.hstack([col.reshape(len(col), 1)/npl.norm(col) for col in new_wfs]) assert np.allclose(new_wfs[:, 1::2], Pdot(new_wfs[:, ::2])) diff --git a/kwant/physics/tests/test_leads.py b/kwant/physics/tests/test_leads.py index cee31e82436cc45390e524b9e7fe0ade08e695c6..bc41f671811bd6c2cdae99d48c7b82fa8ebeeb3d 100644 --- a/kwant/physics/tests/test_leads.py +++ b/kwant/physics/tests/test_leads.py @@ -638,15 +638,11 @@ def test_PHS_TRIM(): for nmodes in (1, 3, n//4, n//2, n): # Random matrix of 'modes.' Take part of a unitary # matrix to ensure that the modes form a basis. - modes = (rng.random_sample((n, n)) - + 1j*rng.random_sample((n, n))) - modes = la.expm(1j*(modes - + modes.T.conj()))[:n, :nmodes] + modes = kwant.rmt.circular(n, 'A', rng=rng)[:, :nmodes] # Ensure modes are particle-hole symmetric and - # normalized + # orthonormal modes = modes + p_mat.dot(modes.conj()) - modes = np.array([col/np.linalg.norm(col) for col - in modes.T]).T + modes = la.qr(modes, mode='economic')[0] # Mix the modes with a random unitary transformation U = kwant.rmt.circular(nmodes, 'A', rng=rng) modes = modes.dot(U) @@ -666,18 +662,14 @@ def test_PHS_TRIM(): for nmodes in (2, 4, n//2, n): # Random matrix of 'modes.' Take part of a unitary # matrix to ensure that the modes form a basis. - modes = rng.rand(n, n) + 1j * rng.rand(n, n) - modes = la.expm(1j*(modes + - modes.T.conj()))[:n, :nmodes] + modes = kwant.rmt.circular(n, 'A', rng=rng)[:, :nmodes] # Ensure modes are particle-hole symmetric and # orthonormal. modes[:, nmodes//2:] = \ p_mat.dot(modes[:, :nmodes//2].conj()) modes = la.qr(modes, mode='economic')[0] # Mix the modes with a random unitary transformation - U = (rng.random_sample((nmodes, nmodes)) - + 1j*rng.random_sample((nmodes, nmodes))) - U = la.expm(1j*(U + U.T.conj())) + U = kwant.rmt.circular(nmodes, 'A', rng=rng) modes = modes.dot(U) # Make the modes PHS symmetric using the method for a # TRIM. @@ -690,7 +682,30 @@ def test_PHS_TRIM(): np.eye(phs_modes.shape[1]), err_msg='Modes are not orthonormal,' ' TRIM PHS in ' + sym) - + # Test the off-diagonal case when p_mat = sigma_x + p_mat = np.array([[0, 1], [1, 0]]) + p_mat = np.kron(p_mat, np.identity(n // len(p_mat))) + for nmodes in (1, 3, n//4, n//2): + if nmodes > n//2: + continue + # Random matrix of 'modes.' Take part of a unitary + # matrix to ensure that the modes form a basis, all modes + # are only in half of the space + modes = kwant.rmt.circular(n//2, 'A', rng=rng)[:, :nmodes] + modes = np.vstack((modes, np.zeros((n//2, nmodes)))) + # Add an orthogonal set of vectors that are ph images + modes = np.hstack((modes, p_mat.dot(modes.conj()))) + # Make the modes PHS symmetric using the method for a + # TRIM. + phs_modes = leads.phs_symmetrization(modes, p_mat)[0] + assert_almost_equal(phs_modes, + p_mat.dot(phs_modes.conj()), + err_msg='PHS broken at a TRIM in ' + 'off-diagonal test') + assert_almost_equal(phs_modes.T.conj().dot(phs_modes), + np.eye(phs_modes.shape[1]), + err_msg='Modes are not orthonormal,' + 'off-diagonal test') def random_onsite_hop(n, rng=0): diff --git a/kwant/plotter.py b/kwant/plotter.py index 92998f772ffee6d1ff9a5fcf9f1a882326157eab..b1a2d537e8325bb0be573c90f7d27aa4f84db600 100644 --- a/kwant/plotter.py +++ b/kwant/plotter.py @@ -16,6 +16,7 @@ system in two or three dimensions. """ from collections import defaultdict +import sys import itertools import functools import warnings @@ -35,9 +36,8 @@ try: import matplotlib.cm from matplotlib.figure import Figure from matplotlib import collections - from matplotlib.backends.backend_agg import FigureCanvasAgg from . import _colormaps - mpl_enabled = True + mpl_available = True try: from mpl_toolkits import mplot3d has3d = True @@ -47,7 +47,7 @@ try: except ImportError: warnings.warn("matplotlib is not available, only iterator-providing " "functions will work.", RuntimeWarning) - mpl_enabled = False + mpl_available = False from . import system, builder, _common @@ -77,7 +77,7 @@ def _sample_array(array, n_samples, rng=None): return array[rng.choice(range(la), min(n_samples, la))] -if mpl_enabled: +if mpl_available: class LineCollection(collections.LineCollection): def __init__(self, segments, reflen=None, **kwargs): super().__init__(segments, **kwargs) @@ -285,8 +285,6 @@ if mpl_enabled: self.set_paths(paths[indx]) if len(self.orig_transforms) > 1: - self.transforms = np.resize(self.orig_transforms, - (vs.shape[1],)) self.transforms = self.transforms[indx] lw_orig = self.linewidths_orig @@ -613,10 +611,9 @@ def output_fig(fig, output_mode='auto', file=None, savefile_opts=None, The output mode to be used. Can be one of the following: 'pyplot' : attach the figure to pyplot, with the same behavior as if pyplot.plot was called to create this figure. - 'ipython' : attach a `FigureCanvasAgg` to the figure and return it. - 'return' : return the figure. - 'file' : same as 'ipython', but also save the figure into a file. - 'auto' : if fname is given, save to a file, else if pyplot + 'return' : attach a `FigureCanvasAgg` to the figure and return it. + 'file' : same as 'return', but also save the figure into a file. + 'auto' : if fname is given, save to a file, otherwise like pyplot is imported, attach to pyplot, otherwise just return. See also the notes below. file : string or a file object @@ -634,24 +631,25 @@ def output_fig(fig, output_mode='auto', file=None, savefile_opts=None, matplotlib in that the `dpi` attribute of the figure is used by defaul instead of the matplotlib config setting. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError('matplotlib is not installed.') + + # We import backends and pyplot only at the last possible moment (=now) + # because this has the side effect of selecting the matplotlib backend for + # good. Warn if backend has not been set yet. This check is the same as + # the one performed inside matplotlib.use. + if 'matplotlib.backends' not in sys.modules: + warnings.warn("Kwant's plotting functions have\nthe side effect of " + "selecting the matplotlib backend. To avoid this " + "warning,\nimport matplotlib.pyplot, " + "matplotlib.backends or call matplotlib.use().", + RuntimeWarning, stacklevel=3) + if output_mode == 'auto': - if file is not None: - output_mode = 'file' - else: - try: - matplotlib.pyplot.get_backend() - output_mode = 'pyplot' - except AttributeError: - output_mode = 'pyplot' + output_mode = 'pyplot' if file is None else 'file' if output_mode == 'pyplot': - try: - fake_fig = matplotlib.pyplot.figure() - except AttributeError: - msg = ('matplotlib.pyplot is unavailable. Execute `import ' - 'matplotlib.pyplot` or use a different output mode.') - raise RuntimeError(msg) + from matplotlib import pyplot + fake_fig = pyplot.figure() fake_fig.canvas.figure = fig fig.canvas = fake_fig.canvas for ax in fig.axes: @@ -660,22 +658,19 @@ def output_fig(fig, output_mode='auto', file=None, savefile_opts=None, except AttributeError: pass if show: - matplotlib.pyplot.show() - return fig - elif output_mode == 'return': - canvas = FigureCanvasAgg(fig) - fig.canvas = canvas - return fig - elif output_mode == 'file': - canvas = FigureCanvasAgg(fig) - if savefile_opts is None: - savefile_opts = ([], {}) - if 'dpi' not in savefile_opts[1]: - savefile_opts[1]['dpi'] = fig.dpi - canvas.print_figure(file, *savefile_opts[0], **savefile_opts[1]) - return fig + pyplot.show() + elif output_mode in ['return', 'file']: + from matplotlib.backends.backend_agg import FigureCanvasAgg + fig.canvas = FigureCanvasAgg(fig) + if output_mode == 'file': + if savefile_opts is None: + savefile_opts = ([], {}) + if 'dpi' not in savefile_opts[1]: + savefile_opts[1]['dpi'] = fig.dpi + fig.canvas.print_figure(file, *savefile_opts[0], **savefile_opts[1]) else: - assert False, 'Unknown output_mode' + raise ValueError('Unknown output_mode') + return fig # Extracting necessary data from the system. @@ -979,6 +974,8 @@ def plot(sys, num_lead_cells=2, unit='nn', show=True, dpi=None, fig_size=None, ax=None): """Plot a system in 2 or 3 dimensions. + An alias exists for this common name: ``kwant.plot``. + Parameters ---------- sys : kwant.builder.Builder or kwant.system.FiniteSystem @@ -1114,7 +1111,7 @@ def plot(sys, num_lead_cells=2, unit='nn', its aspect ratio. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError("matplotlib was not found, but is required " "for plot()") @@ -1448,7 +1445,7 @@ def mask_interpolate(coords, values, a=None, method='nearest', oversampling=3): if min_dist < 1e-6 * np.linalg.norm(cmax - cmin): warnings.warn("Some sites have nearly coinciding positions, " "interpolation may be confusing.", - RuntimeWarning) + RuntimeWarning, stacklevel=2) if a is None: a = min_dist @@ -1546,7 +1543,7 @@ def map(sys, value, colorbar=True, cmap=None, vmin=None, vmax=None, a=None, correspond to exactly one pixel. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError("matplotlib was not found, but is required " "for map()") @@ -1567,7 +1564,8 @@ def map(sys, value, colorbar=True, cmap=None, vmin=None, vmax=None, a=None, raise ValueError('List of values is only allowed as input ' 'for finalized systems.') value = np.array(value) - img, min, max = mask_interpolate(coords, value, a, method, oversampling) + with _common.reraise_warnings(): + img, min, max = mask_interpolate(coords, value, a, method, oversampling) border = 0.5 * (max - min) / (np.asarray(img.shape) - 1) min -= border max += border @@ -1642,7 +1640,7 @@ def bands(sys, args=(), momenta=65, file=None, show=True, dpi=None, See `~kwant.physics.Bands` for the calculation of dispersion without plotting. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError("matplotlib was not found, but is required " "for bands()") @@ -1717,7 +1715,7 @@ def spectrum(syst, x, y=None, params=None, mask=None, file=None, A figure with the output if `ax` is not set, else None. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError("matplotlib was not found, but is required " "for plot_spectrum()") if y is not None and not has3d: @@ -2071,7 +2069,7 @@ def streamplot(field, box, cmap=None, bgcolor=None, linecolor='k', fig : matplotlib figure A figure with the output if `ax` is not set, else None. """ - if not mpl_enabled: + if not mpl_available: raise RuntimeError("matplotlib was not found, but is required " "for current()") @@ -2173,8 +2171,9 @@ def current(syst, current, relwidth=0.05, **kwargs): A figure with the output if `ax` is not set, else None. """ - return streamplot(*interpolate_current(syst, current, relwidth), - **kwargs) + with _common.reraise_warnings(4): + return streamplot(*interpolate_current(syst, current, relwidth), + **kwargs) # TODO (Anton): Fix plotting of parts of the system using color = np.nan. diff --git a/kwant/solvers/common.py b/kwant/solvers/common.py index f5f6197e9b791b2fb02c236ed0e26153fdc93f97..57c6fd35ec733165bd1aaf5ba04f0cfe6f598031 100644 --- a/kwant/solvers/common.py +++ b/kwant/solvers/common.py @@ -302,6 +302,8 @@ class SparseSolver(metaclass=abc.ABCMeta): """ Compute the scattering matrix of a system. + An alias exists for this common name: ``kwant.smatrix``. + Parameters ---------- sys : `kwant.system.FiniteSystem` @@ -394,6 +396,8 @@ class SparseSolver(metaclass=abc.ABCMeta): """ Compute the retarded Green's function of the system between its leads. + An alias exists for this common name: ``kwant.greens_function``. + Parameters ---------- sys : `kwant.system.FiniteSystem` @@ -489,6 +493,8 @@ class SparseSolver(metaclass=abc.ABCMeta): """ Calculate the local density of states of a system at a given energy. + An alias exists for this common name: ``kwant.ldos``. + Parameters ---------- sys : `kwant.system.FiniteSystem` @@ -553,6 +559,8 @@ class SparseSolver(metaclass=abc.ABCMeta): Return a callable object for the computation of the wave function inside the scattering region. + An alias exists for this common name: ``kwant.wave_function``. + Parameters ---------- sys : `kwant.system.FiniteSystem` @@ -570,13 +578,23 @@ class SparseSolver(metaclass=abc.ABCMeta): Notes ----- - The returned object can be itself called like a function. Given a lead number, it returns a 2d NumPy array that contains the wave function within the scattering region due to each incoming mode of the given - lead. Index 0 is the mode number, index 1 is the orbital number. The - modes appear in the same order as incoming modes in - `kwant.physics.modes`. + lead. Index 0 is the mode number, index 1 is the orbital number. + + The modes appear in the same order as the negative velocity modes in + `kwant.physics.PropagatingModes`. In Kwant's convention leads are attached + so that their translational symmetry points *away* from the scattering + region:: + + left lead SR right lead + /---------\ /---\ /---------\ + ...-3-2-1-0-X-X-X-0-1-2-3-... + + This means that incoming modes (coming from infinity towards the + scattering region) have *negative* velocity with respect to the + lead's symmetry direction. Examples -------- diff --git a/kwant/system.py b/kwant/system.py index 62158d41819ef8f2b62a3e2e8c69f1ceed10f0f6..dfa28558d0efdb08a7ef8af788299e474e6b2759 100644 --- a/kwant/system.py +++ b/kwant/system.py @@ -224,6 +224,12 @@ class InfiniteSystem(System, metaclass=abc.ABCMeta): See documentation of `~kwant.physics.PropagatingModes` and `~kwant.physics.StabilizedModes` for the return format details. + + The wave functions of the returned modes are defined over the + *unit cell* of the system, which corresponds to the degrees of + freedom on the first ``cell_sites`` sites of the system + (recall that infinite systems store first the sites in the unit + cell, then connected sites in the neighboring unit cell). """ from . import physics # Putting this here avoids a circular import. ham = self.cell_hamiltonian(args, params=params) diff --git a/kwant/tests/test_builder.py b/kwant/tests/test_builder.py index dee7ced6adb11640066ed49bf1b8b2289475c937..20879c44bed0156df810a16228f9eaf2c02b4573 100644 --- a/kwant/tests/test_builder.py +++ b/kwant/tests/test_builder.py @@ -1012,9 +1012,9 @@ def test_HoppingKind(): assert a.tag - b.tag == delta # test hashability and equality - hk = builder.HoppingKind((1, 0), g) - hk2 = builder.HoppingKind((1, 0), g) - hk3 = builder.HoppingKind((1, 0), g, h) + hk = builder.HoppingKind((1, 0, 0), g) + hk2 = builder.HoppingKind((1, 0, 0), g) + hk3 = builder.HoppingKind((1, 0, 0), g, h) assert hk == hk2 assert hash(hk) == hash(hk2) assert hk != hk3 @@ -1022,6 +1022,21 @@ def test_HoppingKind(): assert len({hk: 0, hk2:1, hk3: 2}) == 2 +def test_invalid_HoppingKind(): + g = kwant.lattice.general(ta.identity(3)) + h = kwant.lattice.general(np.identity(3)[:-1]) # 2D lattice in 3D + + delta = (1, 0, 0) + + # families have incompatible tags + with raises(ValueError): + builder.HoppingKind(delta, g, h) + + # delta is incompatible with tags + with raises(ValueError): + builder.HoppingKind(delta, h) + + def test_ModesLead_and_SelfEnergyLead(): lat = builder.SimpleSiteFamily() hoppings = [builder.HoppingKind((1, 0), lat), diff --git a/kwant/tests/test_plotter.py b/kwant/tests/test_plotter.py index ca3b9b5129b37c65dda8683cf7cea42d4ddb6677..23b0d01a591be30145080734b8202aa6ea95fabe 100644 --- a/kwant/tests/test_plotter.py +++ b/kwant/tests/test_plotter.py @@ -15,14 +15,32 @@ from math import cos, sin import scipy.integrate import scipy.stats import pytest +import sys import kwant -from kwant import plotter from kwant._common import ensure_rng -if plotter.mpl_enabled: +try: from mpl_toolkits import mplot3d + import matplotlib + + # This check is the same as the one performed inside matplotlib.use. + matplotlib_backend_chosen = 'matplotlib.backends' in sys.modules + # If the user did not already choose a backend, then choose + # the one with the least dependencies. + if not matplotlib_backend_chosen: + matplotlib.use('Agg') + from matplotlib import pyplot # pragma: no flakes +except ImportError: + pass + +from kwant import plotter + + +def test_matplotlib_backend_unset(): + """Simply importing Kwant should not set the matplotlib backend.""" + assert matplotlib_backend_chosen is False def test_importable_without_matplotlib(): @@ -93,7 +111,7 @@ def syst_3d(W=3, r1=2, r2=4, a=1, t=1.0): return syst -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_plot(): plot = plotter.plot syst2d = syst_2d() @@ -143,7 +161,7 @@ def bad_transform(pos): x, y = pos return x, y, 0 -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_map(): syst = syst_2d() with tempfile.TemporaryFile('w+b') as out: @@ -179,7 +197,7 @@ def test_mask_interpolate(): coords, np.ones(2 * len(coords))) -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_bands(): syst = syst_2d().finalized().leads[0] @@ -194,7 +212,7 @@ def test_bands(): plotter.bands(syst, ax=ax, file=out) -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_spectrum(): def ham_1d(a, b, c): @@ -405,7 +423,7 @@ def test_current_interpolation(): assert scipy.stats.linregress(np.log(data))[2] < -0.8 -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_current(): syst = syst_2d().finalized() J = kwant.operator.Current(syst) diff --git a/kwant/tests/test_wraparound.py b/kwant/tests/test_wraparound.py index dc8ed3a927ea2248d4991d856b9f00310b812474..eb981d2798fa1b8fd42e36d26a0cd88039274b11 100644 --- a/kwant/tests/test_wraparound.py +++ b/kwant/tests/test_wraparound.py @@ -17,7 +17,7 @@ from kwant import plotter from kwant.wraparound import wraparound, plot_2d_bands from kwant._common import get_parameters -if plotter.mpl_enabled: +if plotter.mpl_available: from mpl_toolkits import mplot3d # pragma: no flakes from matplotlib import pyplot # pragma: no flakes @@ -201,7 +201,7 @@ def test_symmetry(): assert np.all(orig == new) -@pytest.mark.skipif(not plotter.mpl_enabled, reason="No matplotlib available.") +@pytest.mark.skipif(not plotter.mpl_available, reason="Matplotlib unavailable.") def test_plot_2d_bands(): chain = kwant.lattice.chain() square = kwant.lattice.square()