Visualising field using holoviews
#
There are several ways how a field can be visualised, using:
matplotlib
k3d
holoviews
vtk-based libraries, e.g.
pyvista
holoviews
provides interactive two-dimensional plots of fields. Different from the matplotlib
interface it is not required to first slice the field using sel
method to obtain a two dimensional field. Instead, a slider is automatically created for the out-of-plane component of the 3d field, for example.
[1]:
import discretisedfield as df
As an example we study an eliptical cylinder. We initialise the field such that its x and y component describe a vortex-like object (with the vortex core position changing along z direction) and the z component changing (and swapping sign) with the position in z direction. We set valid="norm"
to automatically hide the cells outside the cylinder in all plots.
[2]:
mesh = df.Mesh(
p1=(-20e-9, -10e-9, -5e-9), p2=(20e-9, 10e-9, 5e-9), cell=(1e-9, 1e-9, 1e-9)
)
def value(p):
x, y, z = p
return -y + z, x + z, z
def norm(p):
x, y, _ = p
if (x / 2) ** 2 + y**2 > 10e-9**2:
return 0
else:
return 1
field = df.Field(mesh, nvdim=3, value=value, norm=norm, valid="norm")
We can create a simple plot by calling hv
. We have to pass two of the spatial directions out of x
, y
, and z
to the kdims
variable. We choose the x
and y
directions. This defines the plot x and plot y direction. Furthermore, we automatically get a slider to select a value for the remaining out-of-plane direction.
[3]:
# Note for web users: You should have an active notebook session to interact with the plot
field.hv(kdims=["x", "y"])
[3]:
If there is only one element in the out-of-plane directon the slider is omitted. Note, that holoviews
by default picks different colourmaps depending on the type of data (and the symmetries it detects in them). Changing the colourmap is explained below.
[4]:
field.sel("z").hv(kdims=["x", "y"])
[4]:
Internally, the field is converted to an xarray DataArray
and hvplot
is used to create the plot. The plot consists of two parts, a “scalar” part for the out-of-plane component that is visualised with the colour and an in-plane-component part visualised with arrows. We now discuss how to create and modify these individually and in the end come back to the combined plot.
Scalar plot#
To create a scalar plot we use hv.scalar
.
[5]:
field.z.hv.scalar(kdims=["x", "y"], clim=(-1, 1))
[5]:
In the above example we have extracted a single component for the scalar plot. The hv
interface additionally allows us to do this interactively with a separate widget.
[6]:
field.hv.scalar(kdims=["x", "y"])
[6]:
To filter out parts of the field we can pass an additional variable roi
. For this example we plot the field in the xy plane, normal to the z direction and remove all values where the absolute value of the z component of the vector field is smaller than 0.5. We can pass additional keyword arguments that are directly forwarded to xarray.hvplot
, e.g. clim
and cmap
. For all available options please refer to the documentation of holoviews.
[7]:
roi = df.Field(field.mesh, nvdim=1, value=abs(field.z.array) < 0.5)
field.z.hv.scalar(kdims=["x", "y"], roi=roi, clim=(-1, 1), cmap="plasma")
[7]:
It is possible to reduce the number of points show in the plot with parameter n
. This functionality is rarely needed for scalar plots, refer to the explanation for vector plots below for more details.
[8]:
field.hv.scalar(kdims=["x", "y"], n=(10, 10), clim=(-1, 1), cmap="plasma")
[8]:
[9]:
field.hv.scalar(kdims=["x", "y"], clim=(-1, 1), cmap="plasma")
[9]:
Vector plot#
For vector fields we can create a vector plot where the arrows show direction and length of the in-plane-component and the arrow colour for the out-of-plane component.
[10]:
field.hv.vector(kdims=["x", "y"]).opts(colorbar=True)
[10]:
To uses a different component of the same field for colouring we can pass the name of that component.
[11]:
field.hv.vector(kdims=["x", "y"], cdim="y", cmap="plasma")
[11]:
To disable automatic colouring we can pass use_color=False
. By default the arrows are then shown in black. We can use a different uniform colour by passing color
.
[12]:
field.hv.vector(kdims=["x", "y"], use_color=False, color="blue")
[12]:
For vector plots it is sometimes necessary to reduce the number of arrows shown on the plot. This can be accomplished with the parameter n
. This parameter specifies the number of points show for different dimensions. A tuple with two int
values can be passed. These are interpreted as the number of points in the two kdims
. Internally, a very basic re-sampling is performed (without any interpolation). The extreme points are always kept and additional points are chosen equally
distributed in between. Note that this might potentially hide features in complicated situations and should be used with care.
[13]:
field.hv.vector(kdims=["x", "y"], n=(20, 10))
[13]:
For 3d vector fields we internally assume, that the first field component points in spatial x direction, the second in y direction, and the third in z direction. If we have a 2d vector field (e.g. a projection in some direction) we do no know which vector components correspond to which spatial directions. Therefore, one has to explicitly select components to be plotted in the plot x and plot y directions using vdims
. Here we take the first two components of our field to create a new 2d
vector field.
[14]:
field_2d = field.x << field.y
To avoid confusions we rename the components:
[15]:
field_2d.vdims = ["a", "b"]
[16]:
field_2d
[16]:
- Mesh
- Region
- pmin = [-2e-08, -1e-08, -5e-09]
- pmax = [2e-08, 1e-08, 5e-09]
- dims = ['x', 'y', 'z']
- units = ['m', 'm', 'm']
- n = [40, 20, 10]
- Region
- nvdim = 2
- vdims:
- a
- b
To create a vector plot for a non-three-dimensional field we have to pass vdims
.
First, we assume that the first component a
points in x direction and the second b
in y direction. We pass a list to vdims
. Furthermore, we disable automatic colouring because it is not supported for 2d vector fields. (If we wouldn’t specify use_color=False
we would see a warning that automatic colouring is not supported and that it is disabled. By explicitly setting use_color
to False
we can avoid this warning.)
[17]:
field_2d.hv.vector(kdims=["x", "y"], vdims=["a", "b"], use_color=False)
[17]:
However, if we look along the x axis we only have the b
component pointing in y direction but no component in the z direction. We pass a None
value for the spatial z direction. We can pass the a
component as additional cdim
.
[18]:
field_2d.hv.vector(kdims=["y", "z"], vdims=["b", None], cdim="a")
[18]:
If we have a higher-dimensional field, e.g. four we also have to manually specify vdims
.
[19]:
field_4d = field << field.z**2
field_4d.vdims = ["a", "b", "c", "d"]
field_4d.hv.vector(kdims=["x", "y"], vdims=["a", "d"], cdim="c")
[19]:
In a similar way, we can also explicitly select components for 3d vector fields. For example, we could swap our initial x and y components, so that the first vector component (despite being called x
) points in spatial y direction and the second vector component (called y
) points in spatial x direction:
[20]:
field.hv.vector(kdims=["x", "y"], vdims=["y", "x"])
[20]:
Contour lines#
Contour line plots behave similar to scalar plots. If the passed field is a vector field a widget is create to select one of the field components.
[21]:
field.hv.contour(kdims=["x", "y"])
[21]:
Combining multiple plots#
In the beginning we have seen that there is a convenience function to quickly plot 3d vector fields that combines scalar and vector plots. We can pass additional dictionaries to scalar_kw
and vector_kw
to adjust the different plots. By default vectors are show in black in the combined plot.
[22]:
field.hv(kdims=["x", "y"], scalar_kw={"clim": (-1, 1), "cmap": "coolwarm"})
[22]:
Combined plots also support re-sampling with the parameter n
passed to scalar_kw
or vector_kw
. Most useful is generally just a re-sampling of the vector part to reduce the arrow density. This can be accomplished by only passing n
to vector_kw
. If a dictionary is used to modify slider dimensions the user must take care that the scalar part and vector part of the plot have the same number of points in the slider directions (so the directions not specified in kdims
).
[23]:
field.hv(
kdims=["x", "y"],
scalar_kw={"clim": (-1, 1), "cmap": "coolwarm"},
vector_kw={"n": (20, 15)},
)
[23]:
Filtering is automatically done based on field.valid
. We show this by setting valid=True
.
[24]:
field.valid = True
field.hv(kdims=["x", "y"])
[24]:
We revert back to valid = "norm"
for the rest of this notebook.
[25]:
field.valid = "norm"
field.hv(kdims=["x", "y"])
[25]:
For 2d vector fields we have to pass vdims
to create a vector plot.
[26]:
field_2d.hv(kdims=["x", "y"], vdims=["a", "b"]).opts(width=300, height=200)
[26]:
Otherwise we get a scalar plot with a drop-down selection for the field components.
[27]:
field_2d.hv(kdims=["x", "y"])
[27]:
Fields with more than three vector dimensions behave similar.
[28]:
field_4d.hv(kdims=["x", "y"])
[28]:
[29]:
field_4d.hv(
kdims=["x", "y"], vdims=["a", "b"], vector_kw={"cdim": "d", "use_color": True}
)
[29]:
All functions in the hv
interface return standard holoviews
objects. We can save the returned values and create arbitrary more complicated layouts. For more advanced features please directly refer to the holoviews documentation.
[30]:
import holoviews as hv
hv.extension("bokeh")
hv.output(widget_location="bottom")
Setting hv.output
will fail if no plot has been created beforehand. To avoid this run the following set of commands if you want to change the widget location at the top of your notebook:
import holoviews as hv
hv.extension('bokeh')
hv.output(widget_location="bottom")
[31]:
# hide the following warning from bokeh:
# `UserWarning: found multiple competing values for 'toolbar.active_drag' property; using the latest value`
import warnings
warnings.simplefilter("ignore")
scalar_z = field.hv.scalar(
kdims=["x", "y"], roi=field.norm, clim=(-1, 1), cmap="cividis"
)
contour_z = field.hv.contour(
kdims=["x", "y"], roi=field.norm, cmap="plasma_r", levels=10, show_legend=False
)
vector_z = field.hv.vector(
kdims=["y", "z"], cmap="turbo", cdim="x", clim=(-1, 1), use_color=True
)
(scalar_z * contour_z).opts(frame_height=200) + vector_z.opts(frame_height=200)
[31]: