Creating surfaces from bulk crystals¶
The SurfaceGeneration class has the purpose to otain surfaces from bulk crystals based on the Miller indices of the surface direction.
Details on the implementation and an application is given in doi:10.1038/s41524-024-01224-7.
Starting point is a bulk crystal, here we take the cubic GaAs phase which is set upon initialization of the SurfaceGeneration object:
[1]:
from aim2dat.strct import Structure, SurfaceGeneration
strct_crystal = Structure(
label="GaAs",
elements=["Ga", "As"],
positions=[
[0.0, 0.0, 0.0],
[0.75, 0.75, 0.75],
],
cell=[
[0.0, 4.066, 4.0660001],
[4.066, 0.0, 4.066],
[4.066, 4.066, 0.0],
],
is_cartesian=False,
pbc=[True, True, True],
)
surf_gen = SurfaceGeneration(strct_crystal)
For the cubic system there are three different low index directions: denoted by the (100), (110) and the (111) Miller indices. We can quickly create surface slabs by calling the function generate_surface_slabs which returns a StructureCollection object containing one surface for each termination:
[2]:
surfaces_100 = surf_gen.generate_surface_slabs(
miller_indices=(1, 0, 0),
nr_layers=5,
periodic=False,
vacuum=10.0,
vacuum_factor=0.0,
symmetrize=True,
tolerance=0.01,
symprec=0.005,
angle_tolerance=-1.0,
hall_number=0,
)
The following arguments of the function control the slab’s properties:
miller_indices: gives the surface direction. Since Miller indices are usually defined for the conventional unit cell (in this example we created the primitve unit cell) the class makes use of the spglib python package to transform the primitive into the conventional unit cell before using the ase python package to obtain the surface structures. The last 3 keyword arguments are therefore directly passed to the spglib function to determine the space group.nr_layers: defines the slab size in the non-periodic direction normal to the surface plane in repetition units.periodic: periodic boundary condition in the direction normal to the surface plane.vacuum: amount of vacuum space added to separate the bottom and top surface facet.vacuum_factor: overwrites thevacuumargument if larger than0.0. It adds vacuum space as a multiple of the slab size.symmetrize: whether to return a slab with two equivalent terminations on each side or an asymmetric slab which maintains the stoichiometry of the bulk crystal (the bottom and top termination may be unequivalent).tolerance: numerical tolerance parameter to determine equivalent terminations.symprec,angle_toleranceandhall_numberare parameters passed to spglib to determine the conventional unit cell of the input crystal.
The algorithm found two different terminations for the (100) direction:
[3]:
print(surfaces_100)
----------------------------------------------------------------------
------------------------ Structure Collection ------------------------
----------------------------------------------------------------------
- Number of structures: 2
- Elements: As-Ga
Structures
- GaAs_100_1 Ga22As20 [True True False]
- GaAs_100_2 As22Ga20 [True True False]
----------------------------------------------------------------------
Surfaces as input to high-throughput workflows and AiiDA integration¶
In order to automatically converge the slab size in an efficient way it is useful to have all the building blocks to create different slabs with a certain termination.
This information can be returned using the create_surface function, e.g. in this case for the first termination of the (100) direction:
[4]:
surf_details = surf_gen.create_surface(
miller_indices=(1, 0, 0),
termination=1,
tolerance=0.01,
symprec=0.005,
angle_tolerance=-1.0,
hall_number=0,
)
surf_details.keys()
[4]:
dict_keys(['repeating_structure', 'bottom_structure', 'top_structure', 'top_structure_nsym'])
The output of the function is a dictionary containing the different building blocks to construct a surface slab:
The key
'repeating_structure'contains the structure which is repeated and translated in the non-periodic direction (the number repititions defines the number of layers).The key
'bottom_structure'obtains the structure of the bottom termination of the slab.The keys
'top_structure'and'top_structure_nsym'contain the terminations for a symmetric or a stoichiometric slab, respectively. In case the symmetric slab is already stoichiometric'top_structure_nsym'is set toNone.
The SurfaceData class can store exactly this information as an AiiDA data node and the calculation function create_surface_slab can be included in high-throughput workflows to create surface slabs on the fly.
The AiiDA SurfaceData node can also be created straight-away using the SurfaceGeneration class:
[5]:
from aiida import load_profile
load_profile("tests")
surf_node = surf_gen.to_aiida_surfacedata(miller_indices=(1, 0, 0))
---------------------------------------------------------------------------
ProfileConfigurationError Traceback (most recent call last)
Cell In[5], line 3
1 from aiida import load_profile
----> 3 load_profile("tests")
5 surf_node = surf_gen.to_aiida_surfacedata(miller_indices=(1, 0, 0))
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/__init__.py:165, in load_profile(profile, allow_switch)
152 """Load a global profile, unloading any previously loaded profile.
153
154 .. note:: if a profile is already loaded and no explicit profile is specified, nothing will be done
(...)
161 if another profile has already been loaded and allow_switch is False
162 """
163 from aiida.manage import get_manager
--> 165 return get_manager().load_profile(profile, allow_switch)
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:117, in Manager.load_profile(self, profile, allow_switch)
114 return self._profile
116 if profile is None or isinstance(profile, str):
--> 117 profile = self.get_config().get_profile(profile)
118 elif not isinstance(profile, Profile):
119 raise TypeError(f'profile must be None, a string, or a Profile instance, got: {type(profile)}')
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/config.py:453, in Config.get_profile(self, name)
450 if not name:
451 name = self.default_profile_name
--> 453 self.validate_profile(name)
455 return self._profiles[name]
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/configuration/config.py:435, in Config.validate_profile(self, name)
432 from aiida.common import exceptions
434 if name not in self.profile_names:
--> 435 raise exceptions.ProfileConfigurationError(f'profile `{name}` does not exist')
ProfileConfigurationError: profile `tests` does not exist
And all data nodes for specific Miller indices can be stored in the database in a group using the store_surfaces_in_aiida_db function:
[6]:
surf_gen.store_surfaces_in_aiida_db(
miller_indices=(1, 0, 0), group_label="GaAs_100_surfaces"
)
---------------------------------------------------------------------------
ConfigurationError Traceback (most recent call last)
Cell In[6], line 1
----> 1 surf_gen.store_surfaces_in_aiida_db(
2 miller_indices=(1, 0, 0), group_label="GaAs_100_surfaces"
3 )
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/strct/surface.py:176, in SurfaceGeneration.store_surfaces_in_aiida_db(self, miller_indices, tolerance, symprec, angle_tolerance, hall_number, group_label, group_description)
161 surfaces.append(
162 {
163 "label": (
(...)
173 }
174 )
175 ter += 1
--> 176 return backend_module._store_surfaces(group_label, group_description, surfaces)
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/ext_interfaces/aiida.py:194, in _store_surfaces(group_label, group_description, surfaces)
192 group = None
193 if group_label is not None:
--> 194 group = _create_group(group_label, group_description)
195 for surface_dict in surfaces:
196 surface = _create_surface_node(**surface_dict)
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aim2dat/ext_interfaces/aiida.py:34, in _create_group(group_label, group_description)
32 def _create_group(group_label, group_description):
33 try:
---> 34 group = aiida_orm.Group.collection.get(label=group_label)
35 except NotExistent:
36 group = aiida_orm.Group(label=group_label, description=group_description)
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/common/lang.py:98, in classproperty.__get__(self, instance, owner)
97 def __get__(self, instance: Any, owner: SelfType) -> ReturnType:
---> 98 return self.getter(owner)
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/orm/entities.py:188, in Entity.collection(cls)
182 @classproperty
183 def collection(cls) -> CollectionType: # noqa: N805
184 """Get a collection for objects of this type, with the default backend.
185
186 :return: an object that can be used to access entities of this type
187 """
--> 188 return cls._CLS_COLLECTION.get_cached(cls, get_manager().get_profile_storage())
File ~/checkouts/readthedocs.org/user_builds/aim2dat/envs/latest/lib/python3.10/site-packages/aiida/manage/manager.py:254, in Manager.get_profile_storage(self)
252 profile = self.get_profile()
253 if profile is None:
--> 254 raise ConfigurationError(
255 'Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.'
256 )
258 # request access to the profile (for example, if it is being used by a maintenance operation)
259 ProfileAccessManager(profile).request_access()
ConfigurationError: Could not determine the current profile. Consider loading a profile using `aiida.load_profile()`.