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
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())
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())
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)
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
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)