Source code for crystal_toolkit.components.transformations.grainboundary

import dash
from dash import dcc
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from pymatgen.transformations.advanced_transformations import (
    GrainBoundaryGenerator,
    GrainBoundaryTransformation,
)

from crystal_toolkit.components.transformations.core import TransformationComponent
from crystal_toolkit.helpers.layouts import add_label_help


[docs]class GrainBoundaryTransformationComponent(TransformationComponent): @property def title(self): return "Make a grain boundary" @property def description(self): return """Create a grain boundary within a periodic supercell. This transformation requires sensible inputs, and will be slow to run in certain cases. When using this transformation a new site property is added which can be used to colour-code the top and bottom grains.""" @property def transformation(self): return GrainBoundaryTransformation
[docs] def options_layouts(self, state=None, structure=None): state = state or { "rotation_axis": [0, 0, 1], "rotation_angle": None, "expand_times": 2, "vacuum_thickness": 0, "ab_shift": [0, 0], "normal": False, "ratio": None, "plane": None, "max_search": 20, "tol_coi": 1e-8, "rm_ratio": 0.7, "quick_gen": False, } rotation_axis = self.get_numerical_input( label="Rotation axis", kwarg_label="rotation_axis", state=state, help_str="""Maximum number of atoms allowed in the supercell.""", shape=(3,), ) # sigma isn't a direct input into the transformation, but has # to be calculated from the rotation_axis and structure _, sigma_options, _ = self._get_sigmas_options_and_ratio( structure, state.get("rotation_axis") ) sigma = dcc.Dropdown( id=self.id("sigma"), style={"width": "5rem"}, options=sigma_options, value=sigma_options[0]["value"] if sigma_options else None, ) sigma = add_label_help( sigma, "Sigma", "The unit cell volume of the coincidence site lattice relative to " "input unit cell is denoted by sigma.", ) # likewise, rotation_angle is then a function of sigma, so # best determined using sigma to provide a default value: # this is initialized via a callback rotation_angle = self.get_choice_input( label="Rotation angle", kwarg_label="rotation_angle", state=state, # starts as None help_str="""Rotation angle to generate grain boundary. Options determined by your choice of Σ.""", style={"width": "15rem"}, ) expand_times = self.get_numerical_input( label="Expand times", kwarg_label="expand_times", state=state, help_str="""The multiple number of times to expand one unit grain into a larger grain. This is useful to avoid self-interaction issues when using the grain boundary as an input to further simulations.""", is_int=True, shape=(), min=1, max=6, ) vacuum_thickness = self.get_numerical_input( label="Vacuum thickness /Å", kwarg_label="vacuum_thickness", state=state, help_str="""The thickness of vacuum that you want to insert between the two grains.""", shape=(), ) ab_shift = self.get_numerical_input( label="In-plane shift", kwarg_label="ab_shift", state=state, help_str="""In-plane shift of the two grains given in units of the **a** and **b** vectors of the grain boundary.""", shape=(2,), ) normal = self.get_bool_input( label="Set normal direction", kwarg_label="normal", state=state, help_str="Enable to require the **c** axis of the top grain to be perpendicular to the surface.", ) plane = self.get_numerical_input( label="Grain boundary plane", kwarg_label="plane", state=state, help_str="""Grain boundary plane in the form of a list of integers. If not set, grain boundary will be a twist grain boundary. The plane will be perpendicular to the rotation axis.""", shape=(3,), ) tol_coi = self.get_numerical_input( label="Coincidence Site Tolerance", kwarg_label="tol_coi", state=state, help_str="""Tolerance to find the coincidence sites. To check the number of coincidence sites are correct or not, you can compare the generated grain boundary's sigma with expected number.""", shape=(), ) rm_ratio = self.get_numerical_input( label="Site Merging Tolerance", kwarg_label="rm_ratio", state=state, help_str="""The criteria to remove the atoms which are too close with each other relative to the bond length in the bulk system.""", shape=(), ) return [ rotation_axis, sigma, rotation_angle, expand_times, vacuum_thickness, ab_shift, normal, plane, tol_coi, rm_ratio, ]
@staticmethod def _get_sigmas_options_and_ratio(structure, rotation_axis): rotation_axis = [int(i) for i in rotation_axis] lat_type = ( "c" # assume cubic if no structure specified, just to set initial choices ) ratio = None if structure: sga = SpacegroupAnalyzer(structure) lat_type = sga.get_lattice_type()[0] # this should be fixed in pymatgen try: ratio = GrainBoundaryGenerator(structure).get_ratio() except Exception: ratio = None cutoff = 10 if lat_type.lower() == "c": sigmas = GrainBoundaryGenerator.enum_sigma_cubic( cutoff=cutoff, r_axis=rotation_axis ) elif lat_type.lower() == "t": sigmas = GrainBoundaryGenerator.enum_sigma_tet( cutoff=cutoff, r_axis=rotation_axis, c2_a2_ratio=ratio ) elif lat_type.lower() == "o": sigmas = GrainBoundaryGenerator.enum_sigma_ort( cutoff=cutoff, r_axis=rotation_axis, c2_b2_a2_ratio=ratio ) elif lat_type.lower() == "h": sigmas = GrainBoundaryGenerator.enum_sigma_hex( cutoff=cutoff, r_axis=rotation_axis, c2_a2_ratio=ratio ) elif lat_type.lower() == "r": sigmas = GrainBoundaryGenerator.enum_sigma_rho( cutoff=cutoff, r_axis=rotation_axis, ratio_alpha=ratio ) else: return [], None, ratio options = [] subscript_unicode_map = { 0: "₀", 1: "₁", 2: "₂", 3: "₃", 4: "₄", 5: "₅", 6: "₆", 7: "₇", 8: "₈", 9: "₉", } for sigma in sorted(sigmas): sigma_label = f{sigma}" for k, v in subscript_unicode_map.items(): sigma_label = sigma_label.replace(str(k), v) options.append({"label": sigma_label, "value": sigma}) return sigmas, options, ratio
[docs] def generate_callbacks(self, app, cache): super().generate_callbacks(app, cache) @app.callback( Output(self.id("sigma"), "options"), [Input(self.get_kwarg_id("rotation_axis"), "value")], [State(self.id("input_structure"), "data")], ) def update_sigma_options(rotation_axis, structure): rotation_axis = self.reconstruct_kwarg_from_state( dash.callback_context.inputs, "rotation_axis" ) if (rotation_axis is None) or (not structure): raise PreventUpdate structure = self.from_data(structure) _, sigma_options, _ = self._get_sigmas_options_and_ratio( structure=structure, rotation_axis=rotation_axis ) # TODO: add some sort of error handling here when sigmas is empty return sigma_options @app.callback( Output(self.id("rotation_angle", is_kwarg=True, hint="literal"), "options"), [ Input(self.id("sigma"), "value"), Input(self.get_kwarg_id("rotation_axis"), "value"), ], [State(self.id("input_structure"), "data")], ) def update_rotation_angle_options(sigma, rotation_axis, structure): if not sigma: raise PreventUpdate rotation_axis = self.reconstruct_kwarg_from_state( dash.callback_context.inputs, "rotation_axis" ) if (rotation_axis is None) or (not structure): raise PreventUpdate structure = self.from_data(structure) sigmas, _, _ = self._get_sigmas_options_and_ratio( structure=structure, rotation_axis=rotation_axis ) rotation_angles = sigmas[sigma] options = [] for rotation_angle in sorted(rotation_angles): options.append( {"label": f"{rotation_angle:.2f}º", "value": rotation_angle} ) return options # TODO: make client-side callback @app.callback( [Output(self.id("sigma"), "value"), Output(self.id("sigma"), "disabled")], [ Input(self.id("sigma"), "options"), Input(self.id("enable_transformation"), "on"), ], ) def update_default_value(options, enabled): if not options: raise PreventUpdate return options[0]["value"], enabled # TODO: make client-side callback, or just combine all callbacks here @app.callback( Output(self.id("rotation_angle", is_kwarg=True, hint="literal"), "value"), [ Input( self.id("rotation_angle", is_kwarg=True, hint="literal"), "options" ) ], ) def update_default_value(options): if not options: raise PreventUpdate return options[0]["value"]