Skip to content

Commit b9deaee

Browse files
committed
Adding support for VASP
For now only an example with the ACWF, not yet the 4-state workflow. Note: it needs aiidateam/aiida-common-workflows#350
1 parent 98d5939 commit b9deaee

File tree

4 files changed

+324
-0
lines changed

4 files changed

+324
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env runaiida
2+
"""Launch script for the MagneticExchangeWorkChain."""
3+
import copy
4+
import numpy as np
5+
6+
from aiida import orm
7+
from aiida.engine import submit, calcfunction
8+
from aiida_common_workflows.common import ElectronicType
9+
import ase.io
10+
from ase.geometry import find_mic
11+
12+
from aiida_fourstate import MagneticExchangeWorkChain
13+
14+
GROUP_NAME = 'FourStateProtocol-convergence/CrI3/LDA/abinit/1x1x1cell'
15+
16+
## THOR
17+
code_label = 'abinit-10.0.3@thor-micromamba'
18+
num_machines = 1
19+
num_mpiprocs_per_machine = 48
20+
21+
22+
protocol = 'custom'
23+
24+
kpoints = [12, 12, 1] # For a unit cell, I do a denser k-point mesh
25+
# Will be stored later
26+
tsmear = 7.34986e-05 / 2 # smearing: 1 meV (written in Hartree)
27+
28+
# LDA four-state v0 protocol for Abinit (with coarser k-points)
29+
custom_protocol_BASE = {
30+
'base': {'abinit': {'parameters': {
31+
'autoparal': 1,
32+
'chkprim': 0,
33+
'chksymbreak': 0,
34+
'dilatmx': 1.0,
35+
'ecutsm': 0.0,
36+
'fband': 2.0,
37+
'ionmov': 22,
38+
'nstep': 300,
39+
'occopt': 3,
40+
'optcell': 0,
41+
'shiftk': [[0.0, 0.0, 0.0]],
42+
'tolvrs': 1e-10,
43+
#'npfft': 1, # no MPI-FFT, sometimes it complains
44+
#'iscf': 2, # Slower but safer, in 2x2x1 wasn't converging
45+
'tsmear': tsmear}},
46+
'kpoints': kpoints},
47+
'cutoff_stringency': 'normal',
48+
'description': 'Protocol for the 4-state verification with LDA functional and PseudoDojo pseudopotentials.',
49+
'name': 'fourstate-LDA-v1',
50+
'pseudo_family': 'PseudoDojo/0.4/LDA/SR/standard/psp8'}
51+
52+
53+
# To decide if we want to track also provenance here with a calcfunction
54+
@calcfunction
55+
def create_supercell(structure, supercell_matrix):
56+
"""Create a supercell from the input structure.
57+
58+
:param structure: AiiDA StructureData
59+
:param supercell_matrix: List or tuple of 3 integers [na, nb, nc]
60+
:return: AiiDA StructureData for the supercell
61+
"""
62+
ase_structure = structure.get_ase()
63+
supercell_ase = ase_structure * supercell_matrix
64+
65+
return orm.StructureData(ase=supercell_ase)
66+
67+
def group_by_distance(dist_list, threshold=0.1):
68+
"""
69+
dist_list: list of tuples (idx, symbol, distance)
70+
threshold: maximum spread within one group (angstrom)
71+
72+
Returns:
73+
[
74+
{
75+
'average_dist': float,
76+
'items': [(idx, distance), ...] # sorted by idx
77+
},
78+
...
79+
]
80+
]
81+
"""
82+
if not dist_list:
83+
return []
84+
85+
# Sort items by distance first
86+
dist_list = sorted(dist_list, key=lambda x: x[1])
87+
88+
groups = []
89+
current_group = [dist_list[0]]
90+
91+
for item in dist_list[1:]:
92+
if abs(item[1] - current_group[0][1]) <= threshold:
93+
# same group
94+
current_group.append(item)
95+
else:
96+
# close previous group
97+
groups.append(current_group)
98+
current_group = [item]
99+
100+
# append final group
101+
groups.append(current_group)
102+
103+
# Convert groups to list of dictionaries
104+
result = []
105+
for g in groups:
106+
avg = sum(x[1] for x in g) / len(g)
107+
g_sorted = sorted(g, key=lambda x: x[0]) # sort by idx
108+
result.append({
109+
'average_dist': avg,
110+
'items': g_sorted
111+
})
112+
113+
# Sort outer list by average distance
114+
result = sorted(result, key=lambda x: x['average_dist'])
115+
116+
return result
117+
118+
def find_neighbor(site1, neigh_idx, filter_element, atoms):
119+
positions = atoms.get_positions()
120+
cell = atoms.get_cell()
121+
pbc = atoms.get_pbc()
122+
123+
r0 = positions[site1]
124+
125+
dist_list = []
126+
for i, r in enumerate(positions):
127+
if i == site1:
128+
continue
129+
130+
if atoms[i].symbol != filter_element:
131+
continue
132+
133+
# Minimum-image displacement vector
134+
dr = r - r0
135+
dr_mic = find_mic([dr], cell, pbc)[0]
136+
137+
# Distance
138+
dist = np.linalg.norm(dr_mic)
139+
140+
dist_list.append((i, dist))
141+
142+
# Sort by distance, then by index
143+
dist_list = sorted(dist_list, key=lambda x: (x[1], x[0]))
144+
dist_grouped_by_distance = group_by_distance(dist_list, threshold=0.1)
145+
146+
assert neigh_idx >= 1
147+
assert neigh_idx <= len(dist_grouped_by_distance)
148+
group = dist_grouped_by_distance[neigh_idx-1]
149+
return group['items'][0]
150+
151+
152+
def launch_magnetic_exchange_calculation(neigh_idx = 1, cutoff_wfc=None, ask_for_confirmation=True):
153+
"""Launch a magnetic exchange coupling calculation.
154+
155+
:param neigh_idx: Index of the neighbor to consider (1 means first neighbor)
156+
:param cutoff_wfc: Kinetic energy cutoff for wavefunctions (in Ry). If None, use default from protocol.
157+
:param ask_for_confirmation: If True, ask for user confirmation before submitting.
158+
"""
159+
global code_label, num_mpiprocs_per_machine, kpt_node, custom_protocol
160+
161+
CUTOFF_DUAL = 4. # Hardcoded for now since I know I'm using norm-conserving pseudopotentials
162+
163+
supercell_matrix = [1, 1, 1] ## For convergence I do the 1x1x1 cell
164+
site1 = 0 # Cr
165+
166+
ase_atoms = ase.io.read('../cri3_primitive.cif')
167+
structure_unitcell = orm.StructureData(ase=ase_atoms)
168+
169+
if supercell_matrix == [1, 1, 1]:
170+
structure_supercell = structure_unitcell
171+
else:
172+
# Create the supercell structure
173+
structure_unitcell.store()
174+
structure_supercell = create_supercell(structure_unitcell, supercell_matrix)
175+
176+
print(f"Unit cell has {len(structure_unitcell.sites)} atoms")
177+
print(f"Supercell has {len(structure_supercell.sites)} atoms")
178+
179+
site2, dist = find_neighbor(site1 = site1, neigh_idx=neigh_idx, filter_element="Cr", atoms=structure_supercell.get_ase())
180+
181+
magnetization_per_site = [3. if structure_supercell.get_kind(kind_name).symbol == "Cr" else 0. for kind_name in structure_supercell.get_site_kindnames()]
182+
magnetization_magnitude = 3.0 # For the two sites to flip
183+
184+
######## ADAPT PROTOCOL WITH CUTOFFS, IF ASKED ########
185+
custom_protocol = copy.deepcopy(custom_protocol_BASE)
186+
187+
if cutoff_wfc is not None:
188+
custom_protocol['base']['abinit']['parameters']['ecut'] = cutoff_wfc / 2.
189+
#######################################################
190+
191+
# Generator inputs for the common workflows
192+
generator_inputs = {
193+
'engines': {
194+
'relax': {
195+
'code': code_label,
196+
'options': {
197+
'resources': {
198+
'num_machines': num_machines,
199+
'num_mpiprocs_per_machine': num_mpiprocs_per_machine,
200+
},
201+
'max_wallclock_seconds': 12*3600, # 12 hours
202+
},
203+
}
204+
},
205+
'protocol': protocol,
206+
'custom_protocol': custom_protocol if protocol == 'custom' else None,
207+
'electronic_type': ElectronicType.METAL.value,
208+
'magnetization_per_site': magnetization_per_site
209+
}
210+
211+
print(f"\nMagnetic sites in supercell:")
212+
print(f'{site1=}, {site2=}')
213+
atom1 = structure_supercell.get_ase()[site1]
214+
atom2 = structure_supercell.get_ase()[site2]
215+
print(f" Site 1: {atom1.symbol}, index {site1}")
216+
print(f"Looking for {neigh_idx}-th neighbor of site {site1}")
217+
print(f" Site 2: {atom2.symbol}, index {site2}")
218+
vector = atom2.position - atom1.position
219+
print(f" Distance between selected sites: {np.linalg.norm(vector):.2f} ang\n")
220+
221+
inputs = {
222+
'structure': structure_supercell,
223+
'site1': orm.Int(site1),
224+
'site2': orm.Int(site2),
225+
'magnetization_magnitude': orm.Float(magnetization_magnitude),
226+
'generator_inputs': generator_inputs,
227+
'engine_name': orm.Str('abinit'),
228+
}
229+
230+
if ask_for_confirmation:
231+
print(f"Submitting J({neigh_idx}) for CrI3 to {code_label} on {num_machines} node(s) with {num_mpiprocs_per_machine=}.")
232+
print("Submit? [Ctrl+C to stop]")
233+
input()
234+
235+
print("Submitting (ABINIT) MagneticExchangeWorkChain...")
236+
node = submit(MagneticExchangeWorkChain, **inputs)
237+
print(f"\nSubmitted: J({neigh_idx}), PK = {node.pk}")
238+
node.label = f"CrI3 J({neigh_idx}) calculation"
239+
node.base.extras.set('magnetic_exchange_neigh_idx', neigh_idx)
240+
node.base.extras.set('cutoff_wfc', cutoff_wfc)
241+
242+
group, created = orm.Group.collection.get_or_create(GROUP_NAME)
243+
group.add_nodes([node])
244+
245+
return node
246+
247+
248+
if __name__ == '__main__':
249+
for neigh_idx in [1]:
250+
# Cutoff from PseudoDojo: 94 Ry for Cr
251+
for cutoff_wfc in range(50, 111, 10): # from 50 to 110 Ry, step 10 Ry
252+
print(f"\n\nLaunching QE calculation of J({neigh_idx})*S^2 (ecutwfc={cutoff_wfc})\n")
253+
launch_magnetic_exchange_calculation(neigh_idx=neigh_idx, cutoff_wfc=cutoff_wfc, ask_for_confirmation=False)
254+
print()
255+
print(f"\nAll calculations submitted and added to group {GROUP_NAME}.")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env runaiida
2+
from ase.build import bulk
3+
4+
from aiida.orm import Code, StructureData
5+
from aiida.engine import submit
6+
from aiida.plugins import WorkflowFactory
7+
8+
RelaxWorkChain = WorkflowFactory('common_workflows.relax.vasp')
9+
10+
ase_structure = bulk("Si", "diamond", a=5.45) # Slightly off lattice param
11+
structure = StructureData(ase=ase_structure)
12+
13+
code_label = '[email protected]'
14+
15+
16+
custom_fast = {'name': 'custom-fast',
17+
'description': 'Protocol to relax a structure with low precision at minimal computational cost for testing purposes.',
18+
'potential_family': 'PBE.64',
19+
'potential_mapping': 'RECOMMENDED_PBE',
20+
'kpoint_distance': 0.25,
21+
'relax': {'algo': 'cg', 'threshold_forces': 0.1, 'steps': 200},
22+
'parameters': {'prec': 'Single',
23+
'ediff': 0.0001,
24+
'ismear': 0,
25+
'sigma': 0.2,
26+
'algo': 'Veryfast',
27+
'nelm': 2000}}
28+
29+
input_generator = RelaxWorkChain.get_input_generator()
30+
builder = input_generator.get_builder(engines={
31+
'relax': {
32+
'code': code_label,
33+
'options': {
34+
'resources': {
35+
'num_machines': 1,
36+
},
37+
'max_wallclock_seconds': 900,
38+
}
39+
}
40+
},
41+
structure=structure,
42+
protocol="custom",
43+
custom_protocol=custom_fast,
44+
relax_type="positions_cell", # full relaxation
45+
spin_type="none", # Si is non-magnetic
46+
)
47+
48+
node = submit(builder)
49+
print(f"Submitted CommonRelaxWorkChain<{node.pk}>")
50+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use the same YAML for computer setup and config as those you find in the Quantum ESPRESSO subfolder for eiger.alps
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
append_text: ''
2+
computer: eiger.alps
3+
default_calc_job_plugin: vasp.vasp
4+
description: VASP v6.5.0 (STD) on Eiger at CSCS
5+
filepath_executable: /user-environment/env/vasp/bin/vasp_std
6+
label: vasp-6.5.0-std
7+
prepend_text: '#SBATCH --uenv=vasp/v6.5.0:v1
8+
9+
#SBATCH -A mr32
10+
11+
#SBATCH --view=vasp
12+
13+
#SBATCH --hint=nomultithread
14+
15+
16+
export OMP_NUM_THREADS=1'
17+
use_double_quotes: false
18+
with_mpi: true

0 commit comments

Comments
 (0)