Download IPython notebook here. Binder badge.

Adding new backends

[1]:
import sisl
import sisl.viz

# This is a toy band structure to illustrate the concepts treated throughout the notebook
geom = sisl.geom.graphene(orthogonal=True)
H = sisl.Hamiltonian(geom)
H.construct([(0.1, 1.44), (0, -2.7)], )

band_struct = sisl.BandStructure(H, [[0,0,0], [0.5,0,0]], 10, ["Gamma", "X"])

In the sisl.viz framework, the rendering part of the visualization is completely detached from the processing part. Because of that, we have the flexibility to add new ways of generating the final product by registering what we call backends.

We will guide you through how you might customize this part of the framework. There are however, very distinct scenarios where you might find yourself. Each of the following sections explains the details of each situation, which are ordered in increasing complexity.

Note

Even if you want to go to the most complex situation, make sure that you first understand the simpler ones!

Extending an existing backend

This is by far the easiest situation. For example, sisl already provides a backend to plot bands with plotly, but you are not totally happy with the way it’s done.

In this case, you grab the provided backend:

[2]:
from sisl.viz.backends.plotly import PlotlyBandsBackend

And then create your own class that inherits from it:

[3]:
class MyOwnBandsBackend(PlotlyBandsBackend):
    pass

The only thing left to do now is to let BandsPlot know that there’s a new backend available. This action is called registering a backend.

[4]:
from sisl.viz import BandsPlot

BandsPlot.backends.register("plotly_myown", MyOwnBandsBackend)
# Pass default=True if you want to make it the default backend

All good, you can already use your new backend!

[5]:
band_struct.plot(backend="plotly_myown")

Now that we know that it can be registered, we can try to add new functionality. But of course, we need to know how the backend works if we need to modify it. All backends to draw bands inherit from BandsBackend, and you can find some information there on how it works. Let’s read its documentation:

[6]:
from sisl.viz.backends.templates import BandsBackend

print(BandsBackend.__doc__)
Draws the bands provided by a `BandsPlot`

    The workflow implemented by it is as follows:
        First, `self.draw_bands` draws all bands like:
            for band in bands:
                if (spin texture needs to be drawn):
                    `self._draw_spin_textured_band()`, NO GENERIC IMPLEMENTATION (optional)
                else:
                    `self._draw_band()`, generic implementation that calls `self._draw_line`
        Once all bands are drawn, `self.draw_gaps` loops through all the gaps to be drawn:
            for gap in gaps:
                `self.draw_gap()`, MUST BE IMPLEMENTED!

Note

This already gives you an overview of how the backend works. If you want to know the very fine details, you can always go to the source code.

So, clearly PlotlyBandsBackend already contains the draw_gap method, otherwise it would not work.

From the workflow description, we understand that each band is drawn with the _draw_band method, which calls the generic draw_line method. In plotly, line information is passed as dictionaries that contain several parameters. One of them is, for example, showlegend, which controls whether the line appears in the legend. We can use therefore our plotly knowledge to only show at the legend those bands that are below the fermi level:

[7]:
# Create my new backend
class MyOwnBandsBackend(PlotlyBandsBackend):

    def _draw_band(self, x, y, *args, **kwargs):
        kwargs["showlegend"] = bool(y.max() < 0)
        super()._draw_band(x, y, *args, **kwargs)

# And register it again
BandsPlot.backends.register("plotly_myown", MyOwnBandsBackend)
[8]:
band_struct.plot(backend="plotly_myown")

This is not very interesting, but it does its job at illustrating the fact that you can register a slightly modified backend.

You could use your fresh knowledge to, for example draw something after the bands are drawn:

[9]:
class MyOwnBandsBackend(PlotlyBandsBackend):

    def draw_bands(self, *args, **kwargs):
        super().draw_bands(*args, **kwargs)
        # Now that all bands are drawn, draw a very interesting line at -2eV.
        self.add_hline(y=-2, line_color="red")

BandsPlot.backends.register("plotly_myown", MyOwnBandsBackend)
[10]:
band_struct.plot(backend="plotly_myown")

We finish this section by stating that:

  • To extend a backend, you have to have some knowledge about the corresponding framework (in this case plotly)

  • You don’t need to create a new backend for every modification. You can modify plots interactively however you want after the plot is generated. Creating a backend that extends an existing one is only useful if there are changes that you will always want to do because of personal preference or because you are building a graphical interface, for example.

Creating a backend for a supported framework

Now imagine that, for some reason, sisl didn’t provide a PlotlyBandsBackend. However, sisl does have a generic plotly backend:

[11]:
from sisl.viz.backends.plotly import PlotlyBackend

And also a generic bands backend:

[12]:
from sisl.viz.backends.templates import BandsBackend

In these cases, your situation is not that bad. As you saw, the template backends make use of generic functions like draw_line as much as they can, so the effort to implement a plotly bands backend is reduced to those things that can’t be generalized in that way.

One thing is for sure, we need to combine the two pieces to create the backend that we want:

[13]:
class MyPlotlyBandsBackend(BandsBackend, PlotlyBackend):
    pass

But is this enough? Let’s see the documentation of BandsBackend one more time:

[14]:
print(BandsBackend.__doc__)
Draws the bands provided by a `BandsPlot`

    The workflow implemented by it is as follows:
        First, `self.draw_bands` draws all bands like:
            for band in bands:
                if (spin texture needs to be drawn):
                    `self._draw_spin_textured_band()`, NO GENERIC IMPLEMENTATION (optional)
                else:
                    `self._draw_band()`, generic implementation that calls `self._draw_line`
        Once all bands are drawn, `self.draw_gaps` loops through all the gaps to be drawn:
            for gap in gaps:
                `self.draw_gap()`, MUST BE IMPLEMENTED!

So, there are to things that need to be implemented: draw_spin_textured_band and draw_gap.

We won’t bother to give our backend support for spin texture representations, but the draw_gap method is compulsory, so we have no choice. Let’s understand what is expected from this method:

[15]:
help(BandsBackend.draw_gap)
Help on function draw_gap in module sisl.viz.backends.templates._plots.bands:

draw_gap(self, ks, Es, color, name, **kwargs)
    This method should draw a gap, given the k and E coordinates.

    The color of the line should be determined by `color`, and `name` should be used for labeling.

    Parameters
    -----------
    ks: numpy array of shape (2,)
        The two k coordinates of the gap.
    Es: numpy array of shape (2,)
        The two E coordinates of the gap, sorted from minor to major.
    color: str
        Color with which the gap should be drawn.
    name: str
        Label that should be asigned to the gap.

Quite simple, isn’t it? It seems like we are provided with the coordinates of the gap and then we can display it however we want.

[16]:
class MyPlotlyBandsBackend(BandsBackend, PlotlyBackend):

    def draw_gap(self, ks, Es, color, name, **kwargs):

        self.draw_line(
            ks, Es, name=name,
            text=f"{Es[1]- Es[0]:.2f} eV",
            mode="lines+markers",
            line={"color": color},
            marker_symbol = ["triangle-up", "triangle-down"],
            marker={"color": color, "size": 20},
            **kwargs
        )

# Make it the default backend for bands, since it is awesome.
BandsPlot.backends.register("plotly_fromscratch", MyPlotlyBandsBackend, default=True)

Let’s see our masterpiece:

[17]:
band_struct.plot(gap=True)

Beautiful!

So, to end this section, just two remarks:

  • We have understood that if the framework is supported, the starting point is to combine the generic backend for the framework (PlotlyBackend) with the template backend of the specific plot (BandsBackend). Afterwards, we may have to tweak things a little.

  • Knowing how the generic framework backend works helps to make your code simpler. E.g. if you check PlotlyBackend.__doc__, you will find that we could have easily included some defaults for the axes titles.

Creating a backend for a non supported framework

Armed with our knowledge from the previous sections, we face the most difficult of the challenges: there’s not even a generic backend for the framework that we want to use.

What we have to do is quite clear, develop our own generic backend. But how? Let’s go to the Backend class for help:

[18]:
from sisl.viz.backends.templates import Backend

print(Backend.__doc__)
Base backend class that all backends should inherit from.

    This class contains various methods that need to be implemented by its subclasses.

    Methods that MUST be implemented are marked as abstract methods, therefore you won't
    even be able to use the class if you don't implement them. On the other hand, there are
    methods that are not absolutely essential to the general workings of the framework.
    These are written in this class to raise a NotImplementedError. Therefore, the backend
    will be instantiable but errors may happen during the plotting process.

    Below are all methods that need to be implemented by...

    (1) the generic backend of the framework:
        - `clear`, MUST
        - `draw_on`, optional (highly recommended, otherwise no multiple plot functionality)
        - `draw_line`, optional (highly recommended for 2D)
        - `draw_scatter`, optional (highly recommended for 2D)
        - `draw_line3D`, optional
        - `draw_scatter3D`, optional
        - `draw_arrows3D`, optional
        - `show`, optional

    (2) specific backend of a plot:
        - `draw`, MUST

    Also, you probably need to write an `__init__` method to initialize the state of the plot.
    Usually drawing methods will add to the state and finally on `show` you display the full
    plot.

Note

You can always look at the help of each specific method to understand exactly what you need to implement. E.g. help(Backend.draw_line).

To make it simple, let’s say we want to create a backend for “text”. This backend will store everything as text in its state, and it will print it on show. Here would be a minimal design:

[19]:
class TextBackend(Backend):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.text = ""

    def clear(self):
        self.text = ""

    def draw_line(self, x, y, name, **kwargs):
        self.text += f"\nLINE: {name}\n{x}\n{y}"

    def draw_scatter(self, x, y, name, **kwargs):
        self.text += f"\nSCATTER: {name}\n{x}\n{y}"

    def draw_on(self, other_backend):
        # Set the text attribute to the other backend's text, but store ours
        self_text = self.text
        self.text = other_backend.text
        # Make the plot draw the figure
        self._plot.get_figure(backend=self._backend_name, clear_fig=False)
        # Restore our text attribute
        self.text = self_text

    def show(self):
        print(self.text)

This could very well be our generic backend for the “text” framework. Now we can use the knowledge of the previous section to create a backend for the bands plot:

[20]:
class TextBandsBackend(BandsBackend, TextBackend):

    def draw_gap(self, ks, Es, name, **kwargs):
        self.draw_line(ks, Es, name=name)

# Register it, as always
BandsPlot.backends.register("text", TextBandsBackend)
[21]:
band_struct.plot(backend="text", gap=True, _debug=True)

LINE: 0
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-8.1        -8.07260764 -7.99070941 -7.85514486 -7.66732391 -7.42924537
 -7.14352854 -6.81346515 -6.44310336 -6.03738354]
LINE: 1
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-2.7        -2.78082828 -3.00808297 -3.34614692 -3.75661337 -4.20788704
 -4.67653718 -5.14555076 -5.60235836 -6.03738354]
LINE: 2
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[2.7        2.78082828 3.00808297 3.34614692 3.75661337 4.20788704
 4.67653718 5.14555076 5.60235836 6.03738354]
LINE: 3
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[8.1        8.07260764 7.99070941 7.85514486 7.66732391 7.42924537
 7.14352854 6.81346515 6.44310336 6.03738354]
LINE: Gap
[0.0, 0.0]
[-2.699999999999994, 2.7000000000000126]

And everything works great! Note that since the backend is independent of the processing logic, I can use any setting of BandsPlot and it will work:

[22]:
bands_plot = band_struct.plot(backend="text", gap=True, _debug=True)
bands_plot.update_settings(
    bands_range=[0,1],
    custom_gaps=[{"from": "Gamma", "to": "Gamma"}, {"from": "X", "to": "X"}]
)

LINE: 0
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-8.1        -8.07260764 -7.99070941 -7.85514486 -7.66732391 -7.42924537
 -7.14352854 -6.81346515 -6.44310336 -6.03738354]
LINE: 1
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-2.7        -2.78082828 -3.00808297 -3.34614692 -3.75661337 -4.20788704
 -4.67653718 -5.14555076 -5.60235836 -6.03738354]
LINE: 2
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[2.7        2.78082828 3.00808297 3.34614692 3.75661337 4.20788704
 4.67653718 5.14555076 5.60235836 6.03738354]
LINE: 3
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[8.1        8.07260764 7.99070941 7.85514486 7.66732391 7.42924537
 7.14352854 6.81346515 6.44310336 6.03738354]
LINE: Gap
[0.0, 0.0]
[-2.699999999999994, 2.7000000000000126]
LINE: Gap (Gamma-Gamma)
[0.0, 0.0]
[-2.699999999999994, 2.7000000000000126]
LINE: Gap (X-X)
[0.7374630642229563, 0.7374630642229563]
[-6.037383539249426, 6.037383539249433]

It wasn’t that difficult, right?

We are very thankful that you took the time to understand how to build backends on top of the sisl.viz framework! Any feedback on it will be highly appreciated and we are looking forward to see your implementations!