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!