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, 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 not strict validation checks are done, at variance with the StructureData
. Moreover StructureDataMutable
will have additional set_*
methods to help the user to update the structure.
The properties are stored under the properties
attribute of the structure, which is a pydantic BaseModel
subclass, 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 python dictionary. The format of this dictionary exactly reflects how data are stored in the AiiDA database:
from aiida import load_profile, orm
load_profile()
from aiida_atomistic import StructureData, StructureDataMutable
structure_dict = {
'cell':[[2.75,2.75,0],[0,2.75,2.75],[2.75,0,2.75]],
'pbc': [True,True,True],
'sites':[
{
'symbol':'Si',
'position':[3/4, 3/4, 3/4],
},
{
'symbol':'Si',
'position':[1/2, 1/2, 1/2],
},
],
}
mutable_structure = StructureDataMutable(**structure_dict)
structure = StructureData(**structure_dict)
print("Immutable pbc: ",structure.properties.pbc)
print("Mutable pbc: ",mutable_structure.properties.pbc)
print("Immutable cell: ",structure.properties.cell)
print("Mutable cell: ",mutable_structure.properties.cell)
print("Immutable sites: ",structure.properties.sites)
print("Mutable sites: ",mutable_structure.properties.sites)
print("First immutable site: ",structure.properties.sites[0].dict())
print("First mutable site: ",mutable_structure.properties.sites[0].dict())
Immutable pbc: [True, True, True]
Mutable pbc: [True, True, True]
Immutable cell: [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]]
Mutable cell: [[2.75, 2.75, 0.0], [0.0, 2.75, 2.75], [2.75, 0.0, 2.75]]
Immutable sites: [<SiteImmutable: kind name 'Si' @ 0.75,0.75,0.75>, <SiteImmutable: kind name 'Si' @ 0.5,0.5,0.5>]
Mutable sites: [<SiteMutable: kind name 'Si' @ 0.75,0.75,0.75>, <SiteMutable: kind name 'Si' @ 0.5,0.5,0.5>]
First immutable site: {'symbol': 'Si', 'kind_name': 'Si', 'position': [0.75, 0.75, 0.75], 'mass': 28.0855, 'charge': 0, 'magmom': [0.0, 0.0, 0.0], 'weights': (1,)}
First mutable site: {'symbol': 'Si', 'kind_name': 'Si', 'position': [0.75, 0.75, 0.75], 'mass': 28.0855, 'charge': 0, 'magmom': [0.0, 0.0, 0.0], 'weights': (1,)}
As we provide the structure_dict
to the constructor of our two structure data classes, it is immediately used to feed the properties
model. Each site is store as SiteMutable
(SiteImmutable
) object for the mutable (immutable) case. Mutability (immutability) is inherited from the corresponding StructureData class used.
The full list of properties can be visualized using the to_dict
method of the structure:
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]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Si',
'kind_name': 'Si',
'position': [0.75, 0.75, 0.75],
'mass': 28.0855,
'charge': 0,
'magmom': [0.0, 0.0, 0.0],
'weights': (1,)},
{'symbol': 'Si',
'kind_name': 'Si',
'position': [0.5, 0.5, 0.5],
'mass': 28.0855,
'charge': 0,
'magmom': [0.0, 0.0, 0.0],
'weights': (1,)}],
'cell_volume': 41.59375,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 41.59375},
'charges': [0, 0],
'magmoms': [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
'masses': [28.0855, 28.0855],
'kinds': ['Si', 'Si'],
'symbols': ['Si', 'Si'],
'positions': [[0.75, 0.75, 0.75], [0.5, 0.5, 0.5]],
'formula': 'Si2'}
We can see that some properties are generated automatically, like kinds, charges, dimensionality and so on, and some other properties are set by default if not provided, e.g. the kind_name of each site.
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,])
atoms.set_tags(["2"])
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]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Cu',
'kind_name': 'Cu2',
'position': [0.0, 0.0, 0.0],
'mass': 63.546,
'charge': 1.0,
'magmom': [0.0, 0.0, 0.0],
'weights': (1.0,)}],
'cell_volume': 11.664000000000001,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 11.664000000000001},
'charges': [1.0],
'magmoms': [[0.0, 0.0, 0.0]],
'masses': [63.546],
'kinds': ['Cu2'],
'symbols': ['Cu'],
'positions': [[0.0, 0.0, 0.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.to_dict()
{'pbc': [True, True, True],
'cell': [[3.84, 0.0, 2.351321854362918e-16],
[1.92, 2.7152900397563426, -1.919999999999999],
[0.0, 0.0, 3.84]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Si',
'kind_name': 'Si',
'position': [0.0, 0.0, 0.0],
'mass': 28.0855,
'charge': 1.0,
'magmom': [0.0, 0.0, 0.0],
'weights': (1,)},
{'symbol': 'Si',
'kind_name': 'Si',
'position': [3.84, 1.3576450198781713, 1.9200000000000006],
'mass': 28.0855,
'charge': 0.0,
'magmom': [0.0, 0.0, 0.0],
'weights': (1,)}],
'cell_volume': 40.038580810231124,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 40.038580810231124},
'charges': [1.0, 0.0],
'magmoms': [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
'masses': [28.0855, 28.0855],
'kinds': ['Si', 'Si'],
'symbols': ['Si', 'Si'],
'positions': [[0.0, 0.0, 0.0],
[3.84, 1.3576450198781713, 1.9200000000000006]],
'formula': 'Si2'}
Moreover, we also provide to_ase
and to_pymatgen
methods to obtain the corresponding instances.
Passing from StructureData to StructureDataMutable and viceversa¶
mutable_structure.to_immutable() # returns an instance of StructureData
structure.to_mutable() # returns an instance of StructureDataMutable
<aiida_atomistic.data.structure.structure.StructureDataMutable at 0x7383406166b0>
Mutation of a StructureData
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 (to be implemented) like hubbard, which is not supported in ASE and Pymatgen.
StructureDataMutable
properties can be modified directly, but also the class contains several 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.to_mutable()
mutable_structure.set_charges([1, 0])
mutable_structure.set_kind_names(['Si2','Si1'])
new_structure = mutable_structure.to_immutable()
print(f"new charges, kinds:\n{new_structure.properties.charges}, {new_structure.properties.kinds}")
new charges, kinds:
[1.0, 0.0], ['Si2', 'Si1']
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()
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({
'symbol':'Si',
'position':[3/4, 3/4, 3/4],
'charge': 1,
'kind_name': 'Si2'
})
mutable_structure.add_atom({
'symbol':'Si',
'position':[1/2, 1/2, 1/2],
'charge': 0,
'kind_name': 'Si1'
})
mutable_structure.to_dict()
/home/aiida/codes/aiida-atomistic/src/aiida_atomistic/data/structure/models.py:170: UserWarning: using default cell
warnings.warn("using default cell")
{'pbc': [True, True, True],
'cell': [[0.0, 1.8, 1.8], [1.8, 0.0, 1.8], [1.8, 1.8, 0.0]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Si',
'kind_name': 'Si2',
'position': [0.75, 0.75, 0.75],
'mass': 28.0855,
'charge': 1.0,
'magmom': None,
'weights': (1,)},
{'symbol': 'Si',
'kind_name': 'Si1',
'position': [0.5, 0.5, 0.5],
'mass': 28.0855,
'charge': 0.0,
'magmom': None,
'weights': (1,)}],
'cell_volume': 11.664000000000001,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 11.664000000000001},
'charges': [1.0, 0.0],
'magmoms': [None, None],
'masses': [28.0855, 28.0855],
'kinds': ['Si2', 'Si1'],
'symbols': ['Si', 'Si'],
'positions': [[0.75, 0.75, 0.75], [0.5, 0.5, 0.5]],
'formula': 'Si2'}
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]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Si',
'kind_name': 'Si2',
'position': [0.75, 0.75, 0.75],
'mass': 28.0855,
'charge': 1.0,
'magmom': None,
'weights': (1.0,)}],
'cell_volume': 11.664000000000001,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 11.664000000000001},
'charges': [1.0],
'magmoms': [None],
'masses': [28.0855],
'kinds': ['Si2'],
'symbols': ['Si'],
'positions': [[0.75, 0.75, 0.75]],
'formula': 'Si'}
Automatic kinds generation¶
It is possible to automatically detect kinds 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. In summary, we can generate our StructureData/StructureDataMutable with automatic kind detection in these three ways:
- new_structuredata = StructureData.from_ase(ase_structure, detect_kinds=True)
- new_structuredata = StructureData.from_pymatgen(pymatgen_structure, detect_kinds=True)
- new_structuredata = StructureData(**old_structuredata.to_dict(detect_kinds=True))
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': [{'symbol': 'Fe',
'mass': 55.845,
'position': [0.0, 0.0, 0.0],
'charge': 0.0,
'magmom': [2.5, 0.1, 0.1],
'kind_name': 'Fe'},
{'symbol': 'Fe',
'mass': 55.845,
'position': [1.42015, 1.42015, 1.4201500000000002],
'charge': 0.0,
'magmom': [2.4, 0.1, 0.1],
'kind_name': 'Fe'}]}
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]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Fe',
'kind_name': 'Fe0',
'position': [0.0, 0.0, 0.0],
'mass': 55.845,
'charge': 0.0,
'magmom': [2.5, 0.1, 0.1],
'weights': (1.0,)},
{'symbol': 'Fe',
'kind_name': 'Fe1',
'position': [1.42015, 1.42015, 1.4201500000000002],
'mass': 55.845,
'charge': 0.0,
'magmom': [2.4, 0.1, 0.1],
'weights': (1.0,)}],
'cell_volume': 22.913563806827,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 22.913563806827},
'charges': [0.0, 0.0],
'magmoms': [[2.5, 0.1, 0.1], [2.4, 0.1, 0.1]],
'masses': [55.845, 55.845],
'kinds': ['Fe0', 'Fe1'],
'symbols': ['Fe', 'Fe'],
'positions': [[0.0, 0.0, 0.0], [1.42015, 1.42015, 1.4201500000000002]],
'formula': 'Fe2'}
We can also directly put our new sites in the starting mutable_structure
:
mutable_structure.clear_sites()
for site in new_mutable_structure.to_dict()['sites']:
mutable_structure.add_atom(site)
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]],
'tot_charge': None,
'tot_magnetization': None,
'custom': None,
'sites': [{'symbol': 'Fe',
'kind_name': 'Fe0',
'position': [0.0, 0.0, 0.0],
'mass': 55.845,
'charge': 0.0,
'magmom': [2.5, 0.1, 0.1],
'weights': (1,)},
{'symbol': 'Fe',
'kind_name': 'Fe1',
'position': [1.42015, 1.42015, 1.4201500000000002],
'mass': 55.845,
'charge': 0.0,
'magmom': [2.4, 0.1, 0.1],
'weights': (1,)}],
'cell_volume': 22.913563806827,
'dimensionality': {'dim': 3, 'label': 'volume', 'value': 22.913563806827},
'charges': [0.0, 0.0],
'magmoms': [[2.5, 0.1, 0.1], [2.4, 0.1, 0.1]],
'masses': [55.845, 55.845],
'kinds': ['Fe0', 'Fe1'],
'symbols': ['Fe', 'Fe'],
'positions': [[0.0, 0.0, 0.0], [1.42015, 1.42015, 1.4201500000000002]],
'formula': 'Fe2'}
How to Query StructureData using properties¶
Thanks to the additional computed properties in our StructureData
(formula, symbols, kinds, masses, charges, magmoms, positions, cell_volume, dimensionality), we can easily query for a structure:
from aiida.orm import QueryBuilder
stored = new_mutable_structure.to_immutable().store()
print(stored.pk)
qb = QueryBuilder()
qb.append(StructureData,
filters={'attributes.formula': 'Fe2'},
)
print(qb.all()[-1])
387
[<StructureData: uuid: aa1a31fc-7422-4d56-8c43-9529a3b8c4f9 (pk: 387)>]
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. This can be done by providing as symbol the combination of the symbols, and also 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': [{'symbol': 'CuAl',
'position': [0.0, 0.0, 0.0],
'weights': (0.5,0.5)
}],})
structure.properties.sites[0].dict()
{'symbol': 'CuAl',
'kind_name': 'CuAl',
'position': [0.0, 0.0, 0.0],
'mass': 45.263768999999996,
'charge': 0,
'magmom': [0.0, 0.0, 0.0],
'weights': (0.5, 0.5)}
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(structure.is_alloy)
print(structure.has_vacancies)
True
False
How to add custom properties¶
It is possible to add custom properties at the StructureData
level (not at the Site
level). 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': [{'symbol': 'Cu',
'position': [0.0, 0.0, 0.0],
}],
'custom': {
'electronic_type': 'metal',
}
})
structure.properties.custom
{'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.
Backward compatibility support¶
We can use the to_legacy
method to return the corresponding orm.StructureData
instance starting from a StructureData
or StructureDataMutable
instance, if a given plugin does not yet support the new StructureData
.