Source code for tightbinder.crystal

""" 
Implementation of the Crystal class, responsible of storing all the 
information relative to the Bravais lattice and the reciprocal lattice,
such as high symmetry points, producing the k point mesh or visualizing
the crystal itself.
"""

import numpy as np
from scipy.spatial import ConvexHull
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from numpy.linalg import LinAlgError
import math
import sys
from typing import Union, List

from tightbinder.utils import generate_basis_combinations, alpha_shape_2d


# --------------- Constants ---------------
PI = 3.141592653589793238
EPS = 0.001


# --------------- Routines ---------------
[docs] class Crystal: """ Implementation of Crystal class. Defaults to square lattice if no input is given. :param bravais_lattice: List with the bravais vectors :param motif: List with positions and chemical species of atoms of the motif """ def __init__(self, bravais_lattice: Union[list, np.ndarray] = None, motif: Union[list, np.ndarray] = None) -> None: self.group = None self.reciprocal_basis = None self.high_symmetry_points = None self._ndim = None self.natoms = None self.unit_cell_area = None self._bravais_lattice = None self.bravais_lattice = bravais_lattice self._motif = None self.motif = motif @property def ndim(self) -> int: return self._ndim @ndim.setter def ndim(self, ndim: int) -> None: assert type(ndim) == int if ndim > 3 or ndim < (-1): print("Incorrect dimension for system, exiting...") sys.exit(1) self._ndim = ndim @property def bravais_lattice(self) -> np.ndarray: return self._bravais_lattice @bravais_lattice.setter def bravais_lattice(self, bravais_lattice: Union[list, np.ndarray]) -> None: if bravais_lattice is None: self._bravais_lattice = None self._ndim = 0 else: assert type(bravais_lattice) == list or type(bravais_lattice) == np.ndarray bravais_lattice = np.array(bravais_lattice) if bravais_lattice.shape[1] != 3: raise Exception("Vectors must have three components") self._bravais_lattice = bravais_lattice self._ndim = len(bravais_lattice) self.update() @property def motif(self) -> Union[list, np.ndarray]: return self._motif @motif.setter def motif(self, motif: Union[list, np.ndarray]) -> None: if motif is None: self._motif = None else: assert type(motif) == list or type(motif) == np.ndarray for atom in motif: if len(atom) != 4: raise Exception("Each position must have three components") self._motif = np.array(motif) self.natoms = len(motif)
[docs] def add_atom(self, position: Union[list, np.ndarray], species: int = 0) -> None: """ Method to add one atom from a numbered species into a specific position. :param position: Array with atom coordinaes. Must be three-dimensional. :param species: Index of corresponding chemical species. Defaults to 0. """ assert type(position) == list or type(position) == np.ndarray if len(position) != 3: raise Exception("Vector must have three components") position = np.concatenate([position, [species]]) if type(self.motif) == list: self.motif.append(position) elif type(self.motif) == np.ndarray: self.motif = np.concatenate([self.motif, [position]], axis=0)
[docs] def add_atoms(self, atoms: Union[list, np.ndarray], species: list = None) -> None: """ Method to add a list of atoms of specified species at some positions. Built on top of method add_atom. :param atoms: List of length n, containing the positions of each atom. :param species: List with the indices of the chemical species. If None, all default to zero. """ if species is None: species = np.zeros(len(atoms)) for n, atom in enumerate(atoms): self.add_atom(atom, species[n])
[docs] def remove_atom(self, index: int) -> None: """ Method to remove the atom at the specified index from the motif. Note that this method does not remove existing bonds corresponding to the removed atom. :param index: Index of atom to be removed. """ if type(self.motif) == list: self.motif.pop(index) self.natoms -= 1 elif type(self.motif) == np.ndarray: self.motif = np.delete(self.motif, index, 0)
[docs] def remove_atoms(self, indices: list) -> None: """ Method to remove the atoms specified in a list with all their indices. Built upon the method remove_atom. :param indices: List with the indices of the atoms to be removed. """ indices = np.sort(indices) for n, index in enumerate(indices): self.remove_atom(index - n)
[docs] def update(self) -> None: """ Routine to initialize or update the intrinsic attributes of the Crystal class whenever the Bravais lattice is changed. """ self.crystallographic_group() self.reciprocal_lattice() self.determine_high_symmetry_points() self.compute_unit_cell_area()
[docs] def compute_unit_cell_area(self) -> float: """ Method to compute the area of the unit cell. TODO: Adapt for 1D and 3D :return: Value of unit cell area. """ area = 0.0 if self.ndim == 2: area = np.linalg.norm(np.cross(self.bravais_lattice[0], self.bravais_lattice[1])) self.unit_cell_area = area return area
[docs] def crystallographic_group(self) -> None: """ Determines the crystallographic group associated to the material from the configuration file. NOTE: Only the group associated to the Bravais lattice, reduced symmetry due to presence of motif is not taken into account. Its purpose is to provide a path in k-space to plot the band structure. Workflow is the following: First determine length and angles between basis vectors. Then we separate by dimensionality: first we check angles, and then length to determine the crystal group. """ basis_norm = [np.linalg.norm(vector) for vector in self.bravais_lattice] group = '' basis_angles = [] for i in range(self.ndim): for j in range(self.ndim): if j <= i: continue v1 = self.bravais_lattice[i] v2 = self.bravais_lattice[j] basis_angles.append(math.acos(np.dot(v1, v2)/(np.linalg.norm(v1)*np.linalg.norm(v2)))) if self.ndim == 2: if abs(basis_angles[0] - PI / 2) < EPS: if abs(basis_norm[0] - basis_norm[1]) < EPS: group += 'Square' else: group += 'Rectangular' elif (abs(basis_angles[0] - PI / 3) < EPS or abs(basis_angles[0] - 2 * PI / 3) < EPS) and abs(basis_norm[0] - basis_norm[1]) < EPS: group += 'Hexagonal' else: if abs(basis_norm[0] - basis_norm[1]) < EPS: group += 'Centered rectangular' else: group += 'Oblique' elif self.ndim == 3: if (basis_angles[0] - PI/2) < EPS and (basis_angles[1] - PI/2) < EPS and (basis_angles[2] - PI/2) < EPS: if abs(basis_norm[0] - basis_norm[1]) < EPS and abs(basis_norm[0] - basis_norm[2]) < EPS and abs(basis_norm[1] - basis_norm[2]) < EPS: group += 'Cube' else: print('Work in progress') self.group = group
[docs] def reciprocal_lattice(self) -> None: """ Routine to compute the reciprocal lattice basis vectors from the Bravais lattice basis. The algorithm is based on the fact that a_i\dot b_j=2PI\delta_ij, which can be written as a linear system of equations to solve for b_j. Resulting vectors have 3 components independently of the dimension of the vector space they span. """ if self.bravais_lattice is None: self.reciprocal_basis = None return reciprocal_basis = np.zeros([self.ndim, 3]) coefficient_matrix = np.array(self.bravais_lattice) if self.ndim == 1: reciprocal_basis = 2*PI*coefficient_matrix/(np.linalg.norm(coefficient_matrix)**2) else: coefficient_matrix = coefficient_matrix[:, :self.ndim] for i in range(self.ndim): coefficient_vector = np.zeros(self.ndim) coefficient_vector[i] = 2*PI try: reciprocal_vector = np.linalg.solve(coefficient_matrix, coefficient_vector) except LinAlgError: print('Error: Bravais lattice basis is not linear independent') sys.exit(1) reciprocal_basis[i, 0:self.ndim] = reciprocal_vector self.reciprocal_basis = reciprocal_basis
[docs] def brillouin_zone_mesh(self, mesh: list) -> np.ndarray: """ Routine to compute a mesh of the first Brillouin zone using the Monkhorst-Pack algorithm. :param mesh: List with the number of kpoints along each axis. :return: Array with all kpoints, nk x 3 """ if len(mesh) != self.ndim: print('Error: Mesh does not match dimension of the system') sys.exit(1) kpoints = [] mesh_points = [] for i in range(self.ndim): mesh_points.append(list(range(0, mesh[i]))) mesh_points = np.array(np.meshgrid(*mesh_points)).T.reshape(-1, self.ndim) for point in mesh_points: kpoint = 0 for i in range(self.ndim): kpoint += (2.*point[i] - mesh[i])/(2*mesh[i])*self.reciprocal_basis[i] kpoints.append(kpoint) return np.array(kpoints)
[docs] def determine_high_symmetry_points(self) -> None: """ Routine to compute high symmetry points depending on the dimension of the system. These symmetry points will be used to plot the band structure along the main reciprocal paths of the system (irreducible BZ). Returns a dictionary with pairs {letter denoting high symmetry point: its coordinates} """ norm = np.linalg.norm(self.reciprocal_basis[0]) special_points = {r"$\Gamma$": np.array([0., 0., 0.])} if self.ndim == 1: special_points.update({'K': self.reciprocal_basis[0]/2}) elif self.ndim == 2: special_points.update({'M': self.reciprocal_basis[0]/2}) if self.group == 'Square': special_points.update({'K': self.reciprocal_basis[0]/2 + self.reciprocal_basis[1]/2}) elif self.group == 'Rectangular': special_points.update({'X': special_points['M']}) special_points.update({'Y': self.reciprocal_basis[1]/2}) special_points.update({'S': self.reciprocal_basis[0]/2 + self.reciprocal_basis[1]/2}) elif self.group == 'Hexagonal': # special_points.update({'K',: reciprocal_basis[0]/3 + reciprocal_basis[1]/3}) special_points.update({'K': norm/math.sqrt(3)*(self.reciprocal_basis[0]/2 - self.reciprocal_basis[1]/2)/( np.linalg.norm(self.reciprocal_basis[0]/2 - self.reciprocal_basis[1]/2))}) elif self.group == 'Oblique' or self.group == 'Centered rectangular': if np.linalg.norm(self.bravais_lattice[0]) > np.linalg.norm(self.bravais_lattice[1]): a = np.linalg.norm(self.bravais_lattice[0]) b = np.linalg.norm(self.bravais_lattice[1]) else: a = np.linalg.norm(self.bravais_lattice[1]) b = np.linalg.norm(self.bravais_lattice[0]) gamma = math.acos(np.dot(self.bravais_lattice[0], self.bravais_lattice[1])/(a*b)) eta = (1 - b*math.cos(gamma)/a)/(2*(math.sin(gamma)**2)) nu = 0.5 - eta*a*math.cos(gamma)/b special_points.update({'X': special_points['M']}) special_points.update({'Y': self.reciprocal_basis[1]/2}) special_points.update({'C': self.reciprocal_basis[0]/2 + self.reciprocal_basis[1]/2}) special_points.update({'H': eta*self.reciprocal_basis[0] + (1 - nu)*self.reciprocal_basis[1]}) special_points.update({r"$H_1$": (1- eta)*self.reciprocal_basis[0] + nu*self.reciprocal_basis[1]}) else: print('High symmetry points not implemented yet') else: if self.group == 'Cube': special_points.update({'X': self.reciprocal_basis[0]/2}) special_points.update({'M': self.reciprocal_basis[0]/2 + self.reciprocal_basis[1]/2}) special_points.update({'R': self.reciprocal_basis[0]/2 + self.reciprocal_basis[1]/2 + self.reciprocal_basis[2]/2}) else: print('High symmetry points not implemented yet') self.high_symmetry_points = special_points
def __reorder_high_symmetry_points(self, labels: list) -> None: """ Routine to reorder the high symmetry points according to a given vector of labels for later representation. DEPRECATED with the dictionary update """ points = [] for label in labels: appended = False for point in self.high_symmetry_points: if appended: continue if label == point[0]: points.append(point) appended = True self.high_symmetry_points = points def __strip_labels_from_high_symmetry_points(self) -> None: """ Routine to output both labels and array corresponding to high symmetry points DEPRECATED with the dictionary update """ for n, point in enumerate(self.high_symmetry_points): self.high_symmetry_points[n] = point[1]
[docs] def high_symmetry_path(self, nk: int, points: List[str]) -> list: """ Routine to generate a path in reciprocal space along the high symmetry points (HSPs) indicated. The HSPs must be within those corresponding to the associated symmetry group, otherwise the method will throw an error. :param nk: Number of kpoints in the path. They are evenly distributed between HSPs. :param points: List of labels of desired HSPs. :return: List of kpoints following the path specified with the labels. """ # Transform possible Gamma point to latex for n, point in enumerate(points): if point == "G": points[n] = r"$\Gamma$" elif point == "H1": points[n] = r"$H_1$" kpoints = [] number_of_points = len(points) interval_mesh = int(nk/(number_of_points - 1)) previous_point = self.high_symmetry_points[points[0]] for point in points[1:]: next_point = self.high_symmetry_points[point] kpoints += list(np.linspace(previous_point, next_point, interval_mesh, endpoint=False)) previous_point = next_point kpoints.append(self.high_symmetry_points[points[-1]]) return kpoints
[docs] def identify_motif_edges(self, alpha: float = 0.4, ndim: int = 2) -> np.ndarray: """ Method to obtain the indices of the outermost atoms of the motif. Note that this method returns exclusively the outermost atoms, independently of the boundary conditions. To take into account boundary conditions, one should call identify_boundary() from System. NB: Currently works only in 2D :param alpha: This parameters tunes the shape of the boundary. For alpha=0, the boundary is a convex hull; as alpha increases the boundary becomes more concave. Default value is 0.4 :param ndim: Dimension of the boundary. If the system lies on a plane, the boundary is two-dimensional, so ndim = 2 (default value). :return: Indices of atoms in the boundary. :raises AssertionError: Crystal dimension different from two raises error. :raises ValueError: Crystal motif must have at least three atoms. Raised by Delaunay class. """ if ndim == 2: points = np.array(self.motif)[:, :2] points[:, 0] = (points[:, 0] - np.min(points[:, 0]))/np.max(points[:, 0]) points[:, 1] = (points[:, 1] - np.min(points[:, 1]))/np.max(points[:, 1]) edges = alpha_shape_2d(points, alpha) return edges else: raise AssertionError("Currently working only for ndim=2")
[docs] def identify_convex_hull(self, loop: int = 6) -> np.ndarray: """ Deprecated in favour of identify_motif_edges. Method to obtain the atoms that correspond to the edges of the system. This method uses directly the ConvexHull method from Scipy to identify the outmost points of the motif. Compute the ConvexHull three times, each time removing the points detected in the previous iteration. NB: If given 3d points, they must not be coplanar for the algorithm to work. If they are, then the points must be given as 2d :param loop: Number of times the ConvexHull is applied, to increase number of points in the edge. :return: Indices of atoms belonging to edges of motif. """ atoms_coordinates = np.copy(self.motif[:, :2]) hull = ConvexHull(atoms_coordinates) edge_indices = hull.vertices max_x = np.max(atoms_coordinates[:, 0]) min_x = np.min(atoms_coordinates[:, 0]) max_y = np.max(atoms_coordinates[:, 1]) min_y = np.min(atoms_coordinates[:, 1]) midpoint = np.array([(max_x - min_x)/2, (max_y - min_y)/2]) for index in edge_indices: atoms_coordinates[index, :] = midpoint # Repeat this procedure more times for _ in range(loop): hull = ConvexHull(atoms_coordinates) edge_indices = np.concatenate((edge_indices, hull.vertices)) for index in hull.vertices: atoms_coordinates[index, :] = midpoint return edge_indices
[docs] def crystal_center(self) -> np.ndarray: """ Method to compute the geometric center of the crystal from the positions of the atoms of the motif. Calculated as the average of all atomic positions. :return: Numpy array with the cartesian coordinates of the center """ center = np.sum(self.motif[:, :3], axis=0)/self.natoms return center
[docs] def plot_crystal(self, cell_number: int = 1, crystal_name: str = '') -> None: """ Method to visualize the crystalline structure (Bravais lattice + motif). :param cell_number: Number of cells appended in each direction with respect to the origin. Defaults to 1. :param crystal_name: Name of the crystal. Defaults to the empty string. """ bravais_lattice = np.array(self.bravais_lattice) motif = np.array(self.motif) fig = plt.figure() ax = Axes3D(fig) bravais_vectors_mesh = [] for i in range(self.ndim): bravais_vectors_mesh.append(list(range(-cell_number, cell_number + 1))) bravais_vectors_mesh = np.array(np.meshgrid(*bravais_vectors_mesh)).T.reshape(-1, self.ndim) bravais_vectors = np.zeros([len(bravais_vectors_mesh[:, 0]), 3]) for n, coefs in enumerate(bravais_vectors_mesh): for i, coef in enumerate(coefs): bravais_vectors[n, :] += coef * bravais_lattice[i] all_atoms_list = np.zeros([len(bravais_vectors_mesh)*len(motif), 3]) iterator = 0 for vector in bravais_vectors: for atom in motif: all_atoms_list[iterator, :] = atom[:3] + vector iterator += 1 [min_axis, max_axis] = [np.min(all_atoms_list), np.max(all_atoms_list)] for atom in all_atoms_list: ax.scatter(atom[0], atom[1], atom[2], color='y', s=50) ax.set_xlabel('x (A)') ax.set_ylabel('y (A)') ax.set_zlabel('z (A)') ax.set_title(crystal_name + ' crystal') ax.set_xlim3d(min_axis, max_axis) ax.set_ylim3d(min_axis, max_axis) ax.set_zlim3d(min_axis, max_axis) plt.axis('off') plt.show()
[docs] def plot_2d_crystal(self, ax = None, fontsize: int = 10, size: int = 10) -> None: """ Method to visualize the crystalline structure (Bravais lattice + motif) in two dimensions. If the crystal is 3D, it will plot only the x, y coordinates. :param ax: Axis to plot the projection of the crystal. If None, creates one. :param fontsize: Size of labels. Defaults to 10. :param size: Size of markers. Defaults to 10. """ if ax is None: fig, ax = plt.subplots(1, 1) [min_axis, max_axis] = [np.min(self.motif), np.max(self.motif)] for atom in self.motif: ax.scatter(atom[0], atom[1], color='y', s=size) ax.set_xlabel('x (A)', fontsize=fontsize) ax.set_ylabel('y (A)', fontsize=fontsize) ax.set_xlim(min_axis, max_axis) ax.set_ylim(min_axis, max_axis) ax.axis('equal') ax.tick_params('both', labelsize=fontsize)
[docs] def visualize(self) -> None: """ Method to render a 3d visualization of the crystal using the self.vpython library. """ CrystalView(self).visualize() print("\nPress Ctrl+c to exit visualization or close the window.") while True: pass
[docs] class CrystalView: """ The CrystalView class' aim is to wrap all the logic relative to the visualization of the crystal using the vpython library. This is, rendering the solid appropriately, and also rendering a minimal user interface to interact with the visualization. Note that typehints for VPython components have been deleted to avoid importing the library beforehand. """ def __init__(self, crystal: Crystal) -> None: self.bravais_lattice = crystal.bravais_lattice self.motif = np.array(crystal.motif) self.ndim = crystal.ndim self.atoms = [] self.extra_atoms = [] self.bonds = [] self.extra_bonds = [] self.atom_radius = None try: self.vp = __import__("vpython") except: raise AssertionError("visualize(): Requires having installed the library VPython.") try: self.edge_atoms = crystal.identify_boundary(corner_atoms=True) except AssertionError as e: print("CrystalView.visualize(): Unable to determine open boundary (too few atoms or inexistent with PBC)") try: self.neighbours = crystal.bonds except IndexError or AttributeError as e: print('visualize(): System must be initialized first') raise self.vp.scene.visible = False self.vp.scene.background = self.vp.color.black self.__compute_atom_radius() self.__create_atoms() self.__create_bonds() # Canvas attributes self.drag = False self.initial_position = None self.cell_vectors = None self.cell_boundary = None def __compute_atom_radius(self) -> None: """ Private method to estimate an adequate radius for the atoms based on the distance between first neighbours. """ first_neigh_distance = 0 for initial_atom, final_atom, cell, _ in self.neighbours: bond_length = np.linalg.norm(self.motif[initial_atom, :3] - self.motif[final_atom, :3] - cell) if (bond_length < first_neigh_distance) or first_neigh_distance == 0: first_neigh_distance = bond_length self.atom_radius = first_neigh_distance/7 def __create_atoms(self) -> None: """ Private method to initialize the atoms as spheres. """ # Origin cell mesh_points = generate_basis_combinations(self.ndim) color_list = [self.vp.color.yellow, self.vp.color.red, self.vp.color.white, self.vp.color.blue, self.vp.color.green] for position in self.motif: species = int(position[3]) atom = self.vp.sphere(radius=self.atom_radius, color=color_list[species]) atom.pos.x, atom.pos.y, atom.pos.z = position[:3] self.atoms.append(atom) # Super cell if self.bravais_lattice is not None: for point in mesh_points: unit_cell = np.array([0., 0., 0.]) if np.linalg.norm(point) == 0: continue for n in range(self.ndim): unit_cell += point[n] * self.bravais_lattice[n] for position in self.motif: species = int(position[3]) atom = self.vp.sphere(radius=self.atom_radius, color=color_list[species]) atom.visible = False atom.pos.x, atom.pos.y, atom.pos.z = (position[:3] + unit_cell) self.extra_atoms.append(atom) def __create_bonds(self) -> None: """ Private method to initialized the bonds between atoms as solid lines. """ mesh_points = generate_basis_combinations(self.ndim) try: # Origin cell for initial_atom, final_atom, cell, nn in self.neighbours: unit_cell = self.vp.vector(*cell) bond = self.vp.curve(self.atoms[initial_atom].pos, self.atoms[final_atom].pos + unit_cell) self.bonds.append(bond) if np.linalg.norm(cell) > 1E-4: continue reverse_bond = self.vp.curve(self.atoms[final_atom].pos, self.atoms[initial_atom].pos - unit_cell) self.bonds.append(reverse_bond) # Super cell for point in mesh_points: unit_cell = np.array(([0., 0., 0.])) for n in range(self.ndim): unit_cell += point[n] * self.bravais_lattice[n] unit_cell = self.vp.vector(unit_cell[0], unit_cell[1], unit_cell[2]) for bond in self.bonds: supercell_bond = self.vp.curve(bond.point(0)["pos"] + unit_cell, bond.point(1)["pos"] + unit_cell) supercell_bond.visible = False self.extra_bonds.append(supercell_bond) except AttributeError: print("Warning: To visualize bonds, Viewer must receive an initialized System")
[docs] def visualize(self) -> None: """ Method to render the 3d visualization of the crystal. It also renders a GUI to interact with the visualization. """ self.vp.scene.visible = True self.vp.button(text="supercell", bind=self.__add_unit_cells) self.vp.button(text="primitive unit cell", bind=self.__remove_unit_cells) self.vp.button(text="remove bonds", bind=self.__remove_bonds) self.vp.button(text="show bonds", bind=self.__show_bonds) self.vp.button(text="draw cell boundary", bind=self.__draw_boundary) self.vp.button(text="remove cell boundary", bind=self.__remove_boundary) self.vp.button(text="highlight edge atoms", bind=self.__highlight_edge) self.vp.scene.bind("mousedown", self.__mousedown) self.vp.scene.bind("mouseup", self.__mouseup)
def __mousedown(self, event) -> None: """ Private method to detect mouse buttons press. :param event: vpython.event_return object. """ self.initial_position = self.vp.scene.mouse.pos self.drag = True def __mouseup(self, event) -> None: """ Private method to detect mouse buttons release. :param event: vpython.event_return object. """ if self.drag: increment = self.vp.scene.mouse.pos - self.initial_position self.initial_position = self.vp.scene.mouse.pos self.vp.scene.camera.pos -= increment self.drag = False def __add_unit_cells(self, button) -> None: """ Private method to render additional unit cells from the origin. :param button: vpython.button object. """ for atom in self.extra_atoms: atom.visible = True def __show_bonds(self, button) -> None: """ Private method to render the bonds between atoms. :param button: vpython.button object. """ for bond in self.bonds: bond.visible = True if self.extra_atoms[0].visible: for bond in self.extra_bonds: bond.visible = True def __remove_unit_cells(self, button) -> None: """ Private method to derender the additional unit cells. :param button: vpython.button object. """ for atom in self.extra_atoms: atom.visible = False if self.extra_bonds[0].visible: for bond in self.extra_bonds: bond.visible = False def __remove_bonds(self, button) -> None: """ Private method to derender the bonds between atoms. :param button: vpython.button object. """ for bond in self.bonds: bond.visible = False for bond in self.extra_bonds: bond.visible = False def __draw_boundary(self, button) -> None: """ Private method to draw the boundary of the unit cell. TODO: Requires fixing. :param button: vpython.button object. """ self.cell_vectors = [] mid_point = np.array([0., 0., 0.]) size_vector = [0., 0., 0.] for n in range(self.ndim): mid_point += self.bravais_lattice[n] / 2 size_vector[n] = self.bravais_lattice[n][n] self.cell_boundary = self.vp.box(pos=self.vp.vector(mid_point[0], mid_point[1], mid_point[2]), size=self.vp.vector(size_vector[0], size_vector[1], size_vector[2]), opacity=0.5) for basis_vector in self.bravais_lattice: unit_cell = self.vp.vector(basis_vector[0], basis_vector[1], basis_vector[2]) vector = self.vp.arrow(pos=self.vp.vector(0, 0, 0), axis=unit_cell, color=self.vp.color.green, shaftwidth=0.1) self.cell_vectors.append(vector) def __remove_boundary(self, button) -> None: """ Private method to remove the boundary of the unit cell. :param button: vpython.button object. """ self.cell_boundary.visible = False for vector in self.cell_vectors: vector.visible = False def __highlight_edge(self, button) -> None: """ Private method to highlight the edges of the atom of the solid. :param button: vpython.button object. """ if button.text == "highlight edge atoms": for atom_index in self.edge_atoms: self.atoms[atom_index].color = self.vp.color.green button.text = "dehighlight edge atoms" else: for atom_index in self.edge_atoms: self.atoms[atom_index].color = self.vp.color.yellow button.text = "highlight edge atoms"