# 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__])