Source code for crystal_toolkit.components.symmetry

from fractions import Fraction

import numpy as np
from dash import callback_context, html
from dash.dependencies import Input, Output
from pymatgen.core.structure import Structure
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from pymatgen.util.string import unicodeify_spacegroup, unicodeify_species

from crystal_toolkit.core.panelcomponent import PanelComponent
from crystal_toolkit.helpers.layouts import (
    H5,
    Column,
    Columns,
    Loading,
    get_data_list,
    get_table,
)


[docs]class SymmetryPanel(PanelComponent):
[docs] @staticmethod def pretty_frac_format(x): x = x % 1 fraction = Fraction(x).limit_denominator(8) if np.allclose(x, 1): x_str = "0" elif not np.allclose(x, float(fraction)): x = np.around(x, decimals=3) x_str = f"{x:.3g}" else: x_str = str(fraction) return x_str
@property def title(self): return "Symmetry" @property def description(self): return "Analyze the symmetry of your crystal structure or molecule."
[docs] def contents_layout(self): state = {"symprec": 0.01, "angle_tolerance": 5} symprec = self.get_numerical_input( label="Symmetry-finding tolerance", kwarg_label="symprec", state=state, help_str="Tolerance of distance between atomic positions and between lengths " "of lattice vectors to be tolerated in the symmetry finding in Ã…ngstroms. " "The angle distortion between lattice vectors is converted to a length and " "compared with this distance tolerance.", shape=(), min=0, ) angle_tolerance = self.get_numerical_input( label="Angle tolerance", kwarg_label="angle_tolerance", state=state, help_str="Explicit angle tolerance for symmetry finding in degrees. " "Set to a negative value to disable.", shape=(), ) return html.Div( [ symprec, angle_tolerance, html.Br(), html.Br(), Loading(id=self.id("analysis")), ] )
[docs] def generate_callbacks(self, app, cache): super().generate_callbacks(app, cache) @app.callback( Output(self.id("analysis"), "children"), [ Input(self.id(), "data"), Input(self.get_kwarg_id("symprec"), "value"), Input(self.get_kwarg_id("angle_tolerance"), "value"), ], ) def update_contents(data, symprec, angle_tolerance): if not data: return html.Div() struct = self.from_data(data) if not isinstance(struct, Structure): return html.Div( "Can only analyze symmetry of crystal structures at present." ) kwargs = self.reconstruct_kwargs_from_state(callback_context.inputs) symprec = kwargs["symprec"] angle_tolerance = kwargs["angle_tolerance"] if symprec <= 0: return html.Span( f"Please use a positive symmetry-finding tolerance (currently {symprec})." ) sga = SpacegroupAnalyzer( struct, symprec=symprec, angle_tolerance=angle_tolerance ) try: data = dict() data["Crystal System"] = sga.get_crystal_system().title() data["Lattice System"] = sga.get_lattice_type().title() data["Hall Number"] = sga.get_hall() data["International Number"] = sga.get_space_group_number() data["Symbol"] = unicodeify_spacegroup(sga.get_space_group_symbol()) data["Point Group"] = unicodeify_spacegroup( sga.get_point_group_symbol() ) sym_struct = sga.get_symmetrized_structure() except Exception: return html.Span( f"Failed to calculate symmetry with this combination of " f"symmetry-finding ({symprec}) and angle tolerances ({angle_tolerance})." ) datalist = get_data_list(data) wyckoff_contents = [] wyckoff_data = sorted( zip(sym_struct.wyckoff_symbols, sym_struct.equivalent_sites), key=lambda x: "".join(filter(lambda w: w.isalpha(), x[0])), ) for symbol, equiv_sites in wyckoff_data: wyckoff_contents.append( html.Label( f"{symbol}, {unicodeify_species(equiv_sites[0].species_string)}", className="mpc-label", ) ) site_data = [ ( self.pretty_frac_format(site.frac_coords[0]), self.pretty_frac_format(site.frac_coords[1]), self.pretty_frac_format(site.frac_coords[2]), ) for site in equiv_sites ] wyckoff_contents.append(get_table(site_data)) return Columns( [ Column([H5("Overview"), datalist]), Column([H5("Wyckoff Positions"), html.Div(wyckoff_contents)]), ] )