Download this example as Julia file or Jupyter notebook.

1. Spin wave simulations of CoRh₂O₄

This tutorial introduces Sunny through its features for performing conventional spin wave theory calculations. We consider the crystal CoRh₂O₄ and reproduce the calculations of Ge et al., Phys. Rev. B 96, 064413 (2017).

Get Julia and Sunny

Sunny is implemented in Julia, which allows for interactive development (like Python or Matlab) while also providing high numerical efficiency (like C++ or Fortran). New Julia users should begin with our Getting Started guide. Sunny requires Julia 1.10 or later.

From the Julia prompt, load Sunny and also GLMakie for graphics.

using Sunny, GLMakie

If these packages are not yet installed, Julia will offer to install them. If executing this tutorial gives an error, you may need to update Sunny and GLMakie from the built-in package manager.

Units system

The Units object selects reference energy and length scales, and uses these to provide physical constants. For example, units.K returns one kelvin as 0.086 meV, where the Boltzmann constant is implicit.

units = Units(:meV, :angstrom);

Crystal cell

A crystallographic cell may be loaded from a .cif file, or specified from atom positions and types.

Start by defining the shape of the conventional chemical cell. CoRh₂O₄ has cubic spacegroup 227 (Fd-3m). Its lattice constants are 8.5 Å, and the cell angles are 90°. With this information, lattice_vectors constructs a 3×3 matrix latvecs. Columns of latvecs define the lattice vectors $(𝐚_1, 𝐚_2, 𝐚_3)$ in the global Cartesian coordinate system. Conversely, columns of inv(latvecs) define the global Cartesian axes $(\hat{x}, \hat{y}, \hat{z})$ in components of the lattice vectors.

a = 8.5031 # (Å)
latvecs = lattice_vectors(a, a, a, 90, 90, 90)
3×3 StaticArraysCore.SMatrix{3, 3, Float64, 9} with indices SOneTo(3)×SOneTo(3):
 8.5031  0.0     0.0
 0.0     8.5031  0.0
 0.0     0.0     8.5031

Construct a Crystal cell from spacegroup 227 in the ITA standard setting. Cobalt atoms belong to Wyckoff 8a, which is the diamond cubic lattice.

positions = [[1/8, 1/8, 1/8]]
cryst = Crystal(latvecs, positions, 227; types=["Co"])
Crystal
Spacegroup 'F d -3 m' (227)
Lattice params a=8.503, b=8.503, c=8.503, α=90°, β=90°, γ=90°
Cell volume 614.8
Type 'Co', Wyckoff 8a (site sym. '-43m'):
   1. [1/8, 1/8, 1/8]
   2. [5/8, 5/8, 1/8]
   3. [7/8, 3/8, 3/8]
   4. [3/8, 7/8, 3/8]
   5. [5/8, 1/8, 5/8]
   6. [1/8, 5/8, 5/8]
   7. [3/8, 3/8, 7/8]
   8. [7/8, 7/8, 7/8]

view_crystal launches an interface for interactive inspection and symmetry analysis.

view_crystal(cryst)
Example block output

Spin system

A System will define the spin model. Each cobalt atom carries quantum spin $s = 3/2$, with a $g$-factor of 2. Specify this Moment data for cobalt atom 1. By symmetry, the same moment data also applies to cobalt atoms 2, 3, ... 7. The option :dipole indicates a traditional model type, for which quantum spin is modeled as a dipole expectation value.

sys = System(cryst, [1 => Moment(s=3/2, g=2)], :dipole)
System [Dipole mode]
Supercell (1×1×1)×8
Energy per site 0

Ge et al. demonstrated that inelastic neutron scattering data for CoRh₂O₄ is well modeled by antiferromagnetic nearest neighbor exchange, J = 0.63 meV. Call set_exchange! with the bond that connects atom 1 to atom 3, and has zero displacement between chemical cells. Consistent with the symmetries of spacegroup 227, this interaction will be propagated to all other nearest-neighbor bonds. Calling view_crystal with sys now shows the antiferromagnetic Heisenberg interactions as blue polkadot spheres.

J = +0.63 # (meV)
set_exchange!(sys, J, Bond(2, 3, [0, 0, 0]))
view_crystal(sys)
Example block output

Optimizing spins

To search for the ground state, call randomize_spins! and minimize_energy! in sequence. For this problem, optimization converges rapidly to the expected Néel order. See this with plot_spins, where spins are colored according to their global $z$-component.

randomize_spins!(sys)
minimize_energy!(sys)
plot_spins(sys; color=[S[3] for S in sys.dipoles])
Example block output

The diamond lattice is bipartite, allowing each spin to perfectly anti-align with its 4 nearest-neighbors. Each of these 4 bonds contribute $-J s^2$ to the total energy. Two sites participate in each bond, so the energy per site is $-2 J s^2$. Check this by calling energy_per_site.

@assert energy_per_site(sys) ≈ -2J*(3/2)^2

Reshaping the magnetic cell

The most compact magnetic cell for this Néel order is the primitive unit cell. Columns of the primitive_cell matrix provide the primitive lattice vectors as multiples of the conventional cubic lattice vectors.

shape = primitive_cell(cryst)
3×3 StaticArraysCore.SMatrix{3, 3, Float64, 9} with indices SOneTo(3)×SOneTo(3):
 0.0  0.5  0.5
 0.5  0.0  0.5
 0.5  0.5  0.0

Reduce the magnetic cell size using reshape_supercell. Verify that the energy per site is unchanged after the reshaping the supercell.

sys_prim = reshape_supercell(sys, shape)
@assert energy_per_site(sys_prim) ≈ -2J*(3/2)^2

Plotting sys_prim shows the two spins within the primitive cell, as well as the larger conventional cubic cell for context.

plot_spins(sys_prim; color=[S[3] for S in sys_prim.dipoles])
Example block output

Spin wave theory

With this primitive cell, we will perform a SpinWaveTheory calculation of the structure factor $\mathcal{S}(𝐪,ω)$. The measurement ssf_perp indicates projection of the spin structure factor $\mathcal{S}(𝐪,ω)$ perpendicular to the direction of momentum transfer, as appropriate for unpolarized neutron scattering. The isotropic FormFactor for Co²⁺ dampens intensities at large $𝐪$.

formfactors = [1 => FormFactor("Co2")]
measure = ssf_perp(sys_prim; formfactors)
swt = SpinWaveTheory(sys_prim; measure)
SpinWaveTheory [Dipole mode]
  2 atoms

Select lorentzian broadening with a full-width at half-maximum (FWHM) of 0.8 meV.

kernel = lorentzian(fwhm=0.8)
Lorentzian kernel, fwhm=0.8

Define a q_space_path that connects high-symmetry points in reciprocal space. The $𝐪$-points are given in reciprocal lattice units (RLU) for the original cubic cell. For example, [1/2, 1/2, 0] denotes the sum of the first two reciprocal lattice vectors, $𝐛_1/2 + 𝐛_2/2$. A total of 500 $𝐪$-points will be sampled along the path.

qs = [[0, 0, 0], [1/2, 0, 0], [1/2, 1/2, 0], [0, 0, 0]]
path = q_space_path(cryst, qs, 500)
QPath (500 samples)
  [0, 0, 0] → [1/2, 0, 0] → [1/2, 1/2, 0] → [0, 0, 0]

Calculate single-crystal scattering intensities along this path, for energies between 0 and 6 meV. Use plot_intensities to visualize the result.

energies = range(0, 6, 300)
res = intensities(swt, path; energies, kernel)
plot_intensities(res; units, title="CoRh₂O₄ LSWT")
Example block output

Sometimes experimental data is only available as a powder average, i.e., as an average over all possible crystal orientations. Use powder_average to simulate these intensities. Each $𝐪$-magnitude defines a spherical shell in reciprocal space. Consider 200 radii from 0 to 3 inverse angstroms, and collect 2000 random samples per spherical shell. As configured, this calculation completes in about two seconds. Had we used the conventional cubic cell, the calculation would be an order of magnitude slower.

radii = range(0, 3, 200) # (1/Å)
res = powder_average(cryst, radii, 2000) do qs
    intensities(swt, qs; energies, kernel)
end
plot_intensities(res; units, saturation=1.0, title="CoRh₂O₄ Powder Average")
Example block output

This result can be compared to experimental neutron scattering data from Fig. 5 of Ge et al.

What's next?

  • For more spin wave calculations of this type, browse the SpinW tutorials ported to Sunny.
  • Spin wave theory neglects thermal fluctuations of the magnetic order. The next CoRh₂O₄ tutorial demonstrates how to sample spins in thermal equilibrium, and measure dynamical correlations from the classical spin dynamics.
  • Sunny also offers features that go beyond the dipole approximation of a quantum spin via the theory of SU(N) coherent states. This can be especially useful for systems with strong single-ion anisotropy, as demonstrated in the FeI₂ tutorial.