Beispiel #1
0
def test_cuboid_demags_2D():
    """
    Comparison of the FFT approach for hexagonal meshes, named
    DemagHexagonal, where it is used a system with the double number
    of nodes along the x direction (i.e. a mesh with twice the number
    of nodes of the original mesh), against the full calculation
    of the Demag field
    """
    # Number of atoms
    N = 15
    a = 0.4

    mesh = CuboidMesh(a, a, a, N, N, 1, unit_length=1e-9)
    mu_s = 2 * const.mu_B

    # Centre
    xc = (mesh.Lx * 0.5)
    yc = (mesh.Ly * 0.5)

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_2Dvortex(pos, (xc, yc)))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('DemagFull').compute_field()
    # print sim.get_interaction('DemagFull').field
    DemagFull_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_2Dvortex(pos, (xc, yc)))

    sim2.add(Demag())
    sim2.get_interaction('Demag').compute_field()
    sim2.compute_energy()

    demag_fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (DemagFull_energy - demag_fft_energy) < 1e-10
def test_cuboid_demags_2D():
    """
    Comparison of the FFT approach for hexagonal meshes, named
    DemagHexagonal, where it is used a system with the double number
    of nodes along the x direction (i.e. a mesh with twice the number
    of nodes of the original mesh), against the full calculation
    of the Demag field
    """
    # Number of atoms
    N = 15
    a = 0.4

    mesh = CuboidMesh(a, a, a, N, N, 1, unit_length=1e-9)
    mu_s = 2 * const.mu_B

    # Centre
    xc = (mesh.Lx * 0.5)
    yc = (mesh.Ly * 0.5)

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_2Dvortex(pos, (xc, yc)))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('demag_full').compute_field()
    # print sim.get_interaction('demag_full').field
    demag_full_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_2Dvortex(pos, (xc, yc)))

    sim2.add(Demag())
    sim2.get_interaction('demag').compute_field()
    sim2.compute_energy()

    demag_fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (demag_full_energy - demag_fft_energy) < 1e-10
def test_cuboid_demags_1Dchain():
    """
    Test a brute force calculation of the demagnetising field, called
    DemagFull, based on the sum of the dipolar contributions of the whole
    system for every lattice site, against the default FFT approach for the
    demag field. We compute the energies scaled in meV.
    This test is performed in a cuboid mesh to assure that the DemagFull
    library is calculating the same than the default demag function
    """
    N = 12
    a = 0.4
    mesh = CuboidMesh(a, a, a, N, 1, 1, unit_length=1e-9)
    mu_s = 2 * const.mu_B

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_dw(pos, N, a))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('demag_full').compute_field()
    # print sim.get_interaction('demag_full').field
    demag_full_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_dw(pos, N, a))

    sim2.add(Demag())
    sim2.get_interaction('demag').compute_field()
    sim2.compute_energy()

    demag_fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (demag_full_energy - demag_fft_energy) < 1e-10
Beispiel #4
0
def test_cuboid_demags_1Dchain():
    """
    Test a brute force calculation of the demagnetising field, called
    DemagFull, based on the sum of the dipolar contributions of the whole
    system for every lattice site, against the default FFT approach for the
    demag field. We compute the energies scaled in meV.
    This test is performed in a cuboid mesh to assure that the DemagFull
    library is calculating the same than the default demag function
    """
    N = 12
    a = 0.4
    mesh = CuboidMesh(a, a, a, N, 1, 1, unit_length=1e-9)
    mu_s = 2 * const.mu_B

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_dw(pos, N, a))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('DemagFull').compute_field()
    # print sim.get_interaction('DemagFull').field
    DemagFull_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_dw(pos, N, a))

    sim2.add(Demag())
    sim2.get_interaction('Demag').compute_field()
    sim2.compute_energy()

    demag_fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (DemagFull_energy - demag_fft_energy) < 1e-10
def test_hexagonal_demags_1Dchain():
    """
    Comparison of the FFT approach for hexagonal meshes, named
    DemagHexagonal, where it is used a system with the double number
    of nodes along the x direction (i.e. a mesh with twice the number
    of nodes of the original mesh), against the full calculation
    of the Demag field
    """
    # Number of atoms
    N = 12
    a = 0.4
    mesh = HexagonalMesh(a * 0.5, N, 1,
                         unit_length=1e-9,
                         alignment='square')
    mu_s = 2 * const.mu_B

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_dw(pos, N, a))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('demag_full').compute_field()
    sim.get_interaction('demag_full').field
    demag_full_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach and a larger mesh
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_dw(pos, N, a))

    sim2.add(DemagHexagonal())
    sim2.get_interaction('demag_hex').compute_field()
    sim2.compute_energy()

    demag_2fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (demag_full_energy - demag_2fft_energy) < 1e-10
Beispiel #6
0
def test_hexagonal_demags_1Dchain():
    """
    Comparison of the FFT approach for hexagonal meshes, named
    DemagHexagonal, where it is used a system with the double number
    of nodes along the x direction (i.e. a mesh with twice the number
    of nodes of the original mesh), against the full calculation
    of the Demag field
    """
    # Number of atoms
    N = 12
    a = 0.4
    mesh = HexagonalMesh(a * 0.5, N, 1,
                         unit_length=1e-9,
                         alignment='square')
    mu_s = 2 * const.mu_B

    sim = Sim(mesh)
    sim.mu_s = mu_s

    sim.set_m(lambda pos: m_init_dw(pos, N, a))
    # Brute force demag calculation
    sim.add(DemagFull())

    sim.get_interaction('DemagFull').compute_field()
    sim.get_interaction('DemagFull').field
    DemagFull_energy = sim.compute_energy() / const.meV

    # Demag using the FFT approach and a larger mesh
    sim2 = Sim(mesh)
    sim2.mu_s = mu_s

    sim2.set_m(lambda pos: m_init_dw(pos, N, a))

    sim2.add(DemagHexagonal())
    sim2.get_interaction('DemagHexagonal').compute_field()
    sim2.compute_energy()

    demag_2fft_energy = sim2.compute_energy() / const.meV

    # We compare both energies scaled in meV
    assert (DemagFull_energy - demag_2fft_energy) < 1e-10
Beispiel #7
0
def test_skx_num_atomistic_hexagonal():
    """

    Test the topological charge or skyrmion number for a discrete spins
    simulation in a two dimensional hexagonal lattice, using Berg and Luscher
    definition in [Nucl Phys B 190, 412 (1981)] and simplified in [PRB 93,
    174403 (2016)], which maps a triangulated lattice (using triangles of
    neighbouring spins) area into a unit sphere.

    The areas of two triangles per lattice site cover a unit cell, thus the sum
    cover the whole area of the atomic lattice

    This test generates a skyrmion pointing down and two skyrmions pointing up
    in a PdFe sample using magnetic parameters from: PRL 114, 177203 (2015)

    """

    mesh = HexagonalMesh(0.2715, 41, 41, periodicity=(True, True))

    sim = Sim(mesh, name='skx_number_hexagonal')
    sim.driver.set_tols(rtol=1e-6, atol=1e-6)
    sim.driver.alpha = 1.0
    sim.driver.gamma = 1.0
    sim.mu_s = 3 * const.mu_B

    sim.set_m(lambda pos: init_m(pos, 16.1, 10, 2))

    sim.driver.do_precession = False

    J = 5.881 * const.meV
    exch = UniformExchange(J)
    sim.add(exch)

    D = 1.557 * const.meV
    dmi = DMI(D, dmi_type='interfacial')
    sim.add(dmi)

    sim.add(Anisotropy(0.406 * const.meV, axis=[0, 0, 1]))

    zeeman = Zeeman([0, 0, 2.5])
    sim.add(zeeman)

    sim.relax(dt=1e-13,
              stopping_dmdt=1e-2,
              max_steps=2000,
              save_m_steps=None,
              save_vtk_steps=100)

    skn_single = sim.skyrmion_number(method='BergLuscher')
    print('skx_number_hexagonal', skn_single)

    # Now we generate two skyrmions pointing up
    sim.driver.reset_integrator()
    sim.set_m(
        lambda pos: init_m_multiple_sks(pos, 1, sk_pos=[(9, 6), (18, 12)]))
    sim.get_interaction('Zeeman').update_field([0, 0, -2.5])
    sim.relax(dt=1e-13,
              stopping_dmdt=1e-2,
              max_steps=2000,
              save_m_steps=None,
              save_vtk_steps=None)

    skn_two = sim.skyrmion_number(method='BergLuscher')
    print('skx_number_hexagonal_two', skn_two)

    # Check that we get a right sk number
    assert np.abs(skn_single - (-1)) < 1e-4 and np.sign(skn_single) < 0
    assert np.abs(skn_two - (2)) < 1e-4 and np.sign(skn_two) > 0
Beispiel #8
0
def simulation(nx,
               ny,
               nz,
               dx,
               dy,
               dz,
               j,
               d,
               kc,
               mu_s,
               sim_name,
               bz_min,
               bz_max,
               bz_steps,
               bz_hysteresis=False,
               initial_state_one_bobber=None,
               initial_state_two_bobbers=None,
               initial_state_two_bobbers_asymm=None,
               initial_state_sk_tube=None,
               initial_state_one_dim_mod=None,
               initial_state_helix_angle_x=(None, None),
               stopping_dmdt=1e-5,
               max_steps=4000,
               save_initial_state=None):

    mesh = CuboidMesh(nx=nx,
                      ny=ny,
                      nz=nz,
                      dx=dx,
                      dy=dy,
                      dz=dz,
                      x0=-nx * 0.5,
                      y0=-ny * 0.5,
                      z0=-nz * 0.5,
                      unit_length=1.,
                      periodicity=(True, True, False))

    # J = 1.
    # D = 0.628  # L_D = 2 PI a J / D = 10 * a => D / J = 2 PI / 10.
    # Bz = 0.2   # B_z == (B_z mu_s / J)
    # mu_s = 1.

    sim = Sim(mesh, name=sim_name, integrator='sundials_openmp')

    sim.mu_s = mu_s
    sim.add(Exchange(j))
    sim.add(DMI(d, dmi_type='bulk'))
    sim.add(Zeeman((0.0, 0.0, bz_min * 1e-3)), save_field=True)
    if np.abs(kc) > 0.0:
        sim.add(CubicAnisotropy(kc))

    # .........................................................................

    sim.driver.alpha = 0.9
    sim.driver.do_precession = False

    if not os.path.exists('npys/{}'.format(sim_name)):
        os.makedirs('npys/{}'.format(sim_name))

    if not os.path.exists('txts'):
        os.makedirs('txts')

    for i, B_sweep in enumerate(np.linspace(bz_min, bz_max, bz_steps)):

        print('Bz = {:.0f} mT '.format(B_sweep).ljust(80, '-'))

        Zeeman_int = sim.get_interaction('Zeeman')
        Zeeman_int.update_field((0.0, 0.0, B_sweep * 1e-3))

        if (not bz_hysteresis) or (bz_hysteresis and i < 1):

            if initial_state_one_dim_mod:
                sim.set_m(lambda r: one_dim_mod(r, B_sweep * 1e-3, d))
            elif initial_state_helix_angle_x[0]:
                angle = initial_state_helix_angle_x[0] * np.pi / 180.
                periodic = initial_state_helix_angle_x[1]
                sim.set_m(lambda r: helix_angle_x(r, angle, periodic))
            elif initial_state_sk_tube:
                sk_rad = initial_state_sk_tube
                sim.set_m(
                    lambda r: sk_tube(r, B_sweep * 1e-3, d, sk_rad=sk_rad))
            elif initial_state_two_bobbers:
                bobber_length = initial_state_two_bobbers
                sim.set_m(lambda r: two_bobbers(r,
                                                B_sweep * 1e-3,
                                                d,
                                                mesh.Lz,
                                                bobber_rad=3.,
                                                bobber_length=bobber_length))
            elif initial_state_two_bobbers_asymm:
                bobber_length = initial_state_two_bobbers_asymm
                sim.set_m(
                    lambda r: two_bobbers_asymm(r,
                                                B_sweep * 1e-3,
                                                d,
                                                mesh.Lz,
                                                bobber_rad=3.,
                                                bobber_length=bobber_length))
            elif initial_state_one_bobber:
                bobber_length = initial_state_one_bobber
                sim.set_m(lambda r: one_bobber(r,
                                               B_sweep * 1e-3,
                                               d,
                                               mesh.Lz,
                                               bobber_rad=3.,
                                               bobber_length=bobber_length))
            else:
                raise Exception('Not a valid initial state')

        if save_initial_state:
            save_vtk(sim, sim_name + '_INITIAL', field_mT=B_sweep)
            name = 'npys/{}/m_{}_INITIAL_Bz_{:06d}.npy'.format(
                sim.driver.name, sim_name, int(B_sweep))
            np.save(name, sim.spin)

        # .....................................................................

        sim.relax(stopping_dmdt=stopping_dmdt,
                  max_steps=max_steps,
                  save_m_steps=None,
                  save_vtk_steps=None)

        # .....................................................................

        save_vtk(sim, sim_name, field_mT=B_sweep)
        name = 'npys/{}/m_{}_Bz_{:06d}.npy'.format(sim.driver.name, sim_name,
                                                   int(B_sweep))
        np.save(name, sim.spin)

        sim.driver.reset_integrator()

    shutil.move(sim_name + '.txt', 'txts/{}.txt'.format(sim_name))
Beispiel #9
0
sim.add(DMI(0.727, dmi_type='bulk'))
bz_min = 0.0
sim.add(Zeeman((0.0, 0.0, bz_min * 1e-3)), save_field=True)
kc = -0.05
if np.abs(kc) > 0.0:
    sim.add(CubicAnisotropy(kc))

# .....................................................................

sim.driver.alpha = 0.9
sim.driver.do_precession = False

# FIELD = 80
for i, FIELD in enumerate(range(0, 181, 20)):

    Zeeman_int = sim.get_interaction('Zeeman')
    Zeeman_int.update_field((0.0, 0.0, FIELD * 1e-3))

    if i == 0:
        H_BG = np.load(f'../npys/helix-y_kc-5e-2_L10/m_helix-y_kc-5e-2_L10_Bz_{FIELD:06d}.npy')
        sim.set_m(H_BG)

        # Embed skyrmion in the helical phase
        r = sim.mesh.coordinates
        rho = np.sqrt(r[:, 0] ** 2 + r[:, 1] ** 2)
        phi = np.arctan2(r[:, 1], r[:, 0])
        m = np.copy(sim.spin.reshape(-1, 3))
        # m[rho < 5] = [0, 0, -1.]

        sign = -1
        k = np.pi / 5
Beispiel #10
0
class interactive_simulation(object):
    def __init__(self,
                 config_file=False,
                 simulation='2D_square',
                 D=1.56,
                 J=5.88,
                 ku=0.41,
                 mu_s=3,
                 B=(0, 0, 0),
                 Demag=None,
                 mesh_nx=50,
                 mesh_ny=50,
                 mesh_a=0.2715):
        """

        This class generates an interactive simulation using Ipython widgets.
        By default, the image is a square shaped hexagonal mesh with finite
        boundaries.

        Arguments:

        config_file        :: Not implemented yet

        simulation         :: Any of the following strings:
                              ['2D_square', 'experimental_sample', '1D_chain']

                              The 2D_square option creates a square shaped
                              mesh using an atomistic hexagonal lattice.

                              The experimental_sample option creates a mesh
                              using an image from experimental data and
                              populates it with spins using the atomistic
                              hexagonal mesh. Here the mesh parameters are not
                              used in the class.  The image processing is done
                              through the sim_from_image library.

                              The 1D_chain creates a one dimensional spin chain

        Default magnetic parameters are from Romming et al. [PRL 114, 177203
        (2015)] research on PdFe bilayers on Ir(111)

        Basic usage:

            # Simulation with default magnetic field along z of 0 T
            i_sim = interactive_simulation()

            i_sim.generate_simulation()
            i_sim.generate_m_field()

            # This produces the plot and simulation options
            i_sim.generate_widgets()


        TODO: Add option to pass a custom function for the magnetisation field
              initial state

              Color plot according to magnetisation directions in 360 degrees?

        """

        self.simulation = simulation

        if config_file:
            tmp_config = {}
            configs = execfile(config_file, tmp_config)

            self.D = configs["D"] * const.meV
            self.J = configs["J"] * const.meV
            self.ku = configs["ku"] * const.meV
            self.mu_s = configs["mu_s"] * const.mu_B
            self.m_field = configs["m_field"]
            if configs["B"] is not None:
                self.B = configs["B"]

        else:
            self.D = D * const.meV
            self.J = J * const.meV
            self.ku = ku * const.meV
            self.mu_s = mu_s * const.mu_B
            self.B = B
            self.Demag = Demag

            self.mesh_nx = mesh_nx
            self.mesh_ny = mesh_ny
            self.mesh_a = mesh_a

        # Dictionary to translate a vector component into the corresponding
        # indexes in Fidimag arrays, i.e. x --> 0, y --> 1, z --> 2
        self.v_dict = {'x': 0, 'y': 1, 'z': 2}

        # Measure for dm / dt
        self.DEGREE_PER_NANOSECOND = 2 * np.pi / (360 * 1e-9)

    def skyrmion_m_field(self,
                         pos,
                         sign,
                         sk_pos=None,
                         sk_r=4,
                         core=1,
                         pi_factor=1.,
                         out_skyrmion_dir=None):
        """

        Generate a skyrmionic configuration (an approximation) of radius sk_r
        at the (sk_pos[0], sk_pos[1]) position of the mesh

        core      :: Tells if skyrmion is up or down, it can be +1 or -1
        sign      :: Changes the sk chirality, it can be +1 or -1
        pi_factor :: The skyrmion field is generated by the formula:
                          (sign * np.sin(k * r) * np.cos(phi), ...)
                     where k = pi_factor * np_pi / sk_r. The factor determines
                     the number of twistings of the skyrmion profile
        out_skyrmion_dir :: Direction outside the skyrmion (by default it is
                            the direction opposite to the skyrmion core)

        """

        if sk_pos is None:
            # We assume a square sized hexagonal mesh so the centre
            # is at half of every dimension
            sk_pos = self.sim.mesh.Lx * 0.5, self.sim.mesh.Ly * 0.5

        x = (pos[0] - sk_pos[0])
        y = (pos[1] - sk_pos[1])

        if np.sqrt(x**2 + y**2) <= sk_r:
            # Polar coordinates:
            r = (x**2 + y**2)**0.5
            phi = np.arctan2(y, x)
            # This determines the profile we want for the skyrmion
            # Single twisting: k = pi / R
            k = pi_factor * np.pi / sk_r

            # We define here a 'hedgehog' skyrmion pointing down
            return (sign * np.sin(k * r) * np.cos(phi),
                    sign * np.sin(k * r) * np.sin(phi), core * np.cos(k * r))
        else:
            if not out_skyrmion_dir:
                return (0, 0, -core)
            else:
                return out_skyrmion_dir

    # For TESTing purposes
    # This uses polygon mesh tools
    #def mu_s_in_hexagon(self, pos):
    #    x, y = pos[0], pos[1]

    #    if pmt.in_poly(x=x, y=y, n=6,
    #                   r=self.sim.mesh.Lx * 0.5,
    #                   translate=(self.sim.mesh.Lx * 0.5,
    #                              self.sim.mesh.Ly * 0.5)
    #                   ):

    #        return self.mu_s
    #    else:
    #        return 0

    # TODO: Compute the True helicoid wave length ?
    def helicoid_m_field(self, pos, _lambda=3.):
        """
        Generates a helicoid along the x direction with a default
        period of 3 nm (chirality according to the DMI)
        """
        x, y = pos[0], pos[1]

        return (-np.sin(np.pi * x / _lambda), 0, -np.cos(np.pi * x / _lambda))

    def sk_helicoid_m_field(self, pos, _lambda=2):
        """
        Generates a skyrmion and a helicoid in the same sample, using
        a period of 2 for the helicoid and the skyrmion located
        at 0.3 times the mesh size along x and 0.5 times along y
        """
        x, y = pos[0], pos[1]

        if x > 0.55 * self.sim.mesh.Lx:
            return self.helicoid_m_field(pos, _lambda=_lambda)
        else:
            return self.skyrmion_m_field(pos,
                                         sign=1,
                                         sk_pos=(0.3 * self.sim.mesh.Lx,
                                                 0.5 * self.sim.mesh.Ly))

    def generate_simulation(self,
                            do_precession=False,
                            gamma=1.76e11,
                            load_mu_s=False):
        """

        Generates a simulation according to the self.simulation option

        gamma       :: Default is the free electron gyromagnetic ratio
        load_mu_s   :: A file path to a NPY file with the mu_s information
                       (only for the experimental_sample option)

        """

        # Mesh ----------------------------------------------------------------

        if self.simulation == 'experimental_sample':
            self.sim_from_image = sfi.sim_from_image(sfi.default_image)

            if not load_mu_s:
                self.sim_from_image.generate_magnetic_moments(mu_s=self.mu_s)
            else:
                self.sim_from_image.generate_magnetic_moments(
                    load_file=load_mu_s)

            self.sim = self.sim_from_image.sim

        elif self.simulation == '2D_square':
            # A square sized hexagonal mesh
            mesh = HexagonalMesh(
                self.mesh_a * 0.5,
                self.mesh_nx,
                self.mesh_ny,
                # periodicity=(True, True),
                alignment='square',
                unit_length=1e-9)
            self.sim = Sim(mesh)

            # If we use polygon mesh tools, we can use a hexagon shaped mesh
            # self.sim.mu_s = self.mu_s_in_hexagon

            self.sim.mu_s = self.mu_s

        elif self.simulation == '1D_chain':
            # A 1D chain using a cuboid mesh
            mesh = CuboidMesh(
                dx=self.mesh_a,
                nx=self.mesh_nx,
                ny=1,
                nz=1,
                # periodicity=(True, True),
                unit_length=1e-9)
            self.sim = Sim(mesh)

            # If we use polygon mesh tools, we can use a hexagon shaped mesh
            # self.sim.mu_s = self.mu_s_in_hexagon

            self.sim.mu_s = self.mu_s

        self.sim.driver.do_precession = do_precession
        self.sim.driver.gamma = gamma

        # Interactions --------------------------------------------------------

        exch = UniformExchange(self.J)
        self.sim.add(exch)

        dmi = DMI(D=(self.D), dmi_type='interfacial')
        self.sim.add(dmi)

        zeeman = Zeeman((self.B[0], self.B[1], self.B[2]))
        self.sim.add(zeeman, save_field=True)

        if self.ku:
            # Uniaxial anisotropy along + z-axis
            self.sim.add(Anisotropy(self.ku, axis=[0, 0, 1]))

        if self.Demag:
            print('Using Demag!')
            self.sim.add(DemagHexagonal())

        # ---------------------------------------------------------------------

        self.hls = np.ones_like(self.sim.spin.reshape(-1, 3))
        self.rgbs = np.ones((self.sim.spin.reshape(-1, 3).shape[0], 4))

    def update_DMI(self, D):
        self.D = D * const.meV
        self.sim.get_interaction('DMI').D = D * const.meV
        self.sim.driver.reset_integrator()

    def update_anisotropy(self, Ku):
        self.ku = Ku * const.meV
        self.sim.get_interaction(
            'Anisotropy')._Ku = fidimag.common.helper.init_scalar(
                Ku * const.meV, self.sim.mesh)
        self.sim.driver.reset_integrator()

    def pin_boundaries(self, pin_direction=(0, 0, -1), plot_component=None):
        """
        Pin the spins at the boundaries, setting the spin directions
        to *pin_direction*
        """
        boundary_spins = np.logical_and(
            np.any(self.sim.mesh.neighbours < 0, axis=1), self.sim.mu_s != 0)

        new_m = np.copy(self.sim.spin.reshape(-1, 3))
        new_m[boundary_spins] = np.array(
            [pin_direction[0], pin_direction[1], pin_direction[2]])
        self.sim.set_m(new_m.reshape(-1, ))

        # Now we pin the spins:

        ngbs_filter = np.zeros(self.sim.pins.shape[0])
        # Filter rows by checking if any of the elements is less than zero
        # This means that if any of the neighbours of the i-th lattice site is
        # -1, we pin the spin direction at that site
        ngbs_filter = np.any(self.sim.mesh.neighbours < 0,
                             axis=1,
                             out=ngbs_filter)

        self.sim.set_pins(ngbs_filter)

        # Plot the changes
        if plot_component:
            self.update_plot_component(plot_component)

    def release_boundaries(self):
        """
        Unpin the boundaries
        """
        ngbs_filter = self.sim.mu_s == 0
        self.sim.set_pins(ngbs_filter)

    def generate_m_field(self, m_function=None):
        """

        Requires that self.sim is defined
        This function generates the vector field for the magnetic moments

        If no function is provided, it automatically generates a skyrmion
        with the core pointing up, at the middle of the sample, for
        an interfacial system, where D > 0

        """
        if not m_function:
            m_function = lambda pos: self.skyrmion_m_field(pos, sign=1)

        self.sim.set_m(m_function)

    def plot_canvas(self, figsize=(8, 7)):
        """

        The base plot being updated during the simulation For the 2D
        simulations, we use a scatter plot. For the 1D chain we use a quiver
        plot showing the XZ plane. This plot is saved in the self.plot variable

        Spins are filtered to remove entities with zero magnetic moment mu_s

        """

        # An elongated figure for the 1D example
        if self.simulation == '1D_chain':
            self.fig = plt.figure(figsize=(8, 3))
        else:
            self.fig = plt.figure(figsize=figsize)

        self.ax = self.fig.add_subplot(111)

        # Assuming we have a homogeneous material, we scale the magnetic
        # moments before filtering since the magnitude is propertional to the
        # Bohr magneton, which is too small
        self._filter = self.sim.mu_s / self.mu_s > 1e-5

        if self.simulation == '1D_chain':
            self.plot = self.ax.quiver(
                # The chain is along X, Y is fixed
                self.sim.mesh.coordinates[:, 0][self._filter],  # X
                self.sim.mesh.coordinates[:, 1][self._filter],  # Y
                # Show the m_z components
                self.sim.spin.reshape(-1, 3)[:, 0][self._filter],  # m_x
                self.sim.spin.reshape(-1, 3)[:, 2][self._filter],  # m_z
                # color according to mz
                self.sim.spin.reshape(-1, 3)[:, 2][self._filter],
                cmap=mpl.cm.RdYlBu,
                pivot='mid',
                scale=1 / 0.04,
                linewidth=0.15)

            self.ax.set_xlabel(r'$x\,\, \mathrm{[nm]}$', fontsize=18)
            self.ax.set_ylabel(r'$z\,\, \mathrm{[nm]}$', fontsize=18)

            self.fig.subplots_adjust(top=0.85, bottom=0.2)

        else:
            self.plot = self.ax.scatter(
                self.sim.mesh.coordinates[:, 0][self._filter],
                self.sim.mesh.coordinates[:, 1][self._filter],
                # color according to mz
                c=self.sim.spin.reshape(-1, 3)[:, 2][self._filter],
                # Use hexagons as markers. The size will depend on the mesh
                # sizes
                s=40,
                marker='h',
                lw=0,
                cmap=mpl.cm.RdYlBu,
                vmin=-1,
                vmax=1,
            )
            self.ax.set_xlabel(r'$x\,\, \mathrm{[nm]}$', fontsize=18)
            self.ax.set_ylabel(r'$y\,\, \mathrm{[nm]}$', fontsize=18)

            self.fig.subplots_adjust(bottom=0.1)

        # Labels with simulation infor to update
        self.energy_text = self.ax.text(0,
                                        1.05,
                                        '',
                                        transform=self.ax.transAxes,
                                        va='center',
                                        ha='left')

        self.step_text = self.ax.text(
            1.,
            1.05,
            '',
            transform=self.ax.transAxes,
            # Vertical and horizontal alignment
            va='center',
            ha='right')

        self.step_text_2 = self.ax.text(
            1.,
            1.08,
            '',
            transform=self.ax.transAxes,
            # Vertical and horizontal alignment
            va='bottom',
            ha='right')

        self.title_text = self.ax.text(
            0.5,
            1.05,
            '',
            transform=self.ax.transAxes,
            # Vertical and horizontal alignment
            va='center',
            ha='center')

        # Set the ranges manually  if we use an external image to generate the
        # mesh
        if self.simulation == 'experimental_sample':
            # self.ax.imshow(self.sim_from_image.image_data,
            #                extent=self.sim_from_image.image_range)

            self.ax.set_xlim(self.sim_from_image.image_range[0],
                             self.sim_from_image.image_range[1])
            self.ax.set_ylim(self.sim_from_image.image_range[2],
                             self.sim_from_image.image_range[3])

        # Color bar -----------------------------------------------------------
        # Colour bar (append to not distort the main plot)
        divider = make_axes_locatable(self.ax)
        cax = divider.append_axes("right", size="3%", pad=0.05)

        norm = mpl.colors.Normalize(vmin=-1, vmax=1)
        self.cbar = plt.colorbar(
            self.plot,
            cax=cax,
            cmap='RdYlBu',
            norm=norm,
            ticks=[-1, 0, 1],
            orientation='vertical',
        )
        self.cbar.set_label(r'$m_i$', rotation=270, labelpad=10, fontsize=16)

        # Interactive mode (not sure if necessary)
        plt.ion()

    def generate_widgets(self):
        """

        Generate the plot and the widgets with different options for
        the simulation

        """

        self.plot_canvas()

        # Select which m component to plot ------------------------------------

        mcomp_dropdown = ipw.Dropdown(
            options=['x', 'y', 'z', 'hls'],
            # description=r'$\text{Plot}\,\,m_{i}$',
            value='z')
        mcomp_dropdown.observe(lambda b: self.static_plot_component_colorbar(
            m_component=mcomp_dropdown.value))
        mcomp_dropdown_text = ipw.Label(r'$\text{Plot}\,\,m_{i}$')
        mcomp_dropdown_text.width = '100px'

        mcomp_box = ipw.HBox(children=[mcomp_dropdown_text, mcomp_dropdown])

        # Button to start relaxation ------------------------------------------

        # We use two boxes with the maximum time and the number of steps
        run_box = ipw.FloatText()
        run_box_2 = ipw.FloatText()
        # Default values for the boxes
        run_box.value = 10
        run_box_2.value = 100
        # Change left and right margins
        run_box.layout.margin = '0px 50px'
        run_box.layout.width = '100px'
        run_box_2.layout.margin = '0px 50px'
        run_box_2.layout.width = '100px'

        # The button to start the relaxation of the system, using the values in
        # the boxes
        run_button = ipw.Button(description='Run sim!')
        run_button.layout.margin = '0px 50px'
        # The action requires a variable for the button (?). We just
        # impose it in a lambda function
        run_button.on_click(
            lambda b: self.relax(b,
                                 max_time=run_box.value * 1e-9,
                                 time_steps=run_box_2.value,
                                 m_component=mcomp_dropdown.value))
        # run_button.margin = '0px 20px'
        run_button.layout.background_color = '#9c0909'
        run_button.color = 'white'

        run_box_text = ipw.Label(r'$\text{Time [ns]}$')
        run_box_text.width = '50px'
        run_box_2_text = ipw.Label(r'$\text{Steps}$')
        run_box_2_text.width = '50px'

        # Align everything in a horizontal box
        run_container = ipw.HBox(children=[
            run_box_text, run_box, run_box_2_text, run_box_2, run_button
        ])

        # Boxes to update the Zeeman field ------------------------------------

        zeeman_box_texts = [
            ipw.Label(r'$B_x\,\,\text{[T]}$'),
            ipw.Label(r'$B_y\,\,\text{[T]}$'),
            ipw.Label(r'$B_z\,\,\text{[T]}$')
        ]
        for text in zeeman_box_texts:
            text.width = '50px'

        zeeman_box_x = ipw.FloatText()
        zeeman_box_y = ipw.FloatText()
        zeeman_box_z = ipw.FloatText()

        # Show the default value in the box
        zeeman_box_x.value = self.B[0]
        zeeman_box_x.layout.width = '50px'
        zeeman_box_x.layout.margin = '0px 50px'
        zeeman_box_y.value = self.B[1]
        zeeman_box_y.layout.width = '50px'
        zeeman_box_y.layout.margin = '0px 50px'
        zeeman_box_z.value = self.B[2]
        zeeman_box_z.layout.width = '50px'
        zeeman_box_z.layout.margin = '0px 50px'

        # Update the simulation using a button
        zeeman_button = ipw.Button(description='Update field')
        zeeman_button.on_click(lambda button: self.update_Zeeman_field((
            zeeman_box_x.value,
            zeeman_box_y.value,
            zeeman_box_z.value,
        )))

        # Draw the default Field in the plot title
        self.title_text.set_text('Field: {} T'.format(
            self.sim.get_interaction('Zeeman').B0))
        self.fig.canvas.draw()

        zeeman_container = ipw.HBox(children=[
            zeeman_box_texts[0], zeeman_box_x, zeeman_box_texts[1],
            zeeman_box_y, zeeman_box_texts[2], zeeman_box_z, zeeman_button
        ])

        # DMI magnitude box ---------------------------------------------------

        DMI_box_text = ipw.Label(r'$\text{DMI [meV]}$')
        DMI_box_text.width = '50px'

        DMI_box = ipw.FloatText()

        # Show the default value in the box
        DMI_box.value = round(self.D / const.meV, 2)
        DMI_box.layout.width = '60px'
        DMI_box.layout.margin = '0px 50px'

        # Update the simulation using a button
        DMI_button = ipw.Button(description='Update')
        DMI_button.on_click(lambda button: self.update_DMI(DMI_box.value))

        DMI_container = ipw.HBox(children=[DMI_box_text, DMI_box,
                                           DMI_button], )

        # Anisotropy -----------------------------------------------------------

        # anisotropy_box_text = ipw.Label(r'$\text{Anisotropy [meV]}$')
        # anisotropy_box_text.width = '100px'

        # anisotropy_box = ipw.FloatText()

        # # Show the default value in the box
        # anisotropy_box.value = round(self.ku / const.meV, 2)
        # anisotropy_box.layout.width = '60px'
        # anisotropy_box.layout.margin = '0px 50px'

        # # Update the simulation using a button
        # anisotropy_button = ipw.Button(description='Update')
        # anisotropy_button.on_click(
        #     lambda button: self.update_anisotropy(anisotropy_box.value)
        #     )

        # anisotropy_container = ipw.HBox(children=[anisotropy_box_text,
        #                                           anisotropy_box,
        #                                           anisotropy_button]
        #                                 )

        # Initial state -------------------------------------------------------

        # Options for the initial states. The values of the keys are the
        # initial magnetisation functions for Fidimag
        init_state_select = ipw.RadioButtons(
            options={
                'Skyrmion':
                lambda pos: self.skyrmion_m_field(pos, sign=1),
                '2-PI-Vortex':
                lambda pos: self.skyrmion_m_field(pos,
                                                  sign=1,
                                                  core=-1,
                                                  pi_factor=2,
                                                  out_skyrmion_dir=(0, 0, -1)),
                '3-PI-Vortex':
                lambda pos: self.skyrmion_m_field(
                    pos,
                    sign=1,
                    pi_factor=3,
                ),
                'Helicoid':
                lambda pos: self.helicoid_m_field(pos),
                'Sk-Helicoid':
                lambda pos: self.sk_helicoid_m_field(pos),
                'Random':
                lambda pos: np.random.uniform(-1, 1, 3),
                'Uniform': (0, 0, -1)
            },
            # description=r'$\text{Initial State}$'
        )
        init_state_select.selected_label = 'Skyrmion'

        # We need the extra variable for the buttons action
        def update_state(b):
            self.generate_m_field(m_function=init_state_select.value)
            self.update_plot_component(mcomp_dropdown.value)

        # The selection changes are taken with the observe method
        init_state_select.observe(update_state)

        init_state_select_text = ipw.Label(r'$\text{Initial State}$')
        init_state_select_text.width = '100px'

        init_state_select_box = ipw.HBox(
            children=[init_state_select_text, init_state_select])

        # Pin boundaries button -----------------------------------------------

        pin_button = ipw.Button(description='Pin boundaries')
        pin_button.on_click(lambda button: self.pin_boundaries(
            plot_component=mcomp_dropdown.value))

        unpin_button = ipw.Button(description='Unpin boundaries')
        unpin_button.on_click(lambda button: self.release_boundaries())

        pin_box = ipw.HBox(children=[pin_button, unpin_button])
        pin_box.layout.margin = '20px 0px'

        # Display -------------------------------------------------------------

        display(mcomp_box)
        display(zeeman_container)
        display(DMI_container)
        # display(anisotropy_container)
        display(init_state_select_box)
        display(pin_box)
        display(run_container)

        # return

    def relax(self,
              button,
              max_time=2e-9,
              time_steps=100,
              rtol=1e-8,
              atol=1e-10,
              m_component='z'):
        """

        Relax the system until *max_time*, which has arbitrary units
        (we need to check this in Fidimag)

        time_steps indicate the number of steps to update the plot between 0
        and max_time. We will avoid updating the plot faster than 25 FPS, but
        we have seen that the delay is mostly from the run_until function, not
        from the plot redrawing process.  Thus decreasing the number of steps
        is more effective

        Tolerances are not being used for now

        """

        times = np.linspace(0, max_time, time_steps)

        # We won't call this function more than once every 1/25 seconds
        @throttle(seconds=1 / 25.)
        def local_plot_update(time):
            self.update_plot_component(m_component=m_component)
            # Update title
            self.step_text.set_text('Time: {:.5f} [ns]'.format(time * 1e9))

            # Show maximum dm/dt which seems more useful than the time
            # Avoid dividing by zero on the first steps
            if self.sim.driver.integrator.get_current_step() > 1e-12:
                self.step_text_2.set_text(
                    'Max dm/dt: {:.5f} [deg / ns]'.format(
                        self.sim.driver.compute_dmdt(
                            self.sim.driver.integrator.get_current_step()) /
                        self.DEGREE_PER_NANOSECOND))
            # Update the energy
            self.energy_text.set_text('Energy: {:.4f} eV'.format(
                self.sim.compute_energy() / const.eV))

            # Update plot
            self.fig.canvas.draw()

        # Run the simulation --------------------------------------------------
        for time in times:

            self.sim.driver.run_until(time)

            # Update scatter or quiver plot
            local_plot_update(time)

        # Show the last update
        self.update_plot_component(m_component=m_component)

        # self.sim.save_vtk()

        self.sim.driver.reset_integrator()

    def update_Zeeman_field(self, B=(0, 0, 0)):
        self.B = B
        self.sim.get_interaction('Zeeman').update_field((B[0], B[1], B[2]))
        self.sim.driver.reset_integrator()

        # Update title on plot
        self.title_text.set_text('Field: {} T'.format(
            self.sim.get_interaction('Zeeman').B0))
        self.fig.canvas.draw()

    def convert_to_rgb(self, hls_color):
        return np.array(
            colorsys.hls_to_rgb(hls_color[0] / (2 * np.pi), hls_color[1],
                                hls_color[2]))

    def update_hls(self):
        self.hls[:, 0] = np.arctan2(
            self.sim.spin.reshape(-1, 3)[:, 1],
            self.sim.spin.reshape(-1, 3)[:, 0])
        self.hls[:, 0][self.hls[:, 0] <
                       0] = self.hls[:, 0][self.hls[:, 0] < 0] + 2 * np.pi

        self.hls[:, 1] = 0.5 * (self.sim.spin.reshape(-1, 3)[:, 2] + 1)
        # self.hls[:, 2] = 0.5 * (self.sim.spin.reshape(-1, 3)[:, 2] + 1)

        self.rgbs = np.apply_along_axis(self.convert_to_rgb, 1, self.hls)
        # Some RGB values can get very small magnitudes, like 1e-10:
        self.rgbs[self.rgbs < 0] = 0

    def update_plot_component(self, m_component='z'):
        """
        Update the vector data for the plot (the spins do not move
        so we don't need to update the coordinates) and redraw
        """
        m = self.sim.spin.reshape(-1, 3)
        if self.simulation == '1D_chain':
            self.plot.set_UVC(m[:, 0], m[:, 2], m[:, 2])
        else:
            if m_component == 'hls':
                self.update_hls()
                # self.plot.set_array(self.hls[:, 0][self._filter])
                self.plot.set_color(self.rgbs[self._filter])
            else:
                self.plot.set_array(m[:,
                                      self.v_dict[m_component]][self._filter])
        self.fig.canvas.draw()

    def static_plot_component_colorbar(self, m_component='z'):
        if m_component == 'hls':
            self.plot.set_cmap('hsv')
            self.cbar.set_ticklabels([r'$0$', '', r'$2\pi$'])
            self.fig.canvas.draw()
        else:
            self.cbar.set_ticklabels(['-1', '0', '1'])
            self.plot.set_cmap('RdYlBu')
            self.fig.canvas.draw()

        self.update_plot_component(m_component)

    def relax_sim(self):
        self.sim.driver.do_precession = False
        self.sim.driver.relax(dt=1e-13)
def test_skx_num_atomistic_hexagonal():
    """

    Test the topological charge or skyrmion number for a discrete spins
    simulation in a two dimensional hexagonal lattice, using Berg and Luscher
    definition in [Nucl Phys B 190, 412 (1981)] and simplified in [PRB 93,
    174403 (2016)], which maps a triangulated lattice (using triangles of
    neighbouring spins) area into a unit sphere.

    The areas of two triangles per lattice site cover a unit cell, thus the sum
    cover the whole area of the atomic lattice

    This test generates a skyrmion pointing down and two skyrmions pointing up
    in a PdFe sample using magnetic parameters from: PRL 114, 177203 (2015)

    """

    mesh = HexagonalMesh(0.2715, 41, 41, periodicity=(True, True))

    sim = Sim(mesh, name='skx_number_hexagonal')
    sim.driver.set_tols(rtol=1e-6, atol=1e-6)
    sim.driver.alpha = 1.0
    sim.driver.gamma = 1.0
    sim.mu_s = 3 * const.mu_B

    sim.set_m(lambda pos: init_m(pos, 16.1, 10, 2))

    sim.driver.do_precession = False

    J = 5.881 * const.meV
    exch = UniformExchange(J)
    sim.add(exch)

    D = 1.557 * const.meV
    dmi = DMI(D, dmi_type='interfacial')
    sim.add(dmi)

    sim.add(Anisotropy(0.406 * const.meV, axis=[0, 0, 1]))

    zeeman = Zeeman([0, 0, 2.5])
    sim.add(zeeman)

    sim.relax(dt=1e-13, stopping_dmdt=1e-2, max_steps=2000,
              save_m_steps=None, save_vtk_steps=100)

    skn_single = sim.skyrmion_number(method='BergLuscher')
    print('skx_number_hexagonal', skn_single)

    # Now we generate two skyrmions pointing up
    sim.driver.reset_integrator()
    sim.set_m(lambda pos: init_m_multiple_sks(pos, 1,
                                              sk_pos=[(9, 6), (18, 12)]
                                              )
              )
    sim.get_interaction('Zeeman').update_field([0, 0, -2.5])
    sim.relax(dt=1e-13, stopping_dmdt=1e-2, max_steps=2000,
              save_m_steps=None, save_vtk_steps=None)

    skn_two = sim.skyrmion_number(method='BergLuscher')
    print('skx_number_hexagonal_two', skn_two)

    # Check that we get a right sk number
    assert np.abs(skn_single - (-1)) < 1e-4 and np.sign(skn_single) < 0
    assert np.abs(skn_two - (2)) < 1e-4 and np.sign(skn_two) > 0
Beispiel #12
0
def hysteresis_loop(config_file,
                    D=1.56, J=5.88, k_u=0.41, mu_s=3, B=(0, 0, 0), Demag=None,
                    ):
    """
    The config file must have the following parameters:
        D
        J
        k_u
        mu_s                :: Magnitude in Bohr magneton units. A file path
                               can be specified to load a NPY file with the
                               mu_s values, when using the mesh_from_image
                               option
        Demag               :: Set to True for Demag
        sim_name            :: Simulation name
        initial_state       :: A function or a npy file
        hysteresis_steps    ::
        mesh_from_image     :: [IMAGE_PATH, xmin, xmax, ymin, ymax]
        hexagonal_mesh      :: [nx, ny, a]
        PBC_2D              :: Set to True for Periodic boundaries
        pin_boundaries      :: Set to True to pin the spins at the boundaries.
                               Their orientations are already given from the
                               initial_state NPY file
        llg_dt              ::
        llg_stopping_dmdt   ::
        llg_max_steps       ::
        llg_do_precession   :: False as default
        llg_alpha           :: 0.01 as default

    """

    # Parameters --------------------------------------------------------------

    cf = {}
    # execfile(config_file, cf)
    # Python3:
    exec(open(config_file).read(), cf)

    D = cf["D"] * const.meV
    J = cf["J"] * const.meV
    k_u = cf["k_u"] * const.meV

    if isinstance(cf["mu_s"], int) or isinstance(cf["mu_s"], float):
        mu_s = cf["mu_s"] * const.mu_B

    if isinstance(cf["initial_state"], str):
        init_state = np.load(cf["initial_state"])
    elif isinstance(cf["initial_state"], types.FunctionType):
        init_state = cf["initial_state"]

    # Set up default arguments
    default_args = {"mesh_alignment": 'diagonal',
                    "mesh_unit_length": 1e-9,
                    "llg_dt": 1e-11,
                    "llg_stopping_dmdt": 1e-2,
                    "llg_max_steps": 4000,
                    "llg_do_precession": False,
                    "llg_alpha": 0.01
                    }

    for key in default_args.keys():
        if not cf.get(key):
            print(default_args[key])
            cf[key] = default_args[key]

    # Simulation object -------------------------------------------------------

    if cf.get("hexagonal_mesh"):
        if not cf["PBC_2D"]:
            mesh = HexagonalMesh(cf["hexagonal_mesh"][2] * 0.5,
                                 int(cf["hexagonal_mesh"][0]),
                                 int(cf["hexagonal_mesh"][1]),
                                 alignment=cf["mesh_alignment"],
                                 unit_length=cf["mesh_unit_length"]
                                 )

        else:
            mesh = HexagonalMesh(cf["hexagonal_mesh"][2] * 0.5,
                                 int(cf["hexagonal_mesh"][0]),
                                 int(cf["hexagonal_mesh"][1]),
                                 periodicity=(True, True),
                                 alignment=cf["mesh_alignment"],
                                 unit_length=cf["mesh_unit_length"]
                                 )

        sim = Sim(mesh, name=cf["sim_name"])

    elif cf.get("mesh_from_image"):
        sim_from_image = sfi.sim_from_image(
            cf["mesh_from_image"][0],
            image_range=[float(cf["mesh_from_image"][1]),
                         float(cf["mesh_from_image"][2]),
                         float(cf["mesh_from_image"][3]),
                         float(cf["mesh_from_image"][4])
                         ],
            sim_name=cf["sim_name"]
            )

        if isinstance(cf["mu_s"], str):
            sim_from_image.generate_magnetic_moments(load_file=cf["mu_s"])
        else:
            sim_from_image.generate_magnetic_moments(mu_s=(mu_s))

        sim = sim_from_image.sim

    elif cf.get("truncated_triangle"):
        if len(cf["truncated_triangle"]) == 3:
            sim_triangle = TruncatedTriangleSim(
                cf["truncated_triangle"][0],  # L
                cf["truncated_triangle"][1],  # offset
                cf["truncated_triangle"][2],  # a
                cf["mu_s"],                   # mu_s
                name=cf["sim_name"]
                )
        elif len(cf["truncated_triangle"]) == 5:
            sim_triangle = TruncatedTriangleSim(
                cf["truncated_triangle"][0],    # L
                [float(offs) for offs in cf["truncated_triangle"][1:4]],  # offsets
                cf["truncated_triangle"][4],    # a
                cf["mu_s"],                     # mu_s
                name=cf["sim_name"]
                )

        sim = sim_triangle.sim

    elif cf.get("hexagon"):
        sim_hexagon = HexagonSim(cf["hexagon"][0],    # R
                                 cf["hexagon"][1],    # a
                                 cf["mu_s"],          # mu_s
                                 name=cf["sim_name"]
                                 )
        sim = sim_hexagon.sim

    # Initial state
    sim.set_m(init_state)

    sim.driver.do_precession = cf["llg_do_precession"]
    sim.driver.alpha = cf["llg_alpha"]

    # Material parameters -----------------------------------------------------

    if cf.get("hexagonal_mesh"):
        sim.mu_s = mu_s

    exch = UniformExchange(J)
    sim.add(exch)

    dmi = DMI(D=(D), dmi_type='interfacial')
    sim.add(dmi)

    zeeman = Zeeman((0, 0, 0))
    sim.add(zeeman, save_field=True)

    if k_u:
        # Uniaxial anisotropy along + z-axis
        sim.add(Anisotropy(k_u, axis=[0, 0, 1]))

    if cf.get("Demag"):
        print('Using Demag!')
        sim.add(DemagHexagonal())

    # Pin boundaries ----------------------------------------------------------

    # We will correct the spin directions according to the specified argument,
    # in case the spins at the boundaries are pinned
    if cf.get('pin_boundaries'):
        ngbs_filter = np.zeros(sim.pins.shape[0])
        # Filter rows by checking if any of the elements is less than zero
        # This means that if any of the neighbours of the i-th lattice site is
        # -1, we pin the spin direction at that site
        ngbs_filter = np.any(sim.mesh.neighbours < 0, axis=1, out=ngbs_filter)

        sim.set_pins(ngbs_filter)

    # Hysteresis --------------------------------------------------------------

    for ext in ['npys', 'vtks']:
        if not os.path.exists('{}/{}'.format(ext, cf["sim_name"])):
            os.makedirs('{}/{}'.format(ext, cf["sim_name"]))

    for ext in ['txts', 'dats']:
        if not os.path.exists('{}/'.format(ext)):
            os.makedirs('{}'.format(ext))

    Brange = cf["hysteresis_steps"]
    print('Computing for Fields:', Brange)

    # We will save the hysteresis steps on this file with every row as:
    # step_number field_in_Tesla
    hystfile = '{}_hyst_steps.dat'.format(cf["sim_name"])
    # If the file already exists, we will append the new steps, otherwise
    # we just create a new file (useful for restarting simulations)
    if not os.path.exists(hystfile):
        nsteps = 0
        fstate = 'w'
    else:
        # Move old txt file from the previous simulation, appending an _r
        # everytime a simulation with the same name is started
        txtfile = [f for f in os.listdir('.') if f.endswith('txt')][0]
        txtfile = re.search(r'.*(?=\.txt)', txtfile).group(0)
        shutil.move(txtfile + '.txt', txtfile + '_r.txt')

        nsteps = len(np.loadtxt(hystfile))
        fstate = 'a'

    f = open(hystfile, fstate)

    for i, B in enumerate(Brange):
        sim.get_interaction('Zeeman').update_field(B)

        sim.driver.relax(dt=cf["llg_dt"],
                         stopping_dmdt=cf["llg_stopping_dmdt"],
                         max_steps=cf["llg_max_steps"],
                         save_m_steps=None,
                         save_vtk_steps=None
                         )

        print('Saving NPY for B = {}'.format(B))
        np.save('npys/{0}/step_{1}.npy'.format(cf["sim_name"], i + nsteps),
                sim.spin)

        sim.driver.save_vtk()
        shutil.move('{}_vtks/m_{}.vtk'.format(cf["sim_name"],
                                              str(sim.driver.step).zfill(6)
                                              ),
                    'vtks/{0}/step_{1}.vtk'.format(cf["sim_name"], i + nsteps)
                    )

        f.write('{} {} {} {}\n'.format(i + nsteps,
                                       B[0], B[1], B[2],
                                       )
                )
        f.flush()
        sim.driver.reset_integrator()

    os.rmdir('{}_vtks'.format(cf["sim_name"]))
    shutil.move('{}.txt'.format(cf["sim_name"]), 'txts/')
    shutil.move(hystfile, 'dats/')

    f.close()