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_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 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_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 test_convert_object_to_pythreejs(self): # take different crystal toolkit objects and convert them into pythreejs objects sphere = Spheres(positions=[[0, 0, 0]], color="#00ab24", radius=1.0) assert ("SphereBufferGeometry" in _convert_object_to_pythreejs( scene_obj=sphere)[0].__repr__()) cylinder = Cylinders(positionPairs=[[[0, 0, 0], [0, 1, 1]]], color="#00ab24", radius=1.0) assert ("CylinderBufferGeometry" in _convert_object_to_pythreejs( scene_obj=cylinder)[0].__repr__()) surface = Surface([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) _convert_object_to_pythreejs(scene_obj=surface)[0].__repr__() assert ("BufferGeometry" in _convert_object_to_pythreejs( scene_obj=surface)[0].__repr__())
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 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)
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), Scene("magmoms", contents=magmoms), ], origin=origin, )
def get_site_scene( self, connected_sites: List[ConnectedSite] = None, 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: List[float] = (0, 0, 0), ellipsoid_site_prop: str = None, draw_polyhedra: bool = True, explicitly_calculate_polyhedra_hull: bool = False, ) -> Scene: """ Args: self: connected_sites: connected_sites_not_drawn: hide_incomplete_edges: incomplete_edge_length_scale: connected_sites_colors: connected_sites_not_drawn_colors: origin: ellipsoid_site_prop: explicitly_calculate_polyhedra_hull: Returns: """ atoms = [] bonds = [] polyhedron = [] # for disordered structures is_ordered = self.is_ordered phiStart, phiEnd = None, None occu_start = 0.0 # for thermal ellipsoids etc. def _get_ellipsoids_from_matrix(matrix): raise NotImplementedError # matrix = np.array(matrix) # eigenvalues, eigenvectors = np.linalg.eig(matrix) if ellipsoid_site_prop: matrix = self.properties[ellipsoid_site_prop] ellipsoids = _get_ellipsoids_from_matrix(matrix) else: ellipsoids = None position = np.subtract(self.coords, origin).tolist() # site_color is used for bonds and polyhedra, if multiple colors are # defined for site (e.g. a disordered site), then we use grey all_colors = set(self.properties["display_color"]) if len(all_colors) > 1: site_color = "#555555" else: site_color = list(all_colors)[0] for idx, (sp, occu) in enumerate(self.species.items()): if isinstance(sp, DummySpecie): cube = Cubes( positions=[position], color=self.properties["display_color"][idx], width=0.4, ) atoms.append(cube) else: color = self.properties["display_color"][idx] radius = self.properties["display_radius"][idx] # 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 # TODO: add names for labels # name = "{}".format(sp) # if occu != 1.0: # name += " ({}% occupancy)".format(occu) sphere = Spheres( positions=[position], color=color, radius=radius, phiStart=phiStart, phiEnd=phiEnd, ) 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: all_positions = [] for idx, connected_site in enumerate(connected_sites): connected_position = np.subtract(connected_site.site.coords, origin) 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) 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 = np.subtract(connected_site.site.coords, origin) 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) bonds.append(cylinder) all_positions.append(connected_position.tolist()) if (draw_polyhedra and len(connected_sites) > 3 and not connected_sites_not_drawn): 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=self.properties["display_color"][0], ) ] 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), ], )
def test_scenes(scene_dicts, scene_dict): expected = Scene("125_up", contents=[Surface(positions=[[0]*3, [0.1]*3, [0.2]*3])]) assert scene_dicts.scenes == {"125_up": expected} assert scene_dict.scene("125_up") == expected
def get_site_scene( self, connected_sites=None, connected_site_color=None, origin=(0, 0, 0), ellipsoid_site_prop=None, all_connected_sites_present=True, explicitly_calculate_polyhedra_hull=False, ) -> Scene: """ Sites must have display_radius and display_color, display_vector, display_ellipsoid site properties. TODO: add bond colours to connected_site_properties :param site: :param connected_sites: :param origin: :param all_connected_sites_present: if False, will not calculate polyhedra since this would be misleading :param explicitly_calculate_polyhedra_hull: :return: """ atoms = [] bonds = [] polyhedron = [] # for disordered structures is_ordered = self.is_ordered phiStart, phiEnd = None, None occu_start = 0.0 # for thermal ellipsoids etc. def _get_ellipsoids_from_matrix(matrix): raise NotImplementedError # matrix = np.array(matrix) # eigenvalues, eigenvectors = np.linalg.eig(matrix) if ellipsoid_site_prop: matrix = self.properties[ellipsoid_site_prop] ellipsoids = _get_ellipsoids_from_matrix(matrix) else: ellipsoids = None position = np.subtract(self.coords, origin).tolist() # site_color is used for bonds and polyhedra, if multiple colors are # defined for site (e.g. a disordered site), then we use grey all_colors = set(self.properties["display_color"]) if len(all_colors) > 1: site_color = "#555555" else: site_color = list(all_colors)[0] for idx, (sp, occu) in enumerate(self.species.items()): if isinstance(sp, DummySpecie): cube = Cubes( positions=[position], color=self.properties["display_color"][idx], width=0.4, ) atoms.append(cube) else: color = self.properties["display_color"][idx] radius = self.properties["display_radius"][idx] # 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 # TODO: add names for labels # name = "{}".format(sp) # if occu != 1.0: # name += " ({}% occupancy)".format(occu) sphere = Spheres( positions=[position], color=color, radius=radius, phiStart=phiStart, phiEnd=phiEnd, ellipsoids=ellipsoids, ) 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, ellipsoids=ellipsoids, ) atoms.append(sphere) if connected_sites: all_positions = [] for connected_site in connected_sites: connected_position = np.subtract(connected_site.site.coords, origin) bond_midpoint = np.add(position, connected_position) / 2 cylinder = Cylinders( positionPairs=[[position, bond_midpoint.tolist()]], color=site_color) bonds.append(cylinder) all_positions.append(connected_position.tolist()) if len(connected_sites) > 3 and all_connected_sites_present: 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).vertex_neighbor_vertices vertices = [all_positions[idx] for idx in vertices_indices] polyhedron = [ Surface( positions=vertices, color=self.properties["display_color"][0], ) ] except Exception as e: polyhedron = [] 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), ], )