def setup():
    """
    Create a cuboid mesh representing a magnetic material and two
    dolfin.Functions defined on this mesh:

        m  -- unit magnetisation (linearly varying across the sample)

        Ms_func -- constant function representing the saturation
                   magnetisation Ms


    *Returns*

    A triple (m_space, m, Ms_func), where m_space is the
    VectorFunctionSpace (of type "continuous Lagrange") on which the
    magnetisation m is defined and m, Ms_funct are as above.
    """

    m_space = df.VectorFunctionSpace(mesh, "CG", 1)
    m = Field(m_space, value=df.Expression(("1e-9", "x[0]/10", "0"), degree=1))
    m.set_with_numpy_array_debug(fnormalise(m.get_numpy_array_debug()))

    Ms_space = df.FunctionSpace(mesh, "DG", 0)
    Ms_func = df.interpolate(df.Constant(Ms), Ms_space)

    return m_space, m, Ms_func
예제 #2
0
def compute_scalar_potential_native_gcr(mesh,
                                        m_expr=df.Constant([1, 0, 0]),
                                        Ms=1.0):
    gcrdemag = Demag("GCR")
    V = df.VectorFunctionSpace(mesh, "Lagrange", 1)
    m = Field(V, value=m_expr)
    m.set_with_numpy_array_debug(helpers.fnormalise(m.get_numpy_array_debug()))
    gcrdemag.setup(m, Ms, unit_length=1)
    phi1 = gcrdemag.compute_potential()
    normalise_phi(phi1, mesh)
    return phi1
예제 #3
0
def compute_scalar_potential_llg(mesh, m_expr=df.Constant([1, 0, 0]), Ms=1.):
    S3 = df.VectorFunctionSpace(mesh, "Lagrange", 1, dim=3)
    m = Field(S3, value=m_expr)
    m.set_with_numpy_array_debug(helpers.fnormalise(m.get_numpy_array_debug()))

    demag = Demag()
    demag.setup(m, Field(df.FunctionSpace(mesh, 'DG', 0), Ms), unit_length=1)

    phi = demag.compute_potential()
    normalise_phi(phi, mesh)
    return phi
예제 #4
0
def exchange(mesh, unit_length):
    S3 = df.VectorFunctionSpace(mesh, "Lagrange", 1)
    m = Field(S3,
              value=df.Expression(("x[1]*u", "0", "sqrt(1-pow(x[1]*u, 2))"),
                                  u=unit_length,
                                  degree=1))
    exch = Exchange(A)
    exch.setup(m,
               Field(df.FunctionSpace(mesh, 'DG', 0), Ms),
               unit_length=unit_length)
    H = exch.compute_field()
    E = exch.compute_energy()
    return m.get_numpy_array_debug(), H, E
def three_dimensional_problem():
    x_max = 10e-9
    y_max = 1e-9
    z_max = 1e-9
    mesh = df.BoxMesh(df.Point(0, 0, 0), df.Point(x_max, y_max, z_max), 40, 2,
                      2)

    V = df.VectorFunctionSpace(mesh, 'Lagrange', 1)

    Ms = 8.6e5

    m0_x = "pow(sin(0.2*x[0]*1e9), 2)"
    m0_y = "0"
    m0_z = "pow(cos(0.2*x[0]*1e9), 2)"
    m = Field(V,
              value=vector_valued_function((m0_x, m0_y, m0_z),
                                           V,
                                           normalise=True))

    C = 1.3e-11

    u_exch = Exchange(C)
    u_exch.setup(m, Field(df.FunctionSpace(mesh, 'DG', 0), Ms))
    finmag_exch = u_exch.compute_field()

    magpar_result = os.path.join(MODULE_DIR, 'magpar_result', 'test_exch')
    nodes, magpar_exch = magpar.get_field(magpar_result, 'exch')

    ## Uncomment the line below to invoke magpar to compute the results,
    ## rather than using our previously saved results.
    # nodes, magpar_exch = magpar.compute_exch_magpar(m, A=C, Ms=Ms)

    print magpar_exch

    # Because magpar have changed the order of the nodes!!!

    tmp = df.Function(V)
    tmp_c = mesh.coordinates()
    mesh.coordinates()[:] = tmp_c * 1e9

    finmag_exch, magpar_exch, \
        diff, rel_diff = magpar.compare_field(
            mesh.coordinates(), finmag_exch,
            nodes, magpar_exch)

    return dict(m0=m.get_numpy_array_debug(),
                mesh=mesh,
                exch=finmag_exch,
                magpar_exch=magpar_exch,
                diff=diff,
                rel_diff=rel_diff)
예제 #6
0
파일: sllg.py 프로젝트: whshangl/finmag
class SLLG(object):
    def __init__(self,
                 S1,
                 S3,
                 method='RK2b',
                 checking_length=False,
                 unit_length=1):
        self.S1 = S1
        self.S3 = S3
        self.mesh = S1.mesh()

        self._t = 0
        self.time_scale = 1e-9

        self._m_field = Field(self.S3, name='m')
        self.nxyz = self._m_field.f.vector().size() / 3

        self._T = np.zeros(self.nxyz)
        self._alpha = np.zeros(self.nxyz)
        self.m = np.zeros(3 * self.nxyz)
        self.field = np.zeros(3 * self.nxyz)
        self.grad_m = np.zeros(3 * self.nxyz)
        self.dm_dt = np.zeros(3 * self.nxyz)
        # Note: nxyz for Ms length is more suitable?
        self._Ms = np.zeros(3 * self.nxyz)

        self.pin_fun = None
        self.method = method
        self.checking_length = checking_length
        self.unit_length = unit_length
        self.DG = df.FunctionSpace(self.mesh, "DG", 0)
        self._Ms_dg = Field(self.DG)
        self.effective_field = EffectiveField(self._m_field, self.Ms,
                                              self.unit_length)

        self.zhangli_stt = False

        self.set_default_values()

    def set_default_values(self):
        self.Ms = Field(df.FunctionSpace(self.mesh, 'DG', 0),
                        8.6e5)  # A/m saturation magnetisation
        self._pins = np.array([], dtype="int")
        self.volumes = df.assemble(
            df.dot(df.TestFunction(self.S3), df.Constant([1, 1, 1])) *
            df.dx).array()
        self.Volume = mesh_volume(self.mesh)
        self.real_volumes = self.volumes * self.unit_length**3

        self.m_pred = np.zeros(self.m.shape)

        self.integrator = native_llb.StochasticSLLGIntegrator(
            self.m, self.m_pred, self._Ms, self._T, self.real_volumes,
            self._alpha, self.stochastic_update_field, self.method)

        self.alpha = 0.1
        self._gamma = consts.gamma
        self._seed = np.random.random_integers(4294967295)
        self.dt = 1e-13
        self.T = 0

    @property
    def cur_t(self):
        return self._t * self.time_scale

    @cur_t.setter
    def cur_t(self, value):
        self._t = value / self.time_scale

    @property
    def dt(self):
        return self._dt * self.time_scale

    @property
    def gamma(self):
        return self._gamma

    @property
    def seed(self):
        return self._seed

    @seed.setter
    def seed(self, value):
        self._seed = value
        self.setup_parameters()

    @gamma.setter
    def gamma(self, value):
        self._gamma = value
        self.setup_parameters()

    @dt.setter
    def dt(self, value):
        self._dt = value / self.time_scale
        self.setup_parameters()

    def setup_parameters(self):
        # print 'seed:', self.seed
        self.integrator.set_parameters(self.dt, self.gamma, self.seed,
                                       self.checking_length)
        log.info("seed=%d." % self.seed)
        log.info("dt=%g." % self.dt)
        log.info("gamma=%g." % self.gamma)
        #log.info("checking_length: "+str(self.checking_length))

    def set_m(self, value, normalise=True):
        m_tmp = helpers.vector_valued_function(
            value, self.S3, normalise=normalise).vector().array()
        self._m_field.set_with_numpy_array_debug(m_tmp)
        self.m[:] = self._m_field.get_numpy_array_debug()

    def advance_time(self, t):
        tp = t / self.time_scale

        if tp <= self._t:
            return
        try:
            while tp - self._t > 1e-12:
                if self.zhangli_stt:
                    self.integrator.run_step(self.field, self.grad_m)
                else:
                    self.integrator.run_step(self.field)

                self._m_field.set_with_numpy_array_debug(self.m)

                self._t += self._dt

        except Exception, error:
            log.info(error)
            raise Exception(error)

        if abs(tp - self._t) < 1e-12:
            self._t = tp
예제 #7
0
class UniaxialAnisotropy(EnergyBase):
    """
    Compute the uniaxial anisotropy field.

    .. math::

        E_{\\text{anis}} = \\int_\\Omega K_1 - (1 - a \\cdot m)^2  dx

    *Arguments*
        K1
            The anisotropy constant
        K2
            The anisotropy constant (default=0)
        axis
            The easy axis. Should be a unit vector.
        Ms
            The saturation magnetisation.
        method
            The method used to compute the anisotropy field.
            For alternatives and explanation, see EnergyBase class.

    *Example of Usage*
        .. code-block:: python

            import dolfin as df
            from finmag import UniaxialAnisotropy

            L = 1e-8;
            nL = 5;
            mesh = df.BoxMesh(df.Point(0, L, 0), df.Point(L, 0, L), nL, nL, nL)

            S3 = df.VectorFunctionSpace(mesh, 'Lagrange', 1)
            K = 520e3 # For Co (J/m3)

            a = df.Constant((0, 0, 1)) # Easy axis in z-direction
            m = df.project(df.Constant((1, 0, 0)), V)  # Initial magnetisation
            Ms = 1e6

            anisotropy = UniaxialAnisotropy(K, a)
            anisotropy.setup(S3, m)

            # Print energy
            print anisotropy.compute_energy()

            # Assign anisotropy field
            H_ani = anisotropy.compute_field()

    """
    def __init__(self,
                 K1,
                 axis,
                 K2=0,
                 method="box-matrix-petsc",
                 name='Anisotropy',
                 assemble=True):
        """
        Define a uniaxial anisotropy with (first) anisotropy constant `K1`
        (in J/m^3) and easy axis `axis`.

        K1 and axis can be passed as df.Constant or df.Function, although
        automatic convertion will be attempted from float for K1 and a
        sequence type for axis. It is possible to specify spatially
        varying anisotropy by using df.Functions.

        """
        self.K1_value = K1
        self.K2_value = K2
        self.axis_value = axis
        self.name = name
        self.assemble = assemble
        super(UniaxialAnisotropy, self).__init__(method, in_jacobian=True)

        if K2 != 0:
            self.assemble = False

    @timer.method
    def setup(self, m, Ms, unit_length=1):
        """
        Function to be called after the energy object has been constructed.

        *Arguments*

            m
                magnetisation field (usually normalised)

            Ms
                Saturation magnetisation field

            unit_length
                real length of 1 unit in the mesh

        """
        assert isinstance(m, Field)
        assert isinstance(Ms, Field)

        cg_scalar_functionspace = df.FunctionSpace(m.mesh(), 'CG', 1)
        self.K1 = Field(cg_scalar_functionspace, self.K1_value, name='K1')
        self.K2 = Field(cg_scalar_functionspace, self.K2_value, name='K2')

        cg_vector_functionspace = df.VectorFunctionSpace(m.mesh(), 'CG', 1, 3)
        self.axis = Field(cg_vector_functionspace,
                          self.axis_value,
                          name='axis')
        # Anisotropy energy
        # HF's version inline with nmag, breaks comparison with analytical
        # solution in the energy density test for anisotropy, as this uses
        # the Scholz-Magpar method. Should anyway be an easy fix when we
        # decide on method.
        # FIXME: we should use DG0 space here?

        E_integrand = self.K1.f * \
            (df.Constant(1) - (df.dot(self.axis.f, m.f)) ** 2)
        if self.K2_value != 0:
            E_integrand -= self.K2.f * df.dot(self.axis.f, m.f)**4

        del (self.K1_value)
        del (self.K2_value)
        del (self.axis_value)

        super(UniaxialAnisotropy, self).setup(E_integrand, m, Ms, unit_length)

        if not self.assemble:
            self.H = self.m.get_numpy_array_debug()
            self.Ms = self.Ms.get_numpy_array_debug()
            self.u = self.axis.get_numpy_array_debug()
            self.K1_arr = self.K1.get_numpy_array_debug()
            self.K2_arr = self.K2.get_numpy_array_debug()
            self.volumes = df.assemble(
                df.TestFunction(cg_scalar_functionspace) * df.dx)
            self.compute_field = self.__compute_field_directly

    def __compute_field_directly(self):

        m = self.m.get_numpy_array_debug()

        m.shape = (3, -1)
        self.H.shape = (3, -1)
        self.u.shape = (3, -1)
        native_llg.compute_anisotropy_field(m, self.Ms, self.H, self.u,
                                            self.K1_arr, self.K2_arr)
        m.shape = (-1, )
        self.H.shape = (-1, )
        self.u.shape = (-1, )

        return self.H
예제 #8
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
예제 #9
0
파일: zeeman.py 프로젝트: whshangl/finmag
class Zeeman(object):
    def __init__(self, H, name='Zeeman', **kwargs):
        """
        Specify an external field (in A/m).

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

        """
        self.H_value = H
        self.name = name
        self.kwargs = kwargs
        self.in_jacobian = False

    def setup(self, m, Ms, unit_length=1):
        """
        Function to be called after the energy object has been constructed.

        *Arguments*

            m
                magnetisation field (usually normalised)

            Ms
                Saturation magnetisation (scalar, or scalar dolfin function)

            unit_length
                real length of 1 unit in the mesh

        """
        self.m = m
        self.Ms = Ms
        self.unit_length = unit_length

        dofmap = self.m.functionspace.dofmap()
        self.S1 = df.FunctionSpace(
            m.mesh(),
            "Lagrange",
            1,
            constrained_domain=dofmap.constrained_domain)
        # self.dim = S3.mesh().topology().dim()
        # self.nodal_volume_S1 = nodal_volume(self.S1, self.unit_length)

        self.set_value(self.H_value, **self.kwargs)

    def set_value(self, value, **kwargs):
        """
        Set the value of the field (in A/m).

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

        """
        self.value = value
        dofmap = self.m.functionspace.dofmap()
        dg_vector_functionspace = df.VectorFunctionSpace(
            self.m.mesh(),
            'CG',
            1,
            3,
            constrained_domain=dofmap.constrained_domain)
        self.H = Field(dg_vector_functionspace, value, name='H_ext')
        self.E = -mu0 * self.Ms.f * df.dot(self.m.f,
                                           self.H.f)  # Energy density.

    def average_field(self):
        """
        Compute the average applied field.
        """
        return helpers.average_field(self.compute_field())

    def compute_field(self):
        return self.H.get_numpy_array_debug()

    def compute_energy(self, dx=df.dx):
        dim = self.m.mesh_dim()
        E = df.assemble(self.E * dx) * self.unit_length**dim
        return E

    def energy_density(self):
        """
        Return energy density (as a finmag.Field object).
        """
        # Previous version
        #return df.project(df.dot(self.m.f, self.H.f) * self.Ms.f * -mu0, self.S1).vector().array()
        #
        # New version using the Field class. Note that this is
        # currently ca. 6-7 times *slower* than the version above.
        # However, it is slightly more accurate (no rounding errors
        # due to projection), and we should be able to tune
        # performance in the Field class.
        return self.m.dot(self.H) * self.Ms * -mu0

    def energy_density_function(self):
        if not hasattr(self, "E_density_function"):
            self.E_density_function = self.energy_density().f
        return self.E_density_function