def test_phi_at_x(self): energy = np.array([0.1, 0.2, 0.3, 0.4, 0.5]) coordinates = np.array([-2.0, -1.0, 0.0, 1.0, 2.0]) with patch( 'pyscses.grid.index_of_grid_at_x') as mock_index_of_grid_at_x: mock_index_of_grid_at_x.side_effect = [0, 2, 3] self.assertEqual( phi_at_x(phi=energy, coordinates=coordinates, x=-1.5), 0.1) self.assertEqual( phi_at_x(phi=energy, coordinates=coordinates, x=-0.1), 0.3) self.assertEqual( phi_at_x(phi=energy, coordinates=coordinates, x=0.6), 0.4)
def create_space_charge_region(self, grid, pos_or_neg_scr, scr_limit): """Calculate the space charge region. The space charge region is defined as the region when the electrostatic potential is greater than a predefined limit. Args: grid (:obj:`pyscses.Grid`): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. pos_or_neg_scr (str): 'positive' - for a positive space charge potential. 'negative' - for a negative space charge potential. scr_limit (float): The minimum electrostatic potential that the electrostatic potential must exceed to be included in the space charge region. Returns: list: List of x coordinates for sites within the space charge region. """ space_charge_region = [] self.phi_on_mobile_defect_grid = [ phi_at_x(self.phi, self.grid.x, x) for x in grid.x ] x_and_phi = np.column_stack((grid.x, self.phi_on_mobile_defect_grid)) for i in range(len(x_and_phi)): if pos_or_neg_scr == 'positive': if x_and_phi[i, 1] - x_and_phi[0, 1] > scr_limit: space_charge_region.append(x_and_phi[i, 0]) if pos_or_neg_scr == 'negative': if x_and_phi[i, 1] - x_and_phi[0, 1] < scr_limit: space_charge_region.append(x_and_phi[i, 0]) return space_charge_region
def calculate_probabilities(self, grid: Grid, phi: np.ndarray, temp: float) -> np.ndarray: """ Calculates the probability of a site being occupied by its corresponding defect. Args: grid (Grid): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. phi (array): electrostatic potential on a one-dimensional grid. temp (float): Absolute temperature. Returns: array: probabilities of defects occupying each site using their grid points """ # TODO: This is Jacob's fixed code, but it is inefficient and slow. probability = np.zeros_like(grid.x) for i,j in enumerate(grid.x): prob = [] for site in self.sites: if j == site.x: prob.append(site.probabilities_as_list(phi_at_x(phi, grid.x, site.x), temp)) if len(prob) == 0: probability[i] = 0 else: probability[i] = np.mean(prob) return probability
def solve(self, approximation): """ Self-consistent solving of the Poisson-Boltzmann equation. Iterates until the convergence is less than the convergence limit. The outputs are stored as Calculation attributes. Calculation.phi (array): Electrostatic potential on a one-dimensional grid. Calculation.rho (array): Charge density on a one-dimensional grid. Calculation.niter (int): Number of iterations performed to reach convergence. Args: approximation (str): Approximation used for the defect behaviour. 'mott-schottky' - Some defects immobile / fixed to bulk mole fractions. 'gouy-chapman' - All defects mobile / able to redistribute. """ poisson_solver = MatrixSolver( self.grid, self.dielectric, self.temp, boundary_conditions=self.boundary_conditions) phi = np.zeros_like(self.grid.x) rho = np.zeros_like(self.grid.x) conv = 1 niter = 0 while conv > self.convergence: predicted_phi, rho = poisson_solver.solve(phi) if approximation == 'gouy-chapman': average_phi = self.calculate_average(self.grid, self.bulk_x_min, self.bulk_x_max, predicted_phi) predicted_phi -= average_phi if approximation == 'mott-schottky': vo_predicted_phi = [ phi_at_x(predicted_phi, self.grid.x, x) for x in self.grid.subgrid(self.site_labels[0]).x ] average_vo_predicted_phi = self.calculate_average( self.grid.subgrid(self.site_labels[0]), self.bulk_x_min, self.bulk_x_max, vo_predicted_phi) predicted_phi -= average_vo_predicted_phi phi = self.alpha * predicted_phi + (1.0 - self.alpha) * phi conv = sum((predicted_phi - phi)**2) / len(self.grid.x) prob = self.grid.set_of_sites.calculate_probabilities( self.grid, phi, self.temp) niter += 1 # if niter % 500 == 0.0: # if niter == 1: # print(conv) # print(phi, rho) # stop self.phi = phi self.rho = self.grid.rho(phi, self.temp) self.niter = niter
def calculate_defect_density( self, grid, phi, temp ): """ Calculates the defect density at each site. Args: grid (object): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. phi (array): electrostatic potential on a one-dimensional grid. temp (float): Absolute temperature. Returns: array: defect density for each site using their grid points """ defect_density = np.zeros_like( grid.x ) for site in self.sites: i = index_of_grid_at_x( grid.x, site.x ) defect_density[ i ] += np.asarray( site.probabilities( phi_at_x( phi, grid.x, site.x ), temp ) ) / grid.volumes[ i ] return defect_density
def calculate_probabilities(self, grid, phi, temp): """ Calculates the probability of a site being occupied by its corresponding defect. Args: grid (object): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. phi (array): electrostatic potential on a one-dimensional grid. temp (float): Absolute temperature. Returns: probability (array): probabilities of defects occupying each site using their grid points """ probability = np.zeros_like(grid.x) for site in self.sites: probability[index_of_grid_at_x(grid.x, site.x)] = np.asarray( site.probabilities(phi_at_x(phi, grid.x, site.x), temp)) return probability
def subgrid_calculate_defect_density(self, sub_grid: Grid, full_grid: Grid, phi: np.ndarray, temp: float) -> np.ndarray: """ Calculates the defect density at each site for a given subset of sites. Args: subgrid (Grid): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. For a given subset of sites. full_grid (Grid): Grid object - contains properties of the grid including the x coordinates and the volumes. Used to access the x coordinates. For all sites. phi (array): electrostatic potential on a one-dimensional grid. temp (float): Absolute temperature. Returns: array: defect density for each site using their grid points """ defect_density = np.zeros_like( sub_grid.x ) for site in self.sites: i = index_of_grid_at_x( sub_grid.x, site.x ) defect_density[ i ] += np.asarray( site.probabilities_as_list( phi_at_x( phi, full_grid.x, site.x ), temp ) ) / sub_grid.volumes[ i ] return defect_density
def solve(self, approximation: str, verbose: bool = False) -> None: """ Self-consistent solving of the Poisson-Boltzmann equation. Iterates until the convergence is less than the convergence limit. The outputs are stored as Calculation attributes. Calculation.phi (array): Electrostatic potential on a one-dimensional grid. Calculation.rho (array): Charge density on a one-dimensional grid. Calculation.niter (int): Number of iterations performed to reach convergence. Args: approximation (str): Approximation used for the defect behaviour. 'mott-schottky' - Some defects immobile / fixed to bulk mole fractions. 'gouy-chapman' - All defects mobile / able to redistribute. verbose (optional, bool): Verbose output. Default is False. """ poisson_solver = MatrixSolver( grid=self.grid, dielectric=self.dielectric, temp=self.temp, boundary_conditions=self.boundary_conditions) phi = np.zeros_like(self.grid.x) rho = np.zeros_like(self.grid.x) conv = 1.0 niter = 0 while conv > self.convergence: predicted_phi, rho = poisson_solver.solve(phi) if approximation == 'gouy-chapman': average_phi = self.calculate_average( grid=self.grid, min_cutoff=self.bulk_x_min, max_cutoff=self.bulk_x_max, sc_property=predicted_phi) predicted_phi -= average_phi elif approximation == 'mott-schottky': subgrid = self.grid.subgrid(self.site_labels[0]) predicted_phi_subgrid = np.array([ phi_at_x(phi=predicted_phi, coordinates=self.grid.x, x=x) for x in subgrid.x ]) average_predicted_phi = self.calculate_average( grid=subgrid, min_cutoff=self.bulk_x_min, max_cutoff=self.bulk_x_max, sc_property=predicted_phi_subgrid) predicted_phi -= average_predicted_phi # TODO: This is inefficient. Jacob has some ideas for how to improve things. # TODO: This definition of convergence should not be averaged over all sites. phi = self.alpha * predicted_phi + (1.0 - self.alpha) * phi conv = sum((predicted_phi - phi)**2) / len(self.grid.x) # prob = self.grid.set_of_sites.calculate_probabilities( self.grid, phi, self.temp) # Jacob: Does this do anything? niter += 1 if verbose: if niter % 500 == 0: print( f'Iteration: {niter} -> Convergence: {conv} / {self.convergence}' ) if verbose: print( f'Converged at iteration {niter} -> Convergence: {conv} / {self.convergence}' ) self.phi = phi self.rho = self.grid.rho(phi, self.temp) self.niter = niter