AtomicSymmetries.jl Documentation

A package to enforce symmetry constraints on atomic positions in a crystal structure. It also works on dynamical matrices and force constants.

The package also exploits a memory efficient representation of the symmetry independent components of a tensor. It can be used in pairs with Spglib.jl and the python version of spglib via PyCall for symmetry recognition.

The symmetry group

The key object in this package is the Symmetries type, which represents a symmetry group. The $Symmetries$ is a container for the symmetry operations under which the specific crystal is invariant.

AtomicSymmetries.SymmetriesType
mutable struct Symmetries{T}

The structure containing the symmetries of the system.

Once the symmetries have been initialized, the symmetrizefc! and symmetrizecentroid! functions can be used to symmetrize the force constant matrix and the centroid.

The exchange_symmetry is a vector of length n_particles, where each element identify the id of the particle. If two ids are equal, the particles are indistinguishable.

irt[i][j] is the index of the atom that is equivalent to the j-th atom before the symmetry is applied. The expression is

$v_{\text{irt[i]}} = S v_i$

The name irt stands for "index of the representative of the transformation". and it is in line with the notation used in the Quantum Espresso and the CellConstructor codes.

The $unit_cell_translations$ attribute contains the list of translational vectors for each symmetry that reports atoms after the symmetry application into their equivalent primitive cell position. This is equal to

\[S\vec a = \vec {s(a)} + \vec t_{s, a}\]

where $s(a)$ is the equivalent atom to $a$ in the primitive cell mapped by the $S$ symmetry operation. The translation vector $\vec t_{s, a}$ is stored in $unit_cell_translations$. Each element correspond to the relative symmetry operation, and it is a matrix of dimension $(dimension, n_particles)$. Each column correspond to the translation vector of each atom. ```

source

To build the symmetry group, one can create the following constructor

symmetry_group = get_identity_symmetry_group(Float64)

This will create a symmetry group with the identity operation only. The symmetry group can be extended by adding new symmetry operations with the function add_symmetry!.

add_symmetry!(symmetry_group, symmetry_operation; irt = nothing)

The $symmetry_operation$ is a 3x3 matrix representing the rotation part of the symmetry operation. The optional argument $irt$ is a vector representing how the symmetry operation exchanges the atoms. In particular the simmetry maps atom $i$ to atom $irt[i]$. If it is not provided, the symmetry operation is assumed not to exchange the atoms.

Once the fundamental symmetry operations have been added, the symmetry group can closed by completing the irreducible representation. This is done by the function $complete_symmetry_group!$.

complete_symmetry_group!(symmetry_group)

Once completed, the symmetry group can be exploited to enforce symmetry constraints on displacement vectors and 2-rank tensors. In particular, the following subroutines work in crystal coordinates. See below for how to perform the symmetrization in cartesian coordinates.

symmetry_group.symmetrize_centroid!(vector)
symmetry_group.symmetrize_fc!(matrix)

Apply the symmetry operations

To apply a symmetry operation in general on a vector, we provide a general interface working both in real space and q-space. These subroutines works in cartesian coordinates:

AtomicSymmetries.rotate_vector!Function
rotate_vector!(new_vector :: AbstractVector{T}, old_vector :: AbstractVector{T}, cell :: Matrix{T},
        reciprocal_vectors :: Matrix{T},
        symmetry_group :: Symmetries, sym_index :: Int; buffer=default_buffer())
rotate_vector!(new_vector :: AbstractMatrix{Complex{T}}, old_vector :: AbstractMatrix{Complex{T}},
        cell :: Matrix{T}, reciprocal_vectors :: Matrix{T},
        symmetry_group :: SymmetriesQSpace, sym_index :: Int; buffer=default_buffer())

Apply a single symmetry operation on a vector (e.g. a displacement or force). The vector must be provided in Cartesian coordinates; the conversion to crystal coordinates and back is handled internally.

This function works both with SymmetriesQSpace and with standard real-space Symmetries, thanks to multiple dispatch.

Real-space version — the vector has length $n_\text{dims} \times n_\text{atoms}$ and the operation applies the symmetry rotation plus the atom permutation:

\[\vec v'_{\text{irt}[a]} = S\, \vec v_{a}\]

Q-space version — the vector has size $(n_\text{modes},\, n_q)$ and the operation additionally permutes q-points according to the symmetry:

\[\vec v'_{\text{irt}[a]}(q') = S\, \vec v_{a}(q), \qquad q' = S^{-T} q\]

To symmetrize a vector (average over all symmetries), call this function for each symmetry and average the result. That is equivalent to what symmetrize_vector! (real space) or symmetrize_vector_cartesian_q! (q space) do internally.

Parameters

  • new_vector : Output rotated vector (modified in-place)
  • old_vector : Input vector to be rotated
  • cell : Primitive cell matrix (lattice vectors as columns)
  • reciprocal_vectors : Reciprocal lattice vectors (column-wise)
  • symmetry_group : The symmetry group (Symmetries or SymmetriesQSpace)
  • sym_index : Index of the symmetry operation to apply ($1 \le \text{sym\_index} \le N_\text{sym}$)
  • buffer : Optional Bumper.jl buffer for stack allocations

Example

n_sym = get_nsymmetries(sym_group)
avg = zeros(length(vector))
rotated = zeros(length(vector))
for i in 1:n_sym
    rotated .= 0
    rotate_vector!(rotated, vector, cell, reciprocal_vectors, sym_group, i)
    avg .+= rotated
end
avg ./= n_sym  # same as symmetrize_vector!(vector, cell, sym_group)

See also

source
AtomicSymmetries.rotate_matrix!Function
rotate_matrix!(new_matrix :: AbstractMatrix{T}, old_matrix :: AbstractMatrix{T}, cell :: Matrix{T},
        reciprocal_vectors :: Matrix{T},
        symmetry_group :: Symmetries, sym_index :: Int; buffer=default_buffer()) where T
rotate_matrix!(new_matrix :: AbstractMatrix{T}, old_matrix :: AbstractMatrix{T}, cell :: Matrix{T},
        reciprocal_vectors :: Matrix{T},
        symmetry_group :: SymmetriesQSpace, sym_index :: Int; buffer=default_buffer()) where T

Apply a single symmetry operation on an intensive $n_\text{dims} \times n_\text{dims}$ matrix (e.g. a stress tensor or a dielectric tensor). The matrix must be provided in Cartesian coordinates; the conversion to crystal coordinates and back is handled internally.

The operation performed is a second-rank tensor rotation:

\[M' = S^\top\, M\, S\]

where $S$ is the symmetry rotation matrix (in crystal coordinates).

Since an intensive matrix is q-independent, both the Symmetries and SymmetriesQSpace dispatches perform exactly the same operation (the SymmetriesQSpace version delegates to the Symmetries version).

To symmetrize a matrix, call this function for each symmetry and average. For a cubic crystal, the averaged stress tensor will be proportional to the identity matrix.

Parameters

  • new_matrix : Output rotated matrix (modified in-place)
  • old_matrix : Input matrix to be rotated
  • cell : Primitive cell matrix (lattice vectors as columns)
  • reciprocal_vectors : Reciprocal lattice vectors (column-wise)
  • symmetry_group : The symmetry group (Symmetries or SymmetriesQSpace)
  • sym_index : Index of the symmetry operation to apply ($1 \le \text{sym\_index} \le N_\text{sym}$)
  • buffer : Optional Bumper.jl buffer for stack allocations

Example

n_sym = get_nsymmetries(sym_group)
avg = zeros(3, 3)
rotated = zeros(3, 3)
for i in 1:n_sym
    rotate_matrix!(rotated, stress_tensor, cell, reciprocal_vectors, sym_group, i)
    avg .+= rotated
end
avg ./= n_sym  # symmetrized stress tensor

See also

  • rotate_dynamical_matrix! — for extensive $n_\text{modes} \times n_\text{modes$ matrices with atom-block structure
  • symmetrize_fc! — symmetrize a force constant matrix in real space (Cartesian)
source
AtomicSymmetries.rotate_dynamical_matrix!Function
rotate_dynamical_matrix!(new_matrix :: AbstractMatrix{T}, old_matrix :: AbstractMatrix{T}, cell :: Matrix{T},
        reciprocal_vectors :: Matrix{T},
        symmetry_group :: Symmetries, sym_index :: Int; buffer=default_buffer()) where T
rotate_dynamical_matrix!(new_matrix :: AbstractArray{Complex{T}, 3}, old_matrix :: AbstractArray{Complex{T}, 3}, cell :: Matrix{T},
        reciprocal_vectors :: Matrix{T},
        symmetry_group :: SymmetriesQSpace, sym_index :: Int; buffer=default_buffer()) where T

Apply a single symmetry operation on a dynamical matrix (force-constant-like matrix with atom blocks). The matrix must be provided in Cartesian coordinates; the conversion to crystal coordinates and back is handled internally.

Real-space version — the matrix has size $(n_\text{modes}, n_\text{modes})$ where $n_\text{modes} = n_\text{dims} \times n_\text{atoms}$. Each $n_\text{dims} \times n_\text{dims}$ block $(a, b)$ is rotated by the symmetry and the atom indices are permuted according to irt:

\[\Phi'_{\text{irt}[a],\, \text{irt}[b]} = S^\top\, \Phi_{a b}\, S\]

Q-space version — the matrix has size $(n_\text{modes}, n_\text{modes}, n_q)$. In addition to the block-wise rotation and atom permutation, the q-point is also permuted and phase factors from fractional translations are included:

\[D'_{\text{irt}[a],\, \text{irt}[b]}(q') = e^{2\pi i\, q \cdot (\vec t_a - \vec t_b)}\, S^\top\, D_{a b}(q)\, S\]

where $q' = S^{-T} q$ and $\vec t_a$ are the unit-cell translations that bring the symmetry-transformed atom back into the primitive cell.

To symmetrize a dynamical matrix, call this function for each symmetry and average the result. That is equivalent to what symmetrize_fc! (real space) or symmetrize_matrix_cartesian_q! (q space) do internally.

Parameters

  • new_matrix : Output rotated matrix (modified in-place)
  • old_matrix : Input matrix to be rotated
  • cell : Primitive cell matrix (lattice vectors as columns)
  • reciprocal_vectors : Reciprocal lattice vectors (column-wise)
  • symmetry_group : The symmetry group (Symmetries or SymmetriesQSpace)
  • sym_index : Index of the symmetry operation to apply ($1 \le \text{sym\_index} \le N_\text{sym}$)
  • buffer : Optional Bumper.jl buffer for stack allocations

Example

n_sym = get_nsymmetries(sym_group)
avg = zeros(n_modes, n_modes)
rotated = zeros(n_modes, n_modes)
for i in 1:n_sym
    rotated .= 0
    rotate_dynamical_matrix!(rotated, fc, cell, reciprocal_vectors, sym_group, i)
    avg .+= rotated
end
avg ./= n_sym  # same as symmetrize_fc!(fc, cell, sym_group)

See also

source

Build your own symmetry group (API)

To build a custom symmetry group, you can exploit the following subroutines

AtomicSymmetries.complete_symmetry_group!Function
complete_symmetry_group!(symmetries :: Symmetries{T})

Complete the symmetry group by adding the inverse symmetry operations and the compositions of the symmetry operations, until the group is closed.

Since symmetry are unitary transformations, the inverse symmetry operation is the transpose of the symmetry operation.

source
AtomicSymmetries.add_symmetry!Function
add_symmetry!(sym :: Symmetries{T}, symm :: Matrix{T}; update :: Bool = true, check_existing :: Bool = false) :: where {T}

Add a symmetry to the system.

If update is true, the symmetrizefc! and symmetrizecentroid! functions are updated. If check_existing is true, the symmetry is only added if it is not already in the list of symmetries.

source

Structure and Dynamical Matrix symmetrization

Sometimes it is useful to symmetrize the atomic positions of a crystal structure. To follow the correct Wyckoff positions. This is achieved with the function symmetrize_positions!.

symmetrize_positions!(cartesian_coords, cell, symmetry_group)

This function takes the atomic positions in cartesian coordinates, the cell matrix and the symmetry group. The cell must be column-wise, i.e., each column is a primitive vector of the cell.

It is also possible just to apply the translational symmetries to either a vector or a force constants matrix. This is achieved calling the subroutine

apply_translations!(vector, translations)
apply_translations!(matrix, translations)

where the translations is a vector of vector of Ints, each element represent a translations which maps each respective atomic index in the corresponding one. The translations object can be obtained from a symmetry group using the subroutine get_translations.

For imposing symmetries on a vector or a 2-rank array (like the force constant matrix) directly in cartesian space, you can use

symmetrize_vector!(vector, unit_cell, symmetry_group)
symmetrize_fc!(matrix, unit_cell, symmetry_group)

Here the APIs for these calls

AtomicSymmetries.symmetrize_positions!Function
symmetrize_positions!(positions :: AbstractMatrix{T}, cell :: AbstractMatrix, symmetry_group :: Symmetries; buffer=default_buffer()) where {T}

symmetrize an atomic coordinates in real space. This subroutie symmetrizes a system with Cartesian coordinats (positions) using the specified symmetry group (that must include translations).

If you want to symmetrize a quantity that is invariant under translations (e.g. a force or a displacement), use symmetrize_vector! instead.

The function operates in place, meaning that the final result overwrites the input positions.

This function exploits Bumper.jl stack allocation to avoid memory allocation, you can manually specify the stack buffer as an optional keyword argument buffer.

source
AtomicSymmetries.symmetrize_vector!Function
symmetrize_vector!(vector :: AbstractVector{T}, cell :: AbstractMatrix, symmetry_group :: Symmetries; buffer=default_buffer()) where {T}

Symmetrize the vector vector using the symmetry group symmetry_group. The vector has a length of dim * nat, where nat is the number of atoms in the system. It is assumed to represent a quantity that is invariant under translations (e.g. a force or a displacement). If you want to symmetrize a quantity that is not invariant under translations (e.g. atomic positions), use symmetrize_positions! instead.

This function assumes that the vector is provided in cartesian coordinates, opposite to symmetry_group.symmetrize_centroid!, which assumes that the vector is in crystal coordinates. Note that to symmetrize cartesian coordinates, also the primitive cell is required (cell).

The function operates in place, meaning that the final result overwrites the input vector.

This function exploits Bumper.jl stack allocation to avoid memory allocation. The stack can be manually specified as an optional keyword argument buffer.

source
AtomicSymmetries.symmetrize_fc!Function
symmetrize_fc!(fc :: AbstractMatrix{T}, cell :: AbstractMatrix, symmetry_group :: Symmetries; buffer=default_buffer()) where {T}

Symmetrize the force constant matrix fc using the symmetry group symmetry_group. This function assumes that the force constant matrix is in cartesian coordinates, opposite to symmetry_group.symmetrize_fc!, which assumes that the force constant matrix is in crystal coordinates. Note that to symmetrize cartesian coordinates, also the primitive cell is required (cell).

The function operates in place, meaning that the final result overwrites the input force constant matrix fc.

This function exploits Bumper.jl stack allocation to avoid memory allocation. The stack can be manually specified as an optional keyword argument buffer.

source
AtomicSymmetries.get_translationsFunction
get_translations(symmetry :: GenericSymmetries) :: Vector{Vector{Int}}

Return a vector containin, for each element, the corrispondence of couple of atoms mapped by the respective translational symmetry.

This function extract the translations from the global symmetry object.

Parameters

  • symmetries :: Symmetries : The symmmetry object that contains the symmetries of the structure.
source
get_translations(coords :: AbstractMatrix{T}, supercell, R_lat :: Vector{I}) :: Vector{Vector{I}} where {T, I<: Integer}

Return a vector containin, for each element, the corrispondence of couple of atoms mapped by the respective translational symmetry.

For the mapping, see get_irt! documentation.

Parameters

  • coords : the atomic coordinates in crystalline axes (in the supercell)
  • supercell : The dimension of the supercel (e.g. a 3x3x3 supercell should be a tuple (3,3,3) )
  • R_lat : The translations operated, i.e. the vectors of each translations (of the primitive cell). Note that this are integers.

Results

  • translations : A vector containing the mapping between atoms operated by the respective translations in R_lat.
source
AtomicSymmetries.apply_translations!Function
apply_translations!(matrix :: Matrix{T}, translations :: Vector{Vector{Int}};
apply_translations!(matrix :: Vector{T}, translations :: Vector{Vector{Int}};

Enforce the translational symmetries on a supercell matrix or vector. The translational symmetries are encoded by the translations vector. Each element of the translations vectors contains the maps of the atomic indices mapped one into the other by the specific translation.

\[M_{ab} = \frac{1}{N_t}\sum_{t} M_{t(a)t(b)}\]

where $t(a)$ is the atom in which the translation $t$ maps the $a$ atom.

In the case of a vector, instead, we have

\[v_a = \frac{1}{N_t}\sum_t v_{t(a)}\]

Parameters

  • matrix or vector : the in-place modified tensor to be symmetrized
  • translations : the array of the translations
  • buffer : The stack buffer for caching (Bumper.jl) [optional]
source

The symmetry generators

From the symmetry group, we can obtain a vectorial subspace that is invariant under the symmetry operations. A basis of this subspace is given by the symmetry generators. For vectorial quantities, the generators can be obtained as

generators = get_vector_generators(symmetry_group)

For 2-rank tensors, the generators can be obtained as

generators = get_matrix_generators(symmetry_group)

The generators are vectors of indexes that can be used to build the symmetry independent components of a tensor. This allows to store each generator as a 64-bit integer, which is more memory efficient than storing the full tensor. The full vector/2-rank tensor can be retriven with the function $get_vector_generator!$/$get_matrix_generator!$.

# Retrive the first element from the generators
i = 1
vector = zeros(3)
get_vector_generator!(vector, generators[i], symmetry_group)

And analogously for 2-rank tensors.

The generators can be used to project any vector or 2-rank tensor in the symmetry invariant subspace.

coefficients = zeros(length(generators))
my_vector = ...
get_coefficients_from_vector!(coefficients, my_vector, generators, symmetry_group)

The previous function projects the vector my_vector in the symmetry invariant subspace and stores the coefficients in the vector coefficient. The coefficients can be used to reconstruct the original vector (symmetrized) as

final_vector = similar(my_vector)
get_centroids_from_generators!(final_vector, generators, coefficients, symmetry_group)

The same works for 2-rank tensors.

# Get the coefficients of the matrix projected in the symmetric subspace
coefficients = zeros(length(generators))
my_matrix = ...
get_coefficients_from_fc!(coefficients, my_matrix, generators, symmetry_group)

# And reconstruct bach the matrix from the coefficients
final_matrix = similar(my_matrix)
get_fc_from_generators!(final_matrix, generators, coefficients, symmetry_group)

Generators (API)

AtomicSymmetries.get_vector_generatorsFunction
get_vector_generators(symmetry_group::Symmetries{U},
                      unit_cell :: AbstractMatrix{T};
                   func_apply_constraints! = nothing)
                    :: Vector{Int} where {T, U}

Get the generators of the symmetry group for the vectors.

Arguments

  • symmetry_group::Symmetries{U}: The symmetry group to be considered in the generator creation process.
  • unit_cell :: AbstractMatrix{T}: The unit cell of the system.
  • func_apply_constraints!::Function (optional): A function to apply constraints to the parameters. Defaults to nothing.

Returns

  • Vector{Int}: A vector of indices representing the set of independent generators.

Description

This function generates a set of independent generators for a given symmetry group. These generators proved a basis for the space of vectors that are invariant under the symmetry group.

Note that the generators are computed in cartesian coordinates(FC)

The independence of a generator is determined by its norm and its linear independence from previously accepted generators. If a generator is found to be linearly dependent but not identical to a previous one, the function throws an error indicating that this scenario is not yet implemented.

The function ultimately returns a vector of indices representing the independent generators found during the process.

Examples

symmetry_group = # Symmetries object
generators = get_vector_generators(symmetry_group)
source
AtomicSymmetries.get_matrix_generatorsFunction
get_matrix_generators(symmetry_group::Symmetries{U},
                      unit_cell :: AbstractMatrix{T};
                   func_apply_constraints! = nothing)
                    :: Vector{Int} where {T, U}

Get the generators of the symmetry group for the matrices.

Arguments

  • symmetry_group::Symmetries{U}: The symmetry group to be considered in the generator creation process.
  • unit_cell :: AbstractMatrix{T}: The unit cell of the system.
  • func_apply_constraints!::Function (optional): A function to apply constraints to the parameters. Defaults to nothing.

Returns

  • Vector{Int}: A vector of indices representing the set of independent generators.

Description

This function generates a set of independent generators for a given symmetry group. These generators proved a basis for the space of matrices that are invariant under the symmetry group.

Note that the generators are computed in cartesian coordinates(FC)

The independence of a generator is determined by its norm and its linear independence from previously accepted generators. If a generator is found to be linearly dependent but not identical to a previous one, the function throws an error indicating that this scenario is not yet implemented.

The function ultimately returns a vector of indices representing the independent generators found during the process.

Examples

symmetry_group = # Symmetries object
generators = get_matrix_generators(symmetry_group)
source
AtomicSymmetries.get_vector_generator!Function
get_vector_generator!(generator::Vector{T}, generator_index::Int, n_modes::Int, n_layers::Int, symmetry_group::Symmetries{U};
               use_sqrt_representation=true, optimize_struct=true, optimize_nltransf=true, 
               func_apply_constraints!=nothing, baseline_generator=nothing, normalize=true) where {T, U}

Modify generator in-place to represent a specific generator of a transformation, subject to given constraints and symmetries.

The generatorindex parameter ranges from 1 to Nmax, where N_max represents the maximum dimension of the parameters, and it should be noted that some indices within this range may yield identical generators due to the underlying symmetries or constraints in the system.

Arguments

  • generator::Vector{T}: The generator vector to be modified.
  • generator_index::Int: Index specifying which generator to construct.
  • n_modes::Int: The number of modes in the system.
  • n_layers::Int: The number of layers in the neural network model.
  • symmetry_group::Symmetries{U}: The symmetry group to be imposed on the generator.
  • use_sqrt_representation::Bool (optional): Flag to use the square root representation. Defaults to true.
  • optimize_struct::Bool (optional): Flag to optimize the structure. Defaults to true.
  • optimize_nltransf::Bool (optional): Flag to optimize nonlinear transformations. Defaults to true.
  • func_apply_constraints!::Function (optional): A function to manually apply constraints to the parameters. Defaults to nothing.
  • baseline_generator::Vector{T} (optional): A baseline generator for comparison. Defaults to nothing.
  • normalize::Bool (optional): Flag to normalize the generator. Defaults to true.

Description

This function constructs a generator vector that represents a transformation in a specified manner. It initializes the generator vector with zeros and sets the generator_index element to 1. The function builds a scha (Structured Component Histogram Analysis) and an nltransf (Non-Linear Transform) based on the specified parameters. The scha and nltransf are then used to set the parameters of the generator.

If func_apply_constraints! is provided, it is used to apply constraints to the scha and nltransf. If baseline_generator is provided, the function adjusts the generator relative to this baseline. Symmetry constraints from symmetry_group are imposed on the nltransf and scha.

Finally, the function updates the generator with the parameters obtained from scha and nltransf, optionally subtracting the baseline generator and normalizing the result.

Examples

generator = zeros(Float64, 10)
get_vector_generator!(generator, 2, 5, 3, my_symmetry_group)
source
AtomicSymmetries.get_coefficients_from_vector!Function
get_coefficients_from_vector!(coefficients :: Vector{T}, vector:: Vector{T}, generators :: Vector{Int},
    n_modes::Int, n_layers :: Int;
    use_sqrt_representation=true,
    optimize_struct=true,
    optimize_nltransf=true) where {T}

Get the coefficients obtained as the scalar product between a vector and the generators:

$\alpha_i = \vec g_i \cdot \vec v$

where \alpha_i is the i-th computed coefficient, \vec g_i is the i-th generator, and \vec v is the provided vector.

source
AtomicSymmetries.get_centroids_from_generators!Function
get_centroids_from_generators!(centroids:: AbstractVector{T}, generators::Vector{Int}, coefficients :: Vector{T}, symmetries :: Symmetries, n_modes :: Int; kwargs...)

Return the parameters from the generators and the coefficients. The centroids $\vec v$ are obtained in-place as

$\vec v = \sum_i \alpha_i \vec g_i$

where \alpha_i are the generator coefficients, while \vec g_i is the i-th vector generator.

source
AtomicSymmetries.get_coefficients_from_fc!Function
get_coefficients_from_fc!(coefficients :: Vector{T}, matrix:: Matrix{T}, generators :: Vector{Int},
    n_modes::Int, n_layers :: Int;
    use_sqrt_representation=true,
    optimize_struct=true,
    optimize_nltransf=true) where {T}

Get the coefficients obtained as the scalar product between a given matrix and the generators:

$\alpha_i = \text{Tr} G_i M$

where \alpha_i is the i-th computed coefficient, G_i is the i-th matrix generator, and M is the provided Matrix.

source
AtomicSymmetries.get_fc_from_generators!Function
get_fc_from_generators!(fc:: AbstractMarix{T}, generators::Vector{Int}, coefficients :: Vector{T}, symmetryes :: Symmetries, cell :: AbstractMatrix; kwargs...)

Return the Matrix from the coefficient representation. The fc matrix $M$ is obtained in-place as

$M = \sum_i \alpha_i G_i$

where \alpha_i are the generator coefficients, while G_i is the i-th matrix generator.

source

Spglib integration

The symmetry group can be directly constructed exploiting Spglib to recognize the symmetry operations of a crystal structure.

get_symmetry_group_from_spglib(positions::AbstractMatrix{<: Real}, cell::AbstractMatrix{<:Real}, types::Vector{<:Int};  symprec::Float64 = 1e-6, type::Type = Float64, spglib_py_module = nothing) :: Symmetries

Here the arguments are the atomic positions (in crystal coordinates), the cell matrix and the atomic types. Optionally, the symprec parameter can be used to set the tolerance for the symmetry recognition (passed to Spglib).

Since the Spglib.jl implementation is much less mature than the python version, if needed, it is possible to pass the module of the python version of spglib to the function to replace the Spglib.jl implementation with the Python API from the official spglib package. This requires PyCall.jl to be installed.

Spglib API

AtomicSymmetries.get_symmetry_group_from_spglibFunction
get_symmetry_group_from_spglib(positions :: AbstractMatrix, cell :: AbstractMatrix, types :: Array{Int}; 
    symprec :: Float64 = 1e-6,
    type :: Type = Float64, spglib_py_module = nothing) :: Symmetries

Build a symmetry group from the spglib library. Optionally, this function can be called with a spglib python module. In this way, the python module will be used to get the symmetry operations (since the julia spglib module is buggy).

Arguments

  • positions::AbstractMatrix: The atomic positions in the cell (crystallographic coordinates), with shape (3, N).
  • cell::AbstractMatrix: The cell matrix with shape (3, 3).
  • types::Array{Int}: The atomic types.

Optional arguments:

  • symprec::Float64: The symmetry precision.
  • type::Type: The numerical precision type for the symmetry operations.
  • spglib_py_module: The spglib python module. If not provided, the default julia spglib module is used.

Alternatively, you can pass an ASE Atoms object.

source

How to get crystal coordinates

The code also allows for a quick conversion between cartesian and crystal coordinates. Assuming cartesian_positions is a 3xNat matrix of atomic positions in cartesian coordinates and cell is the 3x3 cell matrix (each column is a primitive vector), the crystal coordinates can be obtained as

crystal_positions = similar(cartesian_positions)
get_crystal_coords!(crystal_positions, cartesian_positions, cell)

Optionally, a Bumper.jl buffer can be passed to the function to avoid memory allocations, otherwise, the default_buffer() is retrived.

The cartesian coordinates can be obtained as

get_cartesian_coords!(cartesian_positions, crystal_positions, cell)

This function does not require any memory allocation.

Notably, a much faster, nonallocating implementation can be used if the reciprocal vectors are available, which can perform transformation of both real space and q space. Reciprocal vectors can be computed using the get_reciprocal_lattice! subroutine, while the crystal-cartesian conversion is obtained through cryst_cart_conv!.

AtomicSymmetries.cryst_cart_conv!Function
cryst_cart_conv!(target, source, primitive_cell, reciprocal_vectors, cryst_to_cart; q_space=false)

In-place conversion of coordinates between crystallographic and Cartesian systems, supporting both real and reciprocal (q) space.

This function supports multiple targets and sources to be converted at once (like many atoms or q-points) by passing them as array of shape (n_dims, n_atoms) or (n_dims, n_q)

The function performs one of four transformations based on the boolean flags cryst_to_cart and q_space. It computes target = α * T * source, where T is the transformation matrix and α is a scaling factor.

Arguments

  • target::AbstractArray{T}: The destination array, which is modified in-place.
  • source::AbstractArray{T}: The source array containing the coordinates to be transformed.
  • primitive_cell::AbstractMatrix{U}: The matrix whose columns represent the primitive lattice vectors (e.g., $A = [a₁, a₂, a₃]$).
  • reciprocal_vectors::AbstractMatrix{U}: The matrix whose columns represent the reciprocal lattice vectors (e.g., $B = [b₁, b₂, b₃]$).
  • cryst_to_cart::Bool: The direction of the transformation.
    • true: Crystallographic coordinates (unitless) to Cartesian (units of length or 1/length).
    • false: Cartesian to Crystallographic.

Keyword Arguments

  • q_space::Bool = false: Toggles between real space and reciprocal (q) space.
    • false: Real-space transformation.
    • true: Reciprocal-space (q-space) transformation.

Operations Performed

Let A = primitive_cell and B = reciprocal_vectors. The function assumes the standard physics definition where $A^T B = 2\pi I$.

The function calculates target = α * T * source based on the following cases:

  1. cryst_to_cart=true, q_space=false: (Cryst → Cart, Real Space)

    • T = A
    • α = 1.0
    • target = A * source
  2. cryst_to_cart=true, q_space=true: (Cryst → Cart, Q-Space)

    • T = B
    • α = 1.0
    • target = B * source
  3. cryst_to_cart=false, q_space=false: (Cart → Cryst, Real Space)

    • T = B' (Transpose of reciprocal_vectors)
    • α = 1.0
    • target = B' * source
  4. cryst_to_cart=false, q_space=true: (Cart → Cryst, Q-Space)

    • T = A' (Transpose of primitive_cell)
    • α = 1 / (2π)
    • target = (1 / (2π)) * A' * source (This is correct, as $B^{-1} = \frac{1}{2\pi} A^T$)
source
AtomicSymmetries.get_reciprocal_lattice!Function
get_reciprocal_lattice!(reciprocal_vectors :: Matrix{T}, cell :: Matrix{T})

Compute the reciprocal lattice vectors from the primitive cell.

Reciprocal lattice vectors $\boldsymbol{B}$ (columns of the matrix) satisfy the property

\[\boldsymbol{B}^\dagger \boldsymbol{A} = 2\pi \boldsymbol{I}\]

where $\boldsymbol{A}$ is the matrix whose columns are the direct lattice vectors, and $\boldsymbol{I}$ is the identity matrix.

Parameters

  • reciprocal_vectors : The $\boldsymbol{B}$ matrix, whose columns are the reciprocal lattice vectors (modified in-place).
  • cell : The matrix whose columns are the primitive direct lattice vectors.
source

Filter symmetries

It is possible to filter symmetries incompatible with a given external perturbation. At this stage, only linear perturbations are supported. For example, to filter the symmetries that are not compatible with a perturbation along the x direction, one can use

filter_invariant_symmetries!(symmetry_group, [1.0, 0.0, 0.0])

All symmetry operations not leaving the perturbation vector invariant are removed from the symmetry group. Since version 0.2, it is possible to parse a vector of size ndimension * natoms, with a different displacement vector acting on each atom.

AtomicSymmetries.filter_invariant_symmetries!Function
filter_invariant_symmetries!(symmetry_group :: Symmetries, vector :: AbstractVector, cell :: AbstractMatrix;
                             buffer=default_buffer())

This subroutine filters the symmetries in symmetry_group that does not leave the vector vector invariant under their transformation.

Note that vector is assumed in Cartesian coordinates (not crystalline) since version 0.6.0

For example, inversion symmetry is not compatible with a vector that is not the null vector, so it will be removed from the symmetry group. While a reflection symmetry is compatible with any vector lying on the reflection plane, so it will be kept in the symmetry group.

This is userful if the symmetries are evaluated from a set of atomic positions, But we then need symmetries that also are invariant under an external perturbation along a certain direction.

source
filter_invariant_symmetries!(symmetry_group :: SymmetriesQSpace, vector :: AbstractVector, cell :: AbstractMatrix;
                             buffer=default_buffer())

This subroutine filters the symmetries in symmetry_group that does not leave the vector vector invariant under their transformation.

It removes the symmetries violated by the vector. This works for symmetry groups expressed in q space

source

Symmetry sparsification

A new feature available since version 0.7 is the possibility to get the symmetry matrices as CSC sparse matrices. To sparsify a symmetry group, you just need to use the sparse method from the SparseArrays library (novel dependency of 0.7)

Using SparseArrays
sparse_symmetry_group = sparse(symmetry_group)

# Apply the symmetry 5th operation on a vector v (previosly defined)
v_new = apply_sparse_symmetry(sparse_symmetry_group.symmetries[5], v)

Notably, this is differentiable via Zygote and Enzyme, so it allows to implement symmetrization in a differentiable way. v could also be a series of vector as a Matrix where each vector is stored as a column of v.

AtomicSymmetries.apply_sparse_symmetryFunction
function apply_sparse_symmetry(sparse_s :: SparseMatrixCSC{T}, v :: AbstractArray{U}) where {T, U}

This function applies the sparse symmetry matrix to a displacement vector.

The inplace version should be nonallocating.

source