Skip to content
Snippets Groups Projects
Forked from empyre / empyre
392 commits behind the upstream repository.
  • Jan Caron's avatar
    838fb104
    Further optimization of the new structure and scripts. · 838fb104
    Jan Caron authored
    package: some minor corrections (mainly import statements), some old functions
             were revived (e.g. in MagData)
    
    scripts: adaption to the new structure and sorting (more work is to do here...)
    
    _version: __version__ string is now imported from this file
    
    phasemapper_core.pyx: added to separate the corresponding numcore_functions
                          from kernel_core.pyx
    838fb104
    History
    Further optimization of the new structure and scripts.
    Jan Caron authored
    package: some minor corrections (mainly import statements), some old functions
             were revived (e.g. in MagData)
    
    scripts: adaption to the new structure and sorting (more work is to do here...)
    
    _version: __version__ string is now imported from this file
    
    phasemapper_core.pyx: added to separate the corresponding numcore_functions
                          from kernel_core.pyx
phasemapper.py 17.53 KiB
# -*- coding: utf-8 -*-
"""This module executes several forward models to calculate the magnetic or electric phase map from
a given projection of a 3-dimensional magnetic distribution (see :mod:`~pyramid.projector`).
For the magnetic phase map, an approach using real space and one using Fourier space is provided.
The electrostatic contribution is calculated by using the assumption of a mean inner potential.

"""


import numpy as np
from numpy import pi

import abc

import pyramid.numcore.phasemapper_core as nc
from pyramid.kernel import Kernel
from pyramid.projector import Projector
from pyramid.magdata import MagData
from pyramid.phasemap import PhaseMap
from pyramid.forwardmodel import ForwardModel

import logging


class PhaseMapper(object):

    '''Abstract base class for the phase calculation from a 2-dimensional distribution.

    The :class:`~.PhaseeMapper-` class represents a strategy for the phasemapping of a
    2-dimensional magnetic distribution with two components onto a scalar phase map.
    :class:`~.Kernel` is an abstract base class and provides a unified interface which should be
    subclassed with custom :func:`__init__` and :func:`__call__` functions. Concrete subclasses
    can be called as a function and take a :class:`~.MagData` object as input and return a
    :class:`~.PhaseMap` object.

    '''

    __metaclass__ = abc.ABCMeta
    LOG = logging.getLogger(__name__+'.PhaseMapper')

    @abc.abstractmethod
    def __init__(self, projector):
        self.LOG.debug('Calling __init__')
        raise NotImplementedError()

    @abc.abstractmethod
    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        raise NotImplementedError()


class PMAdapterFM(PhaseMapper):

    '''Class representing a phase mapping strategy adapting the :class:`~.ForwardModel` class.

    The :class:`~.PMAdapterFM` class is an adapter class to incorporate the forward model from the
    :mod:`~.ForwardModel` module without the need of vector input and output. It directly takes
    :class:`~.MagData` objects and returns :class:`~.PhaseMap` objects.

    Attributes
    ----------
    a : float
        The grid spacing in nm.
    projector : :class:`~.Projector`
        Projector which should be used for the projection of the 3-dimensional magnetization
        distribution.
    b_0 : float, optional
        The magnetic induction corresponding to a magnetization `M`\ :sub:`0` in T.
        The default is 1.
    geometry : {'disc', 'slab'}, optional
        Elementary geometry which is used for the phase contribution of one pixel.
        Default is 'disc'.
    fwd_model : :class:`~.ForwardModel`
        The forward model that is constructed from the given information. It is created internally
        and does not have to be provided by the user.

    '''

    LOG = logging.getLogger(__name__+'.PMAdapterFM')

    def __init__(self, a, projector, b_0=1, geometry='disc'):
        self.LOG.debug('Calling __init__')
        assert isinstance(projector, Projector), 'Argument has to be a Projector object!'
        self.a = a
        self.projector = projector
        self.b_0 = b_0
        self.geometry = geometry
        self.fwd_model = ForwardModel([projector], Kernel(a, projector.dim_uv, b_0, geometry))
        self.LOG.debug('Created '+str(self))

    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        assert isinstance(mag_data, MagData), 'Only MagData objects can be mapped!'
        assert mag_data.a == self.a, 'Grid spacing has to match!'
        assert mag_data.dim == self.projector.dim, 'Dimensions do not match!'
        phase_map = PhaseMap(self.a, np.zeros(self.projector.dim_uv))
        phase_map.phase_vec = self.fwd_model(mag_data.mag_vec)
        return phase_map

    def __repr__(self):
        self.LOG.debug('Calling __repr__')
        return '%s(a=%r, projector=%r, b_0=%r, geometry=%r)' % \
            (self.__class__, self.a, self.projector, self.b_0, self.geometry)

    def __str__(self):
        self.LOG.debug('Calling __str__')
        return 'PMAdapterFM(a=%s, projector=%s, b_0=%s, geometry=%s)' % \
            (self.a, self.projector, self.b_0, self.geometry)


class PMFourier(PhaseMapper):

    '''Class representing a phase mapping strategy using a discretization in Fourier space.

    The :class:`~.PMFourier` class represents a phase mapping strategy involving discretization in
    Fourier space. It utilizes the Fourier transforms, which are inherently calculated in the
    :class:`~.Kernel` class and directly takes :class:`~.MagData` objects and returns
    :class:`~.PhaseMap` objects.

    Attributes
    ----------
    a : float
        The grid spacing in nm.
    projector : :class:`~.Projector`
        Projector which should be used for the projection of the 3-dimensional magnetization
        distribution.
    b_0 : float, optional
        The magnetic induction corresponding to a magnetization `M`\ :sub:`0` in T.
        The default is 1.
    padding : int, optional
        Factor for the zero padding. The default is 0 (no padding). For a factor of n the number
        of pixels is increase by ``(1+n)**2``. Padded zeros are cropped at the end.

    '''

    LOG = logging.getLogger(__name__+'.PMFourier')
    PHI_0 = -2067.83   # magnetic flux in T*nm²

    def __init__(self, a, projector, b_0=1, padding=0):
        self.LOG.debug('Calling __init__')
        assert isinstance(projector, Projector), 'Argument has to be a Projector object!'
        self.a = a
        self.projector = projector
        self.b_0 = b_0
        self.padding = padding
        self.LOG.debug('Created '+str(self))

    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        assert isinstance(mag_data, MagData), 'Only MagData objects can be mapped!'
        assert mag_data.a == self.a, 'Grid spacing has to match!'
        assert mag_data.dim == self.projector.dim, 'Dimensions do not match!'
        v_dim, u_dim = self.projector.dim_uv
        u_mag, v_mag = self.projector(mag_data.mag_vec).reshape((2,)+self.projector.dim_uv)
        # Create zero padded matrices:
        u_pad = int(u_dim/2 * self.padding)
        v_pad = int(v_dim/2 * self.padding)
        u_mag_pad = np.pad(u_mag, ((v_pad, v_pad), (u_pad, u_pad)), 'constant', constant_values=0)
        v_mag_pad = np.pad(v_mag, ((v_pad, v_pad), (u_pad, u_pad)), 'constant', constant_values=0)
        # Fourier transform of the two components:
        u_mag_fft = np.fft.fftshift(np.fft.rfft2(u_mag_pad), axes=0)
        v_mag_fft = np.fft.fftshift(np.fft.rfft2(v_mag_pad), axes=0)
        # Calculate the Fourier transform of the phase:
        f_nyq = 0.5 / self.a  # nyquist frequency
        f_u = np.linspace(0, f_nyq, u_mag_fft.shape[1])
        f_v = np.linspace(-f_nyq, f_nyq, u_mag_fft.shape[0], endpoint=False)
        f_uu, f_vv = np.meshgrid(f_u, f_v)
        coeff = (1j*self.b_0) / (2*self.PHI_0)
        phase_fft = coeff*self.a * (u_mag_fft*f_vv - v_mag_fft*f_uu) / (f_uu**2 + f_vv**2 + 1e-30)
        # Transform to real space and revert padding:
        phase_pad = np.fft.irfft2(np.fft.ifftshift(phase_fft, axes=0))
        phase = phase_pad[v_pad:v_pad+v_dim, u_pad:u_pad+u_dim]
        return PhaseMap(self.a, phase)

    def __repr__(self):
        self.LOG.debug('Calling __repr__')
        return '%s(a=%r, projector=%r, b_0=%r, padding=%r)' % \
            (self.__class__, self.a, self.projector, self.b_0, self.padding)

    def __str__(self):
        self.LOG.debug('Calling __str__')
        return 'PMFourier(a=%s, projector=%s, b_0=%s, padding=%s)' % \
            (self.a, self.projector, self.b_0, self.padding)

class PMElectric(PhaseMapper):

    '''Class representing a phase mapping strategy for the electrostatic contribution.

    The :class:`~.PMElectric` class represents a phase mapping strategy for the electrostatic
    contribution to the electron phase shift which results e.g. from the mean inner potential in
    certain samples and which is sensitive to properties of the electron microscope. It directly
    takes :class:`~.MagData` objects and returns :class:`~.PhaseMap` objects.

    Attributes
    ----------
    a : float
        The grid spacing in nm.
    projector : :class:`~.Projector`
        Projector which should be used for the projection of the 3-dimensional magnetization
        distribution.
    v_0 : float, optional
        The mean inner potential of the specimen in V. The default is 1.
    v_acc : float, optional
        The acceleration voltage of the electron microscope in V. The default is 30000.
    threshold : float, optional
        Threshold for the recognition of the specimen location. The default is 0, which means that
        all voxels with non-zero magnetization will contribute. Should be above noise level.

    '''

    LOG = logging.getLogger(__name__+'.PMElectric')
    H_BAR = 6.626E-34  # Planck constant in J*s
    M_E = 9.109E-31    # electron mass in kg
    Q_E = 1.602E-19    # electron charge in C
    C = 2.998E8        # speed of light in m/s

    def __init__(self, a, projector, v_0=1, v_acc=30000, threshold=0):
        self.LOG.debug('Calling __init__')
        assert isinstance(projector, Projector), 'Argument has to be a Projector object!'
        self.a = a
        self.projector = projector
        self.v_0 = v_0
        self.v_acc = v_acc
        self.threshold = threshold
        self.LOG.debug('Created '+str(self))

    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        assert isinstance(mag_data, MagData), 'Only MagData objects can be mapped!'
        assert mag_data.a == self.a, 'Grid spacing has to match!'
        assert mag_data.dim == self.projector.dim, 'Dimensions do not match!'
        # Coefficient calculation:
        H_BAR, M_E, Q_E, C = self.H_BAR, self.M_E, self.Q_E, self.C
        v_0, v_acc = self.v_0, self.v_acc
        lam = H_BAR / np.sqrt(2 * M_E * Q_E * v_acc * (1 + Q_E*v_acc / (2*M_E*C**2)))
        Ce = 2*pi*Q_E/lam * (Q_E*v_acc + M_E*C**2) / (Q_E*v_acc * (Q_E*v_acc + 2*M_E*C**2))
        # Calculate mask:
        mask = np.sqrt(np.sum(np.array(mag_data.magnitude)**2, axis=0)) > self.threshold
        # Project and calculate phase:
        projection = self.projector(mask.reshape(-1)).reshape(self.projector.dim_uv)
        phase = v_0 * Ce * projection * self.a*1E-9
        return PhaseMap(self.a, phase)

    def __repr__(self):
        self.LOG.debug('Calling __repr__')
        return '%s(a=%r, projector=%r, v_0=%r, v_acc=%r, threshold=%r)' % \
            (self.__class__, self.a, self.projector, self.v_0, self.v_acc, self.threshold)

    def __str__(self):
        self.LOG.debug('Calling __str__')
        return 'PMElectric(a=%s, projector=%s, v_0=%s, v_acc=%s, threshold=%s)' % \
            (self.a, self.projector, self.v_0, self.v_acc, self.threshold)

class PMConvolve(PhaseMapper):

    '''Class representing a phase mapping strategy using real space discretization and Fourier
    space convolution.

    The :class:`~.PMConvolve` class represents a phase mapping strategy involving discretization in
    real space. It utilizes the convolution in Fourier space, directly takes :class:`~.MagData`
    objects and returns :class:`~.PhaseMap` objects.

    Attributes
    ----------
    a : float
        The grid spacing in nm.
    projector : :class:`~.Projector`
        Projector which should be used for the projection of the 3-dimensional magnetization
        distribution.
    b_0 : float, optional
        The magnetic induction corresponding to a magnetization `M`\ :sub:`0` in T.
        The default is 1.
    geometry : {'disc', 'slab'}, optional
        Elementary geometry which is used for the phase contribution of one pixel.
        Default is 'disc'.

    '''

    LOG = logging.getLogger(__name__+'.PMConvolve')

    def __init__(self, a, projector, b_0=1, geometry='disc'):
        self.LOG.debug('Calling __init__')
        assert isinstance(projector, Projector), 'Argument has to be a Projector object!'
        self.a = a
        self.projector = projector
        self.b_0 = b_0
        self.geometry = geometry
        self.kernel = Kernel(a, projector.dim_uv, b_0, geometry)
        self.LOG.debug('Created '+str(self))

    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        assert isinstance(mag_data, MagData), 'Only MagData objects can be mapped!'
        assert mag_data.a == self.a, 'Grid spacing has to match!'
        assert mag_data.dim == self.projector.dim, 'Dimensions do not match!'
        # Process input parameters:
        u_mag, v_mag = self.projector(mag_data.mag_vec).reshape((2,)+self.projector.dim_uv)
        # Fourier transform the projected magnetisation:
        kernel = self.kernel
        u_mag_fft = np.fft.rfftn(u_mag, kernel.dim_fft)
        v_mag_fft = np.fft.rfftn(v_mag, kernel.dim_fft)
        # Convolve the magnetization with the kernel in Fourier space:
        u_phase = np.fft.irfftn(u_mag_fft * kernel.u_fft, kernel.dim_fft)[kernel.slice_fft].copy()
        v_phase = np.fft.irfftn(v_mag_fft * kernel.v_fft, kernel.dim_fft)[kernel.slice_fft].copy()
        # Return the result:
        return PhaseMap(self.a, u_phase-v_phase)

    def __repr__(self):
        self.LOG.debug('Calling __repr__')
        return '%s(a=%r, projector=%r, b_0=%r, geometry=%r)' % \
            (self.__class__, self.a, self.projector, self.b_0, self.geometry)

    def __str__(self):
        self.LOG.debug('Calling __str__')
        return 'PMConvolve(a=%s, projector=%s, b_0=%s, geometry=%s)' % \
            (self.a, self.projector, self.b_0, self.geometry)


class PMReal(PhaseMapper):

    '''Class representing a phase mapping strategy using real space discretization.

    The :class:`~.PMReal` class represents a phase mapping strategy involving discretization in
    real space. It directly takes :class:`~.MagData` objects and returns :class:`~.PhaseMap`
    objects.

    Attributes
    ----------
    a : float
        The grid spacing in nm.
    projector : :class:`~.Projector`
        Projector which should be used for the projection of the 3-dimensional magnetization
        distribution.
    b_0 : float, optional
        The magnetic induction corresponding to a magnetization `M`\ :sub:`0` in T.
        The default is 1.
    threshold : float, optional
        Threshold determining for which voxels the phase contribution will be calculated. The
        default is 0, which means that all voxels with non-zero magnetization will contribute.
        Should be above noise level.
    geometry : {'disc', 'slab'}, optional
        Elementary geometry which is used for the phase contribution of one pixel.
        Default is 'disc'.
    numcore : boolean, optional
        Boolean choosing if Cython enhanced routines from the :mod:`~.pyramid.numcore` module
        should be used. Default is True.

    '''

    LOG = logging.getLogger(__name__+'.PMReal')

    def __init__(self, a, projector, b_0=1, threshold=0, geometry='disc', numcore=True):
        self.LOG.debug('Calling __init__')
        assert isinstance(projector, Projector), 'Argument has to be a Projector object!'
        self.a = a
        self.projector = projector
        self.b_0 = b_0
        self.threshold = threshold
        self.geometry = geometry
        self.kernel = Kernel(a, projector.dim_uv, b_0, geometry)
        self.numcore = numcore
        self.LOG.debug('Created '+str(self))

    def __call__(self, mag_data):
        self.LOG.debug('Calling __call__')
        assert isinstance(mag_data, MagData), 'Only MagData objects can be mapped!'
        assert mag_data.a == self.a, 'Grid spacing has to match!'
        assert mag_data.dim == self.projector.dim, 'Dimensions do not match!'
        # Process input parameters:
        dim_uv = self.projector.dim_uv
        threshold = self.threshold
        u_mag, v_mag = self.projector(mag_data.mag_vec).reshape((2,)+dim_uv)
        # Create kernel (lookup-tables for the phase of one pixel):
        u_phi = self.kernel.u
        v_phi = self.kernel.v
        # Calculation of the phase:
        phase = np.zeros(dim_uv)
        if self.numcore:
            nc.phase_mag_real_core(dim_uv[0], dim_uv[1], v_phi, u_phi,
                                   v_mag, u_mag, phase, threshold)
        else:
            for j in range(dim_uv[0]):
                for i in range(dim_uv[1]):
                    u_phase = u_phi[dim_uv[0]-1-j:(2*dim_uv[0]-1)-j,
                                    dim_uv[1]-1-i:(2*dim_uv[1]-1)-i]
                    if abs(u_mag[j, i]) > threshold:
                        phase += u_mag[j, i] * u_phase
                    v_phase = v_phi[dim_uv[0]-1-j:(2*dim_uv[0]-1)-j,
                                    dim_uv[1]-1-i:(2*dim_uv[1]-1)-i]
                    if abs(v_mag[j, i]) > threshold:
                        phase -= v_mag[j, i] * v_phase
        # Return the phase:
        return PhaseMap(self.a, phase)

    def __repr__(self):
        self.LOG.debug('Calling __repr__')
        return '%s(a=%r, projector=%r, b_0=%r, threshold=%r, geometry=%r, numcore=%r)' % \
            (self.__class__, self.a, self.projector, self.b_0,
             self.threshold, self.geometry, self.numcore)

    def __str__(self):
        self.LOG.debug('Calling __str__')
        return 'PMReal(a=%s, projector=%s, b_0=%s, threshold=%s, geometry=%s, numcore=%s)' % \
            (self.a, self.projector, self.b_0, self.threshold, self.geometry, self.numcore)