-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the
create_magnetic_allotrope
function
In order to pass on the magnetic configuration between calculations, the structure used for the follow-up calculation needs to have the right magnetic kinds to be able to properly assign the magnetisation to each site. Here we add a calculation function that, based on the structure and magnetic moments, returns a new `StructureData` with the required magnetic kinds, as well as a `Dict` with the corresponding magnetic moments for each kind.
- Loading branch information
Showing
4 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
src/aiida_quantumespresso/calculations/functions/create_magnetic_allotrope.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# -*- coding: utf-8 -*- | ||
"""Create a new magnetic allotrope from the given structure based on the desired magnetic moments.""" | ||
from aiida.engine import calcfunction | ||
from aiida.orm import Float | ||
import numpy | ||
|
||
|
||
@calcfunction | ||
def create_magnetic_allotrope(structure, magnetic_moment_per_site, atol=lambda: Float(5E-1), ztol=lambda: Float(5E-2)): | ||
"""Create a new magnetic allotrope from the given structure based on a list of magnetic moments per site. | ||
To create the new list of kinds, the algorithm loops over all the elements in the structure and makes a list of the | ||
sites with that element and their corresponding magnetic moment. Next, it splits this list in three lists: | ||
* Zero magnetic moments: Any site that has an absolute magnetic moment lower than ``ztol`` | ||
* Positive magnetic moments | ||
* Negative magnetic moments | ||
The algorithm then sorts the positive and negative lists from large to small absolute value, and loops over each of | ||
list. New magnetic kinds will be created when the absolute difference between the magnetic moment of the current | ||
kind and the site exceeds ``atol``. | ||
The positive and negative magnetic moments are handled separately to avoid assigning two sites with opposite signs | ||
in their magnetic moment to the same kind and make sure that each kind has the correct magnetic moment, i.e. the | ||
largest magnetic moment in absolute value of the sites corresponding to that kind. | ||
.. important:: the function currently does not support alloys. | ||
:param structure: a `StructureData` instance. | ||
:param magnetic_moment_per_site: list of magnetic moments for each site in the structure. | ||
:param atol: the absolute tolerance on determining if two sites have the same magnetic moment. | ||
:param ztol: threshold for considering a kind to have non-zero magnetic moment. | ||
""" | ||
# pylint: disable=too-many-locals,too-many-branches,too-many-statements | ||
import string | ||
|
||
from aiida.orm import Dict, StructureData | ||
|
||
if structure.is_alloy: | ||
raise ValueError('Alloys are currently not supported.') | ||
|
||
atol = atol.value | ||
rtol = 0 # Relative tolerance used in the ``numpy.is_close()`` calls. | ||
ztol = ztol.value | ||
|
||
allotrope = StructureData(cell=structure.cell, pbc=structure.pbc) | ||
allotrope_magnetic_moments = {} | ||
|
||
for element in structure.get_symbols_set(): | ||
|
||
# Filter the sites and magnetic moments on the site element | ||
element_sites, element_magnetic_moments = zip( | ||
*[(site, magnetic_moment) | ||
for site, magnetic_moment in zip(structure.sites, magnetic_moment_per_site) | ||
if site.kind_name.rstrip(string.digits) == element] | ||
) | ||
|
||
# Split the sites and their magnetic moments by sign to filter out the sites with magnetic moment lower than | ||
# `ztol`and deal with the positive and negative magnetic moment separately. This is important to avoid assigning | ||
# two sites with opposite signs to the same kind and make sure that each kind has the correct magnetic moment, | ||
# i.e. the largest magnetic moment in absolute value of the sites corresponding to that kind. | ||
zero_sites = [] | ||
pos_sites = [] | ||
neg_sites = [] | ||
|
||
for site, magnetic_moment in zip(element_sites, element_magnetic_moments): | ||
|
||
if abs(magnetic_moment) <= ztol: | ||
zero_sites.append((site, 0)) | ||
elif magnetic_moment > 0: | ||
pos_sites.append((site, magnetic_moment)) | ||
else: | ||
neg_sites.append((site, magnetic_moment)) | ||
|
||
kind_index = -1 | ||
kind_names = [] | ||
kind_sites = [] | ||
kind_magnetic_moments = {} | ||
|
||
for site_list in (zero_sites, pos_sites, neg_sites): | ||
|
||
if not site_list: | ||
continue | ||
|
||
# Sort the site list in order to build the kind lists from large to small absolute magnetic moment. | ||
site_list = sorted(site_list, key=lambda x: abs(x[1]), reverse=True) | ||
|
||
sites, magnetic_moments = zip(*site_list) | ||
|
||
kind_index += 1 | ||
current_kind_name = f'{element}{kind_index}' | ||
kind_sites.append(sites[0]) | ||
kind_names.append(current_kind_name) | ||
kind_magnetic_moments[current_kind_name] = magnetic_moments[0] | ||
|
||
for site, magnetic_moment in zip(sites[1:], magnetic_moments[1:]): | ||
|
||
if not numpy.isclose(magnetic_moment, kind_magnetic_moments[current_kind_name], rtol, atol): | ||
kind_index += 1 | ||
current_kind_name = f'{element}{kind_index}' | ||
kind_magnetic_moments[current_kind_name] = magnetic_moment | ||
|
||
kind_sites.append(site) | ||
kind_names.append(current_kind_name) | ||
|
||
# In case there is only a single kind for the element, remove the 0 kind index | ||
if current_kind_name == f'{element}0': | ||
kind_names = len(element_magnetic_moments) * [element] | ||
kind_magnetic_moments = {element: kind_magnetic_moments[current_kind_name]} | ||
|
||
allotrope_magnetic_moments.update(kind_magnetic_moments) | ||
|
||
for name, site in zip(kind_names, kind_sites): | ||
allotrope.append_atom( | ||
name=name, | ||
symbols=(element,), | ||
weights=(1.0,), | ||
position=site.position, | ||
) | ||
|
||
return {'allotrope': allotrope, 'magnetic_moments': Dict(dict=allotrope_magnetic_moments)} |
198 changes: 198 additions & 0 deletions
198
tests/calculations/functions/test_create_magnetic_allotrope.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
# -*- coding: utf-8 -*- | ||
"""Tests for the `create_magnetic_allotrope` calculation function.""" | ||
import pytest | ||
|
||
from aiida.orm import Float, List | ||
from aiida.plugins import CalculationFactory | ||
|
||
create_magnetic_allotrope = CalculationFactory('quantumespresso.create_magnetic_allotrope') | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_00(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: one kind but with equal magnetic moments. | ||
Expected result: no new kind names should be introduced. | ||
""" | ||
kind_names = ['Fe', 'Fe'] | ||
magnetic_moments = List(list=[0.2, 0.2]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
|
||
assert set(allotrope.get_kind_names()) == {'Fe', 'Fe'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe': 0.2} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_01(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: two kinds all with equal magnetic moments. | ||
Expected result: no new kind names should be introduced. | ||
""" | ||
kind_names = ['Fe', 'Fe', 'Ni', 'Ni', 'Ni'] | ||
magnetic_moments = List(list=[0.2, 0.2, 0.5, 0.5, 0.5]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
|
||
assert set(allotrope.get_kind_names()) == {'Fe', 'Ni'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe': 0.2, 'Ni': 0.5} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_02(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: only one kind but with unequal magnetic moments. | ||
Expected result: two new kinds introduced one for each magnetic moment. | ||
""" | ||
kind_names = ['Fe', 'Fe'] | ||
magnetic_moments = List(list=[0.2, 1.0]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
|
||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 1.0, 'Fe1': 0.2} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_03(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: only one kind but with three types of magnetic moments that are not grouped together. | ||
Expected result: two new kinds introduced one for each magnetic moment. | ||
""" | ||
kind_names = ['Fe', 'Fe', 'Fe', 'Fe'] | ||
magnetic_moments = List(list=[0.2, 0.8, 1.5, 0.8]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
|
||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 1.5, 'Fe1': 0.8, 'Fe2': 0.2} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_04(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: only one kind but with four different values of magnetic moments but middle two are within tolerance. | ||
Expected result: two new kinds introduced one for each magnetic moment. | ||
""" | ||
kind_names = ['Fe', 'Fe', 'Fe', 'Fe'] | ||
magnetic_moments = List(list=[0.0, 0.50, 0.45, 0.40]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
|
||
# Default tolerances: just two different kinds | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe1', 'Fe1'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': 0.5} | ||
|
||
# Lower atol to 0.05: 0.5 & 0.45 now one kind, 0.4 new kind -> three different kinds | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments, | ||
atol=Float(0.05)).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe1', 'Fe2'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': 0.5, 'Fe2': 0.4} | ||
|
||
# Increase atol to 0.1, again only two different kinds | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments, | ||
atol=Float(0.1)).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe1', 'Fe1'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': 0.5} | ||
|
||
# Really strict tolerance or atol = 0.01: All sites get different kinds | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments, | ||
atol=Float(1E-2)).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2', 'Fe3'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe2', 'Fe3'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': 0.5, 'Fe2': 0.45, 'Fe3': 0.4} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_05(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: One kind, only negative magnetic moments with one close to zero | ||
Expected result: Depends on tolerance, see below | ||
""" | ||
kind_names = ['Fe', 'Fe', 'Fe', 'Fe'] | ||
magnetic_moments = List(list=[-0.5, -0.6, -1.5, -0.01]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
|
||
# Default tolerance values, one zero site and two magnetic | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe2', 'Fe2'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': -1.5, 'Fe2': -0.6} | ||
|
||
# Strict absolute tolerance, one zero site and three magnetic | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments, | ||
atol=Float(0.05)).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2', 'Fe3'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe2', 'Fe3'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': -1.5, 'Fe2': -0.6, 'Fe3': -0.5} | ||
|
||
# Strict absolute and zero tolerance, four magnetic sites | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope( | ||
structure, magnetic_moments, atol=Float(0.05), ztol=Float(1E-3) | ||
).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2', 'Fe3'} | ||
assert [site.kind_name for site in allotrope.sites] == ['Fe0', 'Fe1', 'Fe2', 'Fe3'] | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': -1.5, 'Fe1': -0.6, 'Fe2': -0.5, 'Fe3': -0.01} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_06(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: Two kinds, magnetic moments with different signs for the first (Fe) | ||
Expected result: Depends on tolerance, see below | ||
""" | ||
kind_names = ['Fe', 'Fe', 'Fe', 'Fe', 'Ni', 'Ni'] | ||
magnetic_moments = List(list=[-0.1, 0.1, -0.2, 0.01, 0.2, 0.25]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
|
||
# Default tolerance values, one zero and two magnetic sites for Fe, one magnetic site for Ni | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2', 'Ni'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe0': 0.0, 'Fe1': 0.1, 'Fe2': -0.2, 'Ni': 0.25} | ||
|
||
# Very strict absolute tolerance, all different sites | ||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments, | ||
atol=Float(0.02)).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe0', 'Fe1', 'Fe2', 'Fe3', 'Ni0', 'Ni1'} | ||
assert allotrope_magnetic_moments.get_dict() == { | ||
'Fe0': 0.0, | ||
'Fe1': 0.1, | ||
'Fe2': -0.2, | ||
'Fe3': -0.1, | ||
'Ni0': 0.25, | ||
'Ni1': 0.2 | ||
} | ||
|
||
|
||
@pytest.mark.usefixtures('aiida_profile') | ||
def test_configuration_07(generate_structure_from_kinds): | ||
"""Test `create_magnetic_allotrope` calculation function. | ||
Case: Two different symbols but the same magnetic moment. | ||
Expected result: One kind with name equal to the element symbol | ||
""" | ||
kind_names = ['Fe0', 'Fe1'] | ||
magnetic_moments = List(list=[0.1, 0.1]) | ||
|
||
structure = generate_structure_from_kinds(kind_names) | ||
|
||
allotrope, allotrope_magnetic_moments = create_magnetic_allotrope(structure, magnetic_moments).values() | ||
assert set(allotrope.get_kind_names()) == {'Fe'} | ||
assert allotrope_magnetic_moments.get_dict() == {'Fe': 0.1} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters