Source code for empyre.fields.vectors

# -*- coding: utf-8 -*-
# Copyright 2016 by Forschungszentrum Juelich GmbH
# Author: J. Caron
#


import logging
from numbers import Number

import numpy as np

from .field import Field


__all__ = ['create_vector_homog', 'create_vector_vortex', 'create_vector_skyrmion', 'create_vector_singularity']

_log = logging.getLogger(__name__)


[docs]def create_vector_homog(dim, phi=0, theta=None, scale=1.0): """Field subclass implementing a homogeneous vector field with 2 or 3 components in 2 or 3 dimensions. Attributes ---------- dim : tuple The dimensions of the grid. phi : float The azimuthal angle. The default is 0, i.e., the vectors point in x direction. theta : float, optional The polar angle. If None (default), only two components will be created (corresponds to pi/2 in 3D, i.e., the vectors are in the xy plane). scale: tuple of float Scaling along the dimensions of the underlying data. """ _log.debug('Calling __init__') assert len(dim) in (2, 3), 'Disc can only be build in 2 or 3 dimensions!' assert isinstance(phi, Number), 'phi has to be an angle in radians!' assert isinstance(theta, Number) or theta is None, 'theta has to be an angle in radians or None!' if len(dim) == 2 and theta is None: # 2D and 3rd component not explicitely wanted: y_comp = np.ones(dim) * np.sin(phi) x_comp = np.ones(dim) * np.cos(phi) data = np.stack([x_comp, y_comp], axis=-1) else: # 3D, always have a third component: if theta is None: theta = np.pi/2 # xy-plane if not specified otherwise! z_comp = np.ones(dim) * np.cos(theta) y_comp = np.ones(dim) * np.sin(theta) * np.sin(phi) x_comp = np.ones(dim) * np.sin(theta) * np.cos(phi) data = np.stack([x_comp, y_comp, z_comp], axis=-1) return Field(data=data, scale=scale, vector=True)
[docs]def create_vector_vortex(dim, center=None, phi_0=np.pi/2, oop_r=None, oop_sign=1, core_r=0, axis=0, scale=1.0): """Field subclass implementing a vortex vector field with 3 components in 2 or 3 dimensions. Attributes ---------- dim : tuple The dimensions of the grid. center : tuple (N=2 or N=3), optional The vortex center, given in 2D `(v, u)` or 3D `(z, y, x)`, where the perpendicular axis is discarded (determined by the `axis` parameter). Is set to the center of the field of view if not specified. The vortex center should be between two pixels to avoid singularities. axis : int, optional The orientation of the vortex axis. The default is 0 and corresponds to the z-axis. Is ignored if dim is only 2D. phi_0 : float, optional Angular offset that allows all arrows to be rotated simultaneously. The default value is `pi/2` and corresponds to an anti-clockwise rotation, while a value of `-pi/2` corresponds to a clockwise rotation. `0` and `pi` lead to arrows that all point in/outward. oop_r : float, optional Radius of a potential out-of-plane ("oop") component in the vortex center. If `None`, the vortex is purely in-plane, which is the default. Set this to a number (in pixels) that determines the radius in which the oop-component is tilted into the plane. A `0` leads to an immediate/sharp tilt (not visible without setting a core radius, see `core_r`), while setting this to the radius of your vortex disc, will let it tilt smoothly until reaching the edge. If this is `None`, only two components will be created for 2-dimensional vortices! oop_sign : {1, -1}, optional Only used if `oop_r` is not None (i.e. there is an out-of-plane component). Can be `1` (default) or `-1` and determines if the core (if it exists) is aligned parallel (`+1`) or antiparallel (`-1`) to the chosen symmetry axis. core_r : float, optional Radius of a potential vortex core that's homogeneously oriented out-of-plane. Is not used when `oop_r` is not set and defaults to `0`, which means the vortex core is infinitely small. scale: tuple of float Scaling along the dimensions of the underlying data. Returns ------- field : `~.Field` object The resulting vector field. """ _log.debug('Calling create_vector_vortex') assert len(dim) in (2, 3), 'Vortex can only be build in 2 or 3 dimensions!' # Find indices of the vortex plane axes: idx_uv = [0, 1, 2] if len(dim) == 3: # 3D: idx_uv.remove(axis) else: # 2D: idx_uv.remove(2) # 2D dimensions: dim_uv = tuple([dim[i] for i in idx_uv]) # Find default values: if center is None: center = tuple([dim[i] / 2 for i in idx_uv]) elif len(center) == 3: # if a 3D-center is given, just take the relevant coordinates: center = list(center) del center[axis] center = tuple(center) # Create vortex plane (2D): coords_uv = np.indices(dim_uv) + 0.5 # 0.5 to get to pixel/voxel center! coords_uv = coords_uv - np.asarray(center, dtype=float)[:, None, None] # Shift by center! phi = np.arctan2(coords_uv[0], coords_uv[1]) - phi_0 rr = np.hypot(coords_uv[0], coords_uv[1]) rr_clip = np.clip(rr - core_r, a_min=0, a_max=None) # negative inside core_r (clipped to 0), positive outside! # Check if a core should be constructed (oop_r != 0): if oop_r is None: w_comp = np.zeros(dim_uv) else: assert oop_r >= 0, 'oop_r has to be a positive number!' assert oop_sign in (1, -1), 'Sign of the out-of-plane component has to be either +1 or -1!' w_comp = 1 - 2/np.pi * np.arcsin(np.tanh(np.pi*rr_clip/(oop_r+1E-30))) # orthogonal: 1 inside, to 0 outside! w_comp *= oop_sign * w_comp v_comp = np.ones(dim_uv) * np.sin(phi) * np.sqrt(1 - np.abs(w_comp)) u_comp = np.ones(dim_uv) * np.cos(phi) * np.sqrt(1 - np.abs(w_comp)) if len(dim) == 3: # Expand to 3D: w_comp = np.expand_dims(w_comp, axis=axis) v_comp = np.expand_dims(v_comp, axis=axis) u_comp = np.expand_dims(u_comp, axis=axis) reps = [1, 1, 1] # repetitions for tiling reps[axis] = dim[axis] # repeat along chosen axis w_comp = np.tile(w_comp, reps) v_comp = np.tile(v_comp, reps) u_comp = np.tile(u_comp, reps) if axis == 0: # z-axis z_comp = w_comp y_comp = -v_comp x_comp = -u_comp elif axis == 1: # y-axis z_comp = v_comp y_comp = w_comp x_comp = u_comp elif axis == 2: # x-axis z_comp = -v_comp y_comp = -u_comp x_comp = w_comp else: raise ValueError(f'{axis} is not a valid argument for axis (has to be 0, 1 or 2)') data = np.stack([x_comp, y_comp, z_comp], axis=-1) return Field(data=data, scale=scale, vector=True)
[docs]def create_vector_skyrmion(dim, center=None, phi_0=0, core_sign=1, skyrm_d=None, wall_d=None, axis=0, scale=1.0): """Create a 3-dimensional magnetic Bloch or Neel type skyrmion distribution. Parameters ---------- dim : tuple The dimensions of the grid. center : tuple (N=2 or N=3), optional The source center, given in 2D `(v, u)` or 3D `(z, y, x)`, where the perpendicular axis is discarded. Is set to the center of the field of view if not specified. The center has to be between two pixels. phi_0 : float, optional Angular offset switching between Neel type (0 [default] or pi) or Bloch type (+/- pi/2) skyrmions. core_sign : {1, -1}, optional Can be `1` (default) or `-1` and determines if the skyrmion core is aligned parallel (`+1`) or antiparallel (`-1`) to the chosen symmetry axis. skyrm_d : float, optional Diameter of the skyrmion. Defaults to half of the smaller dimension perpendicular to the skyrmion axis if not specified. wall_d : float, optional Diameter of the domain wall of the skyrmion. Defaults to `skyrm_d / 4` if not specified. axis : int, optional The orientation of the vortex axis. The default is 0 and corresponds to the z-axis. Is ignored if dim is only 2D. scale: tuple of float Scaling along the dimensions of the underlying data. Returns ------- field : `~.Field` object The resulting vector field. Notes ----- To avoid singularities, the source center should lie between the pixel centers (which reside at coordinates with _.5 at the end), i.e. integer values should be used as center coordinates (e.g. coordinate 1 lies between the first and the second pixel). Skyrmion wall width is dependant on exchange stiffness A [J/m] and anisotropy K [J/m³] The out-of-plane magnetization at the domain wall is described in a paper by Romming et al (see DOI: 10.1103/PhysRevLett.114.177203s) """ def _theta(r): theta_1 = + np.arcsin(np.tanh((r + skyrm_d/2)/(wall_d/2))) theta_2 = - np.arcsin(np.tanh((r - skyrm_d/2)/(wall_d/2))) theta = theta_1 + theta_2 theta /= np.abs(theta).max() / np.pi return theta _log.debug('Calling create_vector_skyrmion') assert len(dim) in (2, 3), 'Skyrmion can only be build in 2 or 3 dimensions!' assert core_sign in (1, -1), 'Sign of the out-of-plane component has to be either +1 or -1!' # Find indices of the skyrmion plane axes: idx_uv = [0, 1, 2] if len(dim) == 3: # 3D: idx_uv.remove(axis) else: # 2D: idx_uv.remove(2) # 2D dimensions: dim_uv = tuple([dim[i] for i in idx_uv]) # Find default values: if skyrm_d is None: skyrm_d = np.min(dim_uv) / 2 if wall_d is None: wall_d = skyrm_d / 4 if center is None: center = tuple([dim[i] / 2 for i in idx_uv]) elif len(center) == 3: # if a 3D-center is given, just take the relevant coordinates: center = list(center) del center[axis] center = tuple(center) # Create skyrmion plane (2D): coords_uv = np.indices(dim_uv) + 0.5 # 0.5 to get to pixel/voxel center! coords_uv = coords_uv - np.asarray(center, dtype=float)[:, None, None] # Shift by center! rr = np.hypot(coords_uv[0], coords_uv[1]) phi = np.arctan2(coords_uv[0], coords_uv[1]) - phi_0 theta = _theta(rr) w_comp = core_sign * np.cos(theta) v_comp = np.sin(theta) * np.sin(phi) u_comp = np.sin(theta) * np.cos(phi) # Expansion to 3D if necessary and component shuffling: if len(dim) == 3: # Expand to 3D: w_comp = np.expand_dims(w_comp, axis=axis) v_comp = np.expand_dims(v_comp, axis=axis) u_comp = np.expand_dims(u_comp, axis=axis) reps = [1, 1, 1] # repetitions for tiling reps[axis] = dim[axis] # repeat along chosen axis w_comp = np.tile(w_comp, reps) v_comp = np.tile(v_comp, reps) u_comp = np.tile(u_comp, reps) if axis == 0: # z-axis z_comp = w_comp y_comp = -v_comp x_comp = -u_comp elif axis == 1: # y-axis z_comp = v_comp y_comp = w_comp x_comp = u_comp elif axis == 2: # x-axis z_comp = -v_comp y_comp = -u_comp x_comp = w_comp else: raise ValueError(f'{axis} is not a valid argument for axis (has to be 0, 1 or 2)') data = np.stack([x_comp, y_comp, z_comp], axis=-1) return Field(data=data, scale=scale, vector=True)
[docs]def create_vector_singularity(dim, center=None, scale=1.0): """Create a 3-dimensional magnetic distribution of a homogeneous magnetized object. Parameters ---------- dim : tuple The dimensions of the grid. center : tuple (N=2 or N=3), optional The source center, given in 2D `(v, u)` or 3D `(z, y, x)`, where the perpendicular axis is discarded. Is set to the center of the field of view if not specified. The source center has to be between two pixels. scale: tuple of float Scaling along the dimensions of the underlying data. Returns ------- field : `~.Field` object The resulting vector field. Notes ----- To avoid singularities, the source center should lie between the pixel centers (which reside at coordinates with _.5 at the end), i.e. integer values should be used as center coordinates (e.g. coordinate 1 lies between the first and the second pixel). Per default, all arrows point outwards, negate the resulting `Field` object with a minus after creation to let them point inwards. """ _log.debug('Calling create_vector_singularity') # Find default values: if center is None: center = tuple([d / 2 for d in dim]) assert len(dim) == len(center), f"Length of dim ({len(dim)}) and center ({len(center)}) don't match!" # Setup coordinates, shape is (c, z, y, x), if 3D, or (c, y, x), if 2D (c: components): coords = np.indices(dim) + 0.5 # 0.5 to get to pixel/voxel center! bc_shape = (len(dim,),) + (1,)*len(dim) # Shape for broadcasting, (3,1,1,1) for 3D, (2,1,1) for 2D! coords = coords - np.reshape(center, bc_shape) # Shift by center (append 1s for broadcasting)! rr = np.sqrt(np.sum([coords[i]**2 for i in range(len(dim))], axis=0)) coords /= rr + 1E-30 # Normalise amplitude (keep direction), rr (z,y,x) is broadcasted to data (c,z,y,x)! # coords has components listed in wrong order (z, y, x) in the first dimension, we need (x, y, z) in the last: coords = np.flip(coords, axis=0) # Finally, the data has the coordinate axis at the end, not at the front: data = np.moveaxis(coords, 0, -1) # (c,z,y,x) -> (z,y,x,c) return Field(data=data, scale=scale, vector=True)