def get_molecule_graph_scene( self, origin=(0, 0, 0), explicitly_calculate_polyhedra_hull=False, draw_polyhedra=True, **kwargs ) -> Scene: primitives = defaultdict(list) for idx, site in enumerate(self.molecule): connected_sites = self.get_connected_sites(idx) site_scene = site.get_scene( connected_sites=connected_sites, all_connected_sites_present=draw_polyhedra, origin=origin, explicitly_calculate_polyhedra_hull=explicitly_calculate_polyhedra_hull, ) for scene in site_scene.contents: primitives[scene.name] += scene.contents return Scene( name=self.molecule.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], )
def get_molecule_graph_scene( self, origin=None, explicitly_calculate_polyhedra_hull=False, legend=None, draw_polyhedra=False, ) -> Scene: legend = legend or Legend(self.molecule) primitives = defaultdict(list) for idx, site in enumerate(self.molecule): connected_sites = self.get_connected_sites(idx) site_scene = site.get_scene( connected_sites=connected_sites, origin=origin, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, legend=legend, draw_polyhedra=draw_polyhedra, ) for scene in site_scene.contents: primitives[scene.name] += scene.contents return Scene( name=self.molecule.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], origin=origin if origin else (0, 0, 0), )
def get_scene_from_molecule(self, origin=None, legend: Optional[Legend] = None): """ Create CTK objects for the lattice and sties Args: self: Structure object origin: fractional coordinate of the origin legend: Legend for the sites Returns: CTK scene object to be rendered """ origin = origin if origin else (0, 0, 0) legend = legend or Legend(self) primitives = defaultdict(list) for idx, site in enumerate(self): site_scene = site.get_scene( origin=origin, legend=legend, ) for scene in site_scene.contents: primitives[scene.name] += scene.contents return Scene( name=self.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], origin=origin, )
def get_structure_scene( self, draw_image_atoms=True, legend: Optional[Legend] = None, origin=None, ) -> Scene: origin = origin or list( -self.lattice.get_cartesian_coords([0.5, 0.5, 0.5])) legend = legend or Legend(self) primitives = defaultdict(list) sites_to_draw = self._get_sites_to_draw(draw_image_atoms=draw_image_atoms) for (idx, jimage) in sites_to_draw: site_scene = self[idx].get_scene(origin=origin, legend=legend) for scene in site_scene.contents: primitives[scene.name] += scene.contents primitives["unit_cell"].append(self.lattice.get_scene(origin=origin)) return Scene( name=self.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], origin=origin, )
def get_scene_and_legend( graph: Optional[Union[StructureGraph, MoleculeGraph]], name, color_scheme=DEFAULTS["color_scheme"], color_scale=None, radius_strategy=DEFAULTS["radius_strategy"], draw_image_atoms=DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell=DEFAULTS[ "bonded_sites_outside_unit_cell"], hide_incomplete_bonds=DEFAULTS["hide_incomplete_bonds"], explicitly_calculate_polyhedra_hull=False, scene_additions=None, show_compass=DEFAULTS["show_compass"], ) -> Tuple[Scene, Dict[str, str]]: # default scene name will be name of component, "_ct_..." # strip leading _ since this will cause problems in JavaScript land scene = Scene(name=name[1:]) if graph is None: return scene, {} struct_or_mol = StructureMoleculeComponent._get_struct_or_mol(graph) # TODO: add radius_scale legend = Legend( struct_or_mol, color_scheme=color_scheme, radius_scheme=radius_strategy, cmap_range=color_scale, ) if isinstance(graph, StructureGraph): scene = graph.get_scene( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, hide_incomplete_edges=hide_incomplete_bonds, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, legend=legend, ) elif isinstance(graph, MoleculeGraph): scene = graph.get_scene(legend=legend) scene.name = name if hasattr(struct_or_mol, "lattice"): axes = struct_or_mol.lattice._axes_from_lattice() # TODO: fix pop-in ? axes.visible = show_compass scene.contents.append(axes) if scene_additions: # TODO: need a Scene.from_json() to make this work raise NotImplementedError scene["contents"].append(scene_additions) return scene.to_json(), legend.get_legend()
def get_scene_and_legend( graph: Optional[Union[StructureGraph, MoleculeGraph]], color_scheme=DEFAULTS["color_scheme"], color_scale=None, radius_strategy=DEFAULTS["radius_strategy"], draw_image_atoms=DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell=DEFAULTS[ "bonded_sites_outside_unit_cell"], hide_incomplete_bonds=DEFAULTS["hide_incomplete_bonds"], explicitly_calculate_polyhedra_hull=False, scene_additions=None, show_compass=DEFAULTS["show_compass"], group_by_site_property=None, ) -> Tuple[Scene, Dict[str, str]]: scene = Scene(name="StructureMoleculeComponentScene") if graph is None: return scene, {} struct_or_mol = StructureMoleculeComponent._get_struct_or_mol(graph) # TODO: add radius_scale legend = Legend( struct_or_mol, color_scheme=color_scheme, radius_scheme=radius_strategy, cmap_range=color_scale, ) if isinstance(graph, StructureGraph): scene = graph.get_scene( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, hide_incomplete_edges=hide_incomplete_bonds, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, group_by_site_property=group_by_site_property, legend=legend, ) elif isinstance(graph, MoleculeGraph): scene = graph.get_scene(legend=legend) scene.name = "StructureMoleculeComponentScene" if hasattr(struct_or_mol, "lattice"): axes = struct_or_mol.lattice._axes_from_lattice() axes.visible = show_compass scene.contents.append(axes) scene_json = scene.to_json() if scene_additions: # TODO: this might be cleaner if we had a Scene.from_json() method scene_json["contents"].append(scene_additions) return scene_json, legend.get_legend()
def scenes(self): result = {} if len(self.scene_dicts) == 0: return {"empty_scene": Scene("empty", contents=[])} for k, v in self.scene_dicts.items(): pos = [ vert for triangle in v["vertices"][v["faces"]].tolist() for vert in triangle ] result[k] = Scene(k, contents=[Surface(positions=pos)]) return result
def get_structure_scene( self, origin: List[float] = None, legend: Optional[Legend] = None, draw_image_atoms: bool = True, ) -> Scene: """ Create CTK objects for the lattice and sties Args: self: Structure object origin: fractional coordinate of the origin legend: Legend for the sites draw_image_atoms: If true draw image atoms that are just outside the periodic boundary Returns: CTK scene object to be rendered """ origin = origin or list( -self.lattice.get_cartesian_coords([0.5, 0.5, 0.5])) legend = legend or Legend(self) primitives = defaultdict(list) sites_to_draw = self._get_sites_to_draw( draw_image_atoms=draw_image_atoms, ) for (idx, jimage) in sites_to_draw: site = self[idx] if jimage != (0, 0, 0): site = PeriodicSite( site.species, np.add(site.frac_coords, jimage), site.lattice, properties=site.properties, ) site_scene = site.get_scene(legend=legend, ) for scene in site_scene.contents: primitives[scene.name] += scene.contents primitives["unit_cell"].append(self.lattice.get_scene()) return Scene( name="Structure", origin=origin, contents=[ Scene(name=k, contents=v, origin=origin) for k, v in primitives.items() ], )
def get_scene_and_legend( graph: Optional[Union[StructureGraph, MoleculeGraph]], color_scale=None, radius_strategy=DEFAULTS["radius_strategy"], draw_image_atoms=DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell=DEFAULTS[ "bonded_sites_outside_unit_cell"], hide_incomplete_bonds=DEFAULTS["hide_incomplete_bonds"], explicitly_calculate_polyhedra_hull=False, scene_additions=None, show_compass=DEFAULTS["show_compass"], ) -> Tuple[Scene, Dict[str, str]]: scene = Scene(name="AceStructureMoleculeComponentScene") if graph is None: return scene, {} structure = StructureComponent._get_structure(graph) # TODO: add radius_scale legend = Legend( structure, color_scheme="VESTA", radius_scheme=radius_strategy, cmap_range=color_scale, ) scene = graph.get_scene( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, hide_incomplete_edges=hide_incomplete_bonds, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, legend=legend, ) scene.name = "StructureComponentScene" if hasattr(structure, "lattice"): axes = structure.lattice._axes_from_lattice() axes.visible = show_compass scene.contents.append(axes) scene = scene.to_json() if scene_additions: # TODO: need a Scene.from_json() to make this work # raise NotImplementedError scene["contents"].append(scene_additions) return scene, legend.get_legend()
def get_preview_layout(self, struct_in, struct_out): if struct_in.lattice == struct_out.lattice: return html.Div() lattice_in = struct_in.lattice.get_scene() lattice_out = struct_out.lattice.get_scene(color="red") scene = Scene("lattices", contents=[lattice_in, lattice_out]) return html.Div( [Simple3DScene(data=scene.to_json())], style={"width": "100px", "height": "100px"}, )
def get_isosurface_scene(self, data_key="total", isolvl=0.5, step_size=3, origin=(0.0, 0.0, 0.0), **kwargs): """Get the isosurface from a VolumetricData object Args: data_key (str, optional): Use the volumetric data from self.data[data_key]. Defaults to 'total'. isolvl (float, optional): The cutoff for the isosurface to using the same units as VESTA so e/bhor and kept grid size independent step_size (int, optional): step_size parameter for marching_cubes_lewiner. Defaults to 3. Returns: [type]: [description] """ vol_data = np.copy(self.data[data_key]) vol = self.structure.volume vol_data = vol_data / vol / _ANGS2_TO_BOHR3 padded_data = np.pad(vol_data, (0, 1), "wrap") vertices, faces, normals, values = measure.marching_cubes_lewiner( padded_data, level=isolvl, step_size=step_size) # transform to fractional coordinates vertices = vertices / (vol_data.shape[0], vol_data.shape[1], vol_data.shape[2]) vertices = np.dot(vertices, self.structure.lattice.matrix) # transform to cartesian pos = [vert for triangle in vertices[faces].tolist() for vert in triangle] return Scene("isosurface", origin=origin, contents=[Surface(pos, show_edges=False, **kwargs)])
def get_volumetric_scene(self, data_key="total", isolvl=0.5, step_size=3, **kwargs): """Get the Scene object which contains a structure and a isosurface components Args: data_key (str, optional): Use the volumetric data from self.data[data_key]. Defaults to 'total'. isolvl (float, optional): The cuoff for the isosurface to using the same units as VESTA so e/bhor and kept grid size independent step_size (int, optional): step_size parameter for marching_cubes_lewiner. Defaults to 3. **kwargs: kwargs for the Structure.get_scene function Returns: [type]: [description] """ struct_scene = self.structure.get_scene(**kwargs) iso_scene = self.get_isosurface_scene( data_key=data_key, isolvl=isolvl, step_size=step_size, origin=struct_scene.origin, ) return Scene( name=self.structure.composition.reduced_formula, origin=struct_scene.origin, contents=[struct_scene, iso_scene], )
def scene(self, name=""): faces = self.scene_dict["faces"] pos = [ vert for triangle in self.scene_dict["vertices"][faces].tolist() for vert in triangle ] return Scene(name=name, contents=[Surface(positions=pos)])
def get_structure_scene( self, origin=None, draw_image_atoms=True, bonded_sites_outside_unit_cell=False, legend: Optional[Legend] = None, ) -> Scene: legend = legend or Legend(self.structure) primitives = defaultdict(list) sites_to_draw = self._get_sites_to_draw( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, ) for (idx, jimage) in sites_to_draw: site = self.structure[idx] if jimage != (0, 0, 0): connected_sites = self.get_connected_sites(idx, jimage=jimage) site = PeriodicSite( site.species, np.add(site.frac_coords, jimage), site.lattice, properties=site.properties, ) else: connected_sites = self.get_connected_sites(idx) site_scene = site.get_scene(origin=origin, legend=legend) for scene in site_scene.contents: primitives[scene.name] += scene.contents primitives["unit_cell"].append( self.structure.lattice.get_scene(origin=origin)) return Scene( name=self.structure.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], origin=origin, )
def _compass_from_lattice( lattice, origin=(0, 0, 0), scale=0.7, offset=0.15, compass_style="corner", **kwargs, ): # TODO: add along lattice """ Get the display components of the compass :param lattice: the pymatgen Lattice object that contains the primitive lattice vectors :param origin: the reference position to place the compass :param scale: scale all the geometric objects that makes up the compass the lattice vectors are normalized before the scaling so everything should be the same size :param offset: shift the compass from the origin by a ratio of the diagonal of the cell relative the size :return: list of cystal_toolkit.helper.scene objects that makes up the compass """ o = -np.array(origin) o = o - offset * (lattice.matrix[0] + lattice.matrix[1] + lattice.matrix[2]) a = lattice.matrix[0] / np.linalg.norm(lattice.matrix[0]) * scale b = lattice.matrix[1] / np.linalg.norm(lattice.matrix[1]) * scale c = lattice.matrix[2] / np.linalg.norm(lattice.matrix[2]) * scale a_arrow = [[o, o + a]] b_arrow = [[o, o + b]] c_arrow = [[o, o + c]] o_sphere = Spheres(positions=[o], color="black", radius=0.1 * scale) return Scene(name='compass', contents=[ Arrows( a_arrow, color="red", radius=0.7 * scale, headLength=2.3 * scale, headWidth=1.4 * scale, **kwargs, ), Arrows( b_arrow, color="blue", radius=0.7 * scale, headLength=2.3 * scale, headWidth=1.4 * scale, **kwargs, ), Arrows( c_arrow, color="green", radius=0.7 * scale, headLength=2.3 * scale, headWidth=1.4 * scale, **kwargs, ), o_sphere, ])
def update_structure(orbital): # orbital is ["0125_up"] scene_additions = Scene( name=orbital[0], contents=[self.scene_dicts.scenes[orbital[0]]]) # Arrows(positionPairs=[[[0,0,0], [s,s,s]]], # radius=0.1, # headLength=0.6, # clickable=True, # headWidth=0.3)]) scene, _ = self.get_scene_and_legend( scene_additions=scene_additions) return scene
def get_volumetric_scene(self, origin=(0, 0, 0), data_key='total', isolvl=2.0, step_size=3, **kwargs): o = -np.array(origin) vertices, faces, normals, values = measure.marching_cubes_lewiner( self.data[data_key], level=isolvl, step_size=step_size) vertices = vertices / self.data[ data_key].shape # transform to fractional coordinates vertices = np.dot(o + vertices, self.structure.lattice.matrix) # transform to cartesian return Scene("volumetric-data", contents=[Surface(vertices, normals, **kwargs)])
def __init__(self, potential_plotter: SitePotentialPlotlyPlotter, eigenvalue_plotter: EigenvaluePlotlyPlotter, scene_dicts: SceneDicts, name: str, vacancy_sites: List[Dict[str, List[float]]] = None, id: str = None, scene_settings: Optional[dict] = None, **kwargs): self.scene_dicts = scene_dicts self.potential_plotter = potential_plotter self.eigenvalue_plotter = eigenvalue_plotter super().__init__(id=id, **kwargs) self.options = [{ "label": i, "value": i } for i in scene_dicts.scenes.keys()] self.create_store("scene_dicts", initial_data=scene_dicts) self.graph = scene_dicts.structure_graph self.vacancy_sites = vacancy_sites or [] # default_data=scene_dicts.structure_graph, self.initial_scene_settings = { "extractAxis": True, "renderer": "webgl", "defaultZoom": 3.0 } if scene_settings: self.initial_scene_settings.update(scene_settings) # self.create_store("scene_settings", # initial_data=self.initial_scene_settings) initial_scene_additions = Scene( name="parchg", contents=[list(scene_dicts.scenes.values())[0]]) self.create_store("scene_additions", initial_data=initial_scene_additions) scene, legend = self.get_scene_and_legend( scene_additions=initial_scene_additions) self.create_store("legend_data", initial_data=legend) self.create_store("graph", initial_data=scene_dicts.structure_graph) self.name = name # this is used by a Simple3DScene component, not a dcc.Store self._initial_data["scene"] = scene
def get_lattice_scene(self, origin=None, show_axes=False, **kwargs): o = -np.array((0, 0, 0)) a, b, c = self.matrix[0], self.matrix[1], self.matrix[2] line_pairs = [ o, o + a, o, o + b, o, o + c, o + a, o + a + b, o + a, o + a + c, o + b, o + b + a, o + b, o + b + c, o + c, o + c + a, o + c, o + c + b, o + a + b, o + a + b + c, o + a + c, o + a + b + c, o + b + c, o + a + b + c, ] line_pairs = [line.tolist() for line in line_pairs] name = ( f"a={self.a}, b={self.b}, c={self.c}, " f"alpha={self.alpha}, beta={self.beta}, gamma={self.gamma}" ) contents = [Lines(line_pairs, **kwargs)] if show_axes: contents.append(self._axes_from_lattice(origin=origin)) return Scene(name, contents, origin=origin)
def get_lattice_scene(self, origin=(0, 0, 0), **kwargs): o = -np.array(origin) a, b, c = self.matrix[0], self.matrix[1], self.matrix[2] line_pairs = [ o, o + a, o, o + b, o, o + c, o + a, o + a + b, o + a, o + a + c, o + b, o + b + a, o + b, o + b + c, o + c, o + c + a, o + c, o + c + b, o + a + b, o + a + b + c, o + a + c, o + a + b + c, o + b + c, o + a + b + c, ] line_pairs = [line.tolist() for line in line_pairs] name = (f"a={self.a}, b={self.b}, c={self.c}, " f"alpha={self.alpha}, beta={self.beta}, gamma={self.gamma}") return Scene(name, contents=[Lines(line_pairs, **kwargs)])
def get_structure_graph_scene( self, origin=(0, 0, 0), draw_image_atoms=True, bonded_sites_outside_unit_cell=True, hide_incomplete_edges=False, incomplete_edge_length_scale=0.3, color_edges_by_edge_weight=True, edge_weight_color_scale="coolwarm", explicitly_calculate_polyhedra_hull=False, ) -> Scene: primitives = defaultdict(list) sites_to_draw = self._get_sites_to_draw( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, ) color_edges = False if color_edges_by_edge_weight: weights = [e[2].get("weight") for e in self.graph.edges(data=True)] weights = np.array([w for w in weights if w]) if any(weights): cmap = get_cmap(edge_weight_color_scale) # try to keep color scheme symmetric around 0 weight_max = max([abs(min(weights)), max(weights)]) weight_min = -weight_max def get_weight_color(weight): if not weight: weight = 0 x = (weight - weight_min) / (weight_max - weight_min) return "#{:02x}{:02x}{:02x}".format( *[int(c * 255) for c in cmap(x)[0:3]]) color_edges = True for (idx, jimage) in sites_to_draw: site = self.structure[idx] if jimage != (0, 0, 0): connected_sites = self.get_connected_sites(idx, jimage=jimage) site = PeriodicSite( site.species, np.add(site.frac_coords, jimage), site.lattice, properties=site.properties, ) else: connected_sites = self.get_connected_sites(idx) connected_sites = [ cs for cs in connected_sites if (cs.index, cs.jimage) in sites_to_draw ] connected_sites_not_drawn = [ cs for cs in connected_sites if (cs.index, cs.jimage) not in sites_to_draw ] if color_edges: connected_sites_colors = [ get_weight_color(cs.weight) for cs in connected_sites ] connected_sites_not_drawn_colors = [ get_weight_color(cs.weight) for cs in connected_sites_not_drawn ] else: connected_sites_colors = None connected_sites_not_drawn_colors = None site_scene = site.get_scene( connected_sites=connected_sites, connected_sites_not_drawn=connected_sites_not_drawn, hide_incomplete_edges=hide_incomplete_edges, incomplete_edge_length_scale=incomplete_edge_length_scale, connected_sites_colors=connected_sites_colors, connected_sites_not_drawn_colors=connected_sites_not_drawn_colors, origin=origin, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, ) for scene in site_scene.contents: primitives[scene.name] += scene.contents # we are here ... # select polyhedra # split by atom type at center # see if any intersect, if yes split further # order sets, with each choice, go to add second set etc if don't intersect # they intersect if centre atom forms vertex of another atom (caveat: centre atom may not actually be inside polyhedra! not checking for this, add todo) # def _set_intersects() ->bool: # def _split_set() ->List: (by type, then..?) # def _order_sets()... pick 1, ask can add 2? etc primitives["unit_cell"].append( self.structure.lattice.get_scene(origin=origin)) return Scene( name=self.structure.composition.reduced_formula, contents=[Scene(name=k, contents=v) for k, v in primitives.items()], )
def __init__( self, struct_or_mol: Optional[Union[Structure, StructureGraph, Molecule, MoleculeGraph]] = None, id: str = None, scene_additions: Optional[Scene] = None, bonding_strategy: str = DEFAULTS["bonding_strategy"], bonding_strategy_kwargs: Optional[dict] = None, color_scheme: str = DEFAULTS["color_scheme"], color_scale: Optional[str] = None, radius_strategy: str = DEFAULTS["radius_strategy"], unit_cell_choice: str = DEFAULTS["unit_cell_choice"], draw_image_atoms: bool = DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell: bool = DEFAULTS[ "bonded_sites_outside_unit_cell"], hide_incomplete_bonds: bool = DEFAULTS["hide_incomplete_bonds"], show_compass: bool = DEFAULTS["show_compass"], scene_settings: Optional[Dict] = None, **kwargs, ): super().__init__(id=id, default_data=struct_or_mol, **kwargs) self.initial_scene_settings = self.default_scene_settings.copy() if scene_settings: self.initial_scene_settings.update(scene_settings) self.create_store("scene_settings", initial_data=self.initial_scene_settings) # unit cell choice and bonding algorithms need to come from a settings # object (in a dcc.Store) guaranteed to be present in layout, rather # than from the controls themselves -- since these are optional and # may not be present in the layout self.create_store( "graph_generation_options", initial_data={ "bonding_strategy": bonding_strategy, "bonding_strategy_kwargs": bonding_strategy_kwargs, "unit_cell_choice": unit_cell_choice, }, ) self.create_store( "display_options", initial_data={ "color_scheme": color_scheme, "color_scale": color_scale, "radius_strategy": radius_strategy, "draw_image_atoms": draw_image_atoms, "bonded_sites_outside_unit_cell": bonded_sites_outside_unit_cell, "hide_incomplete_bonds": hide_incomplete_bonds, "show_compass": show_compass, }, ) if scene_additions: initial_scene_additions = Scene( name="scene_additions", contents=scene_additions).to_json() else: initial_scene_additions = None self.create_store("scene_additions", initial_data=initial_scene_additions) if struct_or_mol: # graph is cached explicitly, this isn't necessary but is an # optimization so that graph is only re-generated if bonding # algorithm changes graph = self._preprocess_input_to_graph( struct_or_mol, bonding_strategy=bonding_strategy, bonding_strategy_kwargs=bonding_strategy_kwargs, ) scene, legend = self.get_scene_and_legend( graph, scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) if hasattr(struct_or_mol, "lattice"): self._lattice = struct_or_mol.lattice else: # component could be initialized without a structure, in which case # an empty scene should be displayed graph = None scene, legend = self.get_scene_and_legend( None, scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) self.create_store("legend_data", initial_data=legend) self.create_store("graph", initial_data=graph) # this is used by a Simple3DScene component, not a dcc.Store self._initial_data["scene"] = scene # hide axes inset for molecules if isinstance(struct_or_mol, Molecule) or isinstance( struct_or_mol, MoleculeGraph): self.scene_kwargs = {"axisView": "HIDDEN"} else: self.scene_kwargs = {}
def get_structure_graph_scene( self, origin=None, draw_image_atoms=True, bonded_sites_outside_unit_cell=True, hide_incomplete_edges=False, incomplete_edge_length_scale=0.3, color_edges_by_edge_weight=True, edge_weight_color_scale="coolwarm", explicitly_calculate_polyhedra_hull=False, legend: Optional[Legend] = None, group_by_symmetry: bool = True, ) -> Scene: origin = origin or list( -self.structure.lattice.get_cartesian_coords([0.5, 0.5, 0.5])) legend = legend or Legend(self.structure) primitives = defaultdict(list) sites_to_draw = self._get_sites_to_draw( draw_image_atoms=draw_image_atoms, bonded_sites_outside_unit_cell=bonded_sites_outside_unit_cell, ) color_edges = False if color_edges_by_edge_weight: weights = [e[2].get("weight") for e in self.graph.edges(data=True)] weights = np.array([w for w in weights if w]) if any(weights): cmap = get_cmap(edge_weight_color_scale) # try to keep color scheme symmetric around 0 weight_max = max([abs(min(weights)), max(weights)]) weight_min = -weight_max def get_weight_color(weight): if not weight: weight = 0 x = (weight - weight_min) / (weight_max - weight_min) return "#{:02x}{:02x}{:02x}".format( *[int(c * 255) for c in cmap(x)[0:3]]) color_edges = True idx_to_wyckoff = {} if group_by_symmetry: sga = SpacegroupAnalyzer(self.structure) struct_sym = sga.get_symmetrized_structure() for equiv_idxs, wyckoff in zip(struct_sym.equivalent_indices, struct_sym.wyckoff_symbols): for idx in equiv_idxs: idx_to_wyckoff[idx] = wyckoff for (idx, jimage) in sites_to_draw: site = self.structure[idx] if jimage != (0, 0, 0): connected_sites = self.get_connected_sites(idx, jimage=jimage) site = PeriodicSite( site.species, np.add(site.frac_coords, jimage), site.lattice, properties=site.properties, ) else: connected_sites = self.get_connected_sites(idx) connected_sites = [ cs for cs in connected_sites if (cs.index, cs.jimage) in sites_to_draw ] connected_sites_not_drawn = [ cs for cs in connected_sites if (cs.index, cs.jimage) not in sites_to_draw ] if color_edges: connected_sites_colors = [ get_weight_color(cs.weight) for cs in connected_sites ] connected_sites_not_drawn_colors = [ get_weight_color(cs.weight) for cs in connected_sites_not_drawn ] else: connected_sites_colors = None connected_sites_not_drawn_colors = None site_scene = site.get_scene( connected_sites=connected_sites, connected_sites_not_drawn=connected_sites_not_drawn, hide_incomplete_edges=hide_incomplete_edges, incomplete_edge_length_scale=incomplete_edge_length_scale, connected_sites_colors=connected_sites_colors, connected_sites_not_drawn_colors=connected_sites_not_drawn_colors, explicitly_calculate_polyhedra_hull= explicitly_calculate_polyhedra_hull, legend=legend, ) for scene in site_scene.contents: if group_by_symmetry and scene.name == "atoms" and idx in idx_to_wyckoff: # will rename to e.g. atoms_N_4e scene.name = f"atoms_{site_scene.name}_{idx_to_wyckoff[idx]}" # this is a proof-of-concept to demonstrate hover labels, could create label # automatically from site properties instead scene.contents[ 0].tooltip = f"{site_scene.name} ({idx_to_wyckoff[idx]})" primitives[scene.name] += scene.contents primitives["unit_cell"].append(self.structure.lattice.get_scene()) return Scene( name="StructureGraph", origin=origin, contents=[ Scene(name=k, contents=v, origin=origin) for k, v in primitives.items() ], )
def get_brillouin_zone_scene(bs: BandStructureSymmLine) -> Scene: if not bs: return Scene(name="brillouin_zone", contents=[]) # TODO: from BSPlotter, merge back into BSPlotter # Brillouin zone bz_lattice = bs.structure.lattice.reciprocal_lattice bz = bz_lattice.get_wigner_seitz_cell() lines = [] for iface in range(len(bz)): # pylint: disable=C0200 for line in itertools.combinations(bz[iface], 2): for jface in range(len(bz)): if (iface < jface and any(np.all(line[0] == x) for x in bz[jface]) and any(np.all(line[1] == x) for x in bz[jface])): lines += [list(line[0]), list(line[1])] zone_lines = Lines(positions=lines) zone_surface = Convex(positions=lines, opacity=0.05, color="#000000") # - Strip latex math wrapping for labels # TODO: add to string utils in pymatgen str_replace = { "$": "", "\\mid": "|", "\\Gamma": "Γ", "\\Sigma": "Σ", "GAMMA": "Γ", "_1": "₁", "_2": "₂", "_3": "₃", "_4": "₄", "_{1}": "₁", "_{2}": "₂", "_{3}": "₃", "_{4}": "₄", "^{*}": "*", } labels = {} for k in bs.kpoints: if k.label: label = k.label for orig, new in str_replace.items(): label = label.replace(orig, new) labels[label] = bz_lattice.get_cartesian_coords(k.frac_coords) labels = [ Spheres(positions=[coords], tooltip=label, radius=0.03, color="#5EB1BF") for label, coords in labels.items() ] path = [] cylinder_pairs = [] for b in bs.branches: start = bz_lattice.get_cartesian_coords( bs.kpoints[b["start_index"]].frac_coords) end = bz_lattice.get_cartesian_coords( bs.kpoints[b["end_index"]].frac_coords) path += [start, end] cylinder_pairs += [[start, end]] # path_lines = Lines(positions=path, color="#ff4b5c",) path_lines = Cylinders(positionPairs=cylinder_pairs, color="#5EB1BF", radius=0.01) ibz_region = Convex(positions=path, opacity=0.2, color="#5EB1BF") contents = [zone_lines, zone_surface, path_lines, ibz_region, *labels] cbm = bs.get_cbm()["kpoint"] vbm = bs.get_vbm()["kpoint"] if cbm and vbm: if cbm.label: cbm_label = cbm.label for orig, new in str_replace.items(): cbm_label = cbm_label.replace(orig, new) cbm_label = f"CBM at {cbm_label}" else: cbm_label = "CBM" if cbm == vbm: cbm_label = f"VBM and {cbm_label}" cbm_coords = bz_lattice.get_cartesian_coords(cbm.frac_coords) cbm = Spheres(positions=[cbm_coords], tooltip=cbm_label, radius=0.05, color="#7E259B") contents.append(cbm) if cbm != vbm: if vbm.label: vbm_label = vbm.label for orig, new in str_replace.items(): vbm_label = vbm_label.replace(orig, new) vbm_label = f"VBM at {vbm_label}" else: vbm_label = "VBM" vbm_coords = bz_lattice.get_cartesian_coords(vbm.frac_coords) vbm = Spheres( positions=[vbm_coords], tooltip=vbm_label, radius=0.05, color="#7E259B", ) contents.append(vbm) return Scene(name="brillouin_zone", contents=contents)
# .convex_hull = [[2, 3, 0], [1, 3, 0], [1, 2, 0], [1, 2, 3]] # .vertex_neighbor_vertices = [1, 2, 3, 2, 3, 0, 1, 3, 0, 1, 2, 0] vertices_indices = Delaunay(all_positions).convex_hull except Exception as e: vertices_indices = [] vertices = [ all_positions[idx] for idx in chain.from_iterable(vertices_indices) ] polyhedron = [Surface(positions=vertices, color=site_color)] else: polyhedron = [Convex(positions=all_positions, color=site_color)] return Scene( self.species_string, [ Scene("atoms", contents=atoms), Scene("bonds", contents=bonds), Scene("polyhedra", contents=polyhedron), Scene("magmoms", contents=magmoms), ], origin=origin, ) Site.get_scene = get_site_scene
def plot_crystal_toolkit( self, spin: Optional[Spin] = None, colors: Optional[Union[str, dict, list]] = None, opacity: float = 1.0, ) -> "Scene": """ Get a crystal toolkit Scene showing the Fermi surface. The Scene can be displayed in an interactive web app using Crystal Toolkit, can be shown interactively in Jupyter Lab using the crystal-toolkit lab extension, or can be converted to JSON to store for future use. Args: spin: Which spin channel to plot. By default plot both spin channels if available. colors: See the docstring for ``get_isosurfaces_and_colors()`` for the available options. opacity: Opacity of surface. Note that due to limitations of WebGL, overlapping semi-transparent surfaces might result in visual artefacts. """ # The implementation here is very similar to the plotly implementation, except # the crystal toolkit scene is constructed using the scene primitives from # crystal toolkit (Spheres, Surface, Lines, etc.) scene_contents = [] isosurfaces, colors = self.get_isosurfaces_and_colors(spin=spin, colors=colors) if isinstance(colors, np.ndarray): colors = (colors * 255).astype(int) colors = ["rgb({},{},{})".format(*c) for c in colors] # create a mesh for each electron band which has an isosurfaces at the Fermi # energy mesh data is generated by a marching cubes algorithm when the # FermiSurface object is created. surfaces = [] for c, (verts, faces) in zip(colors, isosurfaces): positions = verts[faces].reshape(-1, 3).tolist() surface = Surface(positions=positions, color=c, opacity=opacity) surfaces.append(surface) fermi_surface = Scene("fermi_surface", contents=surfaces) scene_contents.append(fermi_surface) # add the cell outline to the plot lines = Lines(positions=list(self.reciprocal_space.lines.flatten())) # alternatively, # cylinders have finite width and are lighted, but no strong reason to choose # one over the other # cylinders = Cylinders(positionPairs=self.reciprocal_space.lines.tolist(), # radius=0.01, color="rgb(0,0,0)") scene_contents.append(lines) spheres = [] for position, label in zip(self._symmetry_pts[0], self._symmetry_pts[1]): sphere = Spheres( positions=[list(position)], tooltip=label, radius=0.05, color="rgb(0, 0, 0)", ) spheres.append(sphere) label_scene = Scene("labels", contents=spheres) scene_contents.append(label_scene) return Scene("ifermi", contents=scene_contents)
def get_site_scene( self, connected_sites: List[ConnectedSite] = None, # connected_site_metadata: None, # connected_sites_to_draw, connected_sites_not_drawn: List[ConnectedSite] = None, hide_incomplete_edges: bool = False, incomplete_edge_length_scale: Optional[float] = 1.0, connected_sites_colors: Optional[List[str]] = None, connected_sites_not_drawn_colors: Optional[List[str]] = None, origin: Optional[List[float]] = None, draw_polyhedra: bool = True, explicitly_calculate_polyhedra_hull: bool = False, bond_radius: float = 0.1, legend: Optional[Legend] = None, ) -> Scene: """ Args: connected_sites: connected_sites_not_drawn: hide_incomplete_edges: incomplete_edge_length_scale: connected_sites_colors: connected_sites_not_drawn_colors: origin: explicitly_calculate_polyhedra_hull: legend: Returns: """ atoms = [] bonds = [] polyhedron = [] legend = legend or Legend(self) # for disordered structures is_ordered = self.is_ordered phiStart, phiEnd = None, None occu_start = 0.0 position = self.coords.tolist() for idx, (sp, occu) in enumerate(self.species.items()): if isinstance(sp, DummySpecie): cube = Cubes(positions=[position], color=legend.get_color(sp, site=self), width=0.4) atoms.append(cube) else: color = legend.get_color(sp, site=self) radius = legend.get_radius(sp, site=self) # TODO: make optional/default to None # in disordered structures, we fractionally color-code spheres, # drawing a sphere segment from phi_end to phi_start # (think a sphere pie chart) if not is_ordered: phi_frac_end = occu_start + occu phi_frac_start = occu_start occu_start = phi_frac_end phiStart = phi_frac_start * np.pi * 2 phiEnd = phi_frac_end * np.pi * 2 name = str(sp) if occu != 1.0: name += " ({}% occupancy)".format(occu) name += f" ({position[0]:.3f}, {position[1]:.3f}, {position[2]:.3f})" sphere = Spheres( positions=[position], color=color, radius=radius, phiStart=phiStart, phiEnd=phiEnd, clickable=True, tooltip=name, ) atoms.append(sphere) if not is_ordered and not np.isclose(phiEnd, np.pi * 2): # if site occupancy doesn't sum to 100%, cap sphere sphere = Spheres( positions=[position], color="#ffffff", radius=self.properties["display_radius"][0], phiStart=phiEnd, phiEnd=np.pi * 2, ) atoms.append(sphere) if connected_sites: # TODO: more graceful solution here # if ambiguous (disordered), re-use last color used site_color = color # TODO: can cause a bug if all vertices almost co-planar # necessary to include center site in case it's outside polyhedra all_positions = [self.coords] for idx, connected_site in enumerate(connected_sites): connected_position = connected_site.site.coords bond_midpoint = np.add(position, connected_position) / 2 if connected_sites_colors: color = connected_sites_colors[idx] else: color = site_color cylinder = Cylinders( positionPairs=[[position, bond_midpoint.tolist()]], color=color, radius=bond_radius, ) bonds.append(cylinder) all_positions.append(connected_position.tolist()) if connected_sites_not_drawn and not hide_incomplete_edges: for idx, connected_site in enumerate(connected_sites_not_drawn): connected_position = connected_site.site.coords bond_midpoint = (incomplete_edge_length_scale * np.add(position, connected_position) / 2) if connected_sites_not_drawn_colors: color = connected_sites_not_drawn_colors[idx] else: color = site_color cylinder = Cylinders( positionPairs=[[position, bond_midpoint.tolist()]], color=color, radius=bond_radius, ) bonds.append(cylinder) all_positions.append(connected_position.tolist()) # ensure intersecting polyhedra are not shown, defaults to choose by electronegativity not_most_electro_negative = map( lambda x: (x.site.specie < self.specie) or (x.site.specie == self.specie), connected_sites, ) all_positions = [list(p) for p in all_positions] if (draw_polyhedra and len(connected_sites) > 3 and not connected_sites_not_drawn and not any(not_most_electro_negative)): if explicitly_calculate_polyhedra_hull: try: # all_positions = [[0, 0, 0], [0, 0, 10], [0, 10, 0], [10, 0, 0]] # gives... # .convex_hull = [[2, 3, 0], [1, 3, 0], [1, 2, 0], [1, 2, 3]] # .vertex_neighbor_vertices = [1, 2, 3, 2, 3, 0, 1, 3, 0, 1, 2, 0] vertices_indices = Delaunay(all_positions).convex_hull except Exception as e: vertices_indices = [] vertices = [ all_positions[idx] for idx in chain.from_iterable(vertices_indices) ] polyhedron = [Surface(positions=vertices, color=site_color)] else: polyhedron = [ Convex(positions=all_positions, color=site_color) ] return Scene( self.species_string, [ Scene("atoms", contents=atoms), Scene("bonds", contents=bonds), Scene("polyhedra", contents=polyhedron), ], origin=origin, )
def __init__( self, struct_or_mol: Optional[Union[Structure, StructureGraph, Molecule, MoleculeGraph]] = None, id: str = None, className: str = "box", scene_additions: Optional[Scene] = None, bonding_strategy: str = DEFAULTS["bonding_strategy"], bonding_strategy_kwargs: Optional[dict] = None, color_scheme: str = DEFAULTS["color_scheme"], color_scale: Optional[str] = None, radius_strategy: str = DEFAULTS["radius_strategy"], unit_cell_choice: str = DEFAULTS["unit_cell_choice"], draw_image_atoms: bool = DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell: bool = DEFAULTS[ "bonded_sites_outside_unit_cell"], hide_incomplete_bonds: bool = DEFAULTS["hide_incomplete_bonds"], show_compass: bool = DEFAULTS["show_compass"], scene_settings: Optional[Dict] = None, group_by_site_property: Optional[str] = None, show_legend: bool = DEFAULTS["show_legend"], show_settings: bool = DEFAULTS["show_settings"], show_controls: bool = DEFAULTS["show_controls"], show_expand_button: bool = DEFAULTS["show_expand_button"], show_image_button: bool = DEFAULTS["show_image_button"], show_export_button: bool = DEFAULTS["show_export_button"], show_position_button: bool = DEFAULTS["show_position_button"], **kwargs, ): """ Create a StructureMoleculeComponent from a structure or molecule. :param struct_or_mol: input structure or molecule :param id: canonical id :param scene_additions: extra geometric elements to add to the 3D scene :param bonding_strategy: bonding strategy from pymatgen NearNeighbors class :param bonding_strategy_kwargs: options for the bonding strategy :param color_scheme: color scheme, see Legend class :param color_scale: color scale, see Legend class :param radius_strategy: radius strategy, see Legend class :param draw_image_atoms: whether to draw repeats of atoms on periodic images :param bonded_sites_outside_unit_cell: whether to draw sites bonded outside the unit cell :param hide_incomplete_bonds: whether to hide or show incomplete bonds :param show_compass: whether to hide or show the compass :param scene_settings: scene settings (lighting etc.) to pass to CrystalToolkitScene :param group_by_site_property: a site property used for grouping of atoms for mouseover/interaction, :param show_legend: show or hide legend panel within the scene :param show_controls: show or hide scene control bar :param show_expand_button: show or hide the full screen button within the scene control bar :param show_image_button: show or hide the image download button within the scene control bar :param show_export_button: show or hide the file export button within the scene control bar :param show_position_button: show or hide the revert position button within the scene control bar e.g. Wyckoff label :param kwargs: extra keyword arguments to pass to MPComponent """ super().__init__(id=id, default_data=struct_or_mol, **kwargs) self.className = className self.show_legend = show_legend self.show_settings = show_settings self.show_controls = show_controls self.show_expand_button = show_expand_button self.show_image_button = show_image_button self.show_export_button = show_export_button self.show_position_button = show_position_button self.initial_scene_settings = self.default_scene_settings.copy() if scene_settings: self.initial_scene_settings.update(scene_settings) self.create_store("scene_settings", initial_data=self.initial_scene_settings) # unit cell choice and bonding algorithms need to come from a settings # object (in a dcc.Store) guaranteed to be present in layout, rather # than from the controls themselves -- since these are optional and # may not be present in the layout self.create_store( "graph_generation_options", initial_data={ "bonding_strategy": bonding_strategy, "bonding_strategy_kwargs": bonding_strategy_kwargs, "unit_cell_choice": unit_cell_choice, }, ) self.create_store( "display_options", initial_data={ "color_scheme": color_scheme, "color_scale": color_scale, "radius_strategy": radius_strategy, "draw_image_atoms": draw_image_atoms, "bonded_sites_outside_unit_cell": bonded_sites_outside_unit_cell, "hide_incomplete_bonds": hide_incomplete_bonds, "show_compass": show_compass, "group_by_site_property": group_by_site_property, }, ) if scene_additions: initial_scene_additions = Scene( name="scene_additions", contents=scene_additions).to_json() else: initial_scene_additions = None self.create_store("scene_additions", initial_data=initial_scene_additions) if struct_or_mol: # graph is cached explicitly, this isn't necessary but is an # optimization so that graph is only re-generated if bonding # algorithm changes struct_or_mol = self._preprocess_structure( struct_or_mol, unit_cell_choice=unit_cell_choice) graph = self._preprocess_input_to_graph( struct_or_mol, bonding_strategy=bonding_strategy, bonding_strategy_kwargs=bonding_strategy_kwargs, ) scene, legend = self.get_scene_and_legend( graph, scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) if hasattr(struct_or_mol, "lattice"): self._lattice = struct_or_mol.lattice else: # component could be initialized without a structure, in which case # an empty scene should be displayed graph = None scene, legend = self.get_scene_and_legend( None, scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) self.create_store("legend_data", initial_data=legend) self.create_store("graph", initial_data=graph) # this is used by a CrystalToolkitScene component, not a dcc.Store self._initial_data["scene"] = scene # hide axes inset for molecules if isinstance(struct_or_mol, Molecule) or isinstance( struct_or_mol, MoleculeGraph): self.scene_kwargs = {"axisView": "HIDDEN"} else: self.scene_kwargs = {}
def _axes_from_lattice(self, origin=None, scale=1, offset=0, **kwargs): """ Get the display components of the compass :param lattice: the pymatgen Lattice object that contains the primitive lattice vectors :param origin: the reference position to place the compass :param scale: scale all the geometric objects that makes up the compass the lattice vectors are normalized before the scaling so everything should be the same size :param offset: shift the compass from the origin by a ratio of the diagonal of the cell relative the size :param **kwargs: keyword args to pass to the Arrows initializer :return: Scene object """ origin = origin or [0, 0, 0] o = np.array(origin) # o = -self.get_cartesian_coords([0.5, 0.5, 0.5]) # o = o - offset * (self.matrix[0] + self.matrix[1] + self.matrix[2]) a = self.matrix[0] / np.linalg.norm(self.matrix[0]) * scale b = self.matrix[1] / np.linalg.norm(self.matrix[1]) * scale c = self.matrix[2] / np.linalg.norm(self.matrix[2]) * scale a_arrow = [[o, o + a]] b_arrow = [[o, o + b]] c_arrow = [[o, o + c]] radius_scale = 0.07 head_scale = 0.24 head_width = 0.14 o_sphere = Spheres(positions=[o], color="white", radius=2 * radius_scale * scale) return Scene( name="axes", contents=[ Arrows( a_arrow, color="red", radius=radius_scale * scale, headLength=head_scale * scale, headWidth=head_width * scale, **kwargs, ), Arrows( b_arrow, color="green", radius=radius_scale * scale, headLength=head_scale * scale, headWidth=head_width * scale, **kwargs, ), Arrows( c_arrow, color="blue", radius=radius_scale * scale, headLength=head_scale * scale, headWidth=head_width * scale, **kwargs, ), o_sphere, ], origin=origin, )
def __init__( self, struct_or_mol: Optional[ Union[Structure, StructureGraph, Molecule, MoleculeGraph] ] = None, id: str = None, scene_additions: Optional[Scene] = None, bonding_strategy: str = DEFAULTS["bonding_strategy"], bonding_strategy_kwargs: Optional[dict] = None, color_scheme: str = DEFAULTS["color_scheme"], color_scale: Optional[str] = None, radius_strategy: str = DEFAULTS["radius_strategy"], unit_cell_choice: str = DEFAULTS["unit_cell_choice"], draw_image_atoms: bool = DEFAULTS["draw_image_atoms"], bonded_sites_outside_unit_cell: bool = DEFAULTS[ "bonded_sites_outside_unit_cell" ], hide_incomplete_bonds: bool = DEFAULTS["hide_incomplete_bonds"], show_compass: bool = DEFAULTS["show_compass"], scene_settings: Optional[Dict] = None, **kwargs, ): """ Create a StructureMoleculeComponent from a structure or molecule. :param struct_or_mol: input structure or molecule :param id: canonical id :param scene_additions: extra geometric elements to add to the 3D scene :param bonding_strategy: bonding strategy from pymatgen NearNeighbors class :param bonding_strategy_kwargs: options for the bonding strategy :param color_scheme: color scheme, see Legend class :param color_scale: color scale, see Legend class :param radius_strategy: radius strategy, see Legend class :param draw_image_atoms: whether to draw repeats of atoms on periodic images :param bonded_sites_outside_unit_cell: whether to draw sites bonded outside the unit cell :param hide_incomplete_bonds: whether to hide or show incomplete bonds :param show_compass: whether to hide or show the compass :param scene_settings: scene settings (lighting etc.) to pass to Simple3DScene :param kwargs: extra keyword arguments to pass to MPComponent """ super().__init__(id=id, default_data=struct_or_mol, **kwargs) # what to show for the title_layout if structure/molecule not loaded self.default_title = "Crystal Toolkit" self.initial_scene_settings = self.default_scene_settings.copy() if scene_settings: self.initial_scene_settings.update(scene_settings) self.create_store("scene_settings", initial_data=self.initial_scene_settings) # unit cell choice and bonding algorithms need to come from a settings # object (in a dcc.Store) guaranteed to be present in layout, rather # than from the controls themselves -- since these are optional and # may not be present in the layout self.create_store( "graph_generation_options", initial_data={ "bonding_strategy": bonding_strategy, "bonding_strategy_kwargs": bonding_strategy_kwargs, "unit_cell_choice": unit_cell_choice, }, ) self.create_store( "display_options", initial_data={ "color_scheme": color_scheme, "color_scale": color_scale, "radius_strategy": radius_strategy, "draw_image_atoms": draw_image_atoms, "bonded_sites_outside_unit_cell": bonded_sites_outside_unit_cell, "hide_incomplete_bonds": hide_incomplete_bonds, "show_compass": show_compass, }, ) if scene_additions: initial_scene_additions = Scene( name="scene_additions", contents=scene_additions ).to_json() else: initial_scene_additions = None self.create_store("scene_additions", initial_data=initial_scene_additions) if struct_or_mol: # graph is cached explicitly, this isn't necessary but is an # optimization so that graph is only re-generated if bonding # algorithm changes graph = self._preprocess_input_to_graph( struct_or_mol, bonding_strategy=bonding_strategy, bonding_strategy_kwargs=bonding_strategy_kwargs, ) scene, legend = self.get_scene_and_legend( graph, name=self.id(), scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) if hasattr(struct_or_mol, "lattice"): self._lattice = struct_or_mol.lattice else: # component could be initialized without a structure, in which case # an empty scene should be displayed graph = None scene, legend = self.get_scene_and_legend( None, name=self.id(), scene_additions=self.initial_data["scene_additions"], **self.initial_data["display_options"], ) self.create_store("legend_data", initial_data=legend) self.create_store("graph", initial_data=graph) # this is used by a Simple3DScene component, not a dcc.Store self._initial_data["scene"] = scene