Skip to article frontmatterSkip to article content

StructureData

A mutable and an immutable StructureData class

In aiida-atomistic we provide two StructureData classes:

  1. StructureData, the AiiDA data type used to store a structure in the AiiDA database. It is immutable, i.e. after the initialization we cannot modify it. It is just a container for structure information;
  2. StructureDataMutable, a python class used to manipulate a structure object before to transform it into the immutable StructureData;

With respect to orm.StructureData, here we provide additional properties to be attached to the structure, e.g. charges and magmoms. Kind based definition of the sites is dropped, and the kind_name is attached to each site as a property. The two StructureData and StructureDataMutable share the same data structure; the difference is that the latter can be modified by the user after its initialization and, at variance with the StructureData, no strict validation checks are performed. Moreover, StructureDataMutable will have additional setter methods. The properties are stored under the properties attribute of the structure, which is a pydantic BaseModel subclass, particularly convenient for data validation.

StructureData(s) initialization

As both StructureData and StructureDataMutable share the same data structure, they also share the same inputs for the constructor: a set of keyword arguments describing the properties. These can be easily organized in a python dictionary. This dictionary and the data stored in the AiiDA database have a 1-to1 correspondence:

from aiida import load_profile
load_profile()

from aiida_atomistic import StructureData, StructureDataMutable

structure_dict = {
    'pbc': [True, True, True],
    'cell': [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]],
    'symbols': ['Si', 'Si'],
    'kinds': ['Si', 'Si'],
    'positions': [
        [0.0, 0.0, 0.0],
        [3.84, 1.3576450198781713, 1.9200000000000006]
    ],
}

mutable_structure = StructureDataMutable(**structure_dict)
structure = StructureData(**structure_dict)

print("Immutable StructureData:\n")
print("Immutable cell: ",structure.properties.cell)
print("Immutable pbc: ",structure.properties.pbc)
print("Immutable sites: ",structure.properties.sites)
print("First immutable site: ",structure.properties.sites[0].dict())

print("\nMutable StructureDataMutable:\n")
print("Mutable cell: ",mutable_structure.properties.cell)
print("Mutable pbc: ",mutable_structure.properties.pbc)
print("Mutable sites: ",mutable_structure.properties.sites)
print("First mutable site: ",mutable_structure.properties.sites[0].dict())
Immutable StructureData:

Immutable cell:  [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]]
Immutable pbc:  [True, True, True]
Immutable sites:  [<SiteImmutable: kind name 'Si' @ 0.0,0.0,0.0>, <SiteImmutable: kind name 'Si' @ 3.84,1.3576450198781713,1.9200000000000006>]
First immutable site:  {'symbols': 'Si', 'kinds': 'Si', 'positions': [0.0, 0.0, 0.0], 'masses': 28.0855, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}

Mutable StructureDataMutable:

Mutable cell:  [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]]
Mutable pbc:  [True, True, True]
Mutable sites:  [<SiteImmutable: kind name 'Si' @ 0.0,0.0,0.0>, <SiteImmutable: kind name 'Si' @ 3.84,1.3576450198781713,1.9200000000000006>]
First mutable site:  {'symbols': 'Si', 'kinds': 'Si', 'positions': [0.0, 0.0, 0.0], 'masses': 28.0855, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}

As we provide the structure_dict to the constructor of our two structure data classes, it is immediately used to build up the properties attribute. Each site is stored as SiteMutable (SiteImmutable) object for the mutable (immutable) case. Mutability (immutability) is inherited from the corresponding StructureData class used.

The structure can be dumped into a dictionary using the to_dict method of the structure, which is nothing else than a wrapper for the pydantic BaseModel model_dump method of the properties attribute:

structure.to_dict()
{'pbc': [True, True, True], 'cell': [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]], 'custom': None, 'symbols': ['Si', 'Si'], 'positions': [[0.0, 0.0, 0.0], [3.84, 1.3576450198781713, 1.9200000000000006]], 'kinds': ['Si', 'Si'], 'weights': None, 'masses': [28.0855, 28.0855], 'charges': None, 'magmoms': None, 'hubbard': {'parameters': [], 'projectors': 'ortho-atomic', 'formulation': 'dudarev'}, 'cell_volume': 41.59375, 'dimensionality': {'dim': 3, 'label': 'volume', 'value': 41.59375}, 'sites': [{'symbols': 'Si', 'kinds': 'Si', 'positions': [0.0, 0.0, 0.0], 'masses': 28.0855, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}, {'symbols': 'Si', 'kinds': 'Si', 'positions': [3.84, 1.3576450198781713, 1.9200000000000006], 'masses': 28.0855, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}], 'formula': 'Si2'}
structure.properties.model_computed_fields.keys()
dict_keys(['cell_volume', 'dimensionality', 'sites', 'formula'])

Useful methods are provided to help the user modify the structure. As a rule, sites cannot be modified directly (these are derived from the list of properties). In general, we can modify a site just using the update_site method of the structure class:

import numpy as np

print("Initial charge on the first site: ", mutable_structure.properties.sites[0].charges)

mutable_structure.update_site(
    site_index=0, 
    charges=+1)

print("Updated charge on the first site: ", mutable_structure.properties.sites[0].charges)
print("Charges in the system: ", mutable_structure.properties.charges)
Initial charge on the first site:  1.0
Updated charge on the first site:  1.0
Charges in the system:  [1, 0]

Other methods to update specific properties are: set_pbc, set_cell, set_charges, set_magmoms, set_automatic_kinds, set_site_property and so on. You can see them using tab-completion.

To print the supported properties, use the get_supported_properties method of the structure class. List of the defined properties for the structure can be accessed using the get_defined_properties.

An additional way to define a structure: a sites list

We can also provide the dictionary containing the sites as list of dictionaries, each of them describing a site. This is useful when we want to provide additional information for each site, such as the occupancy or the magnetic moment. In this case, the dictionary should have the following structure:

structure_dict = {
    'cell':[[2.75,2.75,0],[0,2.75,2.75],[2.75,0,2.75]],
    'pbc': [True,True,True],
    'sites':[
        {
            'symbols':'Si',
            'positions':[3/4, 3/4, 3/4],
        },
        {
            'symbols':'Si',
            'positions':[1/2, 1/2, 1/2],
        },
    ],
}

Initialization from ASE or Pymatgen

If we already have an ASE Atoms or a Pymatgen Structure object, we can initialize our StructureData by means of the built-in from_ase and from_pymatgen methods.

For ASE:

from ase.build import bulk
atoms = bulk('Cu', 'fcc', a=3.6)
atoms.set_initial_charges([1,])

mutable_structure = StructureDataMutable.from_ase(atoms)
structure = StructureData.from_ase(atoms)

structure.to_dict()
{'pbc': [True, True, True], 'cell': [[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]], 'custom': None, 'symbols': ['Cu'], 'positions': [[0.0, 0.0, 0.0]], 'kinds': ['Cu1'], 'weights': [(1.0,)], 'masses': [63.546], 'charges': [1.0], 'magmoms': [[0.0, 0.0, 0.0]], 'hubbard': {'parameters': [], 'projectors': 'ortho-atomic', 'formulation': 'dudarev'}, 'cell_volume': 11.664000000000001, 'dimensionality': {'dim': 3, 'label': 'volume', 'value': 11.664000000000001}, 'sites': [{'symbols': 'Cu', 'kinds': 'Cu1', 'positions': [0.0, 0.0, 0.0], 'masses': 63.546, 'charges': 1.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}], 'formula': 'Cu'}

In the Pymatgen case:

from pymatgen.core import Lattice, Structure, Molecule

coords = [[0, 0, 0], [0.75,0.5,0.75]]
lattice = Lattice.from_parameters(a=3.84, b=3.84, c=3.84, alpha=120,
                                beta=90, gamma=60)
struct = Structure(lattice, ["Si", "Si"], coords)

struct.sites[0].properties["charge"]=1

mutable_structure = StructureDataMutable.from_pymatgen(struct)
structure = StructureData.from_pymatgen(struct)

mutable_structure.properties.charges
[1.0, 0.0]

Moreover, we also provide to_ase and to_pymatgen methods to obtain the corresponding instances.

Mutation of a StructureDataMutable instance

Let’s suppose you want to update some property in the StructureData before to use it in a calculation. You cannot. The way to go is either to use ASE or Pymatgen to modify your object and store it back into StructureData, or to use the StructureDataMutable and its mutation methods, and then convert it into StructureData. The latter method is the preferred one, as you then have support also for additional properties like the Hubbard U, is not supported in ASE and Pymatgen.

Conversion from StructureData to StructureDataMutable and viceversa

We provide two methods to convert back and forth between StructureData and StructureDataMutable. This is useful if we start with a StructureData and we need to modify it:

StructureData.from_mutable(mutable_structure) # returns an instance of StructureData
structure.get_value() # returns an instance of StructureDataMutable
<aiida_atomistic.data.structure.structure.StructureDataMutable at 0x74b7a9f938e0>

Methods to update properties

StructureDataMutable properties can be modified directly, but also the class contains several setter (set_*) methods and more, needed to update a structure. Let’s suppose we start from an immutable StructureData and we want to update the charges (and the corresponding kinds):

mutable_structure = structure.get_value()

print(f"Initial charges, kinds:\n{mutable_structure.properties.charges}, {mutable_structure.properties.kinds}")

mutable_structure.set_charges([-1, +1])
mutable_structure.set_kinds(['Si1','Si2'])

new_structure = StructureData.from_mutable(mutable_structure)

print(f"New charges, kinds:\n{new_structure.properties.charges}, {new_structure.properties.kinds}")
Initial charges, kinds:
[1.0, 0.0], ['Si1', 'Si2']
New charges, kinds:
[-1.0, 1.0], ['Si1', 'Si2']

It is also possible to add_atom, pop_atom, update_site and so on. Indeed, we can start from and empty StructureDataMutable (i.e., from scratch):

mutable_structure = StructureDataMutable()

print("Initial mutable structure is empty:\n", mutable_structure.to_dict()["sites"])

mutable_structure.set_cell([[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]])
mutable_structure.add_atom(**{
            'symbols':'Si',
            'positions':[3/4, 3/4, 3/4],
            'charges': 1,
            'kinds': 'Si1'
        })

mutable_structure.add_atom(
            symbols = 'Si',
            positions = [1/2, 1/2, 1/2],
            charges = 0,
            kinds = 'Si2'
        )

print("\nModified structure is:")
mutable_structure.to_dict()["sites"]
Initial mutable structure is empty:
 []

Modified structure is:
[{'symbols': 'Si', 'kinds': 'Si1', 'positions': [0.75, 0.75, 0.75], 'masses': 28.0855, 'charges': 1.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}, {'symbols': 'Si', 'kinds': 'Si2', 'positions': [0.5, 0.5, 0.5], 'masses': 28.0855, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}]

To remove properties, we can use the clear_property method, providing as input parameter the string representing the name of the property: mutable_structure.clear_property("magmoms")

Slicing a structure

It is possible to slice a structure, i.e. returning only a part of it (in terms of sites). The method returns a new sliced StructureDataMutable (StructureData) instance.

sliced_structure = mutable_structure[:1]
sliced_structure.to_dict()
{'pbc': [True, True, True], 'cell': [[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]], 'custom': None, 'symbols': ['Si'], 'positions': [[0.75, 0.75, 0.75]], 'kinds': ['Si1'], 'weights': [(1.0,)], 'masses': [28.0855], 'charges': [1.0], 'magmoms': [[0.0, 0.0, 0.0]], 'hubbard': {'parameters': [], 'projectors': 'ortho-atomic', 'formulation': 'dudarev'}, 'cell_volume': 11.664000000000001, 'dimensionality': {'dim': 3, 'label': 'volume', 'value': 11.664000000000001}, 'sites': [{'symbols': 'Si', 'kinds': 'Si1', 'positions': [0.75, 0.75, 0.75], 'masses': 28.0855, 'charges': 1.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (1.0,)}], 'formula': 'Si'}

Automatic kinds generation

It is possible to automatically detect kinds for a given structure. This is done by default when initializing the structure from ASE or Pymatgen. Moreover, the kind can be also generated during the to_dict call, such that our output_dictionary will already have the detected kinds. This is valid also for the get_value, from_mutable methods. In summary, we can control automatic kind detection in the initialization of our StructureData/StructureDataMutable:

  1. new_structuredata = StructureData.from_ase(ase_structure, detect_kinds=False) # default is True
  2. new_structuredata = StructureData.from_pymatgen(pymatgen_structure, detect_kinds=False) # default is True
  3. new_structuredata = StructureData(**old_structuredata.to_dict(detect_kinds=True)) # default is False
  4. new_structuredata = StructureData.from_mutable(mutable_structure, detect_kinds=True) # default is False
Fe_BCC_dictionary = {
        'pbc': (True, True, True),
        'cell': [
                [2.8403, 0.0, 1.7391821518091137e-16],
                [-1.7391821518091137e-16, 2.8403, 1.7391821518091137e-16],
                [0.0, 0.0, 2.8403]
        ],
        'sites': [
                {
                        'symbols': 'Fe',
                        'masses': 55.845,
                        'positions': [0.0, 0.0, 0.0],
                        'charges': 0.0,
                        'magmoms': [2.5, 0.1, 0.1],
                        'kinds': 'Fe'
                },
                {
                        'symbols': 'Fe',
                        'masses': 55.845,
                        'positions': [1.42015, 1.42015, 1.4201500000000002],
                        'charges': 0.0,
                        'magmoms': [2.4, 0.1, 0.1],
                        'kinds': 'Fe'
                }
        ]
}

mutable_structure = StructureDataMutable(**Fe_BCC_dictionary)

new_mutable_structure = StructureDataMutable(**mutable_structure.to_dict(detect_kinds=True))
new_mutable_structure.to_dict()

mutable_structure = StructureDataMutable(**Fe_BCC_dictionary)

new_mutable_structure = StructureDataMutable(**mutable_structure.to_dict(detect_kinds=True))
new_mutable_structure.to_dict()
{'pbc': [True, True, True], 'cell': [[2.8403, 0.0, 1.7391821518091137e-16], [-1.7391821518091137e-16, 2.8403, 1.7391821518091137e-16], [0.0, 0.0, 2.8403]], 'custom': None, 'symbols': ['Fe', 'Fe'], 'positions': [[0.0, 0.0, 0.0], [1.42015, 1.42015, 1.4201500000000002]], 'kinds': ['Fe1', 'Fe2'], 'weights': [(1.0,), (1.0,)], 'masses': [55.845, 55.845], 'charges': [0.0, 0.0], 'magmoms': [[2.5, 0.1, 0.1], [2.4, 0.1, 0.1]], 'hubbard': {'parameters': [], 'projectors': 'ortho-atomic', 'formulation': 'dudarev'}, 'cell_volume': 22.913563806827, 'dimensionality': {'dim': 3, 'label': 'volume', 'value': 22.913563806827}, 'sites': [{'symbols': 'Fe', 'kinds': 'Fe1', 'positions': [0.0, 0.0, 0.0], 'masses': 55.845, 'charges': 0.0, 'magmoms': [2.5, 0.1, 0.1], 'weights': (1.0,)}, {'symbols': 'Fe', 'kinds': 'Fe2', 'positions': [1.42015, 1.42015, 1.4201500000000002], 'masses': 55.845, 'charges': 0.0, 'magmoms': [2.4, 0.1, 0.1], 'weights': (1.0,)}], 'formula': 'Fe2'}

To return the list of sites with the automatically generated kinds, you can also invoke the get_kinds method in this way

sites_with_kinds = mutable_structure.get_kinds()
sites_with_kinds
{'kinds': ['Fe1', 'Fe2'], 'masses': array([55.845, 55.845]), 'charges': array([0., 0.]), 'magmoms': array([[2.5, 0.1, 0.1], [2.4, 0.1, 0.1]]), 'symbols': ['Fe', 'Fe'], 'positions': [[0.0, 0.0, 0.0], [1.42015, 1.42015, 1.4201500000000002]]}

How to define alloys and deal with vacancies

It is possible to define more than one element for a given site, i.e. to define an alloy. We can provide the alloy in two ways:

  • a unique string, e.g. ‘CuAl’;
  • a list of strings, e.g. [‘Cu’, ‘Al’]; elements of the list should be valid chemical symbols;

it is mandatory to also provide the corresponding weights tuple.

structure = StructureDataMutable(**{
  'pbc': [True, True, True],
  'cell': [[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]],
  'sites': [{
    'symbols': ['Cu', 'Al', 'Fe'],  # or 'CuAlFe'
    'positions': [0.0, 0.0, 0.0],
    'weights': (0.5, 0.3, 0.1) # sum should be less or equal than one, where <1 means that there is some vacancy
  }],
})

structure.properties.sites[0].dict()
{'symbols': 'CuAlFe', 'kinds': 'CuAlFe', 'positions': [0.0, 0.0, 0.0], 'masses': 50.50217933333333, 'charges': 0.0, 'magmoms': [0.0, 0.0, 0.0], 'weights': (0.5, 0.3, 0.1)}

In the structure data object, the symbols and the kinds will always be stored as (and if necessary converted to) strings, even if we provide the list of elements of a given alloy site. In a separated section we will explain in more detail how to efficiently query alloys. We can verifiy that the site represents an alloy and we can access the list of its elements:

print(f'Is alloy: {structure.properties.sites[0].is_alloy}')
print(f'Alloy list: {structure.properties.sites[0].alloy_list}')
Is alloy: True
Alloy list: ['Cu', 'Al', 'Fe']

Also the structure itself will have the is_alloy property:

print(f'Is the structure an alloy: {structure.is_alloy}')
Is the structure an alloy: True

if not provided, the mass is computed accordingly to the symbols and weights. Vacancies are detected when the sum of the weights is less than 1.

print(f'Has vacancies: {structure.has_vacancies}')
Has vacancies: True

How to add custom properties

It is possible to add custom properties at the StructureData level (not at the Site level). These are defined as properties which are not officially supported in the atomistic package. To do that, it is sufficient to put the corresponding property under the custom Field, a dictionary which should contain the custom property names as keys, followed by the corresponding value:

structure = StructureData(**{
  'pbc': [True, True, True],
  'cell': [[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]],
  'sites': [{
    'symbols': 'Cu',
    'positions': [0.0, 0.0, 0.0],
  }],
  'custom': {
    'electronic_type': 'metal',
  }
})

structure.properties.custom
/opt/conda/lib/python3.10/site-packages/pydantic/main.py:390: UserWarning: Pydantic serializer warnings:
  Expected `dict[any, any]` but got `AttributesFrozendict` with value `<AttributesFrozendict {'e...ctronic_type': 'metal'}>` - serialized value may not be as expected
  return self.__pydantic_serializer__.to_python(
<AttributesFrozendict {'electronic_type': 'metal'}>

Backward compatibility support

As minimal backward compatibility (which will be dropped in future versions), we implemented the to_atomistic method in the old orm.StructureData, so that it is possible to start from an old structure and dump the corresponding instance of the new data type.