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 the vacuum argument if larger than 0.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_tolerance and hall_number are 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 to None.

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()`.