Newer
Older
# -*- coding: utf-8 -*-

Jan Caron
committed
# Copyright 2016 by Forschungszentrum Juelich GmbH
# Author: J. Caron
#

Jan Caron
committed
"""This module provides classes for storing vector and scalar 3D-field."""

Jan Caron
committed
import abc
from numbers import Number

Jan Caron
committed
from matplotlib import pyplot as plt
from matplotlib.ticker import MaxNLocator, FuncFormatter
from matplotlib.colors import ListedColormap
from PIL import Image
from scipy.ndimage.interpolation import zoom
from . import colors

Jan Caron
committed
__all__ = ['VectorData', 'ScalarData']
class FieldData(object, metaclass=abc.ABCMeta):

Jan Caron
committed
"""Class for storing field data.

Jan Caron
committed
Abstract base class for the representatio of magnetic or electric fields (see subclasses).
Fields can be accessed as 3D numpy arrays via the `field` property or as a vector via
`field_vec`. :class:`~.FieldData` objects support negation, arithmetic operators
(``+``, ``-``, ``*``) and their augmented counterparts (``+=``, ``-=``, ``*=``), with numbers

Jan Caron
committed
and other :class:`~.FieldData` objects of the same subclass, if their dimensions and grid
spacings match. It is possible to load data from HDF5 or LLG (.txt) files or to save the data
in these formats. Specialised plotting methods are also provided.
Attributes
----------
a: float
The grid spacing in nm.

Jan Caron
committed
field: :class:`~numpy.ndarray` (N=4)
The field distribution for every 3D-gridpoint.
"""

Jan Caron
committed
_log = logging.getLogger(__name__ + '.FieldData')
@property
def a(self):
return self._a
@a.setter
def a(self, a):
assert isinstance(a, Number), 'Grid spacing has to be a number!'
assert a >= 0, 'Grid spacing has to be a positive number!'
self._a = float(a)

Jan Caron
committed
@property
def shape(self):
"""The shape of the `field` (3D for scalar, 4D vor vector field)."""
return self.field.shape
@property
def dim(self):

Jan Caron
committed
"""Dimensions (z, y, x) of the grid, only 3D coordinates, without components if present."""
return self.shape[-3:]

Jan Caron
committed
def field(self):
"""The field strength for every 3D-gridpoint (scalar: 3D, vector: 4D)."""
return self._field
@field.setter
def field(self, field):
assert isinstance(field, np.ndarray), 'Field has to be a numpy array!'
assert 3 <= len(field.shape) <= 4, 'Field has to be 3- or 4-dimensional (scalar / vector)!'
if len(field.shape) == 4:
assert field.shape[0] == 3, 'A vector field has to have exactly 3 components!'

Jan Caron
committed
def field_amp(self):
"""The field amplitude (returns the field itself for scalar and the vector amplitude
calculated via a square sum for a vector field."""
if len(self.shape) == 4:
return np.sqrt(np.sum(self.field ** 2, axis=0))
else:
return self.field

Jan Caron
committed
def field_vec(self):
"""Vector containing the vector field distribution."""
return np.reshape(self.field, -1)

Jan Caron
committed
@field_vec.setter
def field_vec(self, mag_vec):
assert np.size(mag_vec) == np.prod(self.shape), \
'Vector has to match field shape! {} {}'.format(mag_vec.shape, np.prod(self.shape))
self.field = mag_vec.reshape((3,) + self.dim)

Jan Caron
committed
def __init__(self, a, field):
self._log.debug('Calling __init__')
self.a = a

Jan Caron
committed
self.field = field
self._log.debug('Created ' + str(self))
def __repr__(self):
self._log.debug('Calling __repr__')

Jan Caron
committed
return '%s(a=%r, field=%r)' % (self.__class__, self.a, self.field)
def __str__(self):
self._log.debug('Calling __str__')

Jan Caron
committed
return '%s(a=%s, dim=%s)' % (self.__class__, self.a, self.dim)
def __neg__(self): # -self
self._log.debug('Calling __neg__')
return self.__class__(self.a, -self.field)
def __add__(self, other): # self + other
self._log.debug('Calling __add__')

Jan Caron
committed
assert isinstance(other, (FieldData, Number)), \
'Only FieldData objects and scalar numbers (as offsets) can be added/subtracted!'
if isinstance(other, Number): # other is a Number
self._log.debug('Adding an offset')
return self.__class__(self.a, self.field + other)
elif isinstance(other, FieldData):
self._log.debug('Adding two FieldData objects')
assert other.a == self.a, 'Added phase has to have the same grid spacing!'

Jan Caron
committed
assert other.shape == self.shape, 'Added field has to have the same dimensions!'
return self.__class__(self.a, self.field + other.field)
def __sub__(self, other): # self - other
self._log.debug('Calling __sub__')
return self.__add__(-other)
def __mul__(self, other): # self * other
self._log.debug('Calling __mul__')

Jan Caron
committed
assert isinstance(other, Number), 'FieldData objects can only be multiplied by numbers!'
return self.__class__(self.a, self.field * other)
def __truediv__(self, other): # self / other
self._log.debug('Calling __truediv__')
assert isinstance(other, Number), 'FieldData objects can only be divided by numbers!'
return self.__class__(self.a, self.field / other)
def __floordiv__(self, other): # self // other
self._log.debug('Calling __floordiv__')
assert isinstance(other, Number), 'FieldData objects can only be divided by numbers!'
return self.__class__(self.a, self.field // other)
def __radd__(self, other): # other + self
self._log.debug('Calling __radd__')
return self.__add__(other)
def __rsub__(self, other): # other - self
self._log.debug('Calling __rsub__')
return -self.__sub__(other)
def __rmul__(self, other): # other * self
self._log.debug('Calling __rmul__')
return self.__mul__(other)
def __iadd__(self, other): # self += other
self._log.debug('Calling __iadd__')
return self.__add__(other)
def __isub__(self, other): # self -= other
self._log.debug('Calling __isub__')
return self.__sub__(other)
def __imul__(self, other): # self *= other
self._log.debug('Calling __imul__')
return self.__mul__(other)
def __itruediv__(self, other): # self /= other
self._log.debug('Calling __itruediv__')
return self.__truediv__(other)
def __ifloordiv__(self, other): # self //= other
self._log.debug('Calling __ifloordiv__')
return self.__floordiv__(other)
def __array__(self, dtype=None):
if dtype:
return self.field.astype(dtype)
else:
return self.field
def __array_wrap__(self, array, _=None): # _ catches the context, which is not used.
return type(self)(self.a, array)

Jan Caron
committed
"""Returns a copy of the :class:`~.FieldData` object
Returns
-------

Jan Caron
committed
field_data: :class:`~.FieldData`
A copy of the :class:`~.FieldData`.
self._log.debug('Calling copy')
return self.__class__(self.a, self.field.copy())

Jan Caron
committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def get_mask(self, threshold=0):
"""Mask all pixels where the amplitude of the field lies above `threshold`.
Parameters
----------
threshold : float, optional
A pixel only gets masked, if it lies above this threshold . The default is 0.
Returns
-------
mask : :class:`~numpy.ndarray` (N=3, boolean)
Mask of the pixels where the amplitude of the field lies above `threshold`.
"""
self._log.debug('Calling get_mask')
return np.where(self.field_amp > threshold, True, False)
def contour_plot3d(self, title='Field Distribution', contours=10, opacity=0.25):
"""Plot the field as a 3D-contour plot.
Parameters
----------
title: string, optional
The title for the plot.
contours: int, optional
Number of contours which should be plotted.
opacity: float, optional
Defines the opacity of the contours. Default is 0.25.
Returns
-------
plot : :class:`mayavi.modules.vectors.Vectors`
The plot object.
"""
self._log.debug('Calling quiver_plot3D')
from mayavi import mlab
# Plot them as vectors:
mlab.figure(size=(750, 700))
plot = mlab.contour3d(self.field_amp, contours=contours, opacity=opacity)
mlab.outline(plot)
mlab.axes(plot)
mlab.title(title, height=0.95, size=0.35)
mlab.orientation_axes()
return plot

Jan Caron
committed
def scale_down(self, n):
"""Scale down the field distribution by averaging over two pixels along each axis.
Parameters
----------
n : int, optional
Number of times the field distribution is scaled down. The default is 1.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions and grid spacing accordingly.
Only possible, if each axis length is a power of 2!
"""
pass

Jan Caron
committed
def scale_up(self, n, order):
"""Scale up the field distribution using spline interpolation of the requested order.
Parameters
----------
n : int, optional
Power of 2 with which the grid is scaled. Default is 1, which means every axis is
increased by a factor of ``2**1 = 2``.
order : int, optional
The order of the spline interpolation, which has to be in the range between 0 and 5
and defaults to 0.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions and grid spacing accordingly.
"""
pass

Jan Caron
committed
def get_vector(self, mask):
"""Returns the field as a vector, specified by a mask.
Parameters
----------
mask : :class:`~numpy.ndarray` (N=3, boolean)
Masks the pixels from which the entries should be taken.
Returns
-------
vector : :class:`~numpy.ndarray` (N=1)
The vector containing the field of the specified pixels.
"""
pass

Jan Caron
committed
def set_vector(self, vector, mask):
"""Set the field of the masked pixels to the values specified by `vector`.
Parameters
----------
mask : :class:`~numpy.ndarray` (N=3, boolean), optional
Masks the pixels from which the field should be taken.
vector : :class:`~numpy.ndarray` (N=1)
The vector containing the field of the specified pixels.
Returns
-------
None
"""
pass

Jan Caron
committed
@classmethod
def from_signal(cls, signal):
"""Convert a :class:`~hyperspy.signals.Signal` object to a :class:`~.FieldData` object.
Parameters
----------
signal: :class:`~hyperspy.signals.Signal`
The :class:`~hyperspy.signals.Signal` object which should be converted to FieldData.

Jan Caron
committed
Returns
-------

Jan Caron
committed
magdata: :class:`~.FieldData`
A :class:`~.FieldData` object containing the loaded data.

Jan Caron
committed
Notes
-----
This method recquires the hyperspy package!
"""

Jan Caron
committed
cls._log.debug('Calling from_signal')
return cls(signal.axes_manager[0].scale, signal.data)

Jan Caron
committed

Jan Caron
committed
def to_signal(self):
"""Convert :class:`~.FieldData` data into a HyperSpy signal.

Jan Caron
committed
Returns
-------

Jan Caron
committed
signal: :class:`~hyperspy.signals.Signal`
Representation of the :class:`~.FieldData` object as a HyperSpy Signal.
Notes
-----
This method recquires the hyperspy package!

Jan Caron
committed
"""

Jan Caron
committed
self._log.debug('Calling to_signal')
try: # Try importing HyperSpy:
import hyperspy.api as hs
except ImportError:
self._log.error('This method recquires the hyperspy package!')
return
# Create signal:
signal = hs.signals.BaseSignal(self.field) # All axes are signal axes!
# Set axes:
signal.axes_manager[0].name = 'x-axis'
signal.axes_manager[0].units = 'nm'
signal.axes_manager[0].scale = self.a
signal.axes_manager[1].name = 'y-axis'
signal.axes_manager[1].units = 'nm'
signal.axes_manager[1].scale = self.a
signal.axes_manager[2].name = 'z-axis'
signal.axes_manager[2].units = 'nm'
signal.axes_manager[2].scale = self.a
return signal

Jan Caron
committed
class VectorData(FieldData):
"""Class for storing vector ield data.
Represents 3-dimensional vector field distributions with 3 components which are stored as a
3-dimensional numpy array in `field`, but which can also be accessed as a vector via
`field_vec`. :class:`~.VectorData` objects support negation, arithmetic operators
(``+``, ``-``, ``*``) and their augmented counterparts (``+=``, ``-=``, ``*=``), with numbers
and other :class:`~.VectorData` objects, if their dimensions and grid spacings match. It is
possible to load data from HDF5 or LLG (.txt) files or to save the data in these formats.
Plotting methods are also provided.
Attributes
----------
a: float
The grid spacing in nm.
field: :class:`~numpy.ndarray` (N=4)
The `x`-, `y`- and `z`-component of the vector field for every 3D-gridpoint
as a 4-dimensional numpy array (first dimension has to be 3, because of the 3 components).
"""
_log = logging.getLogger(__name__ + '.VectorData')
def scale_down(self, n=1):

Jan Caron
committed
"""Scale down the field distribution by averaging over two pixels along each axis.
Parameters
----------
n : int, optional

Jan Caron
committed
Number of times the field distribution is scaled down. The default is 1.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions and grid spacing accordingly.
Only possible, if each axis length is a power of 2!
self._log.debug('Calling scale_down')
assert n > 0 and isinstance(n, int), 'n must be a positive integer!'
self.a *= 2 ** n
for t in range(n):
# Pad if necessary:
pz, py, px = self.dim[0] % 2, self.dim[1] % 2, self.dim[2] % 2
if pz != 0 or py != 0 or px != 0:

Jan Caron
committed
self.field = np.pad(self.field, ((0, 0), (0, pz), (0, py), (0, px)),
mode='constant')
# Create coarser grid for the vector field:
shape_4d = (3, self.dim[0] // 2, 2, self.dim[1] // 2, 2, self.dim[2] // 2, 2)

Jan Caron
committed
self.field = self.field.reshape(shape_4d).mean(axis=(6, 4, 2))
def scale_up(self, n=1, order=0):

Jan Caron
committed
"""Scale up the field distribution using spline interpolation of the requested order.
Parameters
----------
n : int, optional
Power of 2 with which the grid is scaled. Default is 1, which means every axis is
increased by a factor of ``2**1 = 2``.
order : int, optional
The order of the spline interpolation, which has to be in the range between 0 and 5
and defaults to 0.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions and grid spacing accordingly.
self._log.debug('Calling scale_up')
assert n > 0 and isinstance(n, int), 'n must be a positive integer!'
assert 5 > order >= 0 and isinstance(order, int), \
'order must be a positive integer between 0 and 5!'
self.a /= 2 ** n

Jan Caron
committed
self.field = np.array((zoom(self.field[0], zoom=2 ** n, order=order),
zoom(self.field[1], zoom=2 ** n, order=order),
zoom(self.field[2], zoom=2 ** n, order=order)))
def pad(self, pad_values):

Jan Caron
committed
"""Pad the current field distribution with zeros for each individual axis.
Parameters
----------
pad_values : tuple of int
Number of zeros which should be padded. Provided as a tuple where each entry
corresponds to an axis. An entry can be one int (same padding for both sides) or again
a tuple which specifies the pad values for both sides of the corresponding axis.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions accordingly.
self._log.debug('Calling pad')
assert len(pad_values) == 3, 'Pad values for each dimension have to be provided!'
pv = np.zeros(6, dtype=np.int)
for i, values in enumerate(pad_values):
assert np.shape(values) in [(), (2,)], 'Only one or two values per axis can be given!'
self.field = np.pad(self.field, ((0, 0), (pv[0], pv[1]), (pv[2], pv[3]), (pv[4], pv[5])),

Jan Caron
committed
mode='constant')
def crop(self, crop_values):

Jan Caron
committed
"""Crop the current field distribution with zeros for each individual axis.
Parameters
----------
crop_values : tuple of int
Number of zeros which should be cropped. Provided as a tuple where each entry
corresponds to an axis. An entry can be one int (same cropping for both sides) or again
a tuple which specifies the crop values for both sides of the corresponding axis.
Returns
-------
None
Notes
-----
Acts in place and changes dimensions accordingly.
self._log.debug('Calling crop')
assert len(crop_values) == 3, 'Crop values for each dimension have to be provided!'
cv = np.zeros(6, dtype=np.int)
for i, values in enumerate(crop_values):
assert np.shape(values) in [(), (2,)], 'Only one or two values per axis can be given!'
cv *= np.resize([1, -1], len(cv))
cv = np.where(cv == 0, None, cv)

Jan Caron
committed
self.field = self.field[:, cv[0]:cv[1], cv[2]:cv[3], cv[4]:cv[5]]
def get_vector(self, mask):

Jan Caron
committed
"""Returns the vector field components arranged in a vector, specified by a mask.
Parameters
----------
mask : :class:`~numpy.ndarray` (N=3, boolean)
Masks the pixels from which the components should be taken.
Returns
-------
vector : :class:`~numpy.ndarray` (N=1)

Jan Caron
committed
The vector containing vector field components of the specified pixels.
Order is: first all `x`-, then all `y`-, then all `z`-components.
self._log.debug('Calling get_vector')
if mask is not None:

Jan Caron
committed
return np.reshape([self.field[0][mask],
self.field[1][mask],
self.field[2][mask]], -1)

Jan Caron
committed
return self.field_vec
def set_vector(self, vector, mask=None):

Jan Caron
committed
"""Set the field components of the masked pixels to the values specified by `vector`.
Parameters
----------
mask : :class:`~numpy.ndarray` (N=3, boolean), optional
Masks the pixels from which the components should be taken.
vector : :class:`~numpy.ndarray` (N=1)

Jan Caron
committed
The vector containing vector field components of the specified pixels.
Order is: first all `x`-, then all `y-, then all `z`-components.
Returns
-------
None
self._log.debug('Calling set_vector')
assert np.size(vector) % 3 == 0, 'Vector has to contain all 3 components for every pixel!'
if mask is not None:

Jan Caron
committed
self.field[0][mask] = vector[:count] # x-component
self.field[1][mask] = vector[count:2 * count] # y-component
self.field[2][mask] = vector[2 * count:] # z-component

Jan Caron
committed
self.field_vec = vector

Jan Caron
committed
"""Flip/mirror the vector field around the specified axis.
Parameters
----------
axis: {'x', 'y', 'z'}, optional

Jan Caron
committed
The axis around which the vector field is flipped.

Jan Caron
committed
magdata_flip: :class:`~.VectorData`

Jan Caron
committed
A flipped copy of the :class:`~.VectorData` object.
self._log.debug('Calling flip')

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, :, :, ::-1]
field_flip = np.array((-mag_x, mag_y, mag_z))

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, :, ::-1, :]
field_flip = np.array((mag_x, -mag_y, mag_z))

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, ::-1, :, :]
field_flip = np.array((mag_x, mag_y, -mag_z))
else:
raise ValueError("Wrong input! 'x', 'y', 'z' allowed!")

Jan Caron
committed
return VectorData(self.a, field_flip)

Jan Caron
committed
"""Rotate the vector field 90° around the specified axis (right hand rotation).
Parameters
----------
axis: {'x', 'y', 'z'}, optional

Jan Caron
committed
The axis around which the vector field is rotated.

Jan Caron
committed
magdata_rot: :class:`~.VectorData`

Jan Caron
committed
A rotated copy of the :class:`~.VectorData` object.
self._log.debug('Calling rot90')

Jan Caron
committed
field_rot = np.zeros((3, self.dim[1], self.dim[0], self.dim[2]))

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, :, :, i]
mag_xrot, mag_yrot, mag_zrot = np.rot90(mag_x), np.rot90(mag_y), np.rot90(mag_z)

Jan Caron
committed
field_rot[:, :, :, i] = np.array((mag_xrot, mag_zrot, -mag_yrot))

Jan Caron
committed
field_rot = np.zeros((3, self.dim[2], self.dim[1], self.dim[0]))

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, :, i, :]
mag_xrot, mag_yrot, mag_zrot = np.rot90(mag_x), np.rot90(mag_y), np.rot90(mag_z)

Jan Caron
committed
field_rot[:, :, i, :] = np.array((mag_zrot, mag_yrot, -mag_xrot))

Jan Caron
committed
field_rot = np.zeros((3, self.dim[0], self.dim[2], self.dim[1]))

Jan Caron
committed
mag_x, mag_y, mag_z = self.field[:, i, :, :]
mag_xrot, mag_yrot, mag_zrot = np.rot90(mag_x), np.rot90(mag_y), np.rot90(mag_z)

Jan Caron
committed
field_rot[:, i, :, :] = np.array((mag_yrot, -mag_xrot, mag_zrot))
else:
raise ValueError("Wrong input! 'x', 'y', 'z' allowed!")

Jan Caron
committed
return VectorData(self.a, field_rot)
def get_slice(self, ax_slice=None, proj_axis='z'):

Jan Caron
committed
"""Extract a slice from the :class:`~.VectorData` object.
Parameters
----------
proj_axis : {'z', 'y', 'x'}, optional
The axis, from which the slice is taken. The default is 'z'.
ax_slice : None or int, optional
The slice-index of the axis specified in `proj_axis`. Defaults to the center slice.

Jan Caron
committed
Returns
-------
u_mag, v_mag : :class:`~numpy.ndarray` (N=2)
The extracted vector field components in plane perpendicular to the `proj_axis`.

Jan Caron
committed
"""
self._log.debug('Calling get_slice')
# Find slice:
assert proj_axis == 'z' or proj_axis == 'y' or proj_axis == 'x', \
'Axis has to be x, y or z (as string).'
if ax_slice is None:
ax_slice = self.dim[{'z': 0, 'y': 1, 'x': 2}[proj_axis]] // 2

Jan Caron
committed
if proj_axis == 'z': # Slice of the xy-plane with z = ax_slice
self._log.debug('proj_axis == z')
u_mag = np.copy(self.field[0][ax_slice, ...]) # x-component
v_mag = np.copy(self.field[1][ax_slice, ...]) # y-component
elif proj_axis == 'y': # Slice of the xz-plane with y = ax_slice
self._log.debug('proj_axis == y')
u_mag = np.copy(self.field[0][:, ax_slice, :]) # x-component
v_mag = np.copy(self.field[2][:, ax_slice, :]) # z-component
elif proj_axis == 'x': # Slice of the yz-plane with x = ax_slice
self._log.debug('proj_axis == x')
u_mag = np.swapaxes(np.copy(self.field[2][..., ax_slice]), 0, 1) # z-component
v_mag = np.swapaxes(np.copy(self.field[1][..., ax_slice]), 0, 1) # y-component
else:
raise ValueError('{} is not a valid argument (use x, y or z)'.format(proj_axis))
return u_mag, v_mag

Jan Caron
committed
"""Convert :class:`~.VectorData` data into a HyperSpy signal.
Returns
-------
signal: :class:`~hyperspy.signals.Signal`

Jan Caron
committed
Representation of the :class:`~.VectorData` object as a HyperSpy Signal.
Notes
-----
This method recquires the hyperspy package!

Jan Caron
committed
signal = super().to_signal()
# Set component axis:
signal.axes_manager[3].name = 'x/y/z-component'
signal.axes_manager[3].units = ''
# Set metadata:

Jan Caron
committed
signal.metadata.Signal.title = 'VectorData'

Jan Caron
committed
def save(self, filename, **kwargs):
"""Saves the VectorData in the specified format.

Jan Caron
committed
The function gets the format from the extension:
- hdf5 for HDF5.
- EMD Electron Microscopy Dataset format (also HDF5).
- llg format.
- ovf format.
- npy or npz for numpy formats.

Jan Caron
committed
If no extension is provided, 'hdf5' is used. Most formats are
saved with the HyperSpy package (internally the fielddata is first
converted to a HyperSpy Signal.

Jan Caron
committed
Each format accepts a different set of parameters. For details
see the specific format documentation.
Parameters
----------

Jan Caron
committed
filename : str, optional
Name of the file which the VectorData is saved into. The extension
determines the saving procedure.

Jan Caron
committed
from .file_io.io_vectordata import save_vectordata
save_vectordata(self, filename, **kwargs)
def plot_field(self, title='Vector Field', axis=None, proj_axis='z', figsize=(9, 8),
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
ax_slice=None, show_mask=True, bgcolor='white', hue_mode='triadic'):
"""Plot a slice of the vector field as a quiver plot.
Parameters
----------
title : string, optional
The title for the plot.
axis : :class:`~matplotlib.axes.AxesSubplot`, optional
Axis on which the graph is plotted. Creates a new figure if none is specified.
proj_axis : {'z', 'y', 'x'}, optional
The axis, from which a slice is plotted. The default is 'z'.
figsize : tuple of floats (N=2)
Size of the plot figure.
ax_slice : int, optional
The slice-index of the axis specified in `proj_axis`. Is set to the center of
`proj_axis` if not specified.
show_mask: boolean
Default is True. Shows the outlines of the mask slice if available.
bgcolor: {'black', 'white'}, optional
Determines the background color of the plot.
hue_mode : {'triadic', 'tetradic'}
Optional string for determining the hue scheme. Use either a triadic or tetradic
scheme (see the according colormaps for more information).
Returns
-------
axis: :class:`~matplotlib.axes.AxesSubplot`
The axis on which the graph is plotted.
"""
self._log.debug('Calling plot_field')
assert proj_axis == 'z' or proj_axis == 'y' or proj_axis == 'x', \
'Axis has to be x, y or z (as string).'
if ax_slice is None:
ax_slice = self.dim[{'z': 0, 'y': 1, 'x': 2}[proj_axis]] // 2
u_mag, v_mag = self.get_slice(ax_slice, proj_axis)
if proj_axis == 'z': # Slice of the xy-plane with z = ax_slice
u_label = 'x-axis [nm]'
v_label = 'y-axis [nm]'
submask = self.get_mask()[ax_slice, ...]
elif proj_axis == 'y': # Slice of the xz-plane with y = ax_slice
u_label = 'x-axis [nm]'
v_label = 'z-axis [nm]'
submask = self.get_mask()[:, ax_slice, :]
elif proj_axis == 'x': # Slice of the yz-plane with x = ax_slice
u_label = 'z-axis [nm]'
v_label = 'y-axis [nm]'
submask = self.get_mask()[..., ax_slice]
else:
raise ValueError('{} is not a valid argument (use x, y or z)'.format(proj_axis))
# If no axis is specified, a new figure is created:
if axis is None:
self._log.debug('axis is None')
fig = plt.figure(figsize=figsize)
axis = fig.add_subplot(1, 1, 1)
axis.set_aspect('equal')
# Plot the field:
dim_uv = u_mag.shape
hue = np.arctan2(v_mag, u_mag) / (2 * np.pi) # Hue according to angle!
hue[hue < 0] += 1 # Shift negative values!
luminance = 0.5 * submask # Luminance according to mask!
if bgcolor == 'white': # Invert luminance:
luminance = 1 - luminance
saturation = np.hypot(u_mag, v_mag) # Saturation according to amplitude!
rgb = colors.rgb_from_hls(hue, luminance, saturation, mode=hue_mode)
axis.imshow(Image.fromarray(rgb), origin='lower', interpolation='none',
extent=(0, dim_uv[1], 0, dim_uv[0]))
# Change background color:
axis.set_axis_bgcolor(bgcolor)
# Show mask:
if show_mask and not np.all(submask): # Plot mask if desired and not trivial!
vv, uu = np.indices(dim_uv) + 0.5 # shift to center of pixel
mask_color = 'white' if bgcolor == 'black' else 'black'
axis.contour(uu, vv, submask, levels=[0.5], colors=mask_color,
linestyles='dotted', linewidths=2)
# Further plot formatting:
axis.set_xlim(0, dim_uv[1])
axis.set_ylim(0, dim_uv[0])
axis.set_title(title, fontsize=18)
axis.set_xlabel(u_label, fontsize=15)
axis.set_ylabel(v_label, fontsize=15)
axis.tick_params(axis='both', which='major', labelsize=14)
if dim_uv[0] >= dim_uv[1]:
u_bin, v_bin = np.max((2, np.floor(9 * dim_uv[1] / dim_uv[0]))), 9
else:
u_bin, v_bin = 9, np.max((2, np.floor(9 * dim_uv[0] / dim_uv[1])))
axis.xaxis.set_major_locator(MaxNLocator(nbins=u_bin, integer=True))
axis.yaxis.set_major_locator(MaxNLocator(nbins=v_bin, integer=True))
axis.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: '{:.3g}'.format(x * self.a)))
axis.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: '{:.3g}'.format(x * self.a)))
# Return plotting axis:
return axis
def plot_streamline(self, title='Vector Field', axis=None, proj_axis='z', figsize=(9, 8),
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
coloring='angle', ax_slice=None, density=2, linewidth=2,
show_mask=True, bgcolor='white', hue_mode='triadic'):
"""Plot a slice of the vector field as a quiver plot.
Parameters
----------
title : string, optional
The title for the plot.
axis : :class:`~matplotlib.axes.AxesSubplot`, optional
Axis on which the graph is plotted. Creates a new figure if none is specified.
proj_axis : {'z', 'y', 'x'}, optional
The axis, from which a slice is plotted. The default is 'z'.
figsize : tuple of floats (N=2)
Size of the plot figure.
coloring : {'angle', 'amplitude', 'uniform'}
Color coding mode of the arrows. Use 'full' (default), 'angle', 'amplitude' or
'uniform'.
ax_slice : int, optional
The slice-index of the axis specified in `proj_axis`. Is set to the center of
`proj_axis` if not specified.
density : float or 2-tuple, optional
Controls the closeness of streamlines. When density = 1, the domain is divided into a
30x30 grid—density linearly scales this grid. Each cebll in the grid can have, at most,
one traversing streamline. For different densities in each direction, use
[density_x, density_y].
linewidth : numeric or 2d array, optional
Vary linewidth when given a 2d array with the same shape as velocities.
show_mask: boolean
Default is True. Shows the outlines of the mask slice if available.
bgcolor: {'black', 'white'}, optional
Determines the background color of the plot.
hue_mode : {'triadic', 'tetradic'}
Optional string for determining the hue scheme. Use either a triadic or tetradic
scheme (see the according colormaps for more information).
Returns
-------
axis: :class:`~matplotlib.axes.AxesSubplot`
The axis on which the graph is plotted.
"""
self._log.debug('Calling plot_quiver')
assert proj_axis == 'z' or proj_axis == 'y' or proj_axis == 'x', \
'Axis has to be x, y or z (as string).'
if ax_slice is None:
ax_slice = self.dim[{'z': 0, 'y': 1, 'x': 2}[proj_axis]] // 2
u_mag, v_mag = self.get_slice(ax_slice, proj_axis)
if proj_axis == 'z': # Slice of the xy-plane with z = ax_slice
u_label = 'x-axis [nm]'
v_label = 'y-axis [nm]'
submask = self.get_mask()[ax_slice, ...]
elif proj_axis == 'y': # Slice of the xz-plane with y = ax_slice
u_label = 'x-axis [nm]'
v_label = 'z-axis [nm]'
submask = self.get_mask()[:, ax_slice, :]
elif proj_axis == 'x': # Slice of the yz-plane with x = ax_slice
u_label = 'z-axis [nm]'
v_label = 'y-axis [nm]'
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
submask = self.get_mask()[..., ax_slice]
else:
raise ValueError('{} is not a valid argument (use x, y or z)'.format(proj_axis))
# Prepare quiver (select only used arrows if ar_dens is specified):
dim_uv = u_mag.shape
uu = np.arange(dim_uv[1]) + 0.5 # shift to center of pixel
vv = np.arange(dim_uv[0]) + 0.5 # shift to center of pixel
u_mag, v_mag = self.get_slice(ax_slice, proj_axis)
# v_mag = np.ma.array(v_mag, mask=submask)
amplitudes = np.hypot(u_mag, v_mag)
# Calculate the arrow colors:
if coloring == 'angle':
self._log.debug('Encoding angles')
color = np.arctan2(v_mag, u_mag) / (2 * np.pi)
color[color < 0] += 1
if hue_mode == 'triadic':
cmap = colors.hls_triadic_cmap
elif hue_mode == 'tetradic':
cmap = colors.hls_tetradic_cmap
else:
raise ValueError('Hue mode {} not understood!'.format(hue_mode))
elif coloring == 'amplitude':
self._log.debug('Encoding amplitude')
color = amplitudes / amplitudes.max()
cmap = 'jet'
elif coloring == 'uniform':
self._log.debug('No color encoding')
color = np.zeros_like(u_mag) # use black arrows!
cmap = 'gray' if bgcolor == 'white' else 'Greys'
else:
raise AttributeError("Invalid coloring mode! Use 'angles', 'amplitude' or 'uniform'!")
# If no axis is specified, a new figure is created:
if axis is None:
self._log.debug('axis is None')
fig = plt.figure(figsize=figsize)
axis = fig.add_subplot(1, 1, 1)
axis.set_aspect('equal')
# Plot the streamlines:
im = plt.streamplot(uu, vv, u_mag, v_mag, density=density, linewidth=linewidth,
color=color, cmap=cmap)
if coloring == 'amplitude':
fig = plt.gcf()
fig.subplots_adjust(right=0.8)
cbar_ax = fig.add_axes([0.82, 0.15, 0.02, 0.7])
cbar = fig.colorbar(im.lines, cax=cbar_ax)
cbar.ax.tick_params(labelsize=14)
cbar_title = u'amplitude'
cbar.set_label(cbar_title, fontsize=15)
# Change background color:
axis.set_axis_bgcolor(bgcolor)
# Show mask:
if show_mask and not np.all(submask): # Plot mask if desired and not trivial!
vv, uu = np.indices(dim_uv) + 0.5 # shift to center of pixel
mask_color = 'white' if bgcolor == 'black' else 'black'
axis.contour(uu, vv, submask, levels=[0.5], colors=mask_color,
linestyles='dotted', linewidths=2)
# Further plot formatting:
axis.set_xlim(0, dim_uv[1])
axis.set_ylim(0, dim_uv[0])
axis.set_title(title, fontsize=18)
axis.set_xlabel(u_label, fontsize=15)
axis.set_ylabel(v_label, fontsize=15)
axis.tick_params(axis='both', which='major', labelsize=14)
if dim_uv[0] >= dim_uv[1]:
u_bin, v_bin = np.max((2, np.floor(9 * dim_uv[1] / dim_uv[0]))), 9
else:
u_bin, v_bin = 9, np.max((2, np.floor(9 * dim_uv[0] / dim_uv[1])))
axis.xaxis.set_major_locator(MaxNLocator(nbins=u_bin, integer=True))
axis.yaxis.set_major_locator(MaxNLocator(nbins=v_bin, integer=True))
axis.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: '{:.3g}'.format(x * self.a)))
axis.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: '{:.3g}'.format(x * self.a)))
# Return plotting axis:
return axis
def plot_quiver(self, title='Vector Field', axis=None, proj_axis='z', figsize=(9, 8),
coloring='angle', ar_dens=1, ax_slice=None, log=False, scaled=True,
scale=1., show_mask=True, bgcolor='white', hue_mode='triadic'):

Jan Caron
committed
"""Plot a slice of the vector field as a quiver plot.
Parameters
----------
title : string, optional
The title for the plot.
axis : :class:`~matplotlib.axes.AxesSubplot`, optional
Axis on which the graph is plotted. Creates a new figure if none is specified.
proj_axis : {'z', 'y', 'x'}, optional
The axis, from which a slice is plotted. The default is 'z'.
figsize : tuple of floats (N=2)
Size of the plot figure.
coloring : {'angle', 'amplitude', 'uniform', matplotlib color}
Color coding mode of the arrows. Use 'full' (default), 'angle', 'amplitude', 'uniform'
(black or white, depending on `bgcolor`), or a matplotlib color keyword.

Jan Caron
committed
ar_dens: int, optional
Number defining the arrow density which is plotted. A higher ar_dens number skips more
arrows (a number of 2 plots every second arrow). Default is 1.
ax_slice : int, optional
The slice-index of the axis specified in `proj_axis`. Is set to the center of
`proj_axis` if not specified.
log : boolean, optional
The loratihm of the arrow length is plotted instead. This is helpful if only the
direction of the arrows is important and the amplitude varies a lot. Default is False.
scaled : boolean, optional
Normalizes the plotted arrows in respect to the highest one. Default is True.
scale: float, optional
Additional multiplicative factor scaling the arrow length. Default is 1
(no further scaling).
show_mask: boolean
Default is True. Shows the outlines of the mask slice if available.
bgcolor: {'black', 'white'}, optional
Determines the background color of the plot.
hue_mode : {'triadic', 'tetradic'}
Optional string for determining the hue scheme. Use either a triadic or tetradic
scheme (see the according colormaps for more information).