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 [ 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 get_extra_scene(pairs, s_radius=0.5, c_radius=2.5): ''' Takes in position pairs and draw a path ''' extra_scene=[] [ini_color, final_color]=[[240, 240, 240], [0, 0, 0]] div = len(pairs) - 1 if div == 0: rgb_list = [tuple(ini_color), tuple(final_color)] else: step_size = [int((final_color[i] - ini_color[i])/(div+1)) for i in range(0,3)] rgb_list = [(ini_color[0] + u*step_size[0], ini_color[1] + u*step_size[1], ini_color[2] + u*step_size[2]) for u in range(1, div+1)] rgb_list.insert(0, tuple(ini_color)) rgb_list.append(tuple(final_color)) rgb_to_html = lambda rgb: '#%02x%02x%02x' % rgb html_colors = [rgb_to_html(i) for i in rgb_list] extra_scene.append(Spheres(positions=[pairs[0][0]], radius=s_radius, color='#00ff88')) #for now all colors are designated as Li light green for i in range(0, len(pairs)): extra_scene.append(Spheres(positions=[pairs[i][1]], radius=s_radius, color='#00ff88')) extra_scene.append(Cylinders(positionPairs=[pairs[i]], radius=c_radius, color='#00ff88')) return extra_scene
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)
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, draw_magmoms: bool = True, magmom_scale: float = 1.0, 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 = [] magmoms = [] 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() radii = [legend.get_radius(sp, site=self) for sp in self.species.keys()] max_radius = float(min(radii)) 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})" if self.properties: for k, v in self.properties.items(): name += f" ({k} = {v})" sphere = Spheres( positions=[position], color=color, radius=radius, phiStart=phiStart, phiEnd=phiEnd, clickable=True, tooltip=name, ) atoms.append(sphere) # Add magmoms if draw_magmoms: if magmom := self.properties.get("magmom"): # enforce type magmom = np.array(Magmom(magmom).get_moment()) magmom = 2 * magmom_scale * max_radius * magmom tail = np.array(position) - 0.5 * np.array(magmom) head = np.array(position) + 0.5 * np.array(magmom) arrow = Arrows( positionPairs=[[tail, head]], color="red", radius=0.20, headLength=0.5, headWidth=0.4, clickable=True, ) magmoms.append(arrow)
arrow = Arrows( positionPairs=[[tail, head]], color="red", radius=0.20, headLength=0.5, headWidth=0.4, clickable=True, ) magmoms.append(arrow) 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=max_radius, 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]
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 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)
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 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), ], )