In aiida-atomistic
we provide two StructureData classes:
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;StructureDataMutable
, a python class used to manipulate a structure object before to transform it into the immutableStructureData
;
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.
Print defined and supported set of properties¶
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:
- new_structuredata = StructureData.from_ase(ase_structure, detect_kinds=False) # default is True
- new_structuredata = StructureData.from_pymatgen(pymatgen_structure, detect_kinds=False) # default is True
- new_structuredata = StructureData(**old_structuredata.to_dict(detect_kinds=True)) # default is False
- 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'}>
Note
Automatic serialization of the custom properties is done when the model is dumped (e.g. when the structure is stored in the AiiDA database). If serialization is not possible, an error is retrieved.
The defined property should be used in the plugin as any other property: if you just want to store additional information on the structure, which do not need to be used in a calculation, store it in the extras
:
structure.base.extras.set('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.