Kwant compatibility
===================
`Kwant `_ is a Python package for numerical tight-binding similar to
pybinding, but it's specialized for transport calculations. Since the two packages work with the
same kind of Hamiltonian matrices, it's possible to build a model in pybinding and use Kwant to
compute the transport properties. The advantage for pybinding users is access to Kwant's transport
solvers in addition to pybinding's builtin :ref:`computational routines `. The
advantage for Kwant users is the much faster system build times made possible by pybinding's model
builder -- see the :doc:`/benchmarks/index`.
.. only:: html
:nbexport:`Download this page as a Jupyter notebook `
Exporting a model
-----------------
The procedure for constructing and solving transport problems in Kwant can be summarized with
the following lines of pseudo-code:
.. code-block:: python
:emphasize-lines: 0
# 1. BUILD model system
builder = kwant.Builder()
... # specify model parameters
system = builder.finalized()
# 2. COMPUTE scattering matrix
smatrix = kwant.smatrix(system)
... # call smatrix methods
If we want to use pybinding to build the model, we can just replace the first part:
.. code-block:: python
:emphasize-lines: 0
# 1. BUILD model system
model = pb.Model(...) # specify model parameters
kwant_system = model.tokwant() # export to kwant format
# 2. COMPUTE scattering matrix
smatrix = kwant.smatrix(kwant_system)
... # call smatrix methods
A pybinding :class:`.Model` is defined as usual and then converted to the Kwant-compatible format
by calling the :meth:`.Model.tokwant` method. The resulting `kwant_system` can be used as expected.
Complete example
----------------
A detailed overview of scattering model construction in pybinding is available in the
:doc:`tutorial `. Here, we present a simple example of a graphene wire
with a potential barrier:
.. plot::
:context: close-figs
from pybinding.repository import graphene
def potential_barrier(v0, x0):
"""Barrier height `v0` in eV with spatial position `-x0 <= x <= x0`"""
@pb.onsite_energy_modifier(is_double=True) # enable double-precision floating-point
def function(energy, x):
energy[np.logical_and(-x0 <= x, x <= x0)] = v0
return energy
return function
def make_model(length, width, v0=0):
model = pb.Model(
graphene.monolayer(),
pb.rectangle(length, width),
potential_barrier(v0, length / 4)
)
model.attach_lead(-1, pb.line([-length/2, -width/2], [-length/2, width/2]))
model.attach_lead(+1, pb.line([ length/2, -width/2], [ length/2, width/2]))
return model
model = make_model(length=1, width=2) # nm
model.plot()
We can then vary the height of the potential barrier and calculate the transmission using Kwant:
.. code-block:: python
import kwant
length, width = 15, 15 # nm
electron_energy = 0.25 # eV
barrier_heights = np.linspace(0, 0.5, 100) # eV
transmission = []
for v in barrier_heights:
model = make_model(length, width, v) # pybinding model
kwant_system = model.tokwant() # export to kwant
smatrix = kwant.smatrix(kwant_system, energy=electron_energy)
transmission.append(smatrix.transmission(1, 0))
For more information about `kwant.smatrix` and other transport calculations, please refer to the
`Kwant website `_. That is outside the scope of this guide. The purpose
of this section is to present the :meth:`.Model.tokwant` compatibility method. The exported system
is then in the domain of Kwant.
From there, it's trivial to plot the results:
.. plot::
:nofigs:
:context: close-figs
:include-source: False
electron_energy = 0.25
barrier_heights = np.linspace(0, 0.5, 100)
transmission = [3.00, 3.00, 2.99, 2.98, 2.96, 2.94, 2.90, 2.84, 2.78, 2.70, 2.60, 2.50, 2.39,
2.27, 2.16, 2.04, 1.94, 1.83, 1.74, 1.66, 1.58, 1.52, 1.46, 1.41, 1.36, 1.32,
1.29, 1.26, 1.23, 1.21, 1.19, 1.17, 1.16, 1.14, 1.13, 1.12, 1.11, 1.10, 1.09,
1.08, 1.08, 1.07, 1.07, 1.06, 1.06, 1.05, 1.05, 1.04, 1.04, 1.03, 0.61, 0.67,
0.39, 1.12, 0.78, 0.67, 0.58, 0.52, 0.54, 0.68, 0.89, 1.02, 0.99, 0.89, 0.80,
0.74, 0.71, 0.70, 0.69, 0.70, 0.71, 0.74, 0.77, 0.82, 0.87, 0.93, 0.98, 1.02,
1.03, 1.01, 0.97, 0.92, 0.86, 0.82, 0.79, 0.78, 0.79, 0.81, 0.85, 0.90, 0.96,
1.03, 1.11, 1.18, 1.27, 1.35, 1.44, 1.52, 1.60, 1.67]
plt.figure(figsize=(3, 2.4))
.. plot::
:context:
plt.plot(barrier_heights, transmission)
plt.ylabel("transmission")
plt.xlabel("barrier height (eV)")
plt.axvline(electron_energy, 0, 0.5, color="gray", linestyle=":")
plt.annotate("electron energy\n{} eV".format(electron_energy), (electron_energy, 0.54),
xycoords=("data", "axes fraction"), horizontalalignment="center")
pb.pltutils.despine() # remove top and right axis lines
Note that the transmission was calculated for an energy value of 0.25 eV. As the height of the
barrier is increased, two regimes are clearly distinguishable: transmission over and through the
barrier.
Performance considerations
--------------------------
The Kwant documentation recommends separating model parameters into two parts: the structural data
which remains constant and fields which can be varied. This yields better performance because only
the field data needs to be repopulated. This is demonstrated with the following pseudo-code which
loops over some parameter `x`:
.. code-block:: python
:emphasize-lines: 0
builder = kwant.Builder()
... # specify structural parameters
system = builder.finalized()
for x in xs:
smatrix = kwant.smatrix(system, args=[x]) # apply fields
... # call smatrix methods
This separation is not required with pybinding. As pointed out in the :doc:`/benchmarks/index`,
the fast builder makes it possible to fully reconstruct the model in every loop iteration at no
extra performance cost. This simplifies the code since all the parameters can be applied in a
single place:
.. code-block:: python
:emphasize-lines: 0
def make_model(x):
return pb.Model(..., x) # all parameters in one place
for x in xs:
smatrix = kwant.smatrix(make_model(x).tokwant()) # constructed all at once
... # call smatrix methods
You can :download:`download ` a full example file which implements transport
through a barrier like the one presented above. The script uses both builders so you can compare
the implementation as well as the performance. Download the example file and try it on your system.
Our results are presented below (measured using Intel Core i7-4960HQ CPU, 16 GiB RAM, Python 3.5,
macOS 10.11). The size of the square scattering region is increased and we measure the total time
required to calculate the transmission:
.. plot::
:context: close-figs
:include-source: False
sizes = [5, 10, 15, 20, 25, 30]
pb_times = [2.374, 6.601, 16.207, 31.400, 64.519, 104.874]
kwant_times = [3.054, 9.781, 22.833, 46.033, 91.581, 146.218]
plt.figure(figsize=(3, 2.4))
pb.pltutils.set_palette("Set1", start=3)
plt.plot(sizes, pb_times, label="pybinding", marker='o', markersize=5, lw=2, zorder=20)
plt.plot(sizes, kwant_times, label="kwant", marker='o', markersize=5, lw=2, zorder=10)
plt.grid(True, which='major')
plt.title("transmission calculation time")
plt.xlabel("system size (nm)")
plt.ylabel("time (seconds)")
plt.xlim(0.8 * min(sizes), 1.05 * max(sizes))
pb.pltutils.despine()
pb.pltutils.legend(loc='upper left', reverse=True)
For each system size, the transmission is calculated as a function of barrier height for 100
values. Even though pybinding reconstructs the entire model every time the barrier is changed, the
system build time is so fast that it doesn't affect the total calculation time. In fact, the
extremely fast build actually enables pybinding to outperform Kwant in the overall
calculation. Even though Kwant only repopulates field data at each loop iteration, this still
takes more time than it does for pybinding to fully reconstruct the system.
Note that this example presents a relatively simple system with a square barrier. This is done
to keep the run time to only a few minutes, for convenience. Here, pybinding speeds up the
overall calculation by about 40%. For more realistic examples with larger scattering regions and
complicated field functions with multiple parameters, a speedup of 3-4 times can be achieved by
using pybinding's model builder.
Floating-point precision
------------------------
Pybinding can generate the Hamiltonian matrix with one of four data types: real or complex numbers
with single or double precision (32-bit or 64-bit floating point). The selection is dynamic. The
starting case is always real with single precision and from there the data type is automatically
promoted as needed by the model. For example, adding translationally symmetry or a magnetic field
will cause the builder to switch to complex numbers -- this is detected automatically. On the other
hand, the switch to double precision needs to be requested by the user. The onsite and hopping
energy :ref:`modifiers ` have an optional `is_double` parameter which can be set to
`True`. The builder switches to double precision if requested by at least one modifier.
Alternatively, :func:`.force_double_precision` can be given to a :class:`.Model` as a direct
parameter.
The reason for all of this is performance. Most solvers work faster with smaller data types: they
consume less memory and bandwidth and SIMD vectorization becomes more efficient. This is assuming
that single precision and/or real numbers are sufficient to describe the given model. In case of
Kwant's solvers, it seems to require double precision in most cases. This is the reason for the
`is_double=True` flag in the above example. Keep this in mind when exporting to Kwant.