"""gustaf/gustaf/show.py.
Everything related to show/visualization.
"""
import sys
import numpy as np
from gustaf import utils
try:
from gustaf.helpers.notebook import K3DPlotterN
except ImportError as err:
from gustaf.helpers.raise_if import ModuleImportRaiser
K3DPlotterN = ModuleImportRaiser("IPython and ipywidgets", err)
# @linux it raises error if vedo is imported inside the function.
try:
import vedo
# class name UGrid is deprecated since 2023.5.0
# After *.5.1 release, we could remove this part by bumping min. version
# requirement
if vedo.__version__ < "2023.5.0":
vedoUGrid = vedo.UGrid
else:
vedoUGrid = vedo.UnstructuredGrid
except ImportError as err:
# overwrites the vedo module with an object which will throw an error
# as soon as it is used the first time. This means that any non vedo
# functionality works as before, but as soon as vedo is used a
# comprehensive exception will be raised which is understandable in
# contrast to the possible errors previously possible
from gustaf.helpers.raise_if import ModuleImportRaiser
vedo = ModuleImportRaiser("vedo", err)
vedoUGrid = vedo
# enable `gus.show()`
# taken from https://stackoverflow.com/questions/1060796/callable-modules
# will use this until this module is renamed
class _CallableShowDotPy(sys.modules[__name__].__class__):
def __call__(self, *args, **kwargs):
"""call show()"""
return show(*args, **kwargs)
sys.modules[__name__].__class__ = _CallableShowDotPy
# True if the current environment is IPython else False.
try:
from IPython import get_ipython
is_ipython = get_ipython() is not None
except ImportError:
is_ipython = False
[docs]
def show(*args, **kwargs):
"""`vedo.show` wrapper. Each args represent one section of window. In other
words len(args) == N, where N corresponds to the parameter for vedo.show().
Parameters
-----------
*args: Union[List[Union[gustaf_obj, vedo_obj]], Dict[str, Any]]]
"""
# vedo plotter parameter
N = len(args)
offs = kwargs.get("offscreen", False)
interact = kwargs.get("interactive", True)
plt = kwargs.get("vedoplot")
skip_clear = kwargs.get("skip_clear", False)
close = kwargs.get("close")
size = kwargs.get("size", "auto")
cam = kwargs.get("cam")
title = kwargs.get("title", "gustaf")
background = kwargs.get("background", "white")
return_show_list = kwargs.get("return_showable_list", False)
axes = kwargs.get("axes")
def clear_vedo_plotter(plotter, num_renderers, skip_cl=skip_clear):
"""enough said."""
# for whatever reason it is desired
if skip_cl:
return None
# tmp workaround for linux
vedo_renderers = getattr(plotter, "renderers", None)
if vedo_renderers is not None and len(vedo_renderers) < num_renderers:
return None
for i in range(num_renderers):
plotter.clear(at=i, deep=True)
return None
def cam_tuple_to_list(dict_cam):
"""if entity is tuple, turns it into list."""
if dict_cam is None:
return None
for key, value in dict_cam.items():
if isinstance(value, tuple):
dict_cam[key] = list(value)
return dict_cam
# get plotter
if plt is None:
if is_ipython and vedo.settings.default_backend == "k3d":
vedo.settings.backend_autoclose = False
plt = K3DPlotterN(N, size, background)
else:
if is_ipython:
utils.log.warning(
"Gustaf plotting in notebooks is only supported with k3d"
"backend. To use this backend, set "
"vedo.settings.default_backend = 'k3d' in your notebook."
" Using the default backend might give unexpected results "
"and errors."
)
plt = vedo.Plotter(
N=N,
sharecam=False,
offscreen=offs,
size=size,
title=title,
bg=background,
)
else:
if is_ipython:
utils.log.warning(
"Please do not provide a plotter in IPython applications."
"This will produce an error shortly."
)
# check if plt has enough Ns
trueN = np.prod(plt.shape)
clear_vedo_plotter(plt, trueN) # always clear.
if trueN != N:
utils.log.warning(
"Number of args exceed given vedo.Plotter's capacity.",
"Assigning a new one",
)
title = plt.title
if close: # only if it is explicitly stated
plt.close() # Hope that this truly releases..
# assign a new one
plt = vedo.Plotter(
N=N,
sharecam=False,
offscreen=offs,
size=size,
title=title,
bg=background,
)
# loop and plot
for i, arg in enumerate(args):
# form valid input type.
if isinstance(arg, dict):
show_list = list(arg.values())
elif isinstance(arg, list):
show_list = arg.copy()
else:
# raise TypeError(
# "For vedo_show, only list or dict is valid input")
utils.log.debug(
"one of args for show_vedo is neither `dict` nor",
"`list`. Putting it naively into a list.",
)
show_list = [arg]
# quick check if the list is gustaf or non-gustaf
# if gustaf, make it vedo-showable.
# if there's spline, we need to pop the element and
# extend showables to the list.
# A show_list is a list to be plotted into a single sub frame of the
# plot
list_of_showables = []
for sl in show_list:
if not isinstance(sl, list):
sl = [sl] # noqa: PLW2901
for _k, item in enumerate(sl):
if hasattr(item, "showable"):
tmp_showable = item.showable(**kwargs)
# splines return dict
# - maybe it is time to do some typing..
if isinstance(tmp_showable, dict):
# add to extend later
list_of_showables.extend(list(tmp_showable.values()))
else:
# replace gustaf_obj with vedo_obj.
list_of_showables.append(tmp_showable)
else:
list_of_showables.extend(sl)
# set interactive to true at last element
if int(i + 1) == len(args):
plt.show(
list_of_showables,
at=i,
interactive=interact,
camera=cam_tuple_to_list(cam),
axes=axes,
)
else:
plt.show(
list_of_showables,
at=i,
interactive=False,
camera=cam_tuple_to_list(cam),
axes=axes,
)
if is_ipython:
plt.display(close=close)
return None
if interact and not offs:
# only way to ensure memory is released
clear_vedo_plotter(plt, np.prod(plt.shape))
if close or close is None: # explicitly given or None.
# It seems to leak some memory, but here it goes.
plt.close() # if i close it, this cannot be reused...
plt = None
if return_show_list:
return (plt, list_of_showables)
else:
return plt
[docs]
def make_showable(obj, as_dict=False, **kwargs):
"""Generates a vedo obj based on `kind` attribute from given obj, as well
as show_options.
Parameters
-----------
obj: gustaf obj
as_dict: bool
If True, returns vedo objects in a dict. Corresponding main objects will
be available with ["main"] key. Else, returns vedo.Assembly object,
where all the objects are grouped together.
**kwargs: kwargs
Will try to overwrite applicable items.
Returns
--------
vedo_obj: vedo obj
"""
# in case kwargs are defined, we will make a copy of the object and
# try to overwrite all the applicable kwargs.
if kwargs:
# keep original ones and assign new show_options temporarily
orig_show_options = obj.show_options
obj._show_options = obj.__show_option__(obj)
orig_show_options.copy_valid_options(obj.show_options)
for key, value in kwargs.items():
try:
obj.show_options[key] = value
except BaseException:
utils.log.debug(
f"Skipping invalid option {key} for "
f"{obj.show_options._helps}"
)
continue
# minimal-initialization of vedo objects
vedo_obj = obj.show_options._initialize_showable()
# as dict?
if as_dict:
return_as_dict = {}
# set common values. Could be a perfect place to try :=, but we want to
# support p3.6.
c = obj.show_options.get("c", None)
if c is not None:
vedo_obj.c(c)
alpha = obj.show_options.get("alpha", None)
if alpha is not None:
vedo_obj.alpha(alpha)
lighting = obj.show_options.get("lighting", None)
if lighting is not None:
vedo_obj.lighting(lighting)
vertex_ids = obj.show_options.get("vertex_ids", False)
element_ids = obj.show_options.get("element_ids", False)
# special treatment for vertex
if obj.kind.startswith("vertex"):
vertex_ids = vertex_ids | element_ids
if element_ids:
utils.log.debug(
"`element_ids` option is True for Vertices. Overwriting it as"
"vertex_ids."
)
element_ids = False
if vertex_ids:
# use vtk font. supposedly faster. And differs from cell id.
vertex_ids = vedo_obj.labels("id", on="points", font="VTK")
if not as_dict:
vedo_obj += vertex_ids
else:
return_as_dict["vertex_ids"] = vertex_ids
if element_ids:
# should only reach here if this obj is not vertex
element_ids = vedo.Points(obj.centers()).labels("id", on="points")
if not as_dict:
vedo_obj += element_ids
else:
return_as_dict["element_ids"] = element_ids
# data plotting
data = obj.show_options.get("data", None)
vertex_data = obj.vertex_data.as_scalar(data, None)
if data is not None and vertex_data is not None:
# transfer data
if obj.kind.startswith("edge"):
vedo_obj.pointdata[data] = vertex_data[obj.edges].reshape(
-1, vertex_data.shape[1]
)
else:
vedo_obj.pointdata[data] = vertex_data
# form cmap kwargs for init
cmap_keys = ("vmin", "vmax")
cmap_kwargs = obj.show_options[cmap_keys]
# set a default cmap if needed
cmap_kwargs["input_cmap"] = obj.show_options.get("cmap", "plasma")
cmap_kwargs["alpha"] = obj.show_options.get("cmap_alpha", 1)
# Discrete set of colors (vedo default is 256)
cmap_kwargs["n_colors"] = obj.show_options.get("cmap_n_colors", 256)
# add data
cmap_kwargs["input_array"] = data
# set cmap
# pass input_cmap as positional arg to support 2023.4.3.
# arg name changed in 2023.4.4
vedo_obj.cmap(cmap_kwargs.pop("input_cmap"), **cmap_kwargs)
# at last, scalarbar
sb_kwargs = obj.show_options.get("scalarbar", None)
if sb_kwargs is not None and sb_kwargs is not False:
sb_kwargs = {} if isinstance(sb_kwargs, bool) else sb_kwargs
vedo_obj.add_scalarbar(**sb_kwargs)
sb3d_kwargs = obj.show_options.get("scalarbar3d", None)
if sb3d_kwargs is not None and sb3d_kwargs is not False:
sb3d_kwargs = {} if isinstance(sb3d_kwargs, bool) else sb3d_kwargs
vedo_obj.add_scalarbar3d(**sb3d_kwargs)
elif data is not None and vertex_data is None:
utils.log.debug(f"No vertex_data named '{data}' for {obj}. Skipping")
# arrow plots - this is independent from data plotting.
arrow_data = obj.show_options.get("arrow_data", None)
# will raise if data is scalar
arrow_data_value = obj.vertex_data.as_arrow(arrow_data, None, True)
if arrow_data is not None and arrow_data_value is not None:
from gustaf.create.edges import from_data
# we are here because this data is not a scalar
# is showable?
if arrow_data_value.shape[1] not in (2, 3):
raise ValueError(
"Only 2D or 3D data can be shown.",
f"Requested data is {arrow_data_value.shape[1]}",
)
as_edges = from_data(
obj,
arrow_data_value,
obj.show_options.get("arrow_data_scale", None),
data_norm=obj.vertex_data.as_scalar(arrow_data),
)
arrows = vedo.Arrows(
as_edges.vertices[as_edges.edges],
c=obj.show_options.get("arrow_data_color", "plasma"),
)
if not as_dict:
vedo_obj += arrows
else:
return_as_dict["arrow_data"] = arrows
axes_kw = obj.show_options.get("axes", None)
# need to explicitly check if it is false
if axes_kw is not None and axes_kw is not False:
axes_kw = {} if isinstance(axes_kw, bool) else axes_kw
axes = vedo.Axes(vedo_obj, **axes_kw)
if not as_dict:
vedo_obj += axes
else:
return_as_dict["axes"] = axes
# set back temporary show_options if needed
if kwargs:
obj._show_options = orig_show_options
if not as_dict:
return vedo_obj
else:
return_as_dict["main"] = vedo_obj
return return_as_dict
# possibly relocate, is this actually used?
# could not find any usage in this repo
[docs]
def interpolate_vedo_dictcam(cameras, resolutions, spline_degree=1):
"""Interpolate between vedo dict cameras.
Parameters
------------
cameras: list or tuple
resolutions: int
spline_degree: int
if > 1 and splinepy is available and there are more than two cameras,
we interpolate all the entries using spline.
Returns
--------
interpolated_cams: list
"""
try:
import splinepy
spp = True
except ImportError:
spp = False
# quick type check loop
cam_keys = ["pos", "focalPoint", "viewup", "distance", "clippingRange"]
for cam in cameras:
if not isinstance(cam, dict):
raise TypeError("Only `dict` description of vedo cam is allowed.")
else:
for key in cam_keys:
if cam[key] is None:
raise ValueError(
f"One of the camera does not contain `{key}` info"
)
interpolated_cams = []
total_cams = int(resolutions) * (len(cameras) - 1)
if spp and spline_degree > 1 and len(cameras) > 2:
if spline_degree > len(cameras):
raise ValueError(
"Not enough camera to interpolate with "
f"spline degree {spline_degree}"
)
ps = []
fs = []
vs = []
ds = []
cs = []
for cam in cameras:
ps.append(list(cam[cam_keys[0]]))
fs.append(list(cam[cam_keys[1]]))
vs.append(list(cam[cam_keys[2]]))
ds.append([float(cam[cam_keys[3]])])
cs.append(list(cam[cam_keys[4]]))
interpolated = {}
for i, prop in enumerate([ps, fs, vs, ds, cs]):
i_spline = splinepy.BSpline()
i_spline.interpolate_curve(
query_points=prop,
degree=spline_degree,
save_query=False,
)
interpolated[cam_keys[i]] = i_spline.sample([total_cams])
for i in range(total_cams):
interpolated_cams.append(
{
cam_keys[0]: interpolated[cam_keys[0]][i].tolist(),
cam_keys[1]: interpolated[cam_keys[1]][i].tolist(),
cam_keys[2]: interpolated[cam_keys[2]][i].tolist(),
cam_keys[3]: interpolated[cam_keys[3]][i][0], # float?
cam_keys[4]: interpolated[cam_keys[4]][i].tolist(),
}
)
else:
i = 0
for start_cam, end_cam in zip(cameras[:-1], cameras[1:]):
if i == 0:
interpolated = [
np.linspace(
start_cam[ckeys],
end_cam[ckeys],
resolutions,
).tolist()
for ckeys in cam_keys
]
else:
interpolated = [
np.linspace(
start_cam[ckeys],
end_cam[ckeys],
int(resolutions + 1),
)[1:].tolist()
for ckeys in cam_keys
]
i += 1
for j in range(resolutions):
interpolated_cams.append(
{
cam_keys[0]: interpolated[0][j],
cam_keys[1]: interpolated[1][j],
cam_keys[2]: interpolated[2][j],
cam_keys[3]: interpolated[3][j], # float?
cam_keys[4]: interpolated[4][j],
}
)
return interpolated_cams