def communicate_f(self): """ Used in communicating the values at the boundary zones for each of the local vectors among all procs. This routine is called to take care of communication (and periodic B.C's) procedures for the distribution function array. """ if(self.performance_test_flag == True): tic = af.time() # Transfer data from af.Array to PETSc.Vec af_to_petsc_glob_array(self, self.f, self._glob_f_array) # The following function takes care of interzonal communications # Additionally, it also automatically applies periodic BCs when necessary self._da_f.globalToLocal(self._glob_f, self._local_f) # Converting back from PETSc.Vec to af.Array: self.f = petsc_local_array_to_af(self, self.N_p1*self.N_p2*self.N_p3, self.N_species, self._local_f_array ) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_communicate_f += toc - tic return
def op_fvm_q(self, dt): self._communicate_f() self._apply_bcs_f() if (self.performance_test_flag == True): tic = af.time() fvm_timestep_RK2(self, dt) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fvm_solver += toc - tic # Solving for tau = 0 systems if (af.any_true( self.physical_system.params.tau(self.q1_center, self.q2_center, self.p1, self.p2, self.p3) == 0)): if (self.performance_test_flag == True): tic = af.time() self.f = self._source(self.f, self.q1_center, self.q2_center, self.p1, self.p2, self.p3, self.compute_moments, self.physical_system.params, True) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_sourcets += toc - tic af.eval(self.f) return
def op_fvm(self, dt): """ Evolves the system defined using FVM. It does so by integrating the function df_dt using an RK2 stepping scheme. After the initial evaluation at the midpoint, we evaluate the currents(J^{n+0.5}) and pass it to the FDTD algo when an electrodynamic case needs to be evolved. The FDTD algo updates the field values, which are used at the next evaluation of df_dt. Parameters ---------- dt : double Time-step size to evolve the system """ self._communicate_f() self._apply_bcs_f() if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.instantaneous_collisions == True): split.strang(self, timestep_fvm, update_for_instantaneous_collisions, dt) else: timestep_fvm(self, dt) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fvm_solver += toc - tic af.eval(self.f) return
def f_interp_2d(self, dt): if(self.performance_test_flag == True): tic = af.time() # Defining a lambda function to perform broadcasting operations # This is done using af.broadcast, which allows us to perform # batched operations when operating on arrays of different sizes addition = lambda a, b:a + b # af.broadcast(function, *args) performs batched operations on # function(*args) q1_center_new = af.broadcast(addition, self.q1_center, - self._A_q1 * dt) q2_center_new = af.broadcast(addition, self.q2_center, - self._A_q2 * dt) # Reordering from (dof, N_q1, N_q2) --> (N_q1, N_q2, dof) self.f = af.approx2(af.reorder(self.f, 1, 2, 0), af.reorder(q1_center_new, 1, 2, 0), af.reorder(q2_center_new, 1, 2, 0), af.INTERP.BICUBIC_SPLINE, xp = af.reorder(self.q1_center, 1, 2, 0), yp = af.reorder(self.q2_center, 1, 2, 0) ) # Reordering from (N_q1, N_q2, dof) --> (dof, N_q1, N_q2) self.f = af.reorder(self.f, 2, 0, 1) af.eval(self.f) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_interp2 += toc - tic return
def jia_step(self, dt): """ Advances the system using the Jia split scheme. reference:<https://www.sciencedirect.com/science/article/pii/S089571771000436X> NOTE: This scheme is computationally expensive, and should only be used for testing/debugging Parameters ---------- dt : double Time-step size to evolve the system """ self.dt = dt if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.solver_method_in_q == 'FVM'): if (self.physical_system.params.solver_method_in_p == 'ASL' and self.physical_system.params.fields_enabled == True): split.jia(self, op_fvm, op_fields, dt) else: op_fvm(self, dt) # Advective Semi-lagrangian method elif (self.physical_system.params.solver_method_in_q == 'ASL'): if (self.physical_system.params.fields_enabled == True): def op_advect_q_and_solve_src(self, dt): return (split.jia(self, op1=op_advect_q, op2=op_solve_src, dt=dt)) if (self.physical_system.params.solver_method_in_p == 'ASL'): split.jia(self, op_advect_q_and_solve_src, op_fields, dt) # For FVM in p-space: else: split.jia(self, op_advect_q_and_solve_src, op_fvm, dt) else: split.jia(self, op_advect_q, op_solve_src, dt) af.eval(self.f) check_divergence(self) self.time_elapsed += dt if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_ts += toc - tic return
def strang_step(self, dt): """ Advances the system using a strang-split scheme. This scheme is 2nd order accurate in time. Parameters ---------- dt : double Time-step size to evolve the system """ self.dt = dt if(self.performance_test_flag == True): tic = af.time() if(self.physical_system.params.solver_method_in_q == 'FVM'): if( self.physical_system.params.solver_method_in_p == 'ASL' and self.physical_system.params.EM_fields_enabled == True ): split.strang(self, op_fvm, op_fields, dt) else: op_fvm(self, dt) # Advective Semi-lagrangian method elif(self.physical_system.params.solver_method_in_q == 'ASL'): if(self.physical_system.params.EM_fields_enabled == True): def op_advect_q_and_solve_src(self, dt): return(split.strang(self, op1 = op_advect_q, op2 = op_solve_src, dt = dt ) ) if(self.physical_system.params.solver_method_in_p == 'ASL'): split.strang(self, op_advect_q_and_solve_src, op_fields, dt) # For FVM in p-space: else: split.strang(self, op_advect_q_and_solve_src, op_fvm, dt) else: split.strang(self, op_advect_q, op_solve_src, dt) check_divergence(self) self.time_elapsed += dt if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_ts += toc - tic return
def communicate_fields(self, on_fdtd_grid=False): """ Used in communicating the values at the boundary zones for each of the local vectors among all procs.This routine is called to take care of communication(and periodic B.C's) procedures for the EM field arrays. The function is used for communicating the EM field values on the cell centered grid which is used by default. Additionally,it can also be used to communicate the values on the Yee-grid which is used by the FDTD solver. """ if (self.performance_test_flag == True): tic = af.time() # Obtaining start coordinates for the local zone # Additionally, we also obtain the size of the local zone ((i_q1_start, i_q2_start), (N_q1_local, N_q2_local)) = self._da_fields.getCorners() N_g = self.N_g # Assigning the values of the af.Array # fields quantities to the PETSc.Vec: if (on_fdtd_grid is True): flattened_global_EM_fields_array = \ af.flat(self.yee_grid_EM_fields[:, :, N_g:-N_g, N_g:-N_g]) flattened_global_EM_fields_array.to_ndarray(self._glob_fields_array) else: flattened_global_EM_fields_array = \ af.flat(self.cell_centered_EM_fields[:, :, N_g:-N_g, N_g:-N_g]) flattened_global_EM_fields_array.to_ndarray(self._glob_fields_array) # Takes care of boundary conditions and interzonal communications: self._da_fields.globalToLocal(self._glob_fields, self._local_fields) # Converting back to af.Array if (on_fdtd_grid is True): self.yee_grid_EM_fields = af.moddims( af.to_array(self._local_fields_array), 6, 1, N_q1_local + 2 * N_g, N_q2_local + 2 * N_g) af.eval(self.yee_grid_EM_fields) else: self.cell_centered_EM_fields = af.moddims( af.to_array(self._local_fields_array), 6, 1, N_q1_local + 2 * N_g, N_q2_local + 2 * N_g) af.eval(self.cell_centered_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_communicate_fields += toc - tic return
def fdtd_evolve_B(self, dt): """ Evolves magnetic fields from B^{n + 1/2} --> B^{n + 3/2} Parameters ---------- dt : double Time-step size to evolve the system """ if (self.performance_test_flag == True): tic = af.time() dq1 = self.dq1 dq2 = self.dq2 E1 = self.yee_grid_EM_fields[0] # (i, j + 1/2) E2 = self.yee_grid_EM_fields[1] # (i + 1/2, j) E3 = self.yee_grid_EM_fields[2] # (i + 1/2, j + 1/2) E1_minus_q2 = af.shift(E1, 0, 0, 0, 1) E2_minus_q1 = af.shift(E2, 0, 0, 1, 0) E3_minus_q1 = af.shift(E3, 0, 0, 1, 0) E3_minus_q2 = af.shift(E3, 0, 0, 0, 1) # dB/dt = -(∇ x E) # dB1/dt = - dE3/dq2 # dB2/dt = + dE3/dq1 # dB3/dt = - (dE2/dq1 - dE1/dq2) # curlE_x = dE3/dq2 curlE_1 = (E3 - E3_minus_q2) / dq2 # (i + 1/2, j) # curlE_y = -dE3/dq1 curlE_2 = -(E3 - E3_minus_q1) / dq1 # (i, j + 1/2) # curlE_z = (dE2/dq1 - dE1/dq2) curlE_3 = (E2 - E2_minus_q1) / dq1 - (E1 - E1_minus_q2) / dq2 # (i, j) # B1 --> (i + 1/2, j) self.yee_grid_EM_fields[3] += -dt * curlE_1 # B2 --> (i, j + 1/2) self.yee_grid_EM_fields[4] += -dt * curlE_2 # B3 --> (i, j) self.yee_grid_EM_fields[5] += -dt * curlE_3 af.eval(self.yee_grid_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldsolver += toc - tic return
def communicate_f(self): """ Used in communicating the values at the boundary zones for each of the local vectors among all procs. This routine is called to take care of communication (and periodic B.C's) procedures for the distribution function array. """ if (self.performance_test_flag == True): tic = af.time() # Obtaining start coordinates for the local zone # Additionally, we also obtain the size of the local zone ((i_q1_start, i_q2_start), (N_q1_local, N_q2_local)) = self._da_f.getCorners() N_g_q = self.N_ghost_q N_g_p = self.N_ghost_p # Assigning the local array only when Dirichlet # boundary conditions are applied. This is needed since # only inflowing characteristics are to be changed by # the apply boundary conditions function. if (self.boundary_conditions.in_q1_left == 'dirichlet' or self.boundary_conditions.in_q1_right == 'dirichlet' or self.boundary_conditions.in_q2_bottom == 'dirichlet' or self.boundary_conditions.in_q2_top == 'dirichlet'): af.flat(self.f).to_ndarray(self._local_f_array) # Global value is non-inclusive of the ghost-zones: af.flat(self.f[:, :, N_g_q:-N_g_q, N_g_q:-N_g_q]).to_ndarray(self._glob_f_array) # The following function takes care of interzonal communications # Additionally, it also automatically applies periodic BCs when necessary self._da_f.globalToLocal(self._glob_f, self._local_f) # Converting back from PETSc.Vec to af.Array: f_flattened = af.to_array(self._local_f_array) self.f = af.moddims(f_flattened, (self.N_p1 + 2 * N_g_p) * (self.N_p2 + 2 * N_g_p) * (self.N_p3 + 2 * N_g_p), self.N_species, N_q1_local + 2 * N_g_q, N_q2_local + 2 * N_g_q) af.eval(self.f) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_communicate_f += toc - tic return
def jia_step(self, dt): """ Advances the system using the Jia split scheme. NOTE: This scheme is computationally expensive, and should only be used for testing/debugging Parameters ---------- dt : float Time-step size to evolve the system """ self.dt = dt if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.solver_method_in_q == 'FVM'): if (self.physical_system.params.solver_method_in_p == 'ASL' and self.physical_system.params.charge_electron != 0): split.strang(self, op_fvm_q, op_fields, dt) else: op_fvm_q(self, dt) # Advective Semi-lagrangian method elif (self.physical_system.params.solver_method_in_q == 'ASL'): if (self.physical_system.params.charge_electron == 0): split.jia(self, op_advect_q, op_solve_src, dt) else: def op_advect_q_and_solve_src(self, dt): return (split.jia(self, op1=op_advect_q, op2=op_solve_src, dt=dt)) split.jia(self, op_advect_q_and_solve_src, op_fields, dt) check_divergence(self) self.time_elapsed += dt if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_ts += toc - tic return
def fft_poisson(self, f=None): """ Solves the Poisson Equation using the FFTs: Used as a backup solver in case of low resolution runs (ie. used on a single node) with periodic boundary conditions. """ if (self.performance_test_flag == True): tic = af.time() if (self._comm.size != 1): raise Exception('FFT solver can only be used when run in serial') else: N_g = self.N_ghost rho = af.reorder( self.physical_system.params.charge_electron \ * self.compute_moments('density', f)[:, N_g:-N_g, N_g:-N_g], 1, 2, 0 ) k_q1 = fftfreq(rho.shape[0], self.dq1) k_q2 = fftfreq(rho.shape[1], self.dq2) k_q2, k_q1 = np.meshgrid(k_q2, k_q1) k_q1 = af.to_array(k_q1) k_q2 = af.to_array(k_q2) rho_hat = af.fft2(rho) potential_hat = rho_hat / (4 * np.pi**2 * (k_q1**2 + k_q2**2)) potential_hat[0, 0] = 0 E1_hat = -1j * 2 * np.pi * k_q1 * potential_hat E2_hat = -1j * 2 * np.pi * k_q2 * potential_hat # Non-inclusive of ghost-zones: E1_physical = af.reorder(af.real(af.ifft2(E1_hat)), 2, 0, 1) E2_physical = af.reorder(af.real(af.ifft2(E2_hat)), 2, 0, 1) self.cell_centered_EM_fields[0, N_g:-N_g, N_g:-N_g] = E1_physical self.cell_centered_EM_fields[1, N_g:-N_g, N_g:-N_g] = E2_physical af.eval(self.cell_centered_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldsolver += toc - tic return
def f_interp_2d(self, dt): """ Performs 2D interpolation in the q-space to solve for the equation: df/dt + A_q1 df/dq1 + A_q2 df/dq2 = 0 This is done by backtracing the characteristic curves and interpolating at the origin of the characteristics. Parameters ---------- dt : double Time-step size to evolve the system """ if(self.performance_test_flag == True): tic = af.time() A_q1, A_q2 = af.broadcast(self._A_q, self.f, self.time_elapsed, self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center, self.physical_system.params ) # Using the add method wrapped with af.broadcast q1_center_new = add(self.q1_center, - A_q1 * dt) q2_center_new = add(self.q2_center, - A_q2 * dt) # Reordering from (dof, N_s, N_q1, N_q2) --> (N_q1, N_q2, N_s, dof) # NOTE: To be changed after the implementation of axes specific # interpolation operators gets completed from ArrayFire's end. # Ref:https://github.com/arrayfire/arrayfire/issues/1955 self.f = af.approx2(af.reorder(self.f, 2, 3, 1, 0), af.reorder(q1_center_new, 2, 3, 1, 0), af.reorder(q2_center_new, 2, 3, 1, 0), af.INTERP.BICUBIC_SPLINE, xp = af.reorder(self.q1_center, 2, 3, 1, 0), yp = af.reorder(self.q2_center, 2, 3, 1, 0) ) # Reordering from (N_q1, N_q2, N_s, dof) --> (dof, N_s, N_q1, N_q2) self.f = af.reorder(self.f, 3, 2, 0, 1) af.eval(self.f) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_interp2 += toc - tic return
def swss_step(self, dt): """ Advances the system using a SWSS-split scheme. This scheme is 2nd order accurate in time. Parameters ---------- dt : float Time-step size to evolve the system """ self.dt = dt if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.solver_method_in_q == 'FVM'): if (self.physical_system.params.solver_method_in_p == 'ASL' and self.physical_system.params.charge_electron != 0): split.strang(self, op_fvm_q, op_fields, dt) else: op_fvm_q(self, dt) # Advective Semi-lagrangian method elif (self.physical_system.params.solver_method_in_q == 'ASL'): if (self.physical_system.params.charge_electron == 0): split.swss(self, op_advect_q, op_solve_src, dt) else: def op_advect_q_and_solve_src(self, dt): return (split.swss(self, op1=op_advect_q, op2=op_solve_src, dt=dt)) split.swss(self, op_advect_q_and_solve_src, op_fields, dt) check_divergence(self) self.time_elapsed += dt if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_ts += toc - tic return
def op_fvm(self, dt): if (self.performance_test_flag == True): tic = af.time() timestep_fvm(self, dt) af.eval(self.f) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fvm_solver += toc - tic return
def reconstruct(self, input_array, axis, reconstruction_method): """ Reconstructs the variation within a cell using the reconstruction method specified. Parameters ---------- input_array: af.Array Array holding the cells data. axis: int Axis along which the reconstruction method is to be applied. reconstruction_method: str Reconstruction method which needs to be applied. """ if (self.performance_test_flag == True): tic = af.time() if (reconstruction_method == 'piecewise-constant'): left_face_value = input_array right_face_value = input_array elif (reconstruction_method == 'minmod'): left_face_value, right_face_value = reconstruct_minmod( input_array, axis) elif (reconstruction_method == 'ppm'): left_face_value, right_face_value = reconstruct_ppm(input_array, axis) elif (reconstruction_method == 'weno5'): raise SystemExit('WENO5 is currently buggy. Avoid Using!!!') left_face_value, right_face_value = reconstruct_weno5( input_array, axis) else: raise NotImplementedError( 'Reconstruction method invalid/not-implemented') if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_reconstruct += toc - tic return (left_face_value, right_face_value)
def riemann_solver(self, left_state, right_state, velocity): """ Returns the upwinded state, using the 1st order upwind Riemann solver. Parameters ---------- left_state : af.Array Array holding the values for the state at the left edge of the cells. right_state : af.Array Array holding the values for the state at the right edge of the cells. velocity : af.Array Velocity array whose sign will be used to determine whether the left or right state is chosen. """ if (self.performance_test_flag == True): tic = af.time() # Checking if array isn't 4D: try: size_axis_2 = left_state.shape[2] except: size_axis_2 = 1 try: size_axis_3 = left_state.shape[3] except: size_axis_3 = 1 # Tiling to get to appropriate shape: try: assert (velocity.shape[2] == left_state.shape[2]) except: velocity = af.tile(velocity, 1, 1, size_axis_2, size_axis_3) upwind_state = af.select(velocity > 0, left_state, right_state) af.eval(upwind_state) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_riemann += toc - tic return (upwind_state)
def op_fields(self, dt): """ Evolves the following part of the equations specified: df/dt + A_p1 df/dp1 + A_p2 df/dp1 + A_p3 df/dp1 = 0 Parameters ---------- dt : double Time-step size to evolve the system """ if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.fields_solver == 'electrostatic'): rho = multiply(self.physical_system.params.charge, self.compute_moments('density')) self.fields_solver.compute_electrostatic_fields(rho) # Evolving fields: if (self.physical_system.params.fields_solver == 'fdtd'): J1 = multiply( self.physical_system.params.charge, self.compute_moments('mom_v1_bulk')) # (i + 1/2, j + 1/2) J2 = multiply( self.physical_system.params.charge, self.compute_moments('mom_v2_bulk')) # (i + 1/2, j + 1/2) J3 = multiply( self.physical_system.params.charge, self.compute_moments('mom_v3_bulk')) # (i + 1/2, j + 1/2) self.fields_solver.evolve_electrodynamic_fields(J1, J2, J3, dt) f_interp_p_3d(self, dt) af.eval(self.f) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldstep += toc - tic return
def riemann_solver(self, left_flux, right_flux, left_f, right_f, dim): if (self.performance_test_flag == True): tic = af.time() if (self.physical_system.params.riemann_solver == 'upwind-flux'): if (dim == 'q1'): velocity = af.tile(self._C_q1, 1, left_flux.shape[1], left_flux.shape[2]) elif (dim == 'q2'): velocity = af.tile(self._C_q2, 1, left_flux.shape[1], left_flux.shape[2]) else: raise NotImplementedError('Invalid Option!') flux = upwind_flux(left_flux, right_flux, velocity) elif (self.physical_system.params.riemann_solver == 'lax-friedrichs'): if (dim == 'q1'): c_lax = self.dt / self.dq1 elif (dim == 'q2'): c_lax = self.dt / self.dq2 else: raise NotImplementedError('Invalid Option!') flux = lax_friedrichs_flux(left_flux, right_flux, left_f, right_f, c_lax) else: raise NotImplementedError( 'Riemann solver passed is invalid/not-implemented') if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_riemann += toc - tic return (flux)
def op_solve_src(self, dt): """ Evolves the source term of the equations specified: df/dt = source Parameters ---------- dt : double Time-step size to evolve the system """ if(self.performance_test_flag == True): tic = af.time() # Solving for tau = 0 systems: if(self.physical_system.params.instantaneous_collisions == True): self.f = self._source(self.f, self.time_elapsed, self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center, self.compute_moments, self.physical_system.params, True ) if( self.physical_system.params.source_enabled == True and self.physical_system.params.instantaneous_collisions != True ): self.f = integrators.RK2(self._source, self.f, dt, self.time_elapsed, self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center, self.compute_moments, self.physical_system.params ) af.eval(self.f) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_sourcets += toc - tic return
def bandwidth_test(n_evals): a = af.randu(32, 32, 32**3, dtype=af.Dtype.f64) b = af.randu(32, 32, 32**3, dtype=af.Dtype.f64) c = a + b af.eval(c) af.sync() tic = af.time() for i in range(n_evals): c = a + b af.eval(c) af.sync() toc = af.time() bandwidth_available = memory_bandwidth(a.elements(), 2, 1, n_evals, toc - tic) return (bandwidth_available)
def op_solve_src(self, dt): """ Evolves the source term of the equations specified: df/dt = source Parameters ---------- dt : double Time-step size to evolve the system """ if (self.performance_test_flag == True): tic = af.time() # Solving for tau = 0 systems: tau = self.physical_system.params.tau(self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center) if (af.any_true(tau == 0)): self.f = af.select( tau == 0, self._source(self.f, self.time_elapsed, self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center, self.compute_moments, self.physical_system.params, True), self.f) self.f = integrators.RK2(self._source, self.f, dt, self.time_elapsed, self.q1_center, self.q2_center, self.p1_center, self.p2_center, self.p3_center, self.compute_moments, self.physical_system.params) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_sourcets += toc - tic return
def fdtd_evolve_E(self, dt): if (self.performance_test_flag == True): tic = af.time() dq1 = self.dq1 dq2 = self.dq2 B1 = self.yee_grid_EM_fields[3] B2 = self.yee_grid_EM_fields[4] B3 = self.yee_grid_EM_fields[5] # dE1/dt = + dB3/dq2 # dE2/dt = - dB3/dq1 # dE3/dt = dB2/dq1 - dB1/dq2 B1_shifted_q2 = af.shift(B1, 0, 0, 0, 1) B2_shifted_q1 = af.shift(B2, 0, 0, 1, 0) B3_shifted_q1 = af.shift(B3, 0, 0, 1, 0) B3_shifted_q2 = af.shift(B3, 0, 0, 0, 1) self.yee_grid_EM_fields[0] += (dt / dq2) * (B3 - B3_shifted_q2) - self.J1 * dt self.yee_grid_EM_fields[1] += -(dt / dq1) * (B3 - B3_shifted_q1) - self.J2 * dt self.yee_grid_EM_fields[2] += (dt / dq1) * (B2 - B2_shifted_q1) \ - (dt / dq2) * (B1 - B1_shifted_q2) \ - dt * self.J3 af.eval(self.yee_grid_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldsolver += toc - tic return
def communicate_fields(self, on_fdtd_grid = False): """ Used in communicating the values at the boundary zones for each of the local vectors among all procs.This routine is called to take care of communication(and periodic B.C's) procedures for the EM field arrays. The function is used for communicating the EM field values on the cell centered grid which is used by default. Additionally,it can also be used to communicate the values on the Yee-grid which is used by the FDTD solver. """ if(self.performance_test_flag == True): tic = af.time() # Assigning the values of the af.Array # fields quantities to the PETSc.Vec: if(on_fdtd_grid is True): tmp_array = self.yee_grid_EM_fields else: tmp_array = self.cell_centered_EM_fields af_to_petsc_glob_array(self, tmp_array, self._glob_fields_array) # Takes care of boundary conditions and interzonal communications: self._da_fields.globalToLocal(self._glob_fields, self._local_fields) # Converting back to af.Array tmp_array = petsc_local_array_to_af(self, 6, 1, self._local_fields_array) if(on_fdtd_grid is True): self.yee_grid_EM_fields = tmp_array else: self.cell_centered_EM_fields = tmp_array if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_communicate_fields += toc - tic return
def op_solve_src(self, dt): if (self.performance_test_flag == True): tic = af.time() # Solving for tau = 0 systems if (af.any_true( self.physical_system.params.tau(self.q1_center, self.q2_center, self.p1, self.p2, self.p3) == 0)): self.f = self._source(self.f, self.q1_center, self.q2_center, self.p1, self.p2, self.p3, self.compute_moments, self.physical_system.params, True) else: self.f = integrators.RK2(self._source, self.f, dt, self.q1_center, self.q2_center, self.p1, self.p2, self.p3, self.compute_moments, self.physical_system.params) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_sourcets += toc - tic return
def fdtd_evolve_B(self, dt): if (self.performance_test_flag == True): tic = af.time() dq1 = self.dq1 dq2 = self.dq2 E1 = self.yee_grid_EM_fields[0] E2 = self.yee_grid_EM_fields[1] E3 = self.yee_grid_EM_fields[2] # dB1/dt = - dE3/dq2 # dB2/dt = + dE3/dq1 # dB3/dt = - (dE2/dq1 - dE1/dq2) E1_shifted_q2 = af.shift(E1, 0, 0, 0, -1) E2_shifted_q1 = af.shift(E2, 0, 0, -1, 0) E3_shifted_q1 = af.shift(E3, 0, 0, -1, 0) E3_shifted_q2 = af.shift(E3, 0, 0, 0, -1) self.yee_grid_EM_fields[3] += -(dt / dq2) * (E3_shifted_q2 - E3) self.yee_grid_EM_fields[4] += (dt / dq1) * (E3_shifted_q1 - E3) self.yee_grid_EM_fields[5] += - (dt / dq1) * (E2_shifted_q1 - E2) \ + (dt / dq2) * (E1_shifted_q2 - E1) af.eval(self.yee_grid_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldsolver += toc - tic return
def riemann_solver(self, left_flux, right_flux, left_f, right_f, method, dim): """ Calls the appropriate Riemann solver as defined by the user. Parameters ---------- left_flux : af.Array Array holding the values for the flux at the left edge of the cells. right_flux : af.Array Array holding the values for the flux at the right edge of the cells. left_f : af.Array Array holding the values for the distribution function at the left edge of the cells. right_f : af.Array Array holding the values for the distribution function at the right edge of the cells. method : str Riemann solver method which is to be used. dim: str Dimension along which the Riemann solver is to be applied. """ if (self.performance_test_flag == True): tic = af.time() if (method == 'upwind-flux'): if (dim == 'q1' or dim == 'q2'): if (self._C_q1.elements() != left_flux.elements()): if (dim == 'q1'): velocity = af.tile(self._C_q1, 1, 1, left_flux.shape[2], left_flux.shape[3]) elif (dim == 'q2'): velocity = af.tile(self._C_q2, 1, 1, left_flux.shape[2], left_flux.shape[3]) else: if (dim == 'q1'): velocity = self._C_q1 elif (dim == 'q2'): velocity = self._C_q2 else: if (dim == 'p1'): velocity = self._C_p1 elif (dim == 'p2'): velocity = self._C_p2 elif (dim == 'p3'): velocity = self._C_p3 else: raise NotImplementedError('Invalid Option!') flux = upwind_flux(left_flux, right_flux, velocity) elif (self.physical_system.params.riemann_solver_in_q == 'lax-friedrichs'): if (dim == 'q1'): c_lax = self.dt / self.dq1 elif (dim == 'q2'): c_lax = self.dt / self.dq2 elif (dim == 'p1'): c_lax = self.dt / self.dp1 elif (dim == 'p2'): c_lax = self.dt / self.dp2 elif (dim == 'p3'): c_lax = self.dt / self.dp3 else: raise NotImplementedError('Invalid Dimension!') flux = lax_friedrichs_flux(left_flux, right_flux, left_f, right_f, c_lax) else: raise NotImplementedError( 'Riemann solver passed is invalid/not-implemented') if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_riemann += toc - tic return (flux)
def op_fields(self, dt): """ Evolves the following part of the equations specified: df/dt + A_p1 df/dp1 + A_p2 df/dp1 + A_p3 df/dp1 = 0 Parameters ---------- dt : double Time-step size to evolve the system """ if(self.performance_test_flag == True): tic = af.time() if(self.physical_system.params.fields_solver == 'electrostatic'): rho = multiply(self.physical_system.params.charge, self.compute_moments('density') ) self.fields_solver.compute_electrostatic_fields(rho) # Evolving fields: if(self.physical_system.params.fields_solver == 'fdtd'): if(self.physical_system.params.hybrid_model_enabled == True): communicate_fields(self.fields_solver, True) B1 = self.fields_solver.yee_grid_EM_fields[3] # (i + 1/2, j) B2 = self.fields_solver.yee_grid_EM_fields[4] # (i, j + 1/2) B3 = self.fields_solver.yee_grid_EM_fields[5] # (i, j) B1_plus_q2 = af.shift(B1, 0, 0, 0, -1) B2_plus_q1 = af.shift(B2, 0, 0, -1, 0) B3_plus_q1 = af.shift(B3, 0, 0, -1, 0) B3_plus_q2 = af.shift(B3, 0, 0, 0, -1) # curlB_x = dB3/dq2 curlB_1 = (B3_plus_q2 - B3) / self.dq2 # (i, j + 1/2) # curlB_y = -dB3/dq1 curlB_2 = -(B3_plus_q1 - B3) / self.dq1 # (i + 1/2, j) # curlB_z = (dB2/dq1 - dB1/dq2) curlB_3 = (B2_plus_q1 - B2) / self.dq1 - (B1_plus_q2 - B1) / self.dq2 # (i + 1/2, j + 1/2) # c --> inf limit: J = (∇ x B) / μ mu = self.physical_system.params.mu J1 = curlB_1 / mu # (i, j + 1/2) J2 = curlB_2 / mu # (i + 1/2, j) J3 = curlB_3 / mu # (i + 1/2, j + 1/2) # Using Generalized Ohm's Law for electric field: # (v X B)_x = B3 * v2 - B2 * v3 # (v X B)_x --> (i, j + 1/2) v_cross_B_1 = 0.5 * (B3_plus_q2 + B3) * self.compute_moments('mom_v2_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))) \ / self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))) \ - B2 * self.compute_moments('mom_v3_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))) \ / self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))) # (v X B)_y = B1 * v3 - B3 * v1 # (v X B)_y --> (i + 1/2, j) v_cross_B_2 = B1 * self.compute_moments('mom_v3_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))) \ / self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))) \ - 0.5 * (B3_plus_q1 + B3) * self.compute_moments('mom_v1_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))) \ / self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))) # (v X B)_z = B2 * v1 - B1 * v2 # (v X B)_z --> (i + 1/2, j + 1/2) v_cross_B_3 = 0.5 * (B2_plus_q1 + B2) * self.compute_moments('mom_v1_bulk', f = self.f) \ / self.compute_moments('density', f = self.f) \ - 0.5 * (B1_plus_q2 + B1) * self.compute_moments('mom_v2_bulk', f = self.f) \ / self.compute_moments('density', f = self.f) # (J X B)_x = B3 * J2 - B2 * J3 # (J X B)_x --> (i, j + 1/2) J_cross_B_1 = 0.5 * (B3_plus_q2 + B3) * ( J2 + af.shift(J2, 0, 0, 0, -1) + af.shift(J2, 0, 0, 1) + af.shift(J2, 0, 0, 1, -1) ) * 0.25 \ - B2 * (af.shift(J3, 0, 0, 1) + J3) * 0.5 # (J X B)_y = B1 * J3 - B3 * J1 # (J X B)_y --> (i + 1/2, j) J_cross_B_2 = B1 * (af.shift(J3, 0, 0, 0, 1) + J3) * 0.5 \ - 0.5 * (B3_plus_q1 + B3) * ( J1 + af.shift(J1, 0, 0, 0, 1) + af.shift(J1, 0, 0, -1) + af.shift(J1, 0, 0, -1, 1) ) * 0.25 # (J X B)_z = B2 * J1 - B1 * J2 # (J X B)_z --> (i + 1/2, j + 1/2) J_cross_B_3 = 0.5 * (B2_plus_q1 + B2) * (af.shift(J1, 0, 0, -1) + J1) * 0.5 \ - 0.5 * (B1_plus_q2 + B1) * (af.shift(J2, 0, 0, 0, -1) + J2) * 0.5 n_i = self.compute_moments('density') T_e = self.physical_system.params.fluid_electron_temperature # Using a 4th order stencil: dn_q1 = (- af.shift(n_i, 0, 0, -2) + 8 * af.shift(n_i, 0, 0, -1) - 8 * af.shift(n_i, 0, 0, 1) + af.shift(n_i, 0, 0, 2) ) / (12 * self.dq1) dn_q2 = (- af.shift(n_i, 0, 0, 0, -2) + 8 * af.shift(n_i, 0, 0, 0, -1) - 8 * af.shift(n_i, 0, 0, 0, 1) + af.shift(n_i, 0, 0, 0, 2) ) / (12 * self.dq2) # E = -(v X B) + (J X B) / (en) - T ∇n / (en) E1 = -v_cross_B_1 + J_cross_B_1 \ / (multiply(self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))), self.physical_system.params.charge ) ) \ - 0.5 * T_e * (dn_q1 + af.shift(dn_q1, 0, 0, 1)) / multiply(self.physical_system.params.charge, n_i) # (i, j + 1/2) E2 = -v_cross_B_2 + J_cross_B_2 \ / (multiply(self.compute_moments('density', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))), self.physical_system.params.charge ) ) \ - 0.5 * T_e * (dn_q2 + af.shift(dn_q2, 0, 0, 0, 1)) / multiply(self.physical_system.params.charge, n_i) # (i + 1/2, j) E3 = -v_cross_B_3 + J_cross_B_3 \ / (multiply(self.compute_moments('density', f = self.f), self.physical_system.params.charge ) ) # (i + 1/2, j + 1/2) self.fields_solver.yee_grid_EM_fields[0] = E1 self.fields_solver.yee_grid_EM_fields[1] = E2 self.fields_solver.yee_grid_EM_fields[2] = E3 af.eval(self.fields_solver.yee_grid_EM_fields) else: J1 = multiply(self.physical_system.params.charge, self.compute_moments('mom_v1_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 1))) ) # (i, j + 1/2) J2 = multiply(self.physical_system.params.charge, self.compute_moments('mom_v2_bulk', f = 0.5 * (self.f + af.shift(self.f, 0, 0, 0, 1))) ) # (i + 1/2, j) J3 = multiply(self.physical_system.params.charge, self.compute_moments('mom_v3_bulk', f = f) ) # (i + 1/2, j + 1/2) self.fields_solver.evolve_electrodynamic_fields(J1, J2, J3, self.dt) f_interp_p_3d(self, dt) af.eval(self.f) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_fieldstep += toc - tic return
def apply_bcs_fields(self, on_fdtd_grid=False): """ Applies boundary conditions to the EM fields as specified by the user in params. """ if (self.performance_test_flag == True): tic = af.time() # Obtaining start coordinates for the local zone # Additionally, we also obtain the size of the local zone ((i_q1_start, i_q2_start), (N_q1_local, N_q2_local)) = self._da_fields.getCorners() # Obtaining the end coordinates for the local zone (i_q1_end, i_q2_end) = (i_q1_start + N_q1_local - 1, i_q2_start + N_q2_local - 1) # If local zone includes the left physical boundary: if (i_q1_start == 0): if (self.boundary_conditions.in_q1_left == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'left', on_fdtd_grid) elif (self.boundary_conditions.in_q1_left == 'mirror'): apply_mirror_bcs_fields(self, 'left', on_fdtd_grid) elif (self.boundary_conditions.in_q1_left == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'left', on_fdtd_grid) apply_dirichlet_bcs_fields(self, 'left', on_fdtd_grid) # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q1_left == 'periodic'): pass elif (self.boundary_conditions.in_q1_left == 'shearing-box'): apply_shearing_box_bcs_fields(self, 'left', on_fdtd_grid) else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the right physical boundary: if (i_q1_end == self.N_q1 - 1): if (self.boundary_conditions.in_q1_right == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'right', on_fdtd_grid) elif (self.boundary_conditions.in_q1_right == 'mirror'): apply_mirror_bcs_fields(self, 'right', on_fdtd_grid) elif (self.boundary_conditions.in_q1_right == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'right', on_fdtd_grid) apply_dirichlet_bcs_fields(self, 'right', on_fdtd_grid) # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q1_right == 'periodic'): pass elif (self.boundary_conditions.in_q1_right == 'shearing-box'): apply_shearing_box_bcs_fields(self, 'right', on_fdtd_grid) else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the bottom physical boundary: if (i_q2_start == 0): if (self.boundary_conditions.in_q2_bottom == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'bottom', on_fdtd_grid) elif (self.boundary_conditions.in_q2_bottom == 'mirror'): apply_mirror_bcs_fields(self, 'bottom', on_fdtd_grid) elif (self.boundary_conditions.in_q2_bottom == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'bottom', on_fdtd_grid) apply_dirichlet_bcs_fields(self, 'bottom', on_fdtd_grid) # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q2_bottom == 'periodic'): pass elif (self.boundary_conditions.in_q2_bottom == 'shearing-box'): apply_shearing_box_bcs_fields(self, 'bottom', on_fdtd_grid) else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the top physical boundary: if (i_q2_end == self.N_q2 - 1): if (self.boundary_conditions.in_q2_top == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'top', on_fdtd_grid) elif (self.boundary_conditions.in_q2_top == 'mirror'): apply_mirror_bcs_fields(self, 'top', on_fdtd_grid) elif (self.boundary_conditions.in_q2_top == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'top', on_fdtd_grid) apply_dirichlet_bcs_fields(self, 'top', on_fdtd_grid) # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q2_top == 'periodic'): pass elif (self.boundary_conditions.in_q2_top == 'shearing-box'): apply_shearing_box_bcs_fields(self, 'top', on_fdtd_grid) else: raise NotImplementedError('Unavailable/Invalid boundary condition') af.eval(self.cell_centered_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_apply_bcs_fields += toc - tic return
def f_interp_p_3d(self, dt): """ Since the interpolation function are being performed in velocity space, the arrays used in the computation need to be in p_expanded form. Hence we will need to convert the same: """ # Following Strang Splitting: # af.broadcast, allows us to perform batched operations # when operating on arrays of different sizes # af.broadcast(function, *args) performs batched operations on # function(*args) if(self.performance_test_flag == True): tic = af.time() E1 = self.cell_centered_EM_fields_at_n[0] E2 = self.cell_centered_EM_fields_at_n[1] E3 = self.cell_centered_EM_fields_at_n[2] B1 = self.cell_centered_EM_fields_at_n[3] B2 = self.cell_centered_EM_fields_at_n[4] B3 = self.cell_centered_EM_fields_at_n[5] (A_p1, A_p2, A_p3) = af.broadcast(self._A_p, self.q1_center, self.q2_center, self.p1, self.p2, self.p3, E1, E2, E3, B1, B2, B3, self.physical_system.params ) # Defining a lambda function to perform broadcasting operations # This is done using af.broadcast, which allows us to perform # batched operations when operating on arrays of different sizes addition = lambda a,b:a + b # af.broadcast(function, *args) performs batched operations on # function(*args) p1_new = af.broadcast(addition, self.p1, - dt * A_p1) p2_new = af.broadcast(addition, self.p2, - dt * A_p2) p1_new = self._convert_to_p_expanded(p1_new) p2_new = self._convert_to_p_expanded(p2_new) # Transforming interpolant to go from [0, N_p - 1]: p1_lower_boundary = self.p1_start + 0.5 * self.dp1 p2_lower_boundary = self.p2_start + 0.5 * self.dp2 p1_interpolant = (p1_new - p1_lower_boundary) / self.dp1 p2_interpolant = (p2_new - p2_lower_boundary) / self.dp2 if(self.physical_system.params.p_dim == 3): p3_new = af.broadcast(addition, self.p3, - 0.5 * dt * A_p3) p3_new = self._convert_to_p_expanded(p3_new) p3_lower_boundary = self.p3_start + 0.5 * self.dp3 # Reordering p3_interpolant to bring variation in p3 to the 0-th axis: p3_interpolant = af.reorder((p3_new - p3_lower_boundary) / self.dp3, 2, 0, 1) # We perform the 3d interpolation by performing # individual 1d + 2d interpolations. Reordering to bring the # variation in values along axis 0 and axis 1 self.f = self._convert_to_p_expanded(self.f) if(self.physical_system.params.p_dim == 3): self.f = af.approx1(af.reorder(self.f, 2, 0, 1), p3_interpolant, af.INTERP.CUBIC_SPLINE ) self.f = af.reorder(self.f, 1, 2, 0) self.f = af.approx2(self.f, p1_interpolant, p2_interpolant, af.INTERP.BICUBIC_SPLINE ) if(self.physical_system.params.p_dim == 3): self.f = af.approx1(af.reorder(self.f, 2, 0, 1), p3_interpolant, af.INTERP.CUBIC_SPLINE ) self.f = af.reorder(self.f, 1, 2, 0) self.f = self._convert_to_q_expanded(self.f) af.eval(self.f) if(self.performance_test_flag == True): af.sync() toc = af.time() self.time_interp3 += toc - tic return
def apply_bcs_fields(self): if (self.performance_test_flag == True): tic = af.time() # Obtaining start coordinates for the local zone # Additionally, we also obtain the size of the local zone ((i_q1_start, i_q2_start), (N_q1_local, N_q2_local)) = self._da_fields.getCorners() # Obtaining the end coordinates for the local zone (i_q1_end, i_q2_end) = (i_q1_start + N_q1_local - 1, i_q2_start + N_q2_local - 1) # If local zone includes the left physical boundary: if (i_q1_start == 0): if (self.boundary_conditions.in_q1_left == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'left') elif (self.boundary_conditions.in_q1_left == 'mirror'): apply_mirror_bcs_fields(self, 'left') elif (self.boundary_conditions.in_q1_left == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'left') apply_dirichlet_bcs_fields(self, 'left') # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q1_left == 'periodic'): try: assert (self.boundary_conditions.in_q1_right == 'periodic') except: raise Exception( 'Periodic boundary conditions need to be applied to \ both the boundaries of a particular axis') pass else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the right physical boundary: if (i_q1_end == self.N_q1 - 1): if (self.boundary_conditions.in_q1_right == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'right') elif (self.boundary_conditions.in_q1_right == 'mirror'): apply_mirror_bcs_fields(self, 'right') elif (self.boundary_conditions.in_q1_right == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'right') apply_dirichlet_bcs_fields(self, 'right') # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q1_right == 'periodic'): pass else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the bottom physical boundary: if (i_q2_start == 0): if (self.boundary_conditions.in_q2_bottom == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'bottom') elif (self.boundary_conditions.in_q2_bottom == 'mirror'): apply_mirror_bcs_fields(self, 'bottom') elif (self.boundary_conditions.in_q2_bottom == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'bottom') apply_dirichlet_bcs_fields(self, 'bottom') # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q2_bottom == 'periodic'): try: assert (self.boundary_conditions.in_q2_top == 'periodic') except: raise Exception( 'Periodic boundary conditions need to be applied to \ both the boundaries of a particular axis') pass else: raise NotImplementedError('Unavailable/Invalid boundary condition') # If local zone includes the top physical boundary: if (i_q2_end == self.N_q2 - 1): if (self.boundary_conditions.in_q2_top == 'dirichlet'): apply_dirichlet_bcs_fields(self, 'top') elif (self.boundary_conditions.in_q2_top == 'mirror'): apply_mirror_bcs_fields(self, 'top') elif (self.boundary_conditions.in_q2_top == 'mirror+dirichlet'): apply_mirror_bcs_fields(self, 'top') apply_dirichlet_bcs_fields(self, 'top') # This is automatically handled by the PETSc function globalToLocal() elif (self.boundary_conditions.in_q2_top == 'periodic'): pass else: raise NotImplementedError('Unavailable/Invalid boundary condition') af.eval(self.cell_centered_EM_fields) if (self.performance_test_flag == True): af.sync() toc = af.time() self.time_apply_bcs_fields += toc - tic return