|
| 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}.") |
0 commit comments