Spin Hamiltonian#

Hint

To visualize the one- and two-spin interaction of the Hamiltonian you can use the experimental function of Magnopy experimental.plot_spinham().

For the theoretical background on the spin Hamiltonian see Spin Hamiltonian.

Spin Hamiltonian in Magnopy is an instance of the SpinHamiltonian class.

Three objects are required to create it: Cell, Atoms/Sites, and Convention.

>>> import numpy as np
>>> import magnopy
>>> cell = np.eye(3)
>>> atoms = {
...     "names" : ["Fe1", "Fe2"],
...     "positions" : [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
...     "spins" : [5/2, 5/2],
...     "g_factors" : [2, 2],
...     "spglib_types" : [1, 1],
... }
>>> convention = magnopy.Convention(
...     multiple_counting=True, spin_normalized=False, c1=1, c21=1, c22=1, c33=1, c45=1,
... )

Once those three objects are defined, the spin Hamiltonian can be created as

>>> spinham = magnopy.SpinHamiltonian(cell=cell, atoms=atoms, convention=convention)

The cell and atoms of the Hamiltonian can be viewed at any time as

>>> spinham.cell
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
>>> spinham.atoms
{'names': ['Fe1', 'Fe2'], 'positions': [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]], 'spins': [2.5, 2.5], 'g_factors': [2, 2], 'spglib_types': [1, 1]}

However, they can not be changed, to avoid inconsistencies later on

>>> spinham.cell = 2 * np.eye(3)
Traceback (most recent call last):
...
AttributeError: Change of the cell attribute is not allowed after the creation of SpinHamiltonian instance. SpinHamiltonian.cell is immutable.
>>> spinham.atoms = {}
Traceback (most recent call last):
...
AttributeError: Change of the atoms dictionary is not allowed after the creation of SpinHamiltonian instance. SpinHamiltonian.atoms is immutable.

Hint

>>> spinham.atoms.names
['Fe1', 'Fe2']

is equivalent to

>>> spinham.atoms["names"]
['Fe1', 'Fe2']

Accessing the parameters#

First of all, one need to be able to access the parameters of the Hamiltonian. The main method for that task is SpinHamiltonian.parameters(). It returns an iterator over the parameters of the Hamiltonian:

# For the explanation of nus, alphas, and parameter see next sections
>>> for nus, alphas, parameter in spinham.parameters():
...     print(nus, alphas, parameter)

Each element of the iterator is a tuple (nus, alphas, parameter) with the following properties

  • len(alphas) == n

  • len(nus) == n - 1

  • atom alphas[0] is located in the (0, 0, 0) unit cell.

  • atom alphas[i] is located in the nus[i-1] unit cell for all 1 <= i < n

nus is a tuple of indices \((\nu_2, ..., \nu_n)\), alphas is a tuple of indices \((\alpha_1, ..., \alpha_n)\), parameter is the vector/matrix/tensor of the interaction parameter \(J^{i_1, ..., i_n}_{\nu_2, ..., \nu_n; \alpha_1, ..., \alpha_n}\), where n is the amount of spin operators involved in the term. More on the meaning of nus, alphas, and parameter can be found in the next sections.

SpinHamiltonian.parameters() can take two optional arguments n and p_n that filter the parameters by eleven types as described in Spin Hamiltonian page.

  • n selects the number of spin operators in the term of the Hamiltonian.

  • p_n for each n selects one of the sub-types of parameters. For example, for n=2 there are two sub-types of parameters: p_n = 1 and p_n = 2. The first one filters interaction where both spin operators reside at the exact same spot in the real space. The second one filters interactions where spin operators are located at different positions in the real space.

Hint

There are eleven properties of SpinHamiltonian that are equivalent to the call of SpinHamiltonian.parameters() with the right values of n and p_n as summarized in the table below.

property

same as

SpinHamiltonian.p1

SpinHamiltonian.parameters(n=1, p_n=1)

SpinHamiltonian.p21

SpinHamiltonian.parameters(n=2, p_n=1)

SpinHamiltonian.p22

SpinHamiltonian.parameters(n=2, p_n=2)

SpinHamiltonian.p31

SpinHamiltonian.parameters(n=3, p_n=1)

SpinHamiltonian.p32

SpinHamiltonian.parameters(n=3, p_n=2)

SpinHamiltonian.p33

SpinHamiltonian.parameters(n=3, p_n=3)

SpinHamiltonian.p41

SpinHamiltonian.parameters(n=4, p_n=1)

SpinHamiltonian.p42

SpinHamiltonian.parameters(n=4, p_n=2)

SpinHamiltonian.p43

SpinHamiltonian.parameters(n=4, p_n=3)

SpinHamiltonian.p44

SpinHamiltonian.parameters(n=4, p_n=4)

SpinHamiltonian.p45

SpinHamiltonian.parameters(n=4, p_n=5)

So far the spin Hamiltonian that we created does not have any parameters in it

>>> len(spinham.parameters())
0

Adding parameters#

The method SpinHamiltonian.add() can be used to add any interaction parameter to the spin Hamiltonian.

This method follows the notation of the algebraic form of the Hamiltonian described in the Spin Hamiltonian page and expects two bits of information

  • Positions of all spin operators involved in a term:

    • nus, that are expected to be either \((\mu, \mu+\nu_2, ..., \mu+\nu_n)\) or \((\nu_2, ..., \nu_n)\). More on those two options in the Translational symmetry section.

    • alphas, that are expected to be \((\alpha_1, ..., \alpha_n)\).

  • Vector/matrix/tensor of the interaction parameter: parameter.

Let us give examples for every value of 1 <= n <= 4.

\(n = 1\)#

The algebraic form of a single term (omitting the convention constant) is

\[J^{i_1}_{\alpha_1} S_{\mu, \alpha_1}^{i_1}\]

Let us pick some values for the indices and parameters

>>> alpha_1 = 0
>>> mu = (0, 0, 0)
>>> parameter = np.array([1, 2, 3])

Then the term can be added to the Hamiltonian in two equivalent ways

>>> spinham.add(nus = [mu], alphas = [alpha_1], parameter = parameter)

or

>>> spinham.add(nus = [], alphas = [alpha_1], parameter = parameter)

\(n = 2\)#

The algebraic form of a single term (omitting the convention constant) is

\[J^{i_1, i_2}_{\nu_2; \alpha_1, \alpha_2} S_{\mu, \alpha_1}^{i_1} S_{\mu + \nu_2, \alpha_2}^{i_2}\]

Let us pick some values for the indices and parameters

>>> alpha_1 = 0
>>> alpha_2 = 1
>>> mu = (0, 0, 0)
>>> nu_2 = (-1, -1, -1)
>>> parameter = np.eye(3)

Then the term can be added to the Hamiltonian in two equivalent ways

>>> mu_plus_nu_2 = tuple([mu[i] + nu_2[i] for i in range(3)])
>>> spinham.add(
...     nus = [mu, mu_plus_nu_2],
...     alphas = [alpha_1, alpha_2],
...     parameter = parameter,
... )

or

>>> spinham.add(
...     nus = [nu_2],
...     alphas = [alpha_1, alpha_2],
...     parameter = parameter,
... )

\(n = 3\)#

The algebraic form of a single term (omitting the convention constant) is

\[J^{i_1, i_2, i_3}_{\nu_2, \nu_3; \alpha_1, \alpha_2, \alpha_3} S_{\mu, \alpha_1}^{i_1} S_{\mu + \nu_2, \alpha_1}^{i_2} S_{\mu + \nu_3, \alpha_3}^{i_3}\]

Let us pick some values for the indices and parameters

>>> alpha_1 = 0
>>> alpha_2 = 1
>>> alpha_3 = 0
>>> mu = (0, 0, 0)
>>> nu_2 = (-1, -1, -1)
>>> nu_3 = (1, 0, 0)
>>> parameter = np.ones((3, 3, 3))

Then the term can be added to the Hamiltonian in two equivalent ways

>>> mu_plus_nu_2 = tuple([mu[i] + nu_2[i] for i in range(3)])
>>> mu_plus_nu_3 = tuple([mu[i] + nu_3[i] for i in range(3)])
>>> spinham.add(
...     nus = [mu, mu_plus_nu_2, mu_plus_nu_3],
...     alphas = [alpha_1, alpha_2, alpha_3],
...     parameter = parameter
... )

or

>>> spinham.add(
...     nus = [nu_2, nu_3],
...     alphas = [alpha_1, alpha_2, alpha_3],
...     parameter = parameter
... )

\(n = 4\)#

The algebraic form of a single term (omitting the convention constant) is

\[J^{i_1, i_2, i_3, i_4}_{\nu_2, \nu_3, \nu_4; \alpha_1, \alpha_2, \alpha_3, \alpha_4} S_{\mu, \alpha_1}^{i_1} S_{\mu + \nu_2, \alpha_2}^{i_2} S_{\mu + \nu_3, \alpha_3}^{i_3} S_{\mu + \nu_4, \alpha_4}^{i_4}\]

Let us pick some values for the indices and parameters

>>> alpha_1 = 0
>>> alpha_2 = 1
>>> alpha_3 = 0
>>> alpha_4 = 1
>>> mu = (0, 0, 0)
>>> nu_2 = (-1, -1, -1)
>>> nu_3 = (1, 0, 0)
>>> nu_4 = (0, 1, 0)
>>> parameter = np.ones((3, 3, 3, 3))

Then the term can be added to the Hamiltonian in two equivalent ways

>>> mu_plus_nu_2 = tuple([mu[i] + nu_2[i] for i in range(3)])
>>> mu_plus_nu_3 = tuple([mu[i] + nu_3[i] for i in range(3)])
>>> mu_plus_nu_4 = tuple([mu[i] + nu_4[i] for i in range(3)])
>>> spinham.add(
...     nus = [mu, mu_plus_nu_2, mu_plus_nu_3, mu_plus_nu_4],
...     alphas = [alpha_1, alpha_2, alpha_3, alpha_4],
...     parameter = parameter
... )

or

>>> spinham.add(
...     nus = [nu_2, nu_3, nu_4],
...     alphas = [alpha_1, alpha_2, alpha_3, alpha_4],
...     parameter = parameter
... )

Translational symmetry#

As you can see in the Adding parameters section, there are two equivalent methods to add a parameter to the Hamiltonian due to the translational symmetry of the underlying lattice.

Note that the interaction parameters do not depend on the unit cell index \(\mu\). Therefore, it is enough to store only the parameters with a single fixed value of the unit cell index \(\mu\). We choose that value to be \((0, 0, 0)\).

Therefore, when the user provides nus with the length of n - 1, Magnopy interprets nus as \((\nu_2, ..., \nu_n)\) and \(\mu = (0, 0, 0)\) is implied.

However, when the user provides nus with the length of n, Magnopy interprets nus as \((\mu, \mu+\nu_2, ..., \mu+\nu_n)\). In this case the user is free to use any value for \(\mu\). Magnopy, will automatically shift all elements of nus to enforce \(\mu = (0, 0, 0)\) (i. e. nus[0] == (0, 0, 0)).

Let us demonstrate the latter with an example. First, we create two copies of the spin Hamiltonian with no parameters in it

>>> spinham_v1 = spinham.get_empty()
>>> spinham_v2 = spinham.get_empty()

Then we add single interaction to the first Hamiltonian

>>> spinham_v1.add(nus=[(0,0,0), (1,0,0)], alphas = [0,1], parameter = np.eye(3))

and add transitionally equivalent interaction to the second Hamiltonian

>>> spinham_v2.add(nus=[(-3, 34, 21), (-2, 34, 21)], alphas = [0,1], parameter = np.eye(3))

Now both Hamiltonians have the same parameter stored, with \(\mu\) forced to be \((0, 0, 0)\).

>>> for nus, alphas, parameter in spinham_v1.p22:
...     print(nus, alphas, parameter, sep="\n")
((1, 0, 0),)
(0, 1)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
>>> for nus, alphas, parameter in spinham_v2.p22:
...     print(nus, alphas, parameter, sep="\n")
((1, 0, 0),)
(0, 1)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Equivalent parameters#

See Equivalent parameters for the introduction to the concept of equivalent parameters.

We use the term with two spin operators that are located at different positions in real space as an example in this section. Similar logic applies to other terms of the Hamiltonian.

First, we create an empty copy of the Hamiltonian

>>> spinham = spinham.get_empty()

Equivalent set contains two parameters in the case of the term with two spin operators located at different positions. Consider the interaction from the "Fe1" atom in the unit cell \((0, 0, 0)\) to the "Fe2" atom in the unit cell \((-1, -1, -1)\) with the matrix of the interaction parameter

>>> parameter1 = [
...     [1, 0.5, 0],
...     [-0.5, 1, 0],
...     [0, 0, 1],
... ]

Its equivalent interaction is the one from the "Fe2" atom in the unit cell \((0, 0, 0)\) to the "Fe1" atom in the unit cell \((1, 1, 1)\) with the matrix of the interaction parameter

>>> # Note that parameter2 == parameter1.T
>>> parameter2 = [
...     [1, -0.5, 0],
...     [0.5, 1, 0],
...     [0, 0, 1],
... ]

The behavior of Magnopy depends on the value of multiple_counting property of the Hamiltonian's convention.

multiple_counting = True#

>>> spinham.convention.multiple_counting
True

In this case the user is expected to add both parameters to the Hamiltonian by hand

>>> spinham.add(
...     nus = [(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = parameter1
... )
>>> spinham.add(
...     nus = [(1, 1, 1)],
...     alphas = [1, 0],
...     parameter = parameter2
... )

The amount of equivalent parameters in the set grows with the increasing amount of involved spin operators. For example, sets of the (4, 5) terms contain 24 interactions. To avoid the necessity of adding all of them by hand you can pass the keyword argument populate_equivalent=True to the SpinHamiltonian.add() method.

>>> spinham_pe = spinham.get_empty()
>>> spinham_pe.add(
...     nus = [(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = parameter1,
...     populate_equivalent=True
... )

The results are equivalent

>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 1.   0.5  0. ]
 [-0.5  1.   0. ]
 [ 0.   0.   1. ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 1.  -0.5  0. ]
 [ 0.5  1.   0. ]
 [ 0.   0.   1. ]]
>>> for nus, alphas, parameter in spinham_pe.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 1.   0.5  0. ]
 [-0.5  1.   0. ]
 [ 0.   0.   1. ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 1.  -0.5  0. ]
 [ 0.5  1.   0. ]
 [ 0.   0.   1. ]]

multiple_counting = False#

>>> spinham = spinham.get_empty()
>>> spinham.convention = spinham.convention.get_modified(multiple_counting=False)
>>> spinham.convention.multiple_counting
False

In the user is expected to add only one parameter from the set to the Hamiltonian (any parameter from the set will work).

>>> spinham.add(
...     nus = [(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = np.array(parameter1) + np.array(parameter2).T
... )

Then, if you try to add the second parameter from the set, Magnopy will raise an error as the representative parameter of the set is already added to the Hamiltonian

>>> spinham.add(
...     nus = [(1, 1, 1)],
...     alphas = [1, 0],
...     parameter = np.array(parameter2) + np.array(parameter1).T
... )
Traceback (most recent call last):
...
ValueError: Parameter with such specs is already present.

Note that if you change the convention back to multiple_counting=True, the parameters are the same as before

>>> spinham.convention = spinham.convention.get_modified(multiple_counting=True)
>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 1.   0.5  0. ]
 [-0.5  1.   0. ]
 [ 0.   0.   1. ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 1.  -0.5  0. ]
 [ 0.5  1.   0. ]
 [ 0.   0.   1. ]]

Symmetrization#

See Equivalent parameters for the introduction to the concept of equivalent parameters and their symmetrization.

This section is only relevant when multiple_counting=True. In that case, if you do not use populate_equivalent=True while adding parameters to the Hamiltonian with SpinHamiltonian.add() method, then you a free to add non-symmetrized matrices/tensors of parameters to the Hamiltonian. Magnopy silently symmetrizes those parameters when necessary. In other words, while the Hamiltonian can contain non-symmetrized parameters, we do not guarantee that they stay non-symmetrized if some operations are performed with the Hamiltonian.

Let us illustrate that with an example. First, we create an empty copy of the Hamiltonian

>>> spinham = spinham.get_empty()
>>> spinham.convention = spinham.convention.get_modified(multiple_counting=True)

Then we consider the same pair of equivalent parameters as in the previous section:

An interaction from the "Fe1" atom in the unit cell \((0, 0, 0)\) to the "Fe2" atom in the unit cell \((-1, -1, -1)\) and an interaction from the "Fe2" atom in the unit cell \((0, 0, 0)\) to the "Fe1" atom in the unit cell \((1, 1, 1)\). However, this time we choose the matrices of interaction parameters to be non-symmetrized (while keeping the same physics as in the previous section)

>>> parameter1 = [
...     [0, 0.3, 0],
...     [-0.5, 1, 0],
...     [0, 0, 1.68],
... ]
>>> parameter2 = [
...     [2, -0.5, 0],
...     [0.7, 1, 0],
...     [0, 0, 0.32],
... ]

Now we add those parameters to the Hamiltonian

>>> spinham.add(
...     nus = [(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = parameter1
... )
>>> spinham.add(
...     nus = [(1, 1, 1)],
...     alphas = [1, 0],
...     parameter = parameter2
... )

And check that the parameters are indeed non-symmetrized

>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 0.    0.3   0.  ]
 [-0.5   1.    0.  ]
 [ 0.    0.    1.68]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 2.   -0.5   0.  ]
 [ 0.7   1.    0.  ]
 [ 0.    0.    0.32]]

Now one can symmetrize those parameters by calling the method SpinHamiltonian.set_distribution().

>>> spinham.set_distribution(strategy="symmetrize")

And now the parameters are symmetrized and are the same as in the previous section

>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 1.   0.5  0. ]
 [-0.5  1.   0. ]
 [ 0.   0.   1. ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 1.  -0.5  0. ]
 [ 0.5  1.   0. ]
 [ 0.   0.   1. ]]

Note

SpinHamiltonian.set_distribution() is different from using populate_equivalent=True in the SpinHamiltonian.add(). The former takes the parameters that are already present in the Hamiltonian and redistribute them, while the latter adds extra parameters to the Hamiltonian.

Here is an example

>>> spinham_v1 = spinham.get_empty()
>>> spinham_v2 = spinham.get_empty()
>>> parameter = [
...     [0, 0.3, 0],
...     [-0.5, 1, 0],
...     [0, 0, 2],
... ]

In the first case we add a parameter and then symmetrize the Hamiltonian

>>> spinham_v1.add(
...     nus=[(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = parameter,
... )
>>> spinham_v1.set_distribution(strategy="symmetrize")

In the second case we request the code to populate equivalent parameters

>>> spinham_v2.add(
...     nus=[(-1, -1, -1)],
...     alphas = [0, 1],
...     parameter = parameter,
...     populate_equivalent=True,
... )

In both cases there are two interaction parameters

>>> len(spinham_v1.p22)
2
>>> len(spinham_v2.p22)
2

However, the values of the parameters are different (meaning that two Hamiltonians describe different physics)

>>> for nus, alphas, parameter in spinham_v1.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 0.    0.15  0.  ]
 [-0.25  0.5   0.  ]
 [ 0.    0.    1.  ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 0.   -0.25  0.  ]
 [ 0.15  0.5   0.  ]
 [ 0.    0.    1.  ]]
>>> for nus, alphas, parameter in spinham_v2.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (-1, -1, -1)
((-1, -1, -1),)
(0, 1)
[[ 0.   0.3  0. ]
 [-0.5  1.   0. ]
 [ 0.   0.   2. ]]
Fe2 Fe1 (1, 1, 1)
((1, 1, 1),)
(1, 0)
[[ 0.  -0.5  0. ]
 [ 0.3  1.   0. ]
 [ 0.   0.   2. ]]

Removing a parameter#

To remove a parameter from the Hamiltonian use SpinHamiltonian.remove() method that expects nus and alphas as arguments.

First, we add a single parameter to the Hamiltonian

>>> spinham = spinham.get_empty()
>>> spinham.add(
...     nus = [],
...     alphas = [0],
...     parameter = [1,2,3]
... )

There is one parameter in the Hamiltonian now

>>> len(spinham.parameters())
1

Now we can remove it

>>> spinham.remove(
...     nus = [],
...     alphas = [0],
... )

And there is no parameters in the Hamiltonian anymore

>>> len(spinham.parameters())
0

Convention#

Convention of the Hamiltonian is stored as its attribute (SpinHamiltonian.convention).

>>> print(spinham.convention)
"custom" convention where
  * Bonds are counted multiple times in the sum;
  * Spin vectors are not normalized;
  * c1 = 1.0;
  * c21 = 1.0;
  * c22 = 1.0;
  * Undefined c31 factor;
  * Undefined c32 factor;
  * c33 = 1.0;
  * Undefined c41 factor;
  * Undefined c42 factor;
  * Undefined c43 factor;
  * Undefined c44 factor;
  * c45 = 1.0.

The convention of the Hamiltonian can be changed. When the convention is being changed, the parameters adjust accordingly.

First, we add some parameters to the Hamiltonian

>>> spinham = spinham.get_empty()
>>> # On-site anisotropy
>>> spinham.add(nus=[(0,0,0)], alphas=[0,0], parameter = np.diag([2, 1, 1]))
>>> spinham.add(nus=[(0,0,0)], alphas=[1,1], parameter = np.diag([2, 1, 1]))
>>> # Some of the nearest-neighbors exchange bonds
>>> spinham.add(nus=[(0,0,0)], alphas=[0,1], parameter = np.eye(3), populate_equivalent=True)

Now you can change the constant before the sum

>>> for nus, alphas, parameter in spinham.p21:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="\n")
Fe1
[[2. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
Fe2
[[2. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
>>> spinham.convention = spinham.convention.get_modified(c21=2)
>>> for nus, alphas, parameter in spinham.p21:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="\n")
Fe1
[[1.  0.  0. ]
 [0.  0.5 0. ]
 [0.  0.  0.5]]
Fe2
[[1.  0.  0. ]
 [0.  0.5 0. ]
 [0.  0.  0.5]]

Or the multiple counting of the parameters

>>> len(spinham.p22)
2
>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe1 Fe2 (0, 0, 0)
((0, 0, 0),)
(0, 1)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
Fe2 Fe1 (0, 0, 0)
((0, 0, 0),)
(1, 0)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
>>> spinham.convention = spinham.convention.get_modified(multiple_counting=False)
>>> len(spinham.p22)
1
>>> for nus, alphas, parameter in spinham.p22:
...     print(spinham.atoms.names[alphas[0]], spinham.atoms.names[alphas[1]], nus[0])
...     print(nus, alphas, parameter, sep="\n")
Fe2 Fe1 (0, 0, 0)
((0, 0, 0),)
(1, 0)
[[2. 0. 0.]
 [0. 2. 0.]
 [0. 0. 2.]]

Or any other property of the convention.

Hint

The main principle of changing the convention can be formulated as "Physical properties of the Hamiltonian do not depend on its convention".

Units#

SpinHamiltonian supports a number of units for its parameters. See Hamiltonian's parameters for the full list.

First, we prepare an empty Hamiltonian for the examples in this section

>>> spinham = spinham.get_empty()
>>> spinham.convention = spinham.convention.get_modified(multiple_counting=True)

When you use SpinHamiltonian.add() method the vector/matrix/tensor that you pass as the parameter argument is interpreted in the units of SpinHamiltonian.units. By default, those are milli-electronvolts (meV).

>>> spinham.units
'meV'

There is a number of ways to control the units of the parameters. Let us add a single exchange interaction to the Hamiltonian to illustrate those.

>>> spinham.add(nus=[(0,0,0)], alphas=[0,1], parameter = np.eye(3))

Now the interactions parameter is an isotropic exchange with the value of 1 meV.

>>> for _, _, parameter in spinham.p22:
...     print(parameter)
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

You can change the units at the hamiltonian level as

>>> spinham.units = 'Joule'
>>> spinham.units
'Joule'

All parameters of the Hamiltonian are automatically converted to the new units

>>> for _, _, parameter in spinham.p22:
...     print(parameter)
[[1.60217663e-22 0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 1.60217663e-22 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00 1.60217663e-22]]

and all parameters that will be added later on are expected to be given in the units of Joule.

Alternatively, you can specify the units of the parameter while adding it to the Hamiltonian as

>>> spinham.add(
...     nus=[(0,0,0)],
...     alphas=[1,0],
...     parameter=np.eye(3),
...     units='meV'
... )

Then the parameter is automatically converted to the units of the Hamiltonian

>>> for _, _, parameter in spinham.p22:
...     print(parameter)
[[1.60217663e-22 0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 1.60217663e-22 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00 1.60217663e-22]]
[[1.60217663e-22 0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 1.60217663e-22 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00 1.60217663e-22]]

Magnetic vs non-magnetic atoms#

The Hamiltonian can contain any amount of atoms (SpinHamiltonian.atoms). However, not all of them necessarily have parameters associated with them.

In magnopy we classify atoms into two types

  • Magnetic atoms

    Those are the atoms that have at least one parameter of the spin Hamiltonian associated with them. Even if all elements of the corresponding vector/matrix/tensor of the parameter are zeros.

  • Non-magnetic atoms

    All other atoms.

Note

Concept of magnetic and non-magnetic atoms is not related to the physical properties of the atom (such as spin, g-factor, etc.). It only appears in the context of the spin Hamiltonian.

Now, magnetic atoms are the ones that contribute to the physics of the spin Hamiltonian. In fact in the paper about Magnopy only magnetic atoms are considered.

However, non-magnetic atoms together with the magnetic ones are typically used to define the crystal structure and relevant high-symmetry points and paths in the reciprocal space. Thus, Magnopy keeps track of all atoms and of the magnetic atoms.

From the perspective of the user the following is important:

Let us give an example to illustrate this concept.

>>> spinham = spinham.get_empty()
>>> spinham.atoms.names
['Fe1', 'Fe2']

There are no parameters in the Hamiltonian

>>> len(spinham.parameters())
0

Therefore, there are no magnetic atoms in the Hamiltonian

>>> spinham.magnetic_atoms.names
[]

Now we add an exchange interaction between "Fe2" from the unit cell \((0, 0, 0)\) and "Fe2" from the unit cell \((1, 0, 0)\).

>>> spinham.add(nus=[(0,0,0)], alphas=[1,1], parameter = np.eye(3))

Now there is a single interaction in the Hamiltonian that involves only "Fe2" atom.

>>> len(spinham.parameters())
1
>>> for _, alphas, _ in spinham.parameters():
...     print(alphas)
(1, 1)

Therefore, "Fe2" is the only magnetic atom in the Hamiltonian now, while "Fe1" is still non-magnetic.

>>> spinham.magnetic_atoms.names
['Fe2']

Hint

You can always convert the index of an atom in SpinHamiltonian.atoms to the index of the same atom in SpinHamiltonian.magnetic_atoms and vice versa using the properties SpinHamiltonian.map_to_magnetic and SpinHamiltonian.map_to_all.

Recall that there are two atoms in the Hamiltonian: "Fe1" and "Fe2". "Fe1" is non-magnetic and "Fe2" is magnetic.

>>> spinham.atoms.names
['Fe1', 'Fe2']
>>> spinham.magnetic_atoms.names
['Fe2']

The index of the "Fe2" atom in SpinHamiltonian.atoms is 1, while its index in SpinHamiltonian.magnetic_atoms is 0, in other words

>>> index_in_atoms = 1
>>> index_in_magnetic_atoms = 0

To convert from one to another use

>>> spinham.map_to_magnetic[index_in_atoms]
0

and

>>> spinham.map_to_all[index_in_magnetic_atoms]
1

SpinHamiltonian.map_to_magnetic and SpinHamiltonian.map_to_all are simply lists of indices

>>> spinham.map_to_magnetic
[None, 0]
>>> spinham.map_to_all
[1]

Note that you can not convert the index of the "Fe1" atom in SpinHamiltonian.atoms to the index in SpinHamiltonian.magnetic_atoms as atom "Fe1" is non-magnetic.

Magnetic field#

Zeeman interaction can be added by hand using the SpinHamiltonian.add(), as it has the form of ( 1 ) terms. However, we recommend to use pre-defined method that does it automatically: SpinHamiltonian.set_magnetic_field().

Zeeman term in Magnopy is stored as part of the SpinHamiltonian.p1 parameters. The value of the magnetic flux density \(\boldsymbol{B}\) can be accessed via SpinHamiltonian.magnetic_field property.

First, we create an empty Hamiltonian

>>> spinham = spinham.get_empty()
>>> spinham.units = "meV"

Which atoms?#

By default magnetic field is being added only to the magnetic atoms. Since there is no magnetic atoms in the Hamiltonian

>>> spinham.magnetic_atoms.names
[]

addition of the magnetic field has no effect

>>> spinham.set_magnetic_field([1, 0, 0])
>>> len(spinham.p1)
0
>>> spinham.magnetic_field
array([0., 0., 0.])

Typically, there are some interaction parameters already added to the Hamiltonian and the magnetic atoms are clearly identified. For example, exchange interaction

>>> spinham.add(nus=[(0, 0, 0)], alphas=[0, 1], parameter = np.eye(3))
>>> spinham.magnetic_atoms.names
['Fe1', 'Fe2']

If we add the magnetic field now

>>> spinham.set_magnetic_field([1, 0, 0])
>>> len(spinham.p1)
2
>>> for nus, alphas, parameter in spinham.p1:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]

We see that the Zeeman term is being added to both atoms, as they are both magnetic.

Alternatively, you can control explicitly to which atoms the magnetic field is being added by supplying the atom's indices of SpinHamiltonian.atoms

>>> # First, reset the parameters of the Hamiltonian
>>> spinham = spinham.get_empty()
>>> # There is no magnetic atoms in the Hamiltonian
>>> spinham.magnetic_atoms.names
[]
>>> spinham.set_magnetic_field([1, 0, 0], alphas=[0, 1])

We can check that the magnetic field have been added to both atoms, even though they were non-magnetic

>>> len(spinham.p1)
2
>>> for nus, alphas, parameter in spinham.p1:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]

Note that now both atoms are magnetic afterwards, as they both have Zeeman interaction associated with them.

>>> spinham.magnetic_atoms.names
['Fe1', 'Fe2']

Shortcut#

The value of the magnetic field can be checked at any time as

>>> spinham.magnetic_field
array([1., 0., 0.])

Moreover, you can use the same property to change its value

>>> spinham.magnetic_field = [0, 2, 0]
>>> spinham.magnetic_field
array([0., 2., 0.])
>>> for nus, alphas, parameter in spinham.p1:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [0.         0.23153527 0.        ]
Fe2   [0.         0.23153527 0.        ]

The latter is equivalent to

>>> spinham.set_magnetic_field(B=[0, 2, 0])
>>> spinham.magnetic_field
array([0., 2., 0.])
>>> for nus, alphas, parameter in spinham.p1:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [0.         0.23153527 0.        ]
Fe2   [0.         0.23153527 0.        ]

Note that the method SpinHamiltonian.set_magnetic_field() is more powerful as it allows to control with which atoms the magnetic field is interacting, while the property SpinHamiltonian.magnetic_field always adds Zeeman term only for the magnetic atoms.

Zeeman parameters#

Zeeman interaction is store as part of the ( 1 ) terms of the Hamiltonian. The combined effect of the Zeeman term and any other ( 1 ) terms can always be checked by looking at SpinHamiltonian.p1 parameters.

However, if you would like to check the Zeeman parameters by themselves, you can use the property SpinHamiltonian.zeeman_parameters.

Let us illustrate with an example. First, create an empty Hamiltonian

>>> spinham = spinham.get_empty()

Then add some non-Zeeman ( 1 ) terms to the Hamiltonian

>>> spinham.add(nus=[], alphas=[0], parameter = [-1, 0, 2])
>>> spinham.add(nus=[], alphas=[1], parameter = [0, 0, 7])

Now add the Zeeman term

>>> spinham.magnetic_field = [1, 0, 0]

If we check the SpinHamiltonian.p1 parameters now, we see that the Zeeman term is being added to the existing ( 1 ) terms

>>> for nus, alphas, parameter in spinham.p1:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [-0.88423236  0.          2.        ]
Fe2   [0.11576764 0.         7.        ]

However, one can access the Zeeman parameters separately as

>>> for nus, alphas, parameter in spinham.zeeman_parameters:
...     print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]

Incremental change#

The method SpinHamiltonian.set_magnetic_field() and the property SpinHamiltonian.magnetic_field set the value of the magnetic field. However, if you'd like to change the value of the magnetic field incrementally, you can use the method SpinHamiltonian.add_magnetic_field().

First, get an empty Hamiltonian

>>> spinham = spinham.get_empty()

Then, increase the magnetic field from 0 to 1 Tesla in steps of 0.1 Tesla

>>> for i in range(10):
...     if i == 0: print("-"*40)
...
...     spinham.add_magnetic_field(B = [0.1, 0, 0], alphas=[0, 1])
...
...     print(f"B = {np.round(spinham.magnetic_field, decimals=1)} Tesla")
...     for nus, alphas, parameter in spinham.p1:
...         print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
...     print("-"*40)
----------------------------------------
B = [0.1 0.  0. ] Tesla
Fe1   [0.01157676 0.         0.        ]
Fe2   [0.01157676 0.         0.        ]
----------------------------------------
B = [0.2 0.  0. ] Tesla
Fe1   [0.02315353 0.         0.        ]
Fe2   [0.02315353 0.         0.        ]
----------------------------------------
B = [0.3 0.  0. ] Tesla
Fe1   [0.03473029 0.         0.        ]
Fe2   [0.03473029 0.         0.        ]
----------------------------------------
B = [0.4 0.  0. ] Tesla
Fe1   [0.04630705 0.         0.        ]
Fe2   [0.04630705 0.         0.        ]
----------------------------------------
B = [0.5 0.  0. ] Tesla
Fe1   [0.05788382 0.         0.        ]
Fe2   [0.05788382 0.         0.        ]
----------------------------------------
B = [0.6 0.  0. ] Tesla
Fe1   [0.06946058 0.         0.        ]
Fe2   [0.06946058 0.         0.        ]
----------------------------------------
B = [0.7 0.  0. ] Tesla
Fe1   [0.08103735 0.         0.        ]
Fe2   [0.08103735 0.         0.        ]
----------------------------------------
B = [0.8 0.  0. ] Tesla
Fe1   [0.09261411 0.         0.        ]
Fe2   [0.09261411 0.         0.        ]
----------------------------------------
B = [0.9 0.  0. ] Tesla
Fe1   [0.10419087 0.         0.        ]
Fe2   [0.10419087 0.         0.        ]
----------------------------------------
B = [1. 0. 0.] Tesla
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]
----------------------------------------

The same can be achieved with SpinHamiltonian.magnetic_field or SpinHamiltonian.set_magnetic_field() as well, see the dropdown below.

Alternative styles
>>> spinham = spinham.get_empty()
>>> fields = np.linspace([0.1, 0, 0], [1, 0, 0], 10)
>>> for field in fields:
...
...     spinham.set_magnetic_field(B = field, alphas=[0, 1])
...
...     print(f"B = {np.round(spinham.magnetic_field, decimals=1)} Tesla")
...     for nus, alphas, parameter in spinham.p1:
...         print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
...     print("-"*40)
B = [0.1 0.  0. ] Tesla
Fe1   [0.01157676 0.         0.        ]
Fe2   [0.01157676 0.         0.        ]
----------------------------------------
B = [0.2 0.  0. ] Tesla
Fe1   [0.02315353 0.         0.        ]
Fe2   [0.02315353 0.         0.        ]
----------------------------------------
B = [0.3 0.  0. ] Tesla
Fe1   [0.03473029 0.         0.        ]
Fe2   [0.03473029 0.         0.        ]
----------------------------------------
B = [0.4 0.  0. ] Tesla
Fe1   [0.04630705 0.         0.        ]
Fe2   [0.04630705 0.         0.        ]
----------------------------------------
B = [0.5 0.  0. ] Tesla
Fe1   [0.05788382 0.         0.        ]
Fe2   [0.05788382 0.         0.        ]
----------------------------------------
B = [0.6 0.  0. ] Tesla
Fe1   [0.06946058 0.         0.        ]
Fe2   [0.06946058 0.         0.        ]
----------------------------------------
B = [0.7 0.  0. ] Tesla
Fe1   [0.08103735 0.         0.        ]
Fe2   [0.08103735 0.         0.        ]
----------------------------------------
B = [0.8 0.  0. ] Tesla
Fe1   [0.09261411 0.         0.        ]
Fe2   [0.09261411 0.         0.        ]
----------------------------------------
B = [0.9 0.  0. ] Tesla
Fe1   [0.10419087 0.         0.        ]
Fe2   [0.10419087 0.         0.        ]
----------------------------------------
B = [1. 0. 0.] Tesla
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]
----------------------------------------

Note

Two following examples only work because both atoms are already magnetic. If they are not, then one can promote them to be magnetic with, for example,

>>> spinham.set_magnetic_field(B = [0, 0, 0], alphas=[0, 1])
>>> for i in range(10):
...     if i == 0: print("-"*40)
...
...     spinham.magnetic_field += [0.1, 0, 0]
...
...     print(f"B = {np.round(spinham.magnetic_field, decimals=1)} Tesla")
...     for nus, alphas, parameter in spinham.p1:
...         print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
...     print("-"*40)
----------------------------------------
B = [0.1 0.  0. ] Tesla
Fe1   [0.01157676 0.         0.        ]
Fe2   [0.01157676 0.         0.        ]
----------------------------------------
B = [0.2 0.  0. ] Tesla
Fe1   [0.02315353 0.         0.        ]
Fe2   [0.02315353 0.         0.        ]
----------------------------------------
B = [0.3 0.  0. ] Tesla
Fe1   [0.03473029 0.         0.        ]
Fe2   [0.03473029 0.         0.        ]
----------------------------------------
B = [0.4 0.  0. ] Tesla
Fe1   [0.04630705 0.         0.        ]
Fe2   [0.04630705 0.         0.        ]
----------------------------------------
B = [0.5 0.  0. ] Tesla
Fe1   [0.05788382 0.         0.        ]
Fe2   [0.05788382 0.         0.        ]
----------------------------------------
B = [0.6 0.  0. ] Tesla
Fe1   [0.06946058 0.         0.        ]
Fe2   [0.06946058 0.         0.        ]
----------------------------------------
B = [0.7 0.  0. ] Tesla
Fe1   [0.08103735 0.         0.        ]
Fe2   [0.08103735 0.         0.        ]
----------------------------------------
B = [0.8 0.  0. ] Tesla
Fe1   [0.09261411 0.         0.        ]
Fe2   [0.09261411 0.         0.        ]
----------------------------------------
B = [0.9 0.  0. ] Tesla
Fe1   [0.10419087 0.         0.        ]
Fe2   [0.10419087 0.         0.        ]
----------------------------------------
B = [1. 0. 0.] Tesla
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]
----------------------------------------
>>> fields = np.linspace([0.1, 0, 0], [1, 0, 0], 10)
>>> for field in fields:
...
...     spinham.magnetic_field = field
...
...     print(f"B = {np.round(spinham.magnetic_field, decimals=1)} Tesla")
...     for nus, alphas, parameter in spinham.p1:
...         print(spinham.atoms.names[alphas[0]], parameter, sep="   ")
...     print("-"*40)
B = [0.1 0.  0. ] Tesla
Fe1   [0.01157676 0.         0.        ]
Fe2   [0.01157676 0.         0.        ]
----------------------------------------
B = [0.2 0.  0. ] Tesla
Fe1   [0.02315353 0.         0.        ]
Fe2   [0.02315353 0.         0.        ]
----------------------------------------
B = [0.3 0.  0. ] Tesla
Fe1   [0.03473029 0.         0.        ]
Fe2   [0.03473029 0.         0.        ]
----------------------------------------
B = [0.4 0.  0. ] Tesla
Fe1   [0.04630705 0.         0.        ]
Fe2   [0.04630705 0.         0.        ]
----------------------------------------
B = [0.5 0.  0. ] Tesla
Fe1   [0.05788382 0.         0.        ]
Fe2   [0.05788382 0.         0.        ]
----------------------------------------
B = [0.6 0.  0. ] Tesla
Fe1   [0.06946058 0.         0.        ]
Fe2   [0.06946058 0.         0.        ]
----------------------------------------
B = [0.7 0.  0. ] Tesla
Fe1   [0.08103735 0.         0.        ]
Fe2   [0.08103735 0.         0.        ]
----------------------------------------
B = [0.8 0.  0. ] Tesla
Fe1   [0.09261411 0.         0.        ]
Fe2   [0.09261411 0.         0.        ]
----------------------------------------
B = [0.9 0.  0. ] Tesla
Fe1   [0.10419087 0.         0.        ]
Fe2   [0.10419087 0.         0.        ]
----------------------------------------
B = [1. 0. 0.] Tesla
Fe1   [0.11576764 0.         0.        ]
Fe2   [0.11576764 0.         0.        ]
----------------------------------------