ipynb download badge   Binder badge

GitHub issues by-label

BandsPlot

[1]:
import sisl

# This is just for convenience to retreive files
siesta_files = (
    sisl._environ.get_environ_variable("SISL_FILES_TESTS") / "sisl" / "io" / "siesta"
)
info:0: SislInfo: Please install tqdm (pip install tqdm) for better looking progress bars

Let’s get a bands_plot from a .bands file

[2]:
bands_plot = sisl.get_sile(siesta_files / "SrTiO3.bands").plot()
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[2], line 1
----> 1 bands_plot = sisl.get_sile(siesta_files / "SrTiO3.bands").plot()

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/viz/_plotables.py:58, in ObjectPlotHandler.__call__(self, *args, **kwargs)
     54 if self._default is None:
     55     raise TypeError(
     56         f"No default plotting function has been defined for {self._obj.__class__.__name__}."
     57     )
---> 58 return getattr(self, self._default)(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/_dispatcher.py:68, in AbstractDispatch.__call__(self, *args, **kwargs)
     66 def __call__(self, *args, **kwargs):
     67     _log.debug(f"call {self.__class__.__name__}{args}", extra={"obj": self})
---> 68     return self.dispatch(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/viz/_plotables.py:66, in PlotDispatch.dispatch(self, *args, **kwargs)
     64 def dispatch(self, *args, **kwargs):
     65     """Runs the plotting function by passing the object instance to it."""
---> 66     return self._plot(self._obj, *args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/viz/_plotables.py:316, in register_data_source.<locals>._plot(obj, __params_info, __signature, *args, **kwargs)
    313     if k not in data_kwargs:
    314         data_kwargs[k] = v
--> 316 data = data_source_cls.new(obj, *args, **data_kwargs)
    318 plot_kwargs = bound.arguments.pop(params_info["plot_var_kwarg"], {})
    320 return plot_cls(**{setting_key: data, **bound.arguments, **plot_kwargs})

File ~/.asdf/installs/python/3.12.0/lib/python3.12/functools.py:946, in singledispatchmethod.__get__.<locals>._method(*args, **kwargs)
    944 def _method(*args, **kwargs):
    945     method = self.dispatcher.dispatch(args[0].__class__)
--> 946     return method.__get__(obj, cls)(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/viz/data/bands.py:325, in BandsData.from_siesta_bands(cls, bands_file)
    320 @new.register
    321 @classmethod
    322 def from_siesta_bands(cls, bands_file: bandsSileSiesta):
    323     """Gets the bands data from a SIESTA .bands file"""
--> 325     bands_data = bands_file.read_data(as_dataarray=True)
    326     bands_data.k.attrs["axis"] = {
    327         "tickvals": bands_data.attrs.pop("ticks"),
    328         "ticktext": bands_data.attrs.pop("ticklabels"),
    329     }
    331     return cls.new(bands_data)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/sile.py:695, in sile_fh_open.<locals>._wrapper.<locals>.pre_open(self, *args, **kwargs)
    693 if hasattr(self, "fh"):
    694     return func(self, *args, **kwargs)
--> 695 with self:
    696     reset(self)
    697     return func(self, *args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/sile.py:1076, in Sile.__enter__(self)
   1074 def __enter__(self):
   1075     """Opens the output file and returns it self"""
-> 1076     self._open()
   1077     return self

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/sile.py:1067, in Sile._open(self)
   1065             self.fh = gzip.open(str(self.file), mode=self._mode)
   1066     else:
-> 1067         self.fh = self.file.open(self._mode)
   1069 # the file should restart the file-read (as per instructed)
   1070 self._line = 0

File ~/.asdf/installs/python/3.12.0/lib/python3.12/pathlib.py:1014, in Path.open(self, mode, buffering, encoding, errors, newline)
   1012 if "b" not in mode:
   1013     encoding = io.text_encoding(encoding)
-> 1014 return io.open(self, mode, buffering, encoding, errors, newline)

FileNotFoundError: [Errno 2] No such file or directory: '_THIS_DIRECTORY_DOES_NOT_EXIST_/sisl/io/siesta/SrTiO3.bands'

and see what we’ve got:

[3]:
bands_plot
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 bands_plot

NameError: name 'bands_plot' is not defined

Getting the bands that you want

By default, BandsPlot gives you the 15 bands below and above 0 eV (which is interpreted as the fermi level).

There are two main ways to specify the bands that you want to display: Erange and bands_range.

As you may have guessed, Erange specifies the energy range that is displayed:

[4]:
bands_plot.update_inputs(Erange=[-10, 10])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 bands_plot.update_inputs(Erange=[-10, 10])

NameError: name 'bands_plot' is not defined

while with bands_range you can actually indicate the indices.

However, note that ``Erange`` has preference over ``bands_range``, therefore you need to set Erange to None if you want the change to take effect.

[5]:
bands_plot.update_inputs(bands_range=[6, 15], Erange=None)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 bands_plot.update_inputs(bands_range=[6, 15], Erange=None)

NameError: name 'bands_plot' is not defined

If your fermi level is not correctly set or you want a different energy reference, you can provide a value for E0 to specify where your 0 should be and the bands to display will be automatically calculated from that.

However, if you want to update E0 after the plot has been build and you want BandsPlot to recalculate the bands for you you will need to set Erange and bands_range to None again.

[6]:
bands_plot.update_inputs(E0=-10, bands_range=None, Erange=None)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 bands_plot.update_inputs(E0=-10, bands_range=None, Erange=None)

NameError: name 'bands_plot' is not defined

Notice how only 25 bands are displayed now: the only 10 that are below 0 eV (there are no lower states) and 15 above 0 eV.

[7]:
# Set them back to "normal"
bands_plot = bands_plot.update_inputs(E0=0, bands_range=None, Erange=None)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 2
      1 # Set them back to "normal"
----> 2 bands_plot = bands_plot.update_inputs(E0=0, bands_range=None, Erange=None)

NameError: name 'bands_plot' is not defined

Notice that in spin polarized bands, you can select the spins to display using the ``spin`` setting, just pass a list of spin components (e.g. spin=[0]).

Bands styling

If all you want is to change the color and width of the bands, there’s one simple solution: use the bands_style input to tweak the line styles.

Let’s show them in red:

[8]:
bands_plot.update_inputs(bands_style={"color": "red"})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 bands_plot.update_inputs(bands_style={"color": "red"})

NameError: name 'bands_plot' is not defined

And now in green but also make them wider:

[9]:
bands_plot.update_inputs(bands_style={"color": "green", "width": 3})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 bands_plot.update_inputs(bands_style={"color": "green", "width": 3})

NameError: name 'bands_plot' is not defined

If you have spin polarized bands, bands_style will tweak the colors for the first spin channel, while the second one can be tuned with spindown_style.

Finally, you can pass functions to the keys of bands_style to customize the styles on a band basis, or even on a point basis. The functions should accept data as an argument, which will be an xarray.Dataset containing all the bands data. It should then return a single value or an array of values. It is best shown with examples. Let’s create a function just to see what we receive as an input:

[10]:
def color(data):
    """Dummy function to see what we receive."""
    print(data)
    return "green"


bands_plot.update_inputs(bands_style={"color": color})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 7
      3     print(data)
      4     return "green"
----> 7 bands_plot.update_inputs(bands_style={"color": color})

NameError: name 'bands_plot' is not defined

So, you can see that we receive a Dataset. The most important variable is E, which contains the energy (that depends on k and band). Let’s now play with it to do some custom styling: - The color will be determined by the slope of the band. - We will plot bands that are closer to the fermi level bigger because they are more important.

[11]:
def gradient(data):
    """Function that computes the absolute value of dE/dk.

    This returns a two dimensional array (gradient depends on k and band)
    """
    return abs(data.E.differentiate("k"))


def band_closeness_to_Ef(data):
    """Computes how close one band is to the fermi level.

    This returns a one dimensional array (distance depends only on band)
    """
    dist_from_Ef = abs(data.E).min("k")

    return (1 / dist_from_Ef**0.4) * 5


# Now we are going to set the width of the band according to the distance from the fermi level
# and the color according to the gradient. We are going to set the colorscale also, instead of using
# the default one.
bands_plot.update_inputs(
    bands_style={"width": band_closeness_to_Ef, "color": gradient},
    colorscale="temps",
    Erange=[-10, 10],
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 22
     16     return (1 / dist_from_Ef**0.4) * 5
     19 # Now we are going to set the width of the band according to the distance from the fermi level
     20 # and the color according to the gradient. We are going to set the colorscale also, instead of using
     21 # the default one.
---> 22 bands_plot.update_inputs(
     23     bands_style={"width": band_closeness_to_Ef, "color": gradient},
     24     colorscale="temps",
     25     Erange=[-10, 10],
     26 )

NameError: name 'bands_plot' is not defined

You can see that by providing callables the possibilities are endless, you are only limited by your imagination!

[12]:
bands_plot = bands_plot.update_inputs(bands_style={})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 bands_plot = bands_plot.update_inputs(bands_style={})

NameError: name 'bands_plot' is not defined

Displaying the smallest gaps

The easiest thing to do is to let BandsPlot discover where the (minimum) gaps are.

This is indicated by setting the gap parameter to True. One can also use gap_color if a particular color is desired.

[13]:
bands_plot.update_inputs(gap=True, gap_color="green", Erange=[-10, 10])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 bands_plot.update_inputs(gap=True, gap_color="green", Erange=[-10, 10])

NameError: name 'bands_plot' is not defined

This displays the minimum gaps. However there may be some issues with it: it will show all gaps with the minimum value. That is, if you have repeated points in the brillouin zone it will display multiple gaps that are equivalent.

What’s worse, if the region where your gap is is very flat, two consecutive points might have the same energy. Multiple gaps will be displayed one glued to another.

To help cope with this issues, you have the direct_gaps_only and gap_tol.

In this case, since we have no direct gaps, setting direct_gaps_only will hide them all:

[14]:
bands_plot.update_inputs(direct_gaps_only=True)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 bands_plot.update_inputs(direct_gaps_only=True)

NameError: name 'bands_plot' is not defined

This example is not meaningful for gap_tol, but it is illustrative of what gap_tol does. It is the minimum k-distance between two points to consider them “the same point” in the sense that only one of them will be used to show the gap. In this case, if we set gap_tol all the way up to 3, the plot will consider the two gamma points to be part of the same “point” and therefore it will only show the gap once.

[15]:
bands_plot.update_inputs(direct_gaps_only=False, gap_tol=3)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 1
----> 1 bands_plot.update_inputs(direct_gaps_only=False, gap_tol=3)

NameError: name 'bands_plot' is not defined

This is not what gap_tol is meant for, since it is thought to remediate the effect of locally flat bands, but still you can get the idea of what it does.

[16]:
bands_plot = bands_plot.update_inputs(gap=False, gap_tol=0.01)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[16], line 1
----> 1 bands_plot = bands_plot.update_inputs(gap=False, gap_tol=0.01)

NameError: name 'bands_plot' is not defined

Displaying custom gaps

If you are not happy with the gaps that the plot is displaying for you or you simply want gaps that are not the smallest ones, you can always use custom_gaps.

Custom gaps should be a list where each item specifies how to draw that given gap. The key labels of each item are from and to, which specifies the k-points through which you want to draw the gap. The rest of labels are the typical styling labels: color, width

For example, if we want to plot the gamma-gamma gap:

[17]:
bands_plot.update_inputs(custom_gaps=[{"from": "Gamma", "to": "Gamma", "color": "red"}])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 bands_plot.update_inputs(custom_gaps=[{"from": "Gamma", "to": "Gamma", "color": "red"}])

NameError: name 'bands_plot' is not defined

Notice how we got the gap probably not where we wanted, since it would be better to have it in the middle Gamma point, which is more visible. Instead of the K point name, you can also pass the K value.

Now, you’ll be happy to know that you can easily access the k values of all labels, as they are stored as part of the attributes of the k coordinate in the bands dataarray:

[18]:
bands_plot.nodes["bands_data"].get().k.axis
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 bands_plot.nodes["bands_data"].get().k.axis

NameError: name 'bands_plot' is not defined

Now all we need to do is to grab the value for the second gamma point:

[19]:
axis_info = bands_plot.nodes["bands_data"].get().k.axis

gap_k = None
for val, label in zip(axis_info["tickvals"], axis_info["ticktext"]):
    if label == "Gamma":
        gap_k = val
gap_k
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 axis_info = bands_plot.nodes["bands_data"].get().k.axis
      3 gap_k = None
      4 for val, label in zip(axis_info["tickvals"], axis_info["ticktext"]):

NameError: name 'bands_plot' is not defined

And use it to build a custom gap:

[20]:
bands_plot.update_inputs(custom_gaps=[{"from": gap_k, "to": gap_k, "color": "orange"}])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[20], line 1
----> 1 bands_plot.update_inputs(custom_gaps=[{"from": gap_k, "to": gap_k, "color": "orange"}])

NameError: name 'bands_plot' is not defined

Displaying spin texture

If your bands plot comes from a non-colinear spin calculation (or is using a Hamiltonian with non-colinear spin), you can pass "x", "y" or "z" to the spin setting in order to get a display of the spin texture.

Let’s read in a hamiltonian coming from a spin orbit SIESTA calculation, which is obtained from this fantastic spin texture tutorial:

[21]:
import sisl

siesta_files = (
    sisl._environ.get_environ_variable("SISL_FILES_TESTS") / "sisl" / "io" / "siesta"
)
[22]:
H = sisl.get_sile(siesta_files / "Bi2D_BHex.TSHS").read_hamiltonian()
H.spin.is_spinorbit
---------------------------------------------------------------------------
SileError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 H = sisl.get_sile(siesta_files / "Bi2D_BHex.TSHS").read_hamiltonian()
      2 H.spin.is_spinorbit

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/siesta/binaries.py:378, in tshsSileSiesta.read_hamiltonian(self, geometry, **kwargs)
    347 def read_hamiltonian(self, geometry=None, **kwargs) -> Hamiltonian:
    348     """Electronic structure from the siesta.TSHS file
    349
    350     The TSHS file format does *not* contain exact orbital information.
   (...)
    376     >>> H = sisl.get_sile("siesta.TSHS").read_hamiltonian(geometry=geom)
    377     """
--> 378     tshs_g = self.read_geometry()
    379     if geometry is None:
    380         geometry = tshs_g

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/messages.py:106, in deprecate_argument.<locals>.deco.<locals>.wrapped(*args, **kwargs)
    104     if new is not None:
    105         kwargs[new] = kwargs.pop(old)
--> 106 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/siesta/binaries.py:223, in onlysSileSiesta.read_geometry(self, basis)
    220 """Returns Geometry object from a TranSiesta file"""
    222 # Read supercell
--> 223 lattice = self.read_lattice()
    225 na = _siesta.read_tshs_sizes(self.file)[1]
    226 self._fortran_check("read_geometry", "could not read sizes.")

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/siesta/binaries.py:199, in onlysSileSiesta.read_lattice(self)
    197 """Returns a Lattice object from a TranSiesta file"""
    198 n_s = _siesta.read_tshs_sizes(self.file)[3]
--> 199 self._fortran_check("read_lattice", "could not read sizes.")
    200 arr = _siesta.read_tshs_cell(self.file, n_s)
    201 self._fortran_check("read_lattice", "could not read cell.")

File ~/checkouts/readthedocs.org/user_builds/sisl/envs/latest/lib/python3.12/site-packages/sisl/io/siesta/sile.py:82, in SileBinSiesta._fortran_check(self, method, message, ret_msg)
     80     msg = f"{self!s}.{method} {message} (ierr={ierr})"
     81     if not ret_msg:
---> 82         raise SileError(msg)
     83 if ret_msg:
     84     return msg

SileError: tshsSileSiesta(Bi2D_BHex.TSHS, base=_THIS_DIRECTORY_DOES_NOT_EXIST_/sisl/io/siesta).read_lattice could not read sizes. (ierr=2)

Generate the path for our band structure:

[23]:
band_struct = sisl.BandStructure(
    H,
    points=[
        [1.0 / 2, 0.0, 0.0],
        [0.0, 0.0, 0.0],
        [1.0 / 3, 1.0 / 3, 0.0],
        [1.0 / 2, 0.0, 0.0],
    ],
    divisions=301,
    names=["M", r"Gamma", "K", "M"],
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[23], line 2
      1 band_struct = sisl.BandStructure(
----> 2     H,
      3     points=[
      4         [1.0 / 2, 0.0, 0.0],
      5         [0.0, 0.0, 0.0],
      6         [1.0 / 3, 1.0 / 3, 0.0],
      7         [1.0 / 2, 0.0, 0.0],
      8     ],
      9     divisions=301,
     10     names=["M", r"Gamma", "K", "M"],
     11 )

NameError: name 'H' is not defined

And finally generate the plot:

[24]:
spin_texture_plot = band_struct.plot.bands(Erange=[-2, 2])
spin_texture_plot
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[24], line 1
----> 1 spin_texture_plot = band_struct.plot.bands(Erange=[-2, 2])
      2 spin_texture_plot

NameError: name 'band_struct' is not defined

Now it’s time to add spin texture to these bands. Remember the section on styling bands? If you haven’t checked it, take a quick look at it, because it will come handy now. The main point to take from that section for our purpose here is that each key in the styles accepts a callable.

As in other cases through the sisl.viz module, we provide callables that will work out of the box for the most common styling. In this case, what we need is the SpinMoment node. We will import it and use it simply by specifying the axis.

[25]:
from sisl.viz.data_sources import SpinMoment

spin_texture_plot.update_inputs(bands_style={"color": SpinMoment("x"), "width": 3})

# We hide the legend so that the colorbar can be easily seen.
spin_texture_plot.update_layout(showlegend=False)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 3
      1 from sisl.viz.data_sources import SpinMoment
----> 3 spin_texture_plot.update_inputs(bands_style={"color": SpinMoment("x"), "width": 3})
      5 # We hide the legend so that the colorbar can be easily seen.
      6 spin_texture_plot.update_layout(showlegend=False)

NameError: name 'spin_texture_plot' is not defined

There is nothing magic about the SpinMoment node. If you pass a dummy callable as we did in the styling section, you will see that the bands data now contains a spin_moments variable since it comes from a non-colinear calculation. It is just a matter of grabbing that variable:

[26]:
def color(data):
    """Dummy function to see what we receive."""
    print(data)
    return "green"


spin_texture_plot.update_inputs(bands_style={"color": color})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[26], line 7
      3     print(data)
      4     return "green"
----> 7 spin_texture_plot.update_inputs(bands_style={"color": color})

NameError: name 'spin_texture_plot' is not defined

Note that, as shown in the styling section, you can use the colorscale input to change the colorscale, or use the SpinMoment node for the other styling keys. For example, we can set the width of the band to display whether there is some spin moment, and the color can show the sign.

[27]:
spin_texture_plot.update_inputs(
    bands_style={"color": SpinMoment("x"), "width": abs(SpinMoment("x")) * 40}
).update_layout(showlegend=False).show("png")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[27], line 1
----> 1 spin_texture_plot.update_inputs(
      2     bands_style={"color": SpinMoment("x"), "width": abs(SpinMoment("x")) * 40}
      3 ).update_layout(showlegend=False).show("png")

NameError: name 'spin_texture_plot' is not defined

Notice how we did some postprocessing to adapt the values of the spin moment to some number that is suitable for the width. This is possible thanks to the magic of nodes!

We hope you enjoyed what you learned!