# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""
This file contains the Plot class, which should be inherited by all plot classes
"""
import uuid
import inspect
import numpy as np
from copy import deepcopy
import time
from types import MethodType, FunctionType
import itertools
from functools import partial
from pathlib import Path
import sisl
from sisl.messages import info, warn
from .backends._plot_backends import Backends
from .configurable import (
Configurable, ConfigurableMeta,
vizplotly_settings, _populate_with_settings
)
from ._presets import get_preset
from .plotutils import (
init_multiple_plots, repeat_if_children, dictOfLists2listOfDicts,
trigger_notification, spoken_message,
running_in_notebook, check_widgets, call_method_if_present
)
from .input_fields import (
TextInput, SileInput, BoolInput,
OptionsInput, IntegerInput,
ListInput, ProgramaticInput
)
from ._shortcuts import ShortCutable
__all__ = ["Plot", "MultiplePlot", "Animation", "SubPlots"]
class PlotMeta(ConfigurableMeta):
def __call__(cls, *args, **kwargs):
""" This method decides what to return when the plot class is instantiated.
It is supposed to help the users by making the plot class very functional
without the need for the users to use extra methods.
It will catch the first argument and initialize the corresponding plot
if the first argument is:
- A string, it will be assumed that it is a path to a file.
- A plotable object (has a _plot attribute)
Note that both cases are registered in the _plotables.py file, and you
can register new siles/plotables by using the register functions.
"""
if args:
# This is just so that the plotable framework knows from which plot class
# it is being called so that it can build the corresponding plot.
# Only relevant if the plot is built with obj.plot()
plot_method = kwargs.get("plot_method", cls.suffix())
# If a filename is recieved, we will try to find a plot for it
if isinstance(args[0], (str, Path)):
filename = args[0]
sile = sisl.get_sile(filename)
if hasattr(sile, "plot"):
plot = sile.plot(**{**kwargs, "method": plot_method})
else:
raise NotImplementedError(
f'There is no plot implementation for {sile.__class__} yet.')
elif isinstance(args[0], Plot):
plot = args[0].update_settings(**kwargs)
else:
obj = args[0]
# Maybe the first argument is a plotable object (e.g. a geometry)
if hasattr(obj, "plot"):
plot = obj.plot(**{**kwargs, "method": plot_method})
else:
return object.__new__(cls)
return plot
elif 'animate' in kwargs or 'varying' in kwargs or 'subplots' in kwargs:
methods = {'animate': cls.animated, 'varying': cls.multiple, 'subplots': cls.subplots}
# Retrieve the keyword that was actually passed
# and choose the appropiate method
for keyword in ('animate', 'varying', 'subplots'):
variable_settings = kwargs.pop(keyword, None)
if variable_settings is not None:
method = methods[keyword]
break
# Normalize all accepted input types to a dict
if isinstance(variable_settings, str):
variable_settings = [variable_settings]
if isinstance(variable_settings, (list, tuple, np.ndarray)):
variable_settings = {key: kwargs.pop(key) for key in variable_settings}
# Just run the method that will get us the desired plot
plot = method(variable_settings, fixed=kwargs, **kwargs)
return plot
return super().__call__(cls, *args, **kwargs)
[docs]class Plot(ShortCutable, Configurable, metaclass=PlotMeta):
""" Parent class of all plot classes
Implements general things needed by all plots such as settings and shortcut
management.
Parameters
----------
root_fdf: fdfSileSiesta, optional
Path to the fdf file that is the 'parent' of the results.
results_path: str, optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
entry_points_order: array-like, optional
Order with which entry points will be attempted.
backend: optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
Attributes
----------
settings: dict
contains the values for each setting of the plot.
params: dict
for each setting, contains information about their input field.
This information might include valid values, for example.
paramGroups: tuple of dicts
contains the different setting groups present in the plot.
Each group is a dict like { "key": , "name": ,"icon": , "description": }
...
"""
_update_methods = {
"read_data": [],
"set_data": [],
"get_figure": []
}
_param_groups = (
{
"key": "dataread",
"name": "Data reading settings",
"icon": "import_export",
"description": "In such a busy world, one may forget how the files are structured in their computer. Please take a moment to <b>make sure your data is being read exactly in the way you expect<b>."
},
)
_parameters = (
SileInput(
key = "root_fdf", name = "Path to fdf file",
dtype=sisl.io.siesta.fdfSileSiesta,
group="dataread",
help="Path to the fdf file that is the 'parent' of the results.",
params={
"placeholder": "Write the path here..."
}
),
TextInput(
key="results_path", name = "Path to your results",
group="dataread",
default="",
params={
"placeholder": "Write the path here..."
},
help = "Directory where the files with the simulations results are located.<br> This path has to be relative to the root fdf.",
),
ListInput(key="entry_points_order", name="Entry points order",
group="dataread",
default=[],
params={
"itemInput": OptionsInput(key="-", name="-", params={"options": []})
},
help="""Order with which entry points will be attempted."""
),
OptionsInput(
key="backend", name="Backend",
default=None,
params={},
help="Directory where the files with the simulations results are located.<br> This path has to be relative to the root fdf.",
),
)
@property
def read_data_methods(self):
entry_points_names = [entry_point._method.__name__ for entry_point in self.entry_points]
return ["_before_read", "_after_read", "_read_from_sources", *entry_points_names, *self._update_methods["read_data"]]
@property
def set_data_methods(self):
return ["_set_data", *self._update_methods["set_data"]]
@property
def get_figure_methods(self):
return ["_after_get_figure", *self._update_methods["get_figure"]]
def _parse_update_funcs(self, func_names):
""" Decides which functions to run when the settings of the plot are updated
This is called in self._run_updates as a final oportunity to decide what functions to run.
In the case of plots, all we basically want to know is if we need to read the data again (execute
`read_data`), set new data for the plot without needing to read (execute `set_data`) or just update
some aesthetic aspects of the plot (execute `get_figure`).
When Plot sees one of the following functions in the list of functions with updated parameters:
- "_before_read", "_after_read", any entry point function, functions in cls._update_methods["read_data"]
it knows that `read_data` needs to be executed. (which afterwards triggers `set_data` and `get_figure`).
Otherwise, if any of this functions is present:
- "_set_data", functions in cls._update_methods["set_data"]
it executes `set_data` (and `get_figure` subsequentially)
Finally, if it finds:
- "_after_get_figure", functions in cls._update_methods["get_figure"]
it executes `get_figure`.
WARNING: If it doesn't find any of these, it will return the unparsed list of functions,
and all functions will get executed.
Parameters
-----------
func_names: set of str
the unique functions names that are to be executed unless you modify them.
Returns
-----------
array-like of str
the final list of functions that will be executed.
See also
------------
``Configurable._run_updates``
"""
if len(func_names.intersection(self.read_data_methods)) > 0:
return ["read_data"]
if len(func_names.intersection(self.set_data_methods)) > 0:
return ["set_data"]
if len(func_names.intersection(self.get_figure_methods)) > 0:
return ["get_figure"]
return func_names
[docs] @classmethod
def from_plotly(cls, plotly_fig):
""" Converts a plotly plot to a Plot object
Parameters
-----------
plotly_fig: plotly.graph_objs.Figure
the figure that we want to convert into a sisl plot
Returns
-----------
Plot
the converted plot that contains the plotly figure.
"""
plot = cls(only_init=True)
plot.figure = plotly_fig
return plot
[docs] @classmethod
def plot_name(cls):
""" The name of the plot. Used to be displayed in the GUI, for example """
return getattr(cls, "_plot_type", cls.__name__)
[docs] @classmethod
def suffix(cls):
""" Get the suffix that this class adds to plotting functions
See sisl/viz/_plotables.py and particularly the `register_plotable`
function to understand this better.
"""
if cls is Plot:
return None
return getattr(cls, "_suffix", cls.__name__.lower().replace("plot", ""))
[docs] @classmethod
def entry_points_help(cls):
""" Generates a helpful message about the entry points of the plot class """
string = ""
for entry_point in cls.entry_points:
string += f"{entry_point._name.capitalize()}\n------------\n\n"
string += (entry_point.help or "").lstrip()
string += "\nSettings used:\n\t- "
string += '\n\t- '.join(map(lambda ab: ab[1], entry_point._method._settings))
string += "\n\n"
return string
@property
def _innotebook(self):
""" Boolean indicating whether this plot is being used in a notebook
Used to understand how we should display the plot.
"""
return running_in_notebook()
@property
def _widgets(self):
""" Dictionary that informs of which jupyter notebook widgets are available """
return check_widgets()
[docs] @classmethod
def multiple(cls, *args, fixed={}, template_plot=None, merge_method='together', **kwargs):
""" Creates a multiple plot out of a class
This class method returns a multiple plot that contains several plots of the same class.
It is a general method that serves as a common denominator for building MultiplePlot, SubPlot
or Animation objects. Therefore, it is used by the `animated` and `subplots` methods
If no arguments are passed, you will get the default multiple plot for the class, if there is any.
Parameters
-----------
*args:
Depending on what you pass the arguments will be interpreted differently:
- Two arguments:
First: str
Key of the setting that you want to be variable.
Second: array-like
Values that you want the setting to have in each individual plot.
Ex: BandsPlot.multiple("bands_file", ["file1", "file2", "file3"] )
will produce a multiple plot where each plot uses a different bands_file.
- One argument and it is a dictionary:
First: dict
The keys of this dictionary will be the setting keys you want to be variable
and the values are of course the values for each plot for that setting.
It works exactly as the previous case, but in this case we have multiple settings that vary.
- One argument and it is a function:
First: function
With this function you can produce any settings you want without limitations.
It will be used as the `_getInitKwargsList` method of the MultiplePlot object, so it needs to
accept self (the MultiplePlot object) and return a list of dictionaries which are the
settings for each plot.
fixed: dict, optional
A dictionary containing values for settings that will be fixed for all plots.
For the settings that you don't specify here you will get the defaults.
template_plot: sisl Plot, optional
If provided this plot will be used as a template.
It is important to know that it will not act only as the settings template,
but it will also PROVIDE DATA FOR THE OTHER PLOTS in case the data reading
settings are not being varied through the different plots.
This is extremely important to provide when possible, because in some cases the data
that a plot gathers can be very large and therefore it may not be even feasable to store
the repeated data in terms of memory/time.
merge_method: {'together', 'subplots', 'animation'}, optional
the way in which the multiple plots are 'related' to each other (i.e. how they should be displayed).
In most cases, instead of using this argument, you should probably use the specific method (`animated` or
`subplots`). They set this argument accordingly but also do some other work to make your life easier.
**kwargs:
Will be passed directly to initialization, so it can contain the settings for the MultiplePlot object, for example.
If args are not passed and the default multiple plot is being created, some keyword arguments may be used by the method
that generates the default multiple plot. One recurrent example of this is the keyword `wdir`.
Returns
--------
MultiplePlot, SubPlots or Animation
The plot that you asked for.
"""
#Try to retrieve the default animation if no arguments are provided
if len(args) == 0:
return call_method_if_present(cls, "_default_animation", fixed=fixed, **kwargs)
#Define how the getInitkwargsList method will look like
if callable(args[0]):
_getInitKwargsList = args[0]
else:
if len(args) == 2:
variable_settings = {args[0]: args[1]}
elif isinstance(args[0], dict):
variable_settings = args[0]
def _getInitKwargsList(self):
#Adding the fixed values to the list
vals = {
**{key: itertools.repeat(val) for key, val in fixed.items()},
**variable_settings
}
return dictOfLists2listOfDicts(vals)
# Choose the specific class that we want to initialize
MultipleClass = {'together': MultiplePlot, 'subplots': SubPlots, 'animation': Animation}[merge_method]
#Return the initialized multiple plot
return MultipleClass(_plugins={
"_getInitKwargsList": _getInitKwargsList,
"_plot_classes": cls,
**kwargs.pop('_plugins', {})
}, template_plot=template_plot, **kwargs)
[docs] @classmethod
def subplots(cls, *args, fixed={}, template_plot=None, rows=None, cols=None, arrange="rows", **kwargs):
""" Creates subplots where each plot has different settings
Mainly, it uses the `multiple` method to generate them.
Parameters
-----------
*args:
Depending on what you pass the arguments will be interpreted differently:
- Two arguments:
First: str
Key of the setting that you want to vary across subplots.
Second: array-like
Values that you want the setting to have in each subplot.
Ex: BandsPlot.multiple("bands_file", ["file1", "file2", "file3"] )
will produce a layout where each subplot uses a different bands_file.
- One argument and it is a dictionary:
First: dict
The keys of this dictionary will be the setting keys you want to vary across subplots
and the values are of course the values for each plot for that setting.
It works exactly as the previous case, but in this case we have multiple settings that vary.
- One argument and it is a function:
First: function
With this function you can produce any settings you want without limitations.
It will be used as the `_getInitKwargsList` method of the MultiplePlot object, so it needs to
accept self (the MultiplePlot object) and return a list of dictionaries which are the
settings for each plot.
fixed: dict, optional
A dictionary containing values for settings that will be fixed for all subplots.
For the settings that you don't specify here you will get the defaults.
template_plot: sisl Plot, optional
If provided this plot will be used as a template.
It is important to know that it will not act only as the settings template,
but it will also PROVIDE DATA FOR THE OTHER PLOTS in case the data reading
settings are not being varied through the different plots.
This is extremely important to provide when possible, because in some cases the data
that a plot gathers can be very large and therefore it may not be even feasable to store
the repeated data in terms of memory/time.
rows: int, optional
The number of rows of the plot grid. If not provided, it will be inferred from `cols`
and the number of plots. If neither `cols` or `rows` are provided, the `arrange` parameter will decide
how the layout should look like.
cols: int, optional
The number of columns of the subplot grid. If not provided, it will be inferred from `rows`
and the number of plots. If neither `cols` or `rows` are provided, the `arrange` parameter will decide
how the layout should look like.
arrange: {'rows', 'col', 'square'}, optional
The way in which subplots should be aranged if the `rows` and/or `cols`
parameters are not provided.
**kwargs:
Will be passed directly to SubPlots initialization, so it can contain the settings for it, for example.
If args are not passed and the default multiple plot is being created, some keyword arguments may be used by the method
that generates the default multiple plot. One recurrent example of this is the keyword `wdir`.
"""
return cls.multiple(*args, fixed=fixed, template_plot=None, merge_method='subplots',
rows=rows, cols=cols, arrange=arrange, **kwargs)
[docs] @classmethod
def animated(cls, *args, fixed={}, frame_names=None, template_plot=None, **kwargs):
""" Creates an animation out of a class
This class method returns an animation with frames belonging to a given plot class.
For example, if you run `BandsPlot.animated()` you will get an animation made of bands plots.
If no arguments are passed, you will get the default animation for that plot, if there is any.
Parameters
-----------
*args:
Depending on what you pass the arguments will be interpreted differently:
- Two arguments:
First: str
Key of the setting that you want to animate.
Second: array-like
Values that you want the setting to have at each animation frame.
Ex: BandsPlot.animated("bands_file", ["file1", "file2", "file3"] )
will produce an animation where each frame uses a different bands_file.
- One argument and it is a dictionary:
First: dict
The keys of this dictionary will be the setting keys you want to animate
and the values are of course the values for each frame for that setting.
It works exactly as the previous case, but in this case we have multiple settings to animate.
- One argument and it is a function:
First: function
With this function you can produce any settings you want without limitations.
It will be used as the `_getInitKwargsList` method of the animation, so it needs to
accept self (the animation object) and return a list of dictionaries which are the
settings for each frame.
the function will recieve the parameter and can act on it in any way you like.
It doesn't need to return the parameter, just modify it.
In this function, you can call predefined methods of the parameter, for example.
Ex: obj.modify_param("length", lambda param: param.incrementByOne() )
given that you know that this type of parameter has this method.
fixed: dict, optional
A dictionary containing values for settings that will be fixed along the animation.
For the settings that you don't specify here you will get the defaults.
frame_names: list of str or function, optional
If it is a list of strings, each string will be used as the name for the corresponding frame.
If it is a function, it should accept `self` (the animation object) and return a list of strings
with the frame names. Note that you can access the plot instance responsible for each frame under
`self.children`. The function will be run each time the figure is generated, so in this way your
frame names will be dynamic.
FRAME NAMES SHOULD BE UNIQUE, OTHERWISE THE ANIMATION WILL HAVE A WEIRD BEHAVIOR.
If this is not provided, frame names will be generated automatically.
template_plot: sisl Plot, optional
If provided this plot will be used as a template.
It is important to know that it will not act only as the settings template,
but it also will PROVIDE DATA FOR THE OTHER PLOTS in case the data reading
settings are not animated.
This is extremely important to provide when possible, because in some cases the data
that a plot gathers can be very large and therefore it may not be even feasable to store
the repeated data in terms of memory/time.
**kwargs:
Will be passed directly to animation initialization, so it can contain the settings for the animation, for example.
If args are not passed and the default animation is being created. Some keyword arguments may be used by the method
that generates the default animation. One recurrent example of this is the keyword `wdir`.
Returns
--------
Animation
The Animation that you asked for
"""
# And just let the general multiple plot creator do the work
return cls.multiple(*args, fixed=fixed, template_plot=template_plot, merge_method='animation',
frame_names=frame_names, **kwargs)
def __init_subclass__(cls):
""" Whenever a plot class is defined, this method is called
We will use this opportunity to:
- Register entry points.
- Generate a more helpful __init__ method that exposes all the settings.
We could use this to register plotables (see commented code).
However, there is one major problem: how to specify defaults.
This is a problem because sometimes an input field is inherited from one plot
to another, therefore you can not say: "this is the default plotable input".
Probably, defaults should be centralized, but I don't know where just yet.
"""
super().__init_subclass__()
# Register the entry points of this class.
cls.entry_points = []
for key, val in inspect.getmembers(cls, lambda x: isinstance(x, EntryPoint)):
cls.entry_points.append(val)
# After registering an entry point, we will just set the method
setattr(cls, key, _populate_with_settings(val._method, [param["key"] for param in cls._get_class_params()[0]]))
entry_points_order = cls.get_class_param("entry_points_order")
entry_points_order.modify_item_input(
"inputField.params.options",
[{"label": entry._name, "value": entry._name} for entry in cls.entry_points]
)
entry_points_order.modify(
"default",
[entry._name for entry in sorted(cls.entry_points, key=lambda entry: entry._sort_key)]
)
cls.backends = Backends(cls)
[docs] @vizplotly_settings('before', init=True)
def __init__(self, *args, H = None, attrs_for_plot={}, only_init=False, presets=None, layout={}, _debug=False, **kwargs):
# Give an ID to the plot
self.id = str(uuid.uuid4())
# Inform whether the plot is in debug mode or not:
self._debug = _debug
# Initialize shortcut management
ShortCutable.__init__(self)
# Give the user the possibility to do things before initialization (IDK why)
call_method_if_present(self, "_before_init")
#Set the isChildPlot attribute to let the plot know if it is part of a bigger picture (e.g. Animation)
self.isChildPlot = kwargs.get("isChildPlot", False)
#Initialize the variable to store when has been the last data read (0 means never basically)
self.last_dataread = 0
self._files_to_follow = []
# Check if the user has provided a hamiltonian (which can contain a geometry)
# This is not meant to be used by the GUI (in principle), just programatically
self.PROVIDED_H = False
self.PROVIDED_GEOM = False
if H is not None:
self.PROVIDED_H = True
self.H = H
self.setup_hamiltonian()
if presets is not None:
if isinstance(presets, str):
presets = [presets]
# on_figure_change is triggered after get_figure.
self.on_figure_change = None
# Set all the attributes that have been passed
# It is important that this is here so that it can overwrite any of
# the already written attributes
for key, val in attrs_for_plot.items():
setattr(self, key, val)
#If plugins have been provided, then add them.
#Plugins are an easy way of extending a plot. They can be methods, variables...
#They are added to the object instance, not the whole class.
if kwargs.get("_plugins"):
for name, plugin in kwargs.get("_plugins").items():
if isinstance(plugin, FunctionType):
plugin = MethodType(plugin, self)
setattr(self, name, plugin)
# Add the general plot shortcuts
self._general_plot_shortcuts()
#Give the user the possibility to overwrite default settings
call_method_if_present(self, "_after_init")
# If we were supposed to only initialize the plot, stop here
if only_init:
return
#Try to generate the figure (if the settings required are still not there, it won't be generated)
try:
if MultiplePlot in type.mro(self.__class__):
#If its a multiple plot try to inititialize all its child plots
if self.PLOTS_PROVIDED:
self.get_figure()
else:
self.init_all_plots()
else:
self.read_data()
except Exception as e:
if self._debug:
raise e
info(f"The plot has been initialized correctly, but the current settings were not enough to generate the figure.\nError: {e}")
def __str__(self):
""" Information to print about the plot """
string = (
f'Plot class: {self.__class__.__name__} Plot type: {self.plot_name()}\n\n'
'Settings:\n{}'.format("\n".join(["\t- {}: {}".format(key, value) for key, value in self.settings.items()]))
)
return string
def __getattr__(self, key):
""" This method is executed only after python has found that there is no such attribute in the instance
So let's try to find it elsewhere. There are two options:
- The attribute is in the backend object (self._backend)
- The attribute is currently being shared with other plots (only possible if it's a childplot)
"""
if key in ["_backend", "_get_shared_attr"]:
pass
elif hasattr(self, "_backend") and hasattr(self._backend, key):
return getattr(self._backend, key)
else:
#If it is a childPlot, maybe the attribute is in the shared storage to save memory and time
try:
return self._get_shared_attr(key)
except (KeyError, AttributeError):
pass
raise AttributeError(f"The attribute '{key}' was not found either in the plot, its backend, or in shared attributes.")
def __setattr__(self, key, val):
"""
If is a childplot and it has the attribute `_SHOULD_SHARE_WITH_SIBLINGS` set to True, we will submit the attribute to the shared store.
This happens in animations/multiple plots. There's a "leading plot" that reads the data and then shares it with the rest
so that they don't need to read it again, in a collective effort to save memory and time.
Otherwise we set the attribute to the plot itself.
"""
if key != '_SHOULD_SHARE_WITH_SIBLINGS' and getattr(self, '_SHOULD_SHARE_WITH_SIBLINGS', False):
self.share_attr(key, val)
else:
object.__setattr__(self, key, val)
def __getitem__(self, key):
""" Getting an item from plot returns the trace(s) that correspond to the requested indices """
if isinstance(key, (int, slice)):
return self.data[key]
def _general_plot_shortcuts(self):
""" In this method we set the shortcuts that are general to all plots
This is called in `__init__`
"""
self._listening_shortcut()
self.add_shortcut("ctrl+z", "Undo settings", self.undo_settings, _description="Takes the settings of the plot one step back")
@repeat_if_children
@vizplotly_settings('before')
def read_data(self, update_fig=True, **kwargs):
""" This method is responsible for organizing the data-reading step
If everything is done succesfully, it calls the next step (`set_data`)
"""
# Restart the files_to_follow variable so that we can start to fill it with the new files
# Apart from the explicit call in this method, setFiles and setup_hamiltonian also add files to follow
self._files_to_follow = []
call_method_if_present(self, "_before_read")
# We try to read from the different entry points available
self._read_from_sources()
# We don't update the last dataread here in case there has been a succesful data read because we want to
# wait for the after_read() method to be succesful
if self.source is None:
self.last_dataread = 0
call_method_if_present(self, "_after_read")
if self.source is not None:
self.last_dataread = time.time()
if update_fig:
self.set_data(update_fig = update_fig)
return self
def _read_from_sources(self, entry_points_order):
""" Tries to read the data from the different available entry points in the plot class
If it fails to read from all entry points, it raises an exception.
"""
# It is possible that the class does not implement any entry points,
# because it doesn't need to read any data. Then the plotting process
# will basically start at set_data.
if not self.entry_points:
return
errors = []
# Try to read data using all the different entry points
# This is just a first implementation. One of the reasons entry points
# have been implemented is that we can do smarter things than this.
for entry_point_name in entry_points_order:
for entry_point in self.entry_points:
if entry_point._name == entry_point_name:
break
else:
warn(f"Entry point {entry_point_name} not found in {self.__class__.__name__}")
continue
try:
returns = getattr(self, entry_point._method_attr)()
self.source = entry_point
return returns
except Exception as e:
errors.append("\t- {}: {}.{}".format(entry_point._name, type(e).__name__, e))
else:
self.source = None
raise ValueError("Could not read or generate data for {} from any of the possible sources.\nHere are the errors for each source:\n{}"
.format(self.__class__.__name__, "\n".join(errors)))
[docs] def follow(self, *files, to_abs=True, unfollow=False):
""" Makes sure that the object knows which files to follow in order to trigger updates
Parameters
----------
*files: str
a string that represents the path to the file that needs to be followed.
You can pass as many as you want as separate arguments. Note that if you have a list of
files you can pass them separately by doing `follow(*my_list_of_files)`, you don't need to
(and you shouldn't) build a loop :)
to_abs: boolean, optional
whether the paths should be converted to absolute paths to make file following procedures
more robust. It is better to leave it as True unless you have a good reason to change it.
unfollow: boolean, optional
whether the previous files should be unfollowed. If set to False, we are just adding more files.
"""
new_files = [Path(file_path).resolve() if to_abs else Path(file_path) for file_path in files or []]
self._files_to_follow = new_files if unfollow else [*self._files_to_follow, *new_files]
[docs] def get_sile(self, path, results_path, root_fdf, *args, follow=True, follow_kwargs={}, file_contents=None, **kwargs):
""" A wrapper around get_sile so that the reading of the file is registered
It has to main functions:
- Automatically following files that are read, so that you don't neet to go always like:
```
self.follow(file)
sisl.get_sile(file)
```
- Infering files from a root file. For example, using the root_fdf.
Parameters
----------
path: str
the path to the file that you want to read.
It can also be the setting key that you want to read.
*args:
passed to sisl.get_sile
follow: boolean, optional
whether the path should be followed.
follow_kwargs: dict, optional
dictionary of keywords that are passed directly to the follow method.
**kwargs:
passed to sisl.get_sile
"""
# If path is a setting name, retrieve it
if path in self.settings:
setting_key = path
path = self.get_setting(path)
# However, if it wasn't provided, try to infer it.
# For example, if it is a siesta sile, we will try to infer it
# from the fdf file
if not path:
sile_type = self.get_param(setting_key).dtype
# We need to check here if it is a SIESTA related sile!
fdf_sile = sisl.get_sile(root_fdf)
for rule in sisl.get_sile_rules(cls=sile_type):
filename = fdf_sile.get('SystemLabel', default='siesta') + f'.{rule.suffix}'
try:
path = fdf_sile.dir_file(filename, results_path)
return self.get_sile(path, *args, follow=True, follow_kwargs={}, file_contents=None, **kwargs)
except Exception:
pass
else:
raise FileNotFoundError(f"Tried to infer {setting_key} from the 'root_fdf', "
f"but didn't find any {sile_type.__name__} in {Path(fdf_sile._directory) / results_path }")
if follow:
self.follow(path, **follow_kwargs)
return sisl.get_sile(path, *args, **kwargs)
[docs] def updates_available(self):
""" This function checks whether the read files have changed
For it to work properly, one should specify the files that have been read by
their reading methods (usually, the entry points). This is done by using the
`follow()` method or by reading files with `self.get_sile()` instead of `sisl.get_sile()`.
"""
def modified(filepath):
try:
return filepath.stat().st_mtime > self.last_dataread
except FileNotFoundError:
return False # This probably should implement better logic
files_modified = np.array([modified(file_path) for file_path in self._files_to_follow])
return files_modified.any()
[docs] def listen(self, forever=True, show=True, as_animation=False, return_animation=True, return_figWidget=False,
clear_previous=True, notify=False, speak=False, notify_title=None, notify_message=None, speak_message=None, fig_widget=None):
""" Listens for updates in the followed files (see the `updates_available` method)
Parameters
---------
forever: boolean, optional
whether to keep listening after the first plot update
show: boolean, optional
whether to show the plot at the beggining and update the layout when the plot is updated.
as_animation: boolean, optional
will add a new frame each time the plot is updated.
The resulting animation is returned unless return_animation is set to False. This is done because
the Plot object iself is not converted to an animation. Instead, a new animation is created and if you
don't save it in a variable it will be lost, you will have no way to access it later.
If you are seeing two figures at the beggining, it is because you are not storing the animation figure.
Set the return_animation parameter to False if you understand that you are going to "lose" the animation,
you will only be able to see a display of it while it is there.
return_animation: boolean, optional
if as_animation is `True`, whether the animation should be returned.
Important: see as_animation for an explanation on why this is the case
return_figWidget: boolean, optional
it returns the figure widget that is in display in a jupyter notebook in case the plot has
succeeded to display it. Note that, even if you are in a jupyter notebook, you won't get a figure
widget if you don't have the plotly notebook extension enabled. Check `<your_plot>._widgets` to see
if you are missing witget support.
if return_animation is True, both the animation and the figure widget will be returned in a tuple.
Although right now, this does not make much sense because figure widgets don't support frames. You will get None.
clear_previous: boolean, optional
in case show is True, whether the previous version of the plot should be hidden or kept in display.
notify: boolean, optional
trigger a notification everytime the plot updates.
speak: boolean, optional
trigger a spoken message everytime the plot updates.
notify_title: str, optional
the title of the notification.
notify_message: str, optional
the message of the notification.
speak_message: str, optional
the spoken message. Feel free to get creative here!
"""
from IPython.display import clear_output
import asyncio
# This is a weird limitation, because multiple listeners could definitely
# be implemented, but I don't have time now, and I need to ensure that no listeners are left untracked
# If you need it, ask me! (Pol)
self.stop_listening()
pt = self
if as_animation:
pt = Animation(
plots = [self.clone()]
)
if show and fig_widget is None:
fig = pt.show(return_figWidget=True)
fig_widget = fig
if notify:
trigger_notification("SISL", "Notifications will appear here")
if speak:
spoken_message("I will speak when there is an update.")
async def listen():
while True:
if self.updates_available():
try:
self.read_data(update_fig=True)
if as_animation:
new_plot = self.clone()
pt.add_children(new_plot)
pt.get_figure()
if clear_previous and fig_widget is None:
clear_output()
if show and fig_widget is None:
pt.show()
else:
pt._backend._update_ipywidget(fig_widget)
if not forever:
self._listening_task.cancel()
if notify:
title = notify_title or "SISL PLOT UPDATE"
message = notify_message or f"{getattr(self, 'struct', '')} {self.__class__.__name__} updated"
trigger_notification(title, message)
if speak:
spoken_message(speak_message if speak_message is not None else f"Your {self.__class__.__name__} is updated. Check it out")
except Exception as e:
pass
await asyncio.sleep(1)
loop = asyncio.get_event_loop()
self._listening_task = loop.create_task(listen())
self.add_shortcut("ctrl+alt+l", "Stop listening", self.stop_listening, fig_widget=fig_widget, _description="Tell the plot to stop listening for updates")
if as_animation and return_animation:
if return_figWidget:
return pt, fig_widget
else:
return pt
elif return_figWidget:
return fig_widget
def _listening_shortcut(self, fig_widget=None):
""" Adds the shortcut to start listening for updates
This is done here and not in `_general_plot_settings` because
we need to be able to toggle this shortcut each time the plot
starts/stops listening.
Maybe at some point we can have a rule to automatically disable
shortcuts based on state in `ShortCutable`.
"""
self.add_shortcut(
"ctrl+alt+l", "Listen for updates",
self.listen, fig_widget=fig_widget,
_description="Make the plot listen for changes in the files that it reads"
)
[docs] def stop_listening(self, fig_widget=None):
""" Makes the plot stop listening for updates
Using this method only makes sense if you have previously made the plot listen
either through `Plot.listen()` or `Plot.show(listen=True)`
Parameters
-----------
fig_widget: plotly FigureWidget, optional
the figure widget where the plot is currently being displayed.
This is just used to reset the listening shortcut.
YOU WILL MOST LIKELY NOT USE THIS because `Plot` already knows
where is it being displayed in normal situations.
"""
task = getattr(self, "_listening_task", None)
if task is not None:
task.cancel()
self._listening_task = None
self._listening_shortcut(fig_widget=fig_widget)
return self
[docs] @vizplotly_settings('before')
def setup_hamiltonian(self, **kwargs):
""" Sets up the hamiltonian for calculations with sisl """
NEW_FDF = True
if len(self.settings_history) > 1:
NEW_FDF = self.settings_history.was_updated("root_fdf")
if not hasattr(self, "geometry") or NEW_FDF:
try:
fdf_sile = self.get_sile("root_fdf")
self.geometry = fdf_sile.read_geometry(output = True)
except Exception:
pass
if not self.PROVIDED_H and (not hasattr(self, "H") or NEW_FDF):
#Read the hamiltonian
fdf_sile = self.get_sile("root_fdf")
self.H = fdf_sile.read_hamiltonian()
else:
if isinstance(self.H, (str, Path)):
self.H = self.get_sile(self.H)
if isinstance(self.H, sisl.BaseSile):
self.H = self.H.read_hamiltonian(geometry=getattr(self, "geometry", None))
if not hasattr(self, "geometry"):
self.geometry = self.H.geometry
return self
@repeat_if_children
@vizplotly_settings('before')
def set_data(self, update_fig = True, **kwargs):
""" Method to process the data that has been read beforehand by read_data() and prepare the figure
If everything is succesful, it calls the next step in plotting (`get_figure`)
"""
self._for_backend = self._set_data()
if update_fig:
self.get_figure()
return self
#-------------------------------------------
# PLOT DISPLAY METHODS
#-------------------------------------------
[docs] def show(self, *args, listen=False, return_figWidget=False, **kwargs):
""" Displays the plot
Parameters
------
listen: bool, optional
after showing, keeps listening for file changes to update the plot.
This is nice for monitoring.
return_figureWidget: bool, optional
if the plot is displayed in a jupyter notebook, whether you want to
get the figure widget as a return so that you can act on it.
"""
if self._backend is None:
return warn("There is no plotting backend selected, so the plot can't be displayed.")
if listen:
self.listen(show=True, **kwargs)
if self._innotebook and (len(args) == 0 or 'config' in kwargs):
try:
return self._ipython_display_(listen=listen, return_figWidget=return_figWidget, **kwargs)
except Exception as e:
warn(e)
return self._backend.show(*args, **kwargs)
def _ipython_display_(self, return_figWidget=False, **kwargs):
""" Handles all things needed to display the plot in a jupyter notebook
Plotly already knows how to show a plot in the jupyter notebook, however
here we try to extend it to support shortcuts if the appropiate widget is
there (ipyevents, https://github.com/mwcraig/ipyevents).
Parameters
------
return_figureWidget: bool, optional
if the plot is displayed in a jupyter notebook, whether you want to
get the figure widget as a return so that you can act on it.
"""
from IPython.display import display
def _try_backend():
if self._backend is None:
return display(repr(self))
kwargs.pop("listen", None)
display_method = getattr(self._backend, "_ipython_display_", None)
if display_method is not None:
return display_method(**kwargs)
self._backend.show(**kwargs)
if not isinstance(self, Animation):
try:
widget = self._backend.get_ipywidget()
except Exception:
return _try_backend()
if False and self._widgets["events"]:
# For now we want provide keyboard shortcut support
# If ipyevents is available, show with shortcut support
self._ipython_display_with_shortcuts(widget, **kwargs)
else:
# Else, show without shortcut support
display(widget)
self._listening_shortcut(fig_widget=widget)
if return_figWidget:
return widget
else:
_try_backend()
def _ipython_display_with_shortcuts(self, fig_widget, **kwargs):
"""
If the appropiate widget is there (ipyevents, https://github.com/mwcraig/ipyevents),
we extend plotly's FigureWidget to support keypress events so that we can trigger
shortcuts from the notebook.
Parameters
------
fig_widget: plotly.graph_objs.FigureWidget
The figure widget that we need to extend.
"""
from IPython.display import display
from ipyevents import Event
from ipywidgets import HTML, Output
h = HTML("") # This is to display help such as available shortcuts
messages = HTML("") # This is to inform about current status
styles = HTML("<style>.ipyevents-watched:focus {outline: none}</style>")
d = Event(source=fig_widget, watched_events=['keydown', 'keyup'])
def handle_dom_events(event, keys_down=[], last_timestamp=[0], keys_up=[]):
# We will keep track of keydowns because then we will be able to support multiple keys shortcuts
time_threshold = 500 #To remove key up events
try:
# Clear the list
timestamp = event.get("timeStamp")
duplicates = len(keys_down) != len(set(keys_down))
time_diff = timestamp - last_timestamp[0]
if time_diff > 2000 or duplicates:
keys_down *= 0 #Clear the list
if time_diff > time_threshold:
keys_up *= 0
last_timestamp[0] = timestamp
# This means that the key has been held down for a long time
if event.get("repeat", False):
return
key = event.get("key", "").lower()
key_code = event.get("code", "")
ev_type = event.get("type", None)
if ev_type == "keydown":
if key == "control":
key = "ctrl"
# If it's a key down event, record it
keys_down.append(key)
elif ev_type == "keyup" and key in keys_down:
if key == "control":
key = "ctrl"
if len(keys_down) == 1:
keys_up.append(key)
# If it's a key up event, anounce that the key is not down anymore
keys_down.remove(key)
only_down = "+".join(keys_down)
shortcut_key = f'{" ".join(keys_up)} {only_down}'.strip()
if shortcut_key:
messages.value = f'<span style="border-radius: 3px; background: #ccc; padding: 5px 10px">{shortcut_key}</span>'
# Get the help message
if shortcut_key == "shift+?":
h.value = self.shortcuts_summary("html") if not h.value else ""
shortcut = self.shortcut(shortcut_key)
if shortcut is not None:
keys_down *= 0
keys_up *= 0
messages.value = f'Executing "{shortcut["name"]}" because you pressed "{shortcut_key}"...'
self.call_shortcut(shortcut_key)
messages.value = ""
self._update_ipywidget(fig_widget)
except Exception as e:
messages.value = f'<span style="color:darkred; font-weight: bold">{e}</span>'
d.on_dom_event(partial(handle_dom_events))
display(fig_widget, messages, h, styles, Output())
#-------------------------------------------
# PLOT MANIPULATION METHODS
#-------------------------------------------
[docs] def merge(self, others, to="multiple", extend_multiples=True, **kwargs):
""" Merges this plot's instance with the list of plots provided
Parameters
-------
others: array-like of Plot() or Plot()
the plots that we want to merge with this plot instance.
to: {"multiple", "subplots", "animation"}, optional
the merge method. Each option results in a different way of putting all the plots
together:
- "multiple": All plots are shown in the same canvas at the same time. Useful for direct
comparison.
- "subplots": The layout is divided in different subplots.
- "animation": Each plot is converted into the frame of an animation.
extend_multiples: boolean, optional
if True, if `MultiplePlot`s are passed, they are splitted into their children, so that the result
is the merge of its children with the rest.
If False, a `MultiplePlot` is treated as a solid unit.
kwargs:
extra arguments that are directly passed to `MultiplePlot`, `Subplots`
or `Animation` initialization.
Returns
-------
MultiplePlot, Subplots or Animation
depending on the value of the `to` parameter.
"""
#Make sure we deal with a list (user can provide a single plot)
if not isinstance(others, (list, tuple, np.ndarray)):
others = [others]
children = [self, *others]
if extend_multiples:
children = [[pt] if not isinstance(pt, MultiplePlot) else pt.children for pt in children]
# Flatten the list
children = [pt for plots in children for pt in plots]
PlotClass = {
"multiple": MultiplePlot,
"subplots": SubPlots,
"animation": Animation
}[to]
return PlotClass(plots=children, **kwargs)
[docs] def copy(self):
""" Returns a copy of the plot
If you want a plot with the exact plot configuration but newly initialized,
use `clone()` instead.
"""
return deepcopy(self)
[docs] def clone(self, *args, **kwargs):
""" Gets you and exact clone of this plot
You can pass extra args that will overwrite the previous parameters though, if you don't want it to be that exact.
IMPORTANT: IT WILL INITIALIZE A NEW PLOT, THEREFORE IT WILL READ NEW DATA.
IF YOU JUST WANT A COPY, USE THE `copy()` method.
"""
return deepcopy(self)
return self.__class__(*args, **self.settings, **kwargs)
#-------------------------------------------
# LISTENING TO EVENTS
#-------------------------------------------
[docs] def dispatch_event(self, event, *args, **kwargs):
""" Not functional yet """
warn((event, args, kwargs))
# Of course this needs to be done
raise NotImplementedError
#-------------------------------------------
# DATA TRANSFER/STORAGE METHODS
#-------------------------------------------
def __getstate__(self):
"""Returns the object to be pickled"""
# We just simply remove any sile from the settings history (as they are not pickleable)
# and replace it with a posix path. Note that this does not fix the problem if there are
# nested siles.
for key, item in self.settings_history._vals.items():
self.settings_history._vals[key] = [val.file if isinstance(val, sisl.io.BaseSile) else val for val in item]
return self.__dict__
[docs] def save(self, path):
""" Saves the plot so that it can be loaded in the future
Parameters
---------
path: str
The path to the file where you want to save the plot
Returns
---------
self
"""
import dill
if isinstance(path, str):
path = Path(path)
with open(path, 'wb') as handle:
dill.dump(self, handle, protocol=dill.HIGHEST_PROTOCOL)
return True
class EntryPoint:
def __init__(self, name, sort_key, setting_key, method, instance=None):
self._name = name
self._sort_key = sort_key
self._method_attr = method.__name__
self._setting_key = setting_key
self._method = method
self.help = method.__doc__
def entry_point(name, sort_key=0):
""" Helps registering entry points for plots
See the usage section to get a fast intuitive way of how to use it.
Basically, you need to provide some parameters (which are described
in the parameters section), and this function will return a decorator that
you can use in the functions of your plot class that do the reading part.
A function that is meant to read data but it's not marked as an entry_point
will be invisible to Plot.
NOTE: A plot class can have no entry points. This is perfectly fine if the
class does not need to read data for some reason. In this case, we will go straight
into the data setting methods (i.e. set_data).
Examples
-----------
>>> class MyPlot(Plot):
>>> @entry_point('siesta_output')
>>> def _lets_read_from_siesta_output(self):
>>> ...do some work here
>>>
>>> @entry_point('ask_mum'):
>>> def _we_are_quite_lost_so_we_better_ask_mum(self):
>>> self.call_mum()
Parameters
-----------
name: str
the name of the entry point that the decorated function implements.
sort_key: any
the entry points order will be sorted according to this key.
"""
return partial(EntryPoint, name, sort_key, ())
#------------------------------------------------
# CLASSES TO SUPPORT COMPOSITE PLOTS
#------------------------------------------------
[docs]class MultiplePlot(Plot):
""" General handler of a group of plots that need to be rendered together
Parameters
----------
root_fdf: fdfSileSiesta, optional
Path to the fdf file that is the 'parent' of the results.
results_path: str, optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
entry_points_order: array-like, optional
Order with which entry points will be attempted.
backend: optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
"""
_trigger_kw = "varying"
[docs] def __init__(self, *args, plots=None, template_plot=None, **kwargs):
self.shared = {}
# Take the plots if they have already been created and are provided by the user
self.PLOTS_PROVIDED = plots is not None
if self.PLOTS_PROVIDED:
self.set_children(plots)
self.has_template_plot = False
if isinstance(template_plot, Plot):
self.template_plot = template_plot
self.has_template_plot = True
super().__init__(*args, **kwargs)
def __getitem__(self, i):
""" Gets a given child plot """
return self.children[i]
@staticmethod
def _kw_from_cls(cls):
return cls._trigger_kw
@staticmethod
def _cls_from_kw(key):
for cls in MultiplePlot.__subclasses__():
if cls._trigger_kw == key:
return cls
else:
return None
@property
def _attrs_for_children(self):
""" Returns all the attributes that its children should have """
return {
'isChildPlot': True,
'_get_shared_attr': lambda key: self.shared_attr(key),
'share_attr': lambda key, val: self.set_shared_attr(key, val)
}
[docs] def init_all_plots(self, update_fig=True, try_sharing=True):
""" Initializes all child plots
Parameters
-----------
update_fig: boolean, optional
whether we should build the figure if this step is succesful.
try_sharing: boolean, optional
If `True`, we will check if all plots have exactly the same settings to read
data and, in that case, they will share attributes to avoid memory waste.
This is specially essential for plots that read big amounts of data (e.g. `GridPlot`),
but also for those that take significant time to read it.
"""
if not self.PLOTS_PROVIDED:
# If there is a template plot, take its settings as the starting point
template_settings={}
if self.has_template_plot:
self._plot_classes = self.template_plot.__class__
template_settings = self.template_plot.settings
SINGLE_CLASS = isinstance(self._plot_classes, type)
# Initialize all the plots
# In case there is only one class only initialize them, avoid reading data
# In this case, it is extremely important to initialize them all in serial mode, because
# with multiprocessing they won't know that the current instance is their parent
# (objects get copied in multiprocessing) and they won't be able to share data
plots = init_multiple_plots(
self._plot_classes,
kwargsList = [
{**template_settings, **kwargs, "attrs_for_plot": self._attrs_for_children, "only_init": SINGLE_CLASS and try_sharing}
for kwargs in self._getInitKwargsList()
],
serial=SINGLE_CLASS and try_sharing
)
if SINGLE_CLASS and try_sharing:
if not self.has_template_plot:
# Our leading plot will be the first one
leading_plot = plots[0]
else:
leading_plot = self.template_plot
# Now, we get the settings of the first plot
read_data_settings = {
key: leading_plot.get_setting(key) for key, funcs in leading_plot._run_on_update.items()
if set(funcs).intersection(leading_plot.read_data_methods)
}
for i, plot in enumerate(plots):
if not plot.has_these_settings(read_data_settings):
# If there is a plot that needs to read different data, we will just
# make each of them read their own data. (this could be optimized by grouping plots)
self.init_all_plots(try_sharing=False)
break
else:
# In case there is no plot that has different settings, we will
# happily set the data, avoiding the read data step. Plots will take
# their missing attributes from the shared store or from the plot
# template
self.set_children(plots)
if not self.has_template_plot:
leading_plot._SHOULD_SHARE_WITH_SIBLINGS = True
leading_plot.read_data(update_fig=False)
leading_plot._SHOULD_SHARE_WITH_SIBLINGS = False
self.set_data()
else:
# If we haven't tried sharing data, the plots are already prepared (with read data of their own)
self.set_children(plots)
call_method_if_present(self, "_after_children_updated")
if update_fig:
self.get_figure()
return self
[docs] def update_children_settings(self, children_sel=None, **kwargs):
""" Updates the settings of all child plots
Parameters
-----------
children_sel: array-like of int, optional
The indices of the child plots that you want to update.
**kwargs
Keyword arguments specifying the settings that you want to update
and the values you want them to have
"""
return self.update_settings(on_children=True, on_parent_plot=False, children_sel=children_sel, **kwargs)
def _update_settings(self, on_children=False, on_parent_plot=True, children_sel=None, **kwargs):
""" This method takes into account that on plots that contain children, one may want to update only the parent settings or all the child's settings.
Parameters
-----------
on_children: boolean, optional
whether the settings should be updated on child plots
on_parent_plot: boolean, optional
whether the settings should be updated on the parent plot.
children_sel: array-like of int, optional
The indices of the child plots that you want to update.
"""
if on_parent_plot:
super()._update_settings(**kwargs)
if on_children:
repeat_if_children(Configurable._update_settings)(self, children_sel=children_sel, **kwargs)
call_method_if_present(self, "_after_children_updated")
return self
[docs] def set_children(self, plots, keep=False):
""" Sets the children of a multiple plot
Parameters
--------
plots: array-like of sisl.viz.plotly.Plot or plotly Figure
the plots that should be set as children for the animation.
keep: boolean, optional
whether the existing children should be kept.
If `True`, `plots` is added after them.
"""
for plot in plots:
for key, val in self._attrs_for_children.items():
setattr(plot, key, val)
self.children = plots if not keep else [*self.children, *plots]
return self
[docs] def add_children(self, *plots):
""" Append children to the existing ones
Parameters
-----------
*plots: Plot
all the plots that you want to add as child plots of this one.
"""
self.set_children(plots, keep=True)
[docs] def insert_childplot(self, index, plot):
""" Inserts a plot in a given position of the children list
Parameters
----------
index: int
The position where the plot should be inserted
plot: sisl Plot or plotly Figure
The plot to insert in the list
"""
self.children.insert(index, plot)
[docs] def shared_attr(self, key):
""" Gets an attribute that is located in the shared storage of the MultiplePlot
This method will be given to all children so that they can retreive the shared
attributes. This is done in `set_children`.
Parameters
------------
key: str
the name of the attribute that you want to retrieve
Returns
-----------
any
the value that you asked for
"""
# If from the beggining there is a template plot, the shared
# storage is actually that plot.
if self.has_template_plot:
return getattr(self.template_plot, key)
return self.shared[key]
[docs] def set_shared_attr(self, key, val):
""" Sets the value of a shared attribute
Parameters
------------
key: str
the key of the attribute that is to be set.
val: any
the new value for the attribute
"""
self.shared[key] = val
return self
[docs]class Animation(MultiplePlot):
""" Version of MultiplePlot that renders each plot in a different animation frame
Parameters
----------
frame_duration: int, optional
Time (in ms) that each frame will be displayed. This is only
meaningful in the plotly backend
interpolated_frames: int, optional
The number of frames that should be interpolated between two plots.
This is only meaningful in the blender backend.
redraw: bool, optional
Whether each frame of the animation should be redrawn
If False, the animation will try to interpolate between one frame and
the other Set this to False if you are sure that the
frames contain the same number of traces, otherwise new traces will
not appear.
ani_method: optional
It determines how the animation is rendered.
root_fdf: fdfSileSiesta, optional
Path to the fdf file that is the 'parent' of the results.
results_path: str, optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
entry_points_order: array-like, optional
Order with which entry points will be attempted.
backend: optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
"""
_trigger_kw = "animate"
_isAnimation = True
_param_groups = (
{
"key": "animation",
"name": "Animation specific settings",
"icon": "videocam",
"description": "The fact that you have not studied cinematography is not a good excuse for creating ugly animations. <b>Customize your animation with these settings</b>"
},
)
_parameters = (
IntegerInput(
key = "frame_duration", name = "Frame duration",
default = 500,
group = "animation",
params = {
"step": 100
},
help = "Time (in ms) that each frame will be displayed. <br> This is only meaningful in the plotly backend"
),
IntegerInput(
key="interpolated_frames", name="Frames between images",
default=5,
group="animation",
help = "The number of frames that should be interpolated between two plots. This is only meaningful in the blender backend."
),
BoolInput(
key='redraw', name='Redraw each frame',
default=True,
group='animation',
help="""Whether each frame of the animation should be redrawn<br>
If False, the animation will try to interpolate between one frame and the other<br>
Set this to False if you are sure that the frames contain the same number of traces, otherwise new traces will not appear."""
),
OptionsInput(
key='ani_method', name="Animation method",
default=None,
group='animation',
params={
"placeholder": "Choose the animation method...",
"options": [
{"label": "Update", "value": "update"},
{"label": "Animate", "value": "animate"},
],
"isClearable": True,
"isSearchable": True,
"isMulti": False
},
help="""It determines how the animation is rendered. """
)
)
[docs] def __init__(self, *args, frame_names=None, _plugins={}, **kwargs):
if frame_names is not None:
_plugins["_get_frame_names"] = frame_names if callable(frame_names) else lambda self, i: frame_names[i]
elif "_get_frame_names" not in _plugins:
_plugins["_get_frame_names"] = lambda self, i: f"Frame {i}"
super().__init__(*args, **kwargs, _plugins=_plugins)
[docs]class SubPlots(MultiplePlot):
""" Version of MultiplePlot that renders each plot in a separate subplot
Parameters
-----------
arrange: optional
The way in which subplots should be aranged if the `rows` and/or
`cols` parameters are not provided.
rows: int, optional
The number of rows of the plot grid. If not provided, it will be
inferred from `cols` and the number of plots. If neither
`cols` or `rows` are provided, the `arrange` parameter will decide
how the layout should look like.
cols: int, optional
The number of columns of the subplot grid. If not provided, it will
be inferred from `rows` and the number of plots. If
neither `cols` or `rows` are provided, the `arrange` parameter will
decide how the layout should look like.
make_subplots_kwargs: dict, optional
Extra keyword arguments that will be passed to make_subplots.
root_fdf: fdfSileSiesta, optional
Path to the fdf file that is the 'parent' of the results.
results_path: str, optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
entry_points_order: array-like, optional
Order with which entry points will be attempted.
backend: optional
Directory where the files with the simulations results are
located. This path has to be relative to the root fdf.
"""
_trigger_kw = "subplots"
_is_subplots = True
_parameters = (
OptionsInput(key='arrange', name='Automatic arrangement method',
default='rows',
params={
'options': [
{'value': option, 'label': option} for option in ('rows', 'cols', 'square')
],
'placeholder': 'Choose a subplot arrangement method...',
'isMulti': False,
'isSearchable': True,
'isClearable': True,
},
help="""The way in which subplots should be aranged if the `rows` and/or `cols`
parameters are not provided."""
),
IntegerInput(key='rows', name='Rows',
default=None,
help="""The number of rows of the plot grid. If not provided, it will be inferred from `cols`
and the number of plots. If neither `cols` or `rows` are provided, the `arrange` parameter will decide
how the layout should look like."""
),
IntegerInput(key='cols', name='Columns',
default=None,
help="""The number of columns of the subplot grid. If not provided, it will be inferred from `rows`
and the number of plots. If neither `cols` or `rows` are provided, the `arrange` parameter will decide
how the layout should look like."""
),
ProgramaticInput(key='make_subplots_kwargs', name='make_subplot additional arguments',
dtype=dict,
default={},
help="""Extra keyword arguments that will be passed to make_subplots."""
)
)