You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grogu/tests/test_magnetism.py

241 lines
7.9 KiB

# Copyright (c) [2024] []
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from unittest.mock import Mock
import numpy as np
import pytest
from hypothesis import given
from hypothesis import strategies as st
from numpy.testing import assert_allclose, assert_array_almost_equal
from grogupy.magnetism import (
blow_up_orbindx,
calculate_anisotropy_tensor,
calculate_exchange_tensor,
parse_magnetic_entity,
spin_tracer,
)
# Utility to create mock sisl atoms with orbitals
def create_mock_atom(orbitals):
atom = Mock()
atom.orbitals = []
for l in orbitals:
orbital = Mock()
orbital.l = l
atom.orbitals.append(orbital)
return atom
# Test blow_up_orbindx function
def test_blow_up_orbindx_basic():
"""Test basic orbital index expansion"""
orb_indices = np.array([0, 1, 2])
result = blow_up_orbindx(orb_indices)
expected = np.array([0, 1, 2, 3, 4, 5]) # Each index expands to two indices
assert_array_almost_equal(result, expected)
@given(st.lists(st.integers(min_value=0, max_value=100), min_size=1, max_size=10))
def test_blow_up_orbindx_properties(indices):
"""Property-based test for orbital index expansion"""
orb_indices = np.array(indices)
result = blow_up_orbindx(orb_indices)
# Result should be twice as long as input
assert len(result) == 2 * len(orb_indices)
# Each pair should be consecutive numbers
for i in range(0, len(result), 2):
assert result[i + 1] == result[i] + 1
# Test spin_tracer function
def test_spin_tracer_basic():
"""Test basic spin tracing functionality"""
# Create a test matrix in SPIN-BOX representation
size = 2
M = np.zeros((2 * size, 2 * size), dtype=complex)
M[0::2, 0::2] = 1.0 # M11
M[1::2, 1::2] = -1.0 # M22
M[0::2, 1::2] = 0.5j # M12
M[1::2, 0::2] = -0.5j # M21
result = spin_tracer(M)
# Check components
assert_array_almost_equal(result["z"], np.eye(size) * 2.0) # M11 - M22
assert_array_almost_equal(result["x"], np.eye(size)) # M12 + M21
assert_array_almost_equal(result["y"], np.eye(size) * 1.0j) # i(M12 - M21)
assert_array_almost_equal(result["c"], np.zeros((size, size))) # M11 + M22
def test_spin_tracer_hermiticity():
"""Test that spin tracer preserves Hermiticity"""
size = 2
M = np.random.random((2 * size, 2 * size)) + 1j * np.random.random(
(2 * size, 2 * size)
)
M = M + M.conj().T # Make Hermitian
result = spin_tracer(M)
# Check Hermiticity of components
for component in ["x", "y", "z", "c"]:
assert_array_almost_equal(result[component], result[component].conj().T)
# Test parse_magnetic_entity function
def test_parse_magnetic_entity_single_atom():
"""Test parsing of single atom magnetic entity"""
# Create mock geometry
geometry = Mock()
atom = create_mock_atom([0, 2, 2, 2]) # s and d orbitals
geometry.atoms = [atom]
geometry.a2o.return_value = np.array([0, 1, 2, 3])
dh = Mock()
dh.geometry = geometry
# Test with d orbitals only
entity = {"atom": 0, "l": 2}
result = parse_magnetic_entity(dh, **entity)
assert len(result) == 3 # Should only include d orbitals
# Test with all orbitals
entity = {"atom": 0, "l": None}
result = parse_magnetic_entity(dh, **entity)
assert len(result) == 4 # Should include all orbitals
def test_parse_magnetic_entity_multiple_atoms():
"""Test parsing of multiple atom magnetic entity"""
# Create mock geometry with two atoms
geometry = Mock()
atom1 = create_mock_atom([0, 2])
atom2 = create_mock_atom([0, 2])
geometry.atoms = [atom1, atom2]
geometry.a2o.side_effect = [np.array([0, 1]), np.array([2, 3])]
dh = Mock()
dh.geometry = geometry
entity = {"atom": [0, 1], "l": 2}
result = parse_magnetic_entity(dh, **entity)
assert len(result) == 2 # Should include d orbitals from both atoms
# Test calculate_anisotropy_tensor function
def test_calculate_anisotropy_tensor():
"""Test calculation of anisotropy tensor"""
# Create mock magnetic entity with energies
mag_ent = {
"energies": np.array(
[
[0.1, 0.2], # First rotation axis
[0.3, 0.4], # Second rotation axis
[0.5, 0.6], # Third rotation axis
]
)
}
Kxx, Kyy, Kzz, consistency = calculate_anisotropy_tensor(mag_ent)
# Check basic properties
assert isinstance(Kxx, float)
assert isinstance(Kyy, float)
assert isinstance(Kzz, float)
assert isinstance(consistency, float)
# Verify calculations
assert_allclose(Kxx, mag_ent["energies"][1, 1] - mag_ent["energies"][1, 0])
assert_allclose(Kyy, mag_ent["energies"][0, 1] - mag_ent["energies"][0, 0])
assert_allclose(Kzz, 0)
# Test calculate_exchange_tensor function
def test_calculate_exchange_tensor():
"""Test calculation of exchange tensor"""
# Create mock pair with energies
pair = {
"energies": np.zeros((3, 4)) # 3 quantization axes, 4 rotation combinations
}
# Set some test values
pair["energies"][0] = [0.1, 0.2, 0.3, 0.4] # First quantization axis
pair["energies"][1] = [0.2, 0.3, 0.4, 0.5] # Second quantization axis
pair["energies"][2] = [0.3, 0.4, 0.5, 0.6] # Third quantization axis
J_iso, J_S, D, J = calculate_exchange_tensor(pair)
# Check dimensions
assert isinstance(J_iso, float)
assert J_S.shape == (5,) # Symmetric exchange has 5 independent components
assert D.shape == (3,) # DM vector has 3 components
assert J.shape == (3, 3) # Full exchange tensor is 3x3
# Check properties
assert_allclose(np.trace(J) / 3, J_iso) # Isotropic exchange
assert_array_almost_equal(J, J.T) # Symmetric part
# Verify DM vector components
assert_allclose(D[2], (pair["energies"][2, 1] - pair["energies"][2, 2]) / 2)
def test_exchange_tensor_symmetries():
"""Test symmetry properties of exchange tensor calculations"""
pair = {"energies": np.zeros((3, 4))}
# Set symmetric energies
pair["energies"][0] = [0.1, 0.1, 0.1, 0.1]
pair["energies"][1] = [0.2, 0.2, 0.2, 0.2]
pair["energies"][2] = [0.3, 0.3, 0.3, 0.3]
J_iso, J_S, D, J = calculate_exchange_tensor(pair)
# For symmetric energies, DM vector should be zero
assert_array_almost_equal(D, np.zeros(3))
# Exchange tensor should be symmetric
assert_array_almost_equal(J, J.T)
@pytest.mark.parametrize(
"test_energies,expected_D",
[
(
np.array(
[[0.1, 0.2, -0.2, 0.1], [0.1, 0.2, -0.2, 0.1], [0.1, 0.2, -0.2, 0.1]]
),
np.array([0.2, 0.2, 0.2]),
),
(np.zeros((3, 4)), np.zeros(3)),
],
)
def test_exchange_tensor_dm_vector(test_energies, expected_D):
"""Test DM vector calculation with different energy configurations"""
pair = {"energies": test_energies}
_, _, D, _ = calculate_exchange_tensor(pair)
assert_array_almost_equal(D, expected_D)
if __name__ == "__main__":
pytest.main([__file__])