Beispiel #1
0
def test_exchange_periodic_boundary_conditions():

    mesh1 = df.BoxMesh(df.Point(0, 0, 0), df.Point(1, 1, 0.1), 2, 2, 1)
    mesh2 = df.UnitCubeMesh(10, 10, 10)

    print("""
    # for debugging, to make sense of output
    # testrun 0, 1 : mesh1
    # testrun 2,3 : mesh2
    # testrun 0, 2 : normal
    # testrun 1,3 : pbc
    """)
    testrun = 0

    for mesh in [mesh1, mesh2]:
        pbc = PeriodicBoundary2D(mesh)
        S3_normal = df.VectorFunctionSpace(mesh, "Lagrange", 1)
        S3_pbc = df.VectorFunctionSpace(mesh,
                                        "Lagrange",
                                        1,
                                        constrained_domain=pbc)

        for S3 in [S3_normal, S3_pbc]:
            print("Running test {}".format(testrun))
            testrun += 1

            FIELD_TOLERANCE = 6e-7
            ENERGY_TOLERANCE = 0.0

            m_expr = df.Expression(("0", "0", "1"), degree=1)

            m = Field(S3, m_expr, name='m')

            exch = Exchange(1)
            exch.setup(m, Field(df.FunctionSpace(mesh, 'DG', 0), 1))
            field = exch.compute_field()
            energy = exch.compute_energy()
            print("m.shape={}".format(m.vector().array().shape))
            print("m=")
            print(m.vector().array())
            print("energy=")
            print(energy)
            print("shape=")
            print(field.shape)
            print("field=")
            print(field)

            H = field
            print "Asserted zero exchange field for uniform m = (1, 0, 0) " + \
                  "got H =\n{}.".format(H.reshape((3, -1)))
            print "np.max(np.abs(H)) =", np.max(np.abs(H))
            assert np.max(np.abs(H)) < FIELD_TOLERANCE

            E = energy
            print "Asserted zero exchange energy for uniform m = (1, 0, 0), " + \
                  "Got E = {:g}.".format(E)
            assert abs(E) <= ENERGY_TOLERANCE
Beispiel #2
0
 def Ms(self, value):
     dg_fun = Field(self.DG, value)
     self._Ms_dg.vector().set_local(dg_fun.vector().get_local())
     # FIXME: change back to DG space.
     #self._Ms_dg=helpers.scalar_valued_function(value, self.S1)
     self._Ms_dg.name = 'Saturation magnetisation'
     self.volumes = df.assemble(df.TestFunction(self.S1) * df.dx)
     Ms = df.assemble(self._Ms_dg.f * df.TestFunction(self.S1) *
                      df.dx).array() / self.volumes.array()
     self._Ms = Ms.copy()
     self.Ms_av = np.average(self._Ms_dg.vector().array())
Beispiel #3
0
 def Ms(self, value):
     # XXX TODO: Rename _Ms_dg to _Ms because it is not a DG0 function!!!
     # We need a DG function here, so we should use
     # scalar_valued_dg_function
     dg_fun = Field(self.DG, value)#helpers.scalar_valued_dg_function(value, self.DG)
     self._Ms_dg.vector().set_local(dg_fun.vector().get_local())
     # FIXME: change back to DG space.
     #self._Ms_dg=helpers.scalar_valued_function(value, self.S1)
     self._Ms_dg.name = 'Saturation magnetisation'
     self.volumes = df.assemble(df.TestFunction(self.S1) * df.dx)
     Ms = df.assemble(
         self._Ms_dg.f * df.TestFunction(self.S1) * df.dx).array() / self.volumes.array()
     self._Ms = Ms.copy()
     self.Ms_av = np.average(self._Ms_dg.vector().array())
Beispiel #4
0
class LLG(object):

    """
    Solves the Landau-Lifshitz-Gilbert equation.

    The equation reads

    .. math::

        \\frac{d\\vec{M}}{dt} = -\\gamma_{LL} (\\vec{M} \\times \\vec{H}) - \\alpha \\gamma_{LL} (\\vec{M} \\times [ \\vec{M} \\times \\vec{H}])

    where :math:`\\gamma_{LL} = \\frac{\\gamma}{1+\\alpha^2}`. In our code
    :math:`-\\gamma_{LL}` is referred to as *precession coefficient* and
    :math:`-\\alpha\\gamma_{LL}` as *damping coefficient*.

    """
    @timer.method
    def __init__(self, S1, S3, do_precession=True, average=False, unit_length=1):
        """
        S1 and S3 are df.FunctionSpace and df.VectorFunctionSpace objects,
        and the boolean do_precession controls whether the precession of the
        magnetisation around the effective field is computed or not.

        """
        logger.debug("Creating LLG object.")
        self.S1 = S1
        self.S3 = S3
        self.mesh = S1.mesh()
        self.DG = df.FunctionSpace(self.mesh, "DG", 0)

        self.set_default_values()
        self.do_precession = do_precession
        self.unit_length = unit_length
        self.do_slonczewski = False
        self.do_zhangli = False
        self.effective_field = EffectiveField(self._m_field,
                                              self.Ms, self.unit_length)
        # will be computed on demand, and carries volume of the mesh
        self.Volume = None

        self.v2d_xyz, self.v2d_xxx, self.d2v_xyz, self.d2v_xxx = helpers.build_maps(S3)
        self.v2d_scale, self.d2v_scale = helpers.build_maps(S1, dim=1, scalar=True)

    def set_default_values(self):
        self.alpha = df.Function(self.S1)
        self.alpha.assign(df.Constant(0.5))
        self.alpha.rename('alpha', 'Gilbert damping constant')

        self.gamma = consts.gamma
        self.c = 1e11  # 1/s numerical scaling correction \
        #               0.1e12 1/s is the value used by default in nmag 0.2
        self._Ms_dg = Field(self.DG)
        self.Ms = 8.6e5  # A/m saturation magnetisation
        self._m_field = Field(self.S3, name='m')
        self.pins = []  # nodes where the magnetisation gets pinned

        self._dmdt = df.Function(self.S3)

        # used for parallel stuff.
        #self.field = df.Function(self.S3)
        #self.h_petsc = df.as_backend_type(self.field.vector()).vec()

    def set_pins(self, nodes):
        """
        Hold the magnetisation constant for certain nodes in the mesh.

        Pass the indices of the pinned sites as *nodes*. Any type of sequence
        is fine, as long as the indices are between 0 (inclusive) and the highest index.
        This means you CANNOT use python style indexing with negative offsets counting
        backwards.

        """
        if len(nodes) > 0:
            nb_nodes_mesh = len(self._m_field.get_ordered_numpy_array_xxx()) / 3
            if min(nodes) >= 0 and max(nodes) < nb_nodes_mesh:
                self._pins = np.array(nodes, dtype="int")
            else:
                logger.error("Indices of pinned nodes should be in [0, {}), were [{}, {}].".format(
                    nb_nodes_mesh, min(nodes), max(nodes)))
        else:
            self._pins = np.array([], dtype="int")

    def pins(self):
        return self._pins
    pins = property(pins, set_pins)

    def set_alpha(self, value):
        """
        Set the damping constant :math:`\\alpha`.

        The parameter `value` can have any of the types accepted by the
        function :py:func:`finmag.util.helpers.scalar_valued_function` (see its
        docstring for details).

        """
        self.alpha = helpers.scalar_valued_function(value, self.S1)
        self.alpha.rename('alpha', 'Gilbert damping constant')

    @property
    def Ms(self):
        return self._Ms_dg

    @Ms.setter
    def Ms(self, value):
        # XXX TODO: Rename _Ms_dg to _Ms because it is not a DG0 function!!!
        # We need a DG function here, so we should use
        # scalar_valued_dg_function
        dg_fun = Field(self.DG, value)#helpers.scalar_valued_dg_function(value, self.DG)
        self._Ms_dg.vector().set_local(dg_fun.vector().get_local())
        # FIXME: change back to DG space.
        #self._Ms_dg=helpers.scalar_valued_function(value, self.S1)
        self._Ms_dg.name = 'Saturation magnetisation'
        self.volumes = df.assemble(df.TestFunction(self.S1) * df.dx)
        Ms = df.assemble(
            self._Ms_dg.f * df.TestFunction(self.S1) * df.dx).array() / self.volumes.array()
        self._Ms = Ms.copy()
        self.Ms_av = np.average(self._Ms_dg.vector().array())

    @property
    def M(self):
        """The magnetisation, with length Ms."""
        # FIXME:error here
        m = self.m.view().reshape((3, -1))
        Ms = self.Ms.vector().array() if isinstance(
            self.Ms, df.Function) else self.Ms
        M = Ms * m
        return M.ravel()

    @property
    def M_average(self):
        """The average magnetisation, computed with m_average()."""
        volume_Ms = df.assemble(self._Ms_dg * df.dx)
        volume = df.assemble(self._Ms_dg * df.dx)
        return self.m_average * volume_Ms / volume

    @property
    def m(self):
        """The unit magnetisation."""
        raise RuntimeError("DON'T USE llg.m UNTIL FURTHER NOTICE!!!!")

    @property
    def m_field(self):
        """The unit magnetisation."""
        return self._m_field

    @property
    def m_numpy(self):
        """
        Return the magnetisation as a numpy.array. This is not recommended and
        should only be used for debugging!
        """
        return self._m_field.get_ordered_numpy_array_xxx()

    # @m.setter
    # def m(self, value):
    # Not enforcing unit length here, as that is better done
    # once at the initialisation of m.
    #     self._m.vector().set_local(value)

    @property
    def dmdt(self):
        """ dmdt values for all mesh nodes """
        return self._dmdt.vector().array()

    @property
    def sundials_m(self):
        """The unit magnetisation."""
        return self._m_field.get_ordered_numpy_array_xxx()

    @sundials_m.setter
    def sundials_m(self, value):
        # used to copy back from sundials cvode
        self._m_field.set_with_ordered_numpy_array_xxx(value)

    def m_average_fun(self, dx=df.dx):
        """
        Compute and return the average polarisation according to the formula
        :math:`\\langle m \\rangle = \\frac{1}{V} \int m \: \mathrm{d}V`

        """

        # mx = df.assemble(self._Ms_dg * df.dot(self._m, df.Constant([1, 0, 0])) * dx)
        # my = df.assemble(self._Ms_dg * df.dot(self._m, df.Constant([0, 1, 0])) * dx)
        # mz = df.assemble(self._Ms_dg * df.dot(self._m, df.Constant([0, 0, 1])) * dx)
        # volume = df.assemble(self._Ms_dg * dx)
        #
        # return np.array([mx, my, mz]) / volume
        return self._m_field.average(dx=dx)
    m_average = property(m_average_fun)

    def set_m(self, value, normalise=True, **kwargs):
        """
        Set the magnetisation (if `normalise` is True, it is automatically
        normalised to unit length).

        `value` can have any of the forms accepted by the function
        'finmag.util.helpers.vector_valued_function' (see its
        docstring for details).

        You can call this method anytime during the simulation. However, when
        providing a numpy array during time integration, the use of
        the attribute m instead of this method is advised for performance
        reasons and because the attribute m doesn't normalise the vector.

        """
        m0 = helpers.vector_valued_function(value, self.S3, normalise=False, **kwargs).vector().array()[self.v2d_xxx]

        if np.any(np.isnan(m0)):
            raise ValueError("Attempting to initialise m with NaN(s)")

        if normalise:
            m0 = helpers.fnormalise(m0)
        self._m_field.set_with_ordered_numpy_array_xxx(m0)

    def solve_for(self, m, t):
        self._m_field.set_with_ordered_numpy_array_xxx(m)
        value = self.solve(t)
        return value

    def solve(self, t):
        # we don't use self.effective_field.compute(t) for performance reasons
        self.effective_field.update(t)
        H_eff = self.effective_field.H_eff[self.v2d_xxx]  # alias (for readability)
        H_eff.shape = (3, -1)

        timer.start("solve", self.__class__.__name__)
        # Use the same characteristic time as defined by c
        char_time = 0.1 / self.c
        # Prepare the arrays in the correct shape
        m = self._m_field.get_ordered_numpy_array_xxx()
        m.shape = (3, -1)

        dmdt = np.zeros(m.shape)
        alpha__ = self.alpha.vector().array()[self.v2d_scale]
        # Calculate dm/dt
        if self.do_slonczewski:
            if self.fun_slonczewski_time_update != None:
                J_new = self.fun_slonczewski_time_update(t)
                self.J[:] = J_new
            native_llg.calc_llg_slonczewski_dmdt(
                m, H_eff, t, dmdt, self.pins,
                self.gamma, alpha__,
                char_time,
                self.Lambda, self.epsilonprime,
                self.J, self.P, self.d, self._Ms, self.p)
        elif self.do_zhangli:
            if self.fun_zhangli_time_update != None:
                J_profile = self.fun_zhangli_time_update(t)
                self._J = helpers.vector_valued_function(J_profile, self.S3)
                self.J = self._J.vector().array()
                self.compute_gradient_matrix()

            H_gradm = self.compute_gradient_field()
            H_gradm.shape = (3, -1)
            native_llg.calc_llg_zhang_li_dmdt(
                m, H_eff, H_gradm, t, dmdt, self.pins,
                self.gamma, alpha__,
                char_time,
                self.u0, self.beta, self._Ms)
            H_gradm.shape = (-1,)
        else:
            native_llg.calc_llg_dmdt(m, H_eff, t, dmdt, self.pins,
                                     self.gamma, alpha__,
                                     char_time, self.do_precession)
        dmdt.shape = (-1,)
        H_eff.shape = (-1,)

        timer.stop("solve", self.__class__.__name__)

        self._dmdt.vector().set_local(dmdt[self.d2v_xxx])

        return dmdt

    # Computes the dm/dt right hand side ODE term, as used by SUNDIALS CVODE
    def sundials_rhs(self, t, y, ydot):
        ydot[:] = self.solve_for(y, t)
        return 0

    def sundials_psetup(self, t, m, fy, jok, gamma, tmp1, tmp2, tmp3):
        # Note that some of the arguments are deliberately ignored, but they
        # need to be present because the function must have the correct signature
        # when it is passed to set_spils_preconditioner() in the cvode class.
        if not jok:
            self._m_field.set_with_ordered_numpy_array_xxx(m)
            self._reuse_jacobean = True

        return 0, not jok

    def sundials_psolve(self, t, y, fy, r, z, gamma, delta, lr, tmp):
        # Note that some of the arguments are deliberately ignored, but they
        # need to be present because the function must have the correct signature
        # when it is passed to set_spils_preconditioner() in the cvode class.
        z[:] = r
        return 0

    """
    def sundials_rhs_petsc(self, t, y, ydot):
        #only for the testing of parallel stuff, will delete later.

        self.effective_field.update(t)
        self.field.vector().set_local(self.effective_field.H_eff)

        #this is not ideal, will change it after we make use of Field class for damping.
        alpha_petsc = df.as_backend_type(self.alpha.vector()).vec()

        llg_petsc.compute_dm_dt(y,
                                self.h_petsc,
                                ydot,
                                alpha_petsc,
                                self.gamma,
                                self.do_precession,
                                self.c)

        return 0
    """

    # Computes the Jacobian-times-vector product, as used by SUNDIALS CVODE
    @timer.method
    def sundials_jtimes(self, mp, J_mp, t, m, fy, tmp):
        """
        The time integration problem we need to solve is of type

        .. math::

                 \\frac{d y}{d t} = f(y,t)

        where y is the state vector (such as the magnetisation components for
        all sites), t is the time, and f(y,t) is the LLG equation.

        For the implicite integration schemes, sundials' cvode solver
        needs to know the Jacobian J, which is the derivative of the
        (vector-valued) function f(y,t) with respect to the (components
        of the vector) y. The Jacobian is a matrix.

        For a magnetic system N sites, the state vector y has 3N entries
        (because every site has 3 components). The Jacobian matrix J would
        thus have a size of 3N*3N. In general, this is too big to store.

        Fortunately, cvode only needs the result of the multiplication of some
        vector y' (provided by cvode) with the Jacobian. We can thus store
        the Jacobian in our own way (in particular as a sparse matrix
        if we leave out the demag field), and carry out the multiplication of
        J with y' when required, and that is what this function does.

        In more detail: We use the variable name mp to represent m' (i.e. mprime) which
        is just a notation to distinguish m' from m (and not any derivative).

        Our equation is:

        .. math::

             \\frac{dm}{dt} = LLG(m, H)

        And we're interested in computing the Jacobian (J) times vector (m') product

        .. math::

             J m' = [\\frac{dLLG(m, H)}{dm}] m'.

        However, the H field itself depends on m, so the total derivative J m'
        will have two terms

        .. math::

             \\frac{d LLG(m, H)}{dm} = \\frac{\\partial LLG(m, H)}{\\partial m} + [\\frac{\\partial LLG(m, H)}{\\partial H}] [\\frac{\\partial H(m)}{\\partial m}].


        This is a matrix identity, so to make the derivations easier (and since we don't need the full Jacobian matrix) we can write the Jacobian-times-vector product as a directional derivative:

        .. math::

             J m' = \\frac{d LLG(m + a m',H(m + a m'))}{d a}|_{a=0}


        The code to compute this derivative is in ``llg.cc`` but you can see that the derivative will depend
        on m, m', H(m), and dH(m+a m')/da [which is labelled H' in the code].

        Most of the components of the effective field are linear in m; if that's the case,
        the directional derivative H' is just H(m')

        .. math::

             H' = \\frac{d H(m+a m')}{da} = H(m')


        The actual implementation of the jacobian-times-vector product is in src/llg/llg.cc,
        function calc_llg_jtimes(...), which in turn makes use of CVSpilsJacTimesVecFn in CVODE.
        """
        assert m.shape == self._m_field.get_ordered_numpy_array_xxx().shape
        assert mp.shape == m.shape
        assert tmp.shape == m.shape

        # First, compute the derivative H' = dH_eff/dt
        self._m_field.set_with_ordered_numpy_array_xxx(mp)
        Hp = tmp.view()
        Hp[:] = self.effective_field.compute_jacobian_only(t)[self.v2d_xxx]

        if not hasattr(self, '_reuse_jacobean') or not self._reuse_jacobean:
            # If the field m has changed, recompute H_eff as well
            if not np.array_equal(self.m_numpy, m):
                self.m_field.set_with_ordered_numpy_array_xxx(m)
                self.effective_field.update(t)
            else:
                pass
                # print "This actually happened."
                #import sys; sys.exit()

        m.shape = (3, -1)
        mp.shape = (3, -1)
        Hp.shape = (3, -1)
        J_mp.shape = (3, -1)
        # Use the same characteristic time as defined by c
        char_time = 0.1 / self.c
        Heff2 = self.effective_field.H_eff[self.v2d_xxx]
        native_llg.calc_llg_jtimes(m, Heff2.reshape((3, -1)), mp, Hp, t, J_mp, self.gamma,
                                   self.alpha.vector().array()[self.v2d_scale], char_time, self.do_precession, self.pins)
        J_mp.shape = (-1, )
        m.shape = (-1,)
        mp.shape = (-1,)
        tmp.shape = (-1,)

        # Nonnegative exit code indicates success
        return 0

    def use_slonczewski(self, J, P, d, p, Lambda=2, epsilonprime=0.0, with_time_update=None):
        """
        Activates the computation of the Slonczewski spin-torque term in the LLG.

        *Arguments*

        J is the current density in A/m^2 as a number, dolfin function,
          dolfin expression or Python function. In the last case the
          current density is assumed to be spatially constant but can
          vary with time. Thus J=J(t) should be a function expecting a
          single variable t (the simulation time) and return a number.

          Note that a time-dependent current density can also be given
          as a dolfin Expression, but a python function should be much
          more efficient.

        P is the polarisation (between 0 and 1). It is defined as P = (x-y)/(x+y),
        where x and y are the fractions of spin up/down electrons).

        d is the thickness of the free layer in m.

        p is the direction of the polarisation as a triple (is automatically normalised to unit length).

        - Lambda: the Lambda parameter in the Slonczewski/Xiao spin-torque term

        - epsilonprime: the strength of the secondary spin transfer term

        - with_time_update:

             A function of the form J(t), which accepts a time step `t`
             as its only argument and returns the new current density.

             N.B.: For efficiency reasons, the return value is currently
                   assumed to be a number, i.e. J is assumed to be spatially
                   constant (and only varying with time).

        """
        self.do_slonczewski = True
        self.fun_slonczewski_time_update = with_time_update

        self.Lambda = Lambda
        self.epsilonprime = epsilonprime
        if isinstance(J, df.Expression):
            J = df.interpolate(J, self.S1)
        if not isinstance(J, df.Function):
            func = df.Function(self.S1)
            func.assign(df.Constant(J))
            J = func
        self.J = J.vector().array()
        assert P >= 0.0 and P <= 1.0
        self.P = P
        self.d = d
        polarisation = df.Function(self.S3)
        polarisation.assign(df.Constant((p)))
        # we use fnormalise to ensure that p has unit length
        self.p = helpers.fnormalise(
            polarisation.vector().array()).reshape((3, -1))

    def compute_gradient_matrix(self):
        """
        compute (J nabla) m , we hope we can use a matrix M such that M*m = (J nabla)m.

        """
        tau = df.TrialFunction(self.S3)
        sigma = df.TestFunction(self.S3)

        self.nodal_volume_S3 = nodal_volume(self.S3) * self.unit_length

        dim = self.S3.mesh().topology().dim()

        ty = tz = 0

        tx = self._J[0] * df.dot(df.grad(tau)[:, 0], sigma)

        if dim >= 2:
            ty = self._J[1] * df.dot(df.grad(tau)[:, 1], sigma)

        if dim >= 3:
            tz = self._J[2] * df.dot(df.grad(tau)[:, 2], sigma)

        self.gradM = df.assemble((tx + ty + tz) * df.dx)

        #self.gradM = df.assemble(df.dot(df.dot(self._J, df.nabla_grad(tau)),sigma)*df.dx)

    def compute_gradient_field(self):

        self.gradM.mult(self._m_field.f.vector(), self.H_gradm)

        return self.H_gradm.array() / self.nodal_volume_S3

    def use_zhangli(self, J_profile=(1e10, 0, 0), P=0.5, beta=0.01, using_u0=False, with_time_update=None):
        """
        if using_u0 = True, the factor of 1/(1+beta^2) will be dropped.

        With with_time_update should be a function like:
        def f(t):
            return (0, 0, J*g(t))

        We do not use a position dependent function for performance reasons.
        """

        self.do_zhangli = True
        self.fun_zhangli_time_update = with_time_update
        self._J = helpers.vector_valued_function(J_profile, self.S3)
        self.J = self._J.vector().array()
        self.compute_gradient_matrix()
        self.H_gradm = df.PETScVector()

        const_e = 1.602176565e-19
        # elementary charge in As
        mu_B = 9.27400968e-24
        # Bohr magneton

        self.P = P
        self.beta = beta

        u0 = P * mu_B / const_e  # P g mu_B/(2 e Ms) and g=2 for electrons

        if using_u0:
            self.u0 = u0
        else:
            self.u0 = u0 / (1 + beta ** 2)
Beispiel #5
0
class LLG_STT(object):
    """
    Solves the Landau-Lifshitz-Gilbert equation with the nonlocal spin transfer torque.

    """
    def __init__(self, S1, S3, unit_length=1, average=False):
        self.S1 = S1
        self.S3 = S3
        self.unit_length = unit_length

        self.mesh = S1.mesh()

        self._m_field = Field(self.S3, name='m')

        self._delta_m = df.Function(self.S3)

        self.nxyz = len(self.m)
        self._alpha = np.zeros(self.nxyz / 3)
        self.delta_m = np.zeros(self.nxyz)
        self.H_eff = np.zeros(self.nxyz)
        self.dy_m = np.zeros(2 * self.nxyz)  # magnetisation and delta_m
        self.dm_dt = np.zeros(2 * self.nxyz)  # magnetisation and delta_m

        self.set_default_values()
        self.effective_field = EffectiveField(self._m_field, self.Ms,
                                              self.unit_length)

        self._t = 0

    def set_default_values(self):

        self.set_alpha(0.5)

        self.gamma = consts.gamma
        self.c = 1e11  # 1/s numerical scaling correction \
        #               0.1e12 1/s is the value used by default in nmag 0.2
        self.Ms = 8.6e5  # A/m saturation magnetisation

        self.vol = df.assemble(
            df.dot(df.TestFunction(self.S3), df.Constant([1, 1, 1])) *
            df.dx).array()
        self.real_vol = self.vol * self.unit_length**3

        self.pins = []
        self._pre_rhs_callables = []
        self._post_rhs_callables = []
        self.interactions = []

    def set_parameters(self,
                       J_profile=(1e10, 0, 0),
                       P=0.5,
                       D=2.5e-4,
                       lambda_sf=5e-9,
                       lambda_J=1e-9,
                       speedup=1):

        self._J = helpers.vector_valued_function(J_profile, self.S3)
        self.J = self._J.vector().array()
        self.compute_gradient_matrix()
        self.H_gradm = df.PETScVector()

        self.P = P

        self.D = D / speedup
        self.lambda_sf = lambda_sf
        self.lambda_J = lambda_J

        self.tau_sf = lambda_sf**2 / D * speedup
        self.tau_sd = lambda_J**2 / D * speedup

        self.compute_laplace_matrix()
        self.H_laplace = df.PETScVector()

        self.nodal_volume_S3 = nodal_volume(self.S3)

    def set_pins(self, nodes):
        """
        Hold the magnetisation constant for certain nodes in the mesh.

        Pass the indices of the pinned sites as *nodes*. Any type of sequence
        is fine, as long as the indices are between 0 (inclusive) and the highest index.
        This means you CANNOT use python style indexing with negative offsets counting
        backwards.

        """
        if len(nodes) > 0:
            nb_nodes_mesh = len(self._m_field.get_numpy_array_debug()) / 3
            if min(nodes) >= 0 and max(nodes) < nb_nodes_mesh:
                self._pins = np.array(nodes, dtype="int")
            else:
                log.error(
                    "Indices of pinned nodes should be in [0, {}), were [{}, {}]."
                    .format(nb_nodes_mesh, min(nodes), max(nodes)))
        else:
            self._pins = np.array([], dtype="int")

    def pins(self):
        return self._pins

    pins = property(pins, set_pins)

    @property
    def Ms(self):
        return self._Ms_dg

    @Ms.setter
    def Ms(self, value):
        self._Ms_dg = Field(df.FunctionSpace(self.mesh, 'DG', 0), value)
        self._Ms_dg.name = 'Ms'
        self.volumes = df.assemble(df.TestFunction(self.S1) * df.dx)
        Ms = df.assemble(self._Ms_dg.f * df.TestFunction(self.S1) *
                         df.dx).array() / self.volumes
        self._Ms = Ms.copy()
        self.Ms_av = np.average(self._Ms_dg.vector().array())

    @property
    def M(self):
        """The magnetisation, with length Ms."""
        # FIXME:error here
        m = self.m.view().reshape((3, -1))
        Ms = self.Ms.vector().array() if isinstance(self.Ms,
                                                    df.Function) else self.Ms
        M = Ms * m
        return M.ravel()

    @property
    def M_average(self):
        """The average magnetisation, computed with m_average()."""
        volume_Ms = df.assemble(self._Ms_dg * df.dx)
        volume = df.assemble(self._Ms_dg * df.dx)
        return self.m_average * volume_Ms / volume

    @property
    def m(self):
        """The unit magnetisation."""
        return self._m_field.get_numpy_array_debug()

    @m.setter
    def m(self, value):
        # Not enforcing unit length here, as that is better done
        # once at the initialisation of m.
        self._m_field.set_with_numpy_array_debug(value)
        self.dy_m.shape = (2, -1)
        self.dy_m[0][:] = value
        self.dy_m.shape = (-1, )

    @property
    def sundials_m(self):
        """The unit magnetisation."""
        return self.dy_m

    @sundials_m.setter
    def sundials_m(self, value):
        # used to copy back from sundials cvode
        self.dy_m[:] = value[:]
        self.dy_m.shape = (2, -1)
        self._m_field.set_with_numpy_array_debug(self.dy_m[0][:])
        self.dy_m.shape = (-1, )

    def m_average_fun(self, dx=df.dx):
        """
        Compute and return the average polarisation according to the formula
        :math:`\\langle m \\rangle = \\frac{1}{V} \int m \: \mathrm{d}V`

        """

        # mx = df.assemble(self._Ms_dg*df.dot(self._m, df.Constant([1, 0, 0])) * dx)
        # my = df.assemble(self._Ms_dg*df.dot(self._m, df.Constant([0, 1, 0])) * dx)
        # mz = df.assemble(self._Ms_dg*df.dot(self._m, df.Constant([0, 0, 1])) * dx)
        # volume = df.assemble(self._Ms_dg*dx)
        #
        # return np.array([mx, my, mz]) / volume
        return self._m_field.average(dx=dx)

    m_average = property(m_average_fun)

    def set_m(self, value, normalise=True, **kwargs):
        """
        Set the magnetisation (if `normalise` is True, it is automatically
        normalised to unit length).

        `value` can have any of the forms accepted by the function
        'finmag.util.helpers.vector_valued_function' (see its
        docstring for details).

        You can call this method anytime during the simulation. However, when
        providing a numpy array during time integration, the use of
        the attribute m instead of this method is advised for performance
        reasons and because the attribute m doesn't normalise the vector.

        """
        self.m = helpers.vector_valued_function(value,
                                                self.S3,
                                                normalise=normalise,
                                                **kwargs).vector().array()

    def set_alpha(self, value):
        """
        Set the damping constant :math:`\\alpha`.

        The parameter `value` can have any of the types accepted by the
        function :py:func:`finmag.util.helpers.scalar_valued_function` (see its
        docstring for details).

        """
        self._alpha[:] = helpers.scalar_valued_function(
            value, self.S1).vector().array()[:]

    def compute_gradient_matrix(self):
        """
        compute (J nabla) m , we hope we can use a matrix M such that M*m = (J nabla)m.

        """
        tau = df.TrialFunction(self.S3)
        sigma = df.TestFunction(self.S3)

        dim = self.S3.mesh().topology().dim()

        ty = tz = 0

        tx = self._J[0] * df.dot(df.grad(tau)[:, 0], sigma)

        if dim >= 2:
            ty = self._J[1] * df.dot(df.grad(tau)[:, 1], sigma)

        if dim >= 3:
            tz = self._J[2] * df.dot(df.grad(tau)[:, 2], sigma)

        self.gradM = df.assemble(1 / self.unit_length * (tx + ty + tz) * df.dx)

    def compute_gradient_field(self):

        self.gradM.mult(self._m_field.f.vector(), self.H_gradm)

        return self.H_gradm.array() / self.nodal_volume_S3

    def compute_laplace_matrix(self):

        u3 = df.TrialFunction(self.S3)
        v3 = df.TestFunction(self.S3)

        self.laplace_M = df.assemble(self.D / self.unit_length**2 *
                                     df.inner(df.grad(u3), df.grad(v3)) *
                                     df.dx)

    def compute_laplace_field(self):

        self.laplace_M.mult(self._delta_m.vector(), self.H_laplace)

        return -1.0 * self.H_laplace.array() / self.nodal_volume_S3

    def sundials_rhs(self, t, y, ydot):
        self.t = t

        y.shape = (2, -1)
        self._m_field.set_with_numpy_array_debug(y[0])
        self._delta_m.vector().set_local(y[1])
        y.shape = (-1, )

        self.effective_field.update(t)
        H_eff = self.effective_field.H_eff  # alias (for readability)
        H_eff.shape = (3, -1)

        timer.start("sundials_rhs", self.__class__.__name__)
        # Use the same characteristic time as defined by c

        H_gradm = self.compute_gradient_field()
        H_gradm.shape = (3, -1)

        H_laplace = self.compute_laplace_field()
        H_laplace.shape = (3, -1)

        self.dm_dt.shape = (6, -1)

        m = self.m
        m.shape = (3, -1)

        char_time = 0.1 / self.c

        delta_m = self._delta_m.vector().array()
        delta_m.shape = (3, -1)

        native_llg.calc_llg_nonlocal_stt_dmdt(m, delta_m, H_eff, H_laplace,
                                              H_gradm, self.dm_dt, self.pins,
                                              self.gamma, self._alpha,
                                              char_time, self.P, self.tau_sd,
                                              self.tau_sf, self._Ms)

        timer.stop("sundials_rhs", self.__class__.__name__)

        self.dm_dt.shape = (-1, )
        ydot[:] = self.dm_dt[:]

        H_gradm.shape = (-1, )
        H_eff.shape = (-1, )
        m.shape = (-1, )
        delta_m.shape = (-1, )

        return 0
Beispiel #6
0
class Material(object):
    """
    The aim to define this class is to collect materials properties in one class, such as
    the common parameters Ms, A, and K since these properties may have different response
    to temperature T. Another reason is that saturation magnetisation Ms should be
    defined in cells in the framework of FEM but for some reasons it's convenience to use
    the related definition in nodes for dynamics, which will cause some confusion if put them
    in one class.

    Despite the traditional definition that the magnetisation M(r) are separated by the unit
    magnetisation m(r) and Ms which stored in nodes and cells respectively, we just focus on
    magnetisation M(r) and pass it into other classes such as Exchange, Anisotropy and Demag.
    Therefore, Ms in this class in fact is mainly used for users to input.

    Certainly, another way to deal with such confusion is to define different class for
    different scenarios, for example, if the simulation just focus on one material and at
    temperature zero we can define a class have constant Ms.


    We will adapt this class to the situation that LLB case.

    """
    def __init__(self, mesh, name='FePt', unit_length=1):
        self.mesh = mesh
        self.name = name
        self.S1 = df.FunctionSpace(mesh, "Lagrange", 1)
        self.S3 = df.VectorFunctionSpace(mesh, "Lagrange", 1, dim=3)

        self.nxyz = mesh.num_vertices()
        self._m = Field(self.S3, name='m')

        self._T = np.zeros(self.nxyz)
        self._Ms = np.zeros(3 * self.nxyz)
        self._m_e = np.zeros(3 * self.nxyz)
        self.inv_chi_par = np.zeros(self.nxyz)
        self.h = np.zeros(3 * self.nxyz)
        self.unit_length = unit_length

        self.alpha = 0.5
        self.gamma_LL = consts.gamma

        if self.name == 'FePt':
            self.Tc = 660
            self.Ms0 = 1047785.4656
            self.A0 = 2.148042e-11
            self.K0 = 8.201968e6
            self.mu_a = 2.99e-23
        elif self.name == 'Nickel':
            self.Tc = 630
            self.Ms0 = 4.9e5
            self.A0 = 9e-12
            self.K0 = 0
            self.mu_a = 0.61e-23
        elif self.name == 'Permalloy':
            self.Tc = 870
            self.Ms0 = 8.6e5
            self.A0 = 13e-12
            self.K0 = 0
            # TODO: find the correct mu_a for permalloy
            self.mu_a = 1e-23
        else:
            raise NotImplementedError("Only FePt and Nickel available")

        self.volumes = df.assemble(
            df.dot(df.TestFunction(self.S3), df.Constant([1, 1, 1])) *
            df.dx).array()

        self.real_vol = self.volumes * self.unit_length**3

        self.mat = native_llb.Materials(self.Ms0, self.Tc, self.A0, self.K0,
                                        self.mu_a)

        dg = df.FunctionSpace(mesh, "DG", 0)
        self._A_dg = df.Function(dg)
        self._m_e_dg = df.Function(dg)

        self.T = 0
        self.Ms = self.Ms0 * self._m_e_dg.vector().array()

    @property
    def me(self):
        return self._m_e[0]

    def compute_field(self):
        native_llb.compute_relaxation_field(self._T, self.m, self.h, self._m_e,
                                            self.inv_chi_par, self.Tc)
        return self.h

    @property
    def T(self):
        return self._T

    @T.setter
    def T(self, value):
        self._T[:] = helpers.scalar_valued_function(
            value, self.S1).vector().array()[:]

        self._T_dg = helpers.scalar_valued_dg_function(value, self.mesh)

        As = self._A_dg.vector().array()
        Ts = self._T_dg.vector().array()
        mes = self._m_e_dg.vector().array()

        for i in range(len(Ts)):
            As[i] = self.mat.A(Ts[i])
            mes[i] = self.mat.m_e(Ts[i])

        self._A_dg.vector().set_local(As)
        self._m_e_dg.vector().set_local(mes)

        self._m_e.shape = (3, -1)
        for i in range(len(self._T)):
            self._m_e[:, i] = self.mat.m_e(self._T[i])
            self.inv_chi_par[i] = self.mat.inv_chi_par(self._T[i])
        self._m_e.shape = (-1, )

        # TODO: Trying to use spatial parameters
        self.inv_chi_perp = self.mat.inv_chi_perp(self._T[0])

    @property
    def Ms(self):
        return self._Ms

    @Ms.setter
    def Ms(self, value):
        self._Ms_dg = helpers.scalar_valued_dg_function(value, self.mesh)

        tmp_Ms = df.assemble(self._Ms_dg * df.dot(df.TestFunction(
            self.S3), df.Constant([1, 1, 1])) * df.dx) / self.volumes

        self._Ms[:] = tmp_Ms[:]

    @property
    def m(self):
        """
        not too good since this will return a copy
        try to solve this later
        """
        return self._m.vector().array()

    def set_m(self, value, **kwargs):
        """
        Set the magnetisation (scaled automatically).

        There are several ways to use this function. Either you provide
        a 3-tuple of numbers, which will get cast to a dolfin.Constant, or
        a dolfin.Constant directly.
        Then a 3-tuple of strings (with keyword arguments if needed) that will
        get cast to a dolfin.Expression, or directly a dolfin.Expression.
        You can provide a numpy.ndarray of nodal values of shape (3*n,),
        where n is the number of nodes.
        Finally, you can pass a function (any callable object will do) which
        accepts the coordinates of the mesh as a numpy.ndarray of
        shape (3, n) and returns the magnetisation like that as well.

        You can call this method anytime during the simulation. However, when
        providing a numpy array during time integration, the use of
        the attribute m instead of this method is advised for performance
        reasons and because the attribute m doesn't normalise the vector.

        """
        self._m.set(value)