LLG Example - toy mode with Skyrmions¶

This notebook contains the data generation and analysis leading to Figure 2 of the AiiDA-Spirit paper.

Prerequisites¶

In order to use spirit with AiiDA-Spirit make sure that a Code for Spirit's python API is available. Here our Code is a local python installation which has the spirit module installed: pip3 install numpy spirit

$ verdi code show spirit_pypi@localhost
--------------------  ---------------------------------------------
PK                    29784
UUID                  2d7ecbb0-3f68-430e-95a6-920b674e33a7
Label                 spirit_pypi
Description           Spirit python API without pinning and defects
Default plugin        spirit
Type                  remote
Remote machine        localhost
Remote absolute path  /usr/bin/python3
Prepend text
Append text
--------------------  ---------------------------------------------
In [ ]:
from aiida import load_profile
load_profile()

from util1 import *

Set up the toy model¶

Prepare parameters

In [ ]:
parameters = orm.Dict(dict={
        # description of the supercell
        'n_basis_cells': [50, 50, 1],
        # periodicity of the unit cell
        'boundary_conditions': [True, True, False],
        ### External magnetic field vector[T]
        'external_field_magnitude': 25.0,
        'external_field_normal': [0.0, 0.0, 1.0],
        ### spin moment 
        'mu_s': [2.0],
        # limit the number of iterations
        'llg_n_iterations': 100000
    })

Choose run options

In [ ]:
run_opt = orm.Dict(
    dict={
        'simulation_method': 'llg',
        'solver': 'vp',
        'post_processing': """
# post processing: calculate topological charge
from spirit import quantities
topo_charge = quantities.get_topological_charge(p_state)
with open('topological_charge.txt', 'w') as _f:
    _f.write(f'{topo_charge}\\n')
"""
    })

Prepare Structure

In [ ]:
# simple cubic lattice
structure = orm.StructureData(
    cell=[[1.0, 0.0, 0.0],
          [0.0, 1.0, 0.0],
          [0.0, 0.0, 1.0]])
structure.append_atom(position=[0.0, 0.0, 0.0], symbols='Fe')

Prepare Jij/Dij parameters

In [ ]:
jijs_expanded = np.array([
    #i  j da db dc  Jij   Dx   Dy   Dz
    [0, 0, 1, 0, 0, 10.0, 6.0, 0.0, 0.0],
    [0, 0, 0, 1, 0, 10.0, 0.0, 6.0, 0.0],
    [0, 0, 0, 0, 1, 10.0, 0.0, 0.0, 6.0],
])
jij_data = orm.ArrayData()
jij_data.set_array('Jij_expanded', jijs_expanded)

Run the SpiritCalculation

In [ ]:
inputs = {
    'code': orm.Code.get_from_string('spirit_pypi@localhost'),
    # 'code': aiida.orm.Code.get_from_string('code-name@computer'),
    'structure': structure,
    'parameters': parameters,
    'run_options': run_opt,
    'jij_data': jij_data,
    # retrieved the file created in the post-processing step
    'add_to_retrieved': orm.List(list=['topological_charge.txt']),
}

# spirit_calc = engine.submit(plugins.CalculationFactory('spirit'), **inputs)
spirit_calc = orm.load_node('7aee0dec-0d3e-4ab7-b633-c2c06da0f5c4')
In [ ]:
# Read extracted topological charge
print('Topological charge:', get_topo_charge(spirit_calc))

Show the spins¶

In [ ]:
from aiida_spirit.tools.plotting import init_spinview, show_spins
init_spinview(height_px=800, width_percent=100)

Attention: Wait for the init_spinview to initialize, only then spins can be shown.

In [ ]:
show_spins(spirit_calc, show_final_structure=False)
In [ ]:
show_spins(spirit_calc, show_final_structure=True)

Screen SpiritCalculations for different input parameters¶

For varying temperature and external B-field we perform SpiritCalculations and measure the resulting topological charge.

In [ ]:
# collect calculations in the group "spirit_Skx_model"

# orm.Group(label='spirit_Skx_model').store()
calculation_group = orm.load_group(label='spirit_Skx_model') 
In [ ]:
# helper functions for setting up the calculations

def get_parameters(Bz, tempr, llg_seed=20006):
    """get parameters input node"""
    parameters = orm.Dict(dict={
            # description of the supercell
            'n_basis_cells': [50, 50, 1],
            # periodicity of the unit cell
            'boundary_conditions': [True, True, False],
            ### External magnetic field vector[T]
            'external_field_magnitude': Bz,
            'external_field_normal': [0.0, 0.0, 1.0],
            # temperature noise [K]
            'llg_temperature': temp,
            ### spin moment 
            'mu_s': [2.0],
            # limit the number of iterations
            'llg_n_iterations': 100000,
            # seed for random number generator
            'llg_seed': llg_seed,
        })
    return parameters

def get_runopt(iaver = 0):
    """ make run options"""
    run_opt = orm.Dict(
        dict={
            'simulation_method': 'llg',
            'configuration': {'random': iaver+1},
            # Depondt solver to be able to have temperature in the LLG
            'solver': 'depondt',
        'post_processing': """
# post processing: calculate topological charge
from spirit import quantities
topo_charge = quantities.get_topological_charge(p_state)
with open('topological_charge.txt', 'w') as _f:
    _f.write(f'{topo_charge}\\n')
"""
        }
    )
    return run_opt


def get_builder(Bz, temp, iaver):
    """create builder for SpiritCalculation"""

    # update the parameters input node
    parameters = get_parameters(Bz, temp)
    
    # set run options
    run_opt = get_runopt(iaver)
    
    # set of inputs
    inputs = {
        'code': orm.Code.get_from_string('spirit@iffslurm'),
        'structure': structure,
        'jij_data': jij_data,
        'run_options': run_opt,
        'parameters': parameters,
        # retrieved the file created in the post-processing step
        'add_to_retrieved': orm.List(list=['topological_charge.txt']),
        # job settings on cluster
        'metadata': {'options': {
                        # Spirit does not have MPI but by default used OpenMP if that is available
                        'withmpi': False,
                        'resources': {'num_machines': 1, 'tot_num_mpiprocs': 1},
                        # use oscar partition
                        'queue_name': 'th1',
                        # max runtime of the spirit job (here: 10h)
                        'max_wallclock_seconds': 10*3600
                                },
                     'label': label, 
                    }
    }
    
    # create the process builder
    builder = plugins.CalculationFactory('spirit').get_builder()
    for k,v in inputs.items():
        builder[k] = v
        
    return builder

Submit calculations¶

In [ ]:
group_labels = [i.label for i in calculation_group.nodes]
all_calcs = [[temp, Bz] for temp in np.linspace(0, 75, 31) # np.linspace(0, 50, 21)
                        for Bz in np.linspace(-50, 50, 41)]
submission_counter_tot = 0

# out loop for averaging runs
for iaver in range(5):
    builders = []
    submission_counter = 0
    for temp, Bz in tqdm(all_calcs):
        label = f'T={temp}_Bz={Bz}'
        if iaver>0:
            # add iaver index>0 to label
            label += f'_iaver={iaver}'
        if label not in group_labels:
            builders.append(get_builder(Bz, temp, iaver))
            submission_counter += 1
    print('submitting calculations:', submission_counter, f'for iaver={iaver}')
    submission_counter_tot += submission_counter

    # now submit is any jobs are missing
    if len(builders)>0:
        submit_batch_iffslurm_all_free(builders, dry_run=False, calculations_group=calculation_group, blocking_submit=True,
                                       check_free_nodes = False, allowed_queues=['th1']
                                      )

print('total number of submitted calculations:', submission_counter_tot)

Collect results¶

In [ ]:
def get_iaver(spirit_calc):
    """get the iaver index"""
    if 'iaver' in spirit_calc.label:
        return int(spirit_calc.label.split('=')[-1])
    else:
        return 0

data = []
for i in tqdm(calculation_group.nodes):
    # if 'aver' not in i.label and i.is_finished_ok:
    if i.is_finished_ok:
        tempr, bfield = get_input_para(i)
        topo_charge = get_topo_charge(i)
        data.append([tempr, bfield, get_iaver(i), topo_charge])
data = np.array(data)
isort = (data[:,2]+1000*data[:,1]+1000000*data[:,0]).argsort()
data = data[isort]

Figure 2a¶

In [ ]:
ts = np.sort(list(set(data[:,0])))
bs = np.sort(list(set(data[:,1])))
av = np.sort(list(set(data[:,2])))
nT, nB, nav = len(ts), len(bs), len(av)

plt.figure(figsize=(9,5))
tc = data[:,-1].reshape((nT, nB, nav)).mean(axis=2)
plt.pcolormesh(bs, ts, tc, shading='nearest', cmap='seismic', )#vmin=0) #vmax=0)
plt.colorbar()
cl = plt.gci().get_clim(); cl = max(abs(cl[0]), cl[1]); plt.clim(-cl,cl)
plt.ylabel('Temperature (K)')
plt.xlabel('B field (T)')


ms = 12
ms2 = 14

plt.plot(-40, 30, 'v', color='w', ms=ms, )
plt.plot(-40, 30, 'v', color='k', ms=ms, label='(b)', fillstyle='none')
plt.plot(-20, 10, '<', color='w', ms=ms, )
plt.plot(-20, 10, '<', color='k', ms=ms, label='(c)', fillstyle='none')
plt.plot(0, 0, '*', color='w', ms=ms2,)
plt.plot(0, 0, '*', color='k', ms=ms2, label='(d)', fillstyle='none')
plt.plot(20, 0, 'o', color='w', ms=ms, )
plt.plot(20, 0, 'o', color='k', ms=ms, label='(e)', fillstyle='none')
plt.plot(30, 30, '^', color='w', ms=ms, )
plt.plot(30, 30, '^', color='k', ms=ms, label='(f)', fillstyle='none')
plt.plot(40, 20, 'd', color='w', ms=ms, )
plt.plot(40, 20, 'd', color='k', ms=ms, label='(g)', fillstyle='none')

plt.legend(fontsize='x-large')
plt.xlim(-50.5,50.5)
plt.ylim(-1.2,75.5)
plt.tight_layout()

plt.show()

Figure2 b-g: Spin structure images¶

In [ ]:
def get_calc(tempr, bfield, iaver=None):
    """get the calculation from the calculation group by label"""
    label = f'T={tempr:.1f}_Bz={bfield:.1f}'
    if iaver is not None:
        label+= f'_iaver={iaver}'
    nodes = [i for i in calculation_group.nodes if i.label==label]
    if len(nodes)==1:
        return nodes[0]
    elif len(nodes)==0:
        raise ValueError('No node found!')
    else:
        raise ValueError('Label not unique!')
In [ ]:
calc = get_calc(0,0)
print(calc.label, ':', get_topo_charge(calc))
calc = get_calc(0,20)
print(calc.label, ':', get_topo_charge(calc))
calc = get_calc(20,40)
print(calc.label, ':', get_topo_charge(calc))
calc = get_calc(30,30)
print(calc.label, ':', get_topo_charge(calc))
calc = get_calc(30,-40)
print(calc.label, ':', get_topo_charge(calc))
calc = get_calc(70,40)
print(calc.label, ':', get_topo_charge(calc))
In [ ]:
from aiida_spirit.tools.plotting import init_spinview, show_spins
init_spinview(vfr_frame_id='screen', height_px=800)
In [ ]:
calc = get_calc(70,40)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(10,-20)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(30,-40)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(0,0)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(0,20)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(20,40)
show_spins(calc, vfr_frame_id='screen')
In [ ]:
calc = get_calc(30,30)
show_spins(calc, vfr_frame_id='screen')
In [ ]: