|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# --- Input file for spacecraft charging testing in RZ. |
| 4 | +# --- This input defines a conducting sphere (spacecraft) immersed in a thermal |
| 5 | +# --- plasma with the same given initial conditions as in the article: |
| 6 | +# --- (*) J. Deca, G. Lapenta, R. Marchand, S. Markidis; |
| 7 | +# --- Spacecraft charging analysis with the implicit particle-in-cell code iPic3D. |
| 8 | +# --- Part III. A. pages 3-4 |
| 9 | +# --- Phys. Plasmas 1 October 2013; 20 (10): 102902. https://doi.org/10.1063/1.4826951. |
| 10 | +# --- The conducting sphere starts with an initial potential of 1V and will interact with |
| 11 | +# --- the surrounding plasma, initially static. The charging of the spacecraft - by accumulation |
| 12 | +# --- of electrons - leads to a decrease of the potential on the surface over the time |
| 13 | +# --- until reaching an equilibrium floating potential of ~144.5 V (*). |
| 14 | + |
| 15 | +from mpi4py import MPI as mpi |
| 16 | +import numpy as np |
| 17 | +import scipy.constants as scc |
| 18 | + |
| 19 | +from pywarpx import picmi |
| 20 | +from pywarpx.callbacks import installafterEsolve, installafterInitEsolve |
| 21 | +from pywarpx.fields import ExWrapper, EzWrapper, PhiFPWrapper, RhoFPWrapper |
| 22 | +from pywarpx.particle_containers import ParticleBoundaryBufferWrapper |
| 23 | + |
| 24 | + |
| 25 | +# Utilities |
| 26 | +class SpaceChargeFieldCorrector(object): |
| 27 | + """ |
| 28 | + Class used by the callback functions to calculate the |
| 29 | + correct charge on the spacecraft at each initialisation. |
| 30 | + """ |
| 31 | + def __init__(self): |
| 32 | + self.saved_first_iteration_fields = False |
| 33 | + self.spacecraft_potential = 1. # Initial voltage: 1V |
| 34 | + self.spacecraft_capacitance = None |
| 35 | + |
| 36 | + def correct_space_charge_fields(self, q=None): |
| 37 | + """ |
| 38 | + Function that will be called at each iteration, |
| 39 | + after each electrostatic solve in WarpX |
| 40 | + """ |
| 41 | + assert self.saved_first_iteration_fields |
| 42 | + |
| 43 | + # Compute the charge that WarpX thinks there is on the spacecraft |
| 44 | + # from phi and rho after the Poisson solver |
| 45 | + q_v = compute_virtual_charge_on_spacecraft() |
| 46 | + if q is None: |
| 47 | + q = compute_actual_charge_on_spacecraft() |
| 48 | + |
| 49 | + # Correct fields so as to recover the actual charge |
| 50 | + Er = ExWrapper(include_ghosts=True)[:,:] |
| 51 | + Er[...] = Er[...]+(q - q_v)*self.normalized_Er[...] |
| 52 | + Ez = EzWrapper(include_ghosts=True)[:,:] |
| 53 | + Ez[...] += (q - q_v)*self.normalized_Ez[...] |
| 54 | + phi = PhiFPWrapper(include_ghosts=True)[:,:] |
| 55 | + phi[...] += (q - q_v)*self.normalized_phi[...] |
| 56 | + self.spacecraft_potential += (q - q_v)*self.spacecraft_capacitance |
| 57 | + sim.extension.warpx.set_potential_on_eb( "%f" %self.spacecraft_potential ) |
| 58 | + print('Setting potential to %f' %self.spacecraft_potential) |
| 59 | + |
| 60 | + # Confirm that the charge on the spacecraft is now correct |
| 61 | + compute_virtual_charge_on_spacecraft() |
| 62 | + |
| 63 | + def save_normalized_vacuum_Efields(self,): |
| 64 | + # Compute the charge that WarpX thinks there is on the spacecraft |
| 65 | + # from phi and rho after the Poisson solver |
| 66 | + q_v = compute_virtual_charge_on_spacecraft() |
| 67 | + self.spacecraft_capacitance = 1./q_v # the potential was set to 1V |
| 68 | + |
| 69 | + # Check that this iteration corresponded to a vacuum solve |
| 70 | + rho = RhoFPWrapper(include_ghosts=False) |
| 71 | + |
| 72 | + # In principle, we should check that `rho` is exactly 0 |
| 73 | + # However, due to machine precision errors when adding the charge |
| 74 | + # of ions and electrons, this can be slightly different than 0 |
| 75 | + assert np.all( abs(rho[...]) < 1.e-11 ) |
| 76 | + |
| 77 | + # Record fields |
| 78 | + Er = ExWrapper(include_ghosts=True)[:,:] |
| 79 | + self.normalized_Er = Er[...] /q_v |
| 80 | + Ez = EzWrapper(include_ghosts=True)[:,:] |
| 81 | + self.normalized_Ez = Ez[...] /q_v |
| 82 | + phi = PhiFPWrapper(include_ghosts=True)[:,:] |
| 83 | + self.normalized_phi = phi[...] /q_v |
| 84 | + |
| 85 | + self.saved_first_iteration_fields = True |
| 86 | + self.correct_space_charge_fields(q=0) |
| 87 | + |
| 88 | + |
| 89 | +def compute_virtual_charge_on_spacecraft(): |
| 90 | + """ |
| 91 | + Given that we asked WarpX to solve the Poisson |
| 92 | + equation with phi=1 on the spacecraft and phi=0 |
| 93 | + on the boundary of the domain, compute the charge |
| 94 | + that WarpX thinks there should be on the spacecraft. |
| 95 | + """ |
| 96 | + # Get global array for the whole domain (across MPI ranks) |
| 97 | + phi = PhiFPWrapper(include_ghosts=False)[:,:] |
| 98 | + rho = RhoFPWrapper(include_ghosts=False)[:,:] |
| 99 | + |
| 100 | + # Check that this codes correspond to the global size of the box |
| 101 | + assert phi.shape == (nr+1, nz+1) |
| 102 | + assert rho.shape == (nr+1, nz+1) |
| 103 | + |
| 104 | + dr, dz = sim.extension.warpx.Geom(lev=0).data().CellSize() |
| 105 | + |
| 106 | + # Compute integral of grad phi over surfaces of the domain |
| 107 | + r = np.linspace(rmin, rmax, len(phi), endpoint=False) + (rmax - rmin) / (2 * len(phi)) #shift of the r points because the derivaties are calculated in the middle |
| 108 | + face_z0 = 2 * np.pi * 1./dz * ( (phi[:,0]-phi[:,1]) * r ).sum() * dr #here I am assuming that phi is a numpy array that can handle elementwise mult |
| 109 | + face_zend = 2 * np.pi * 1./dz * ( (phi[:,-1]-phi[:,-2]) * r ).sum() * dr |
| 110 | + face_rend = 2 * np.pi * 1./dr*((phi[-1,:]-phi[-2,:]) * rmax).sum() * dz |
| 111 | + grad_phi_integral = face_z0 + face_zend + face_rend |
| 112 | + |
| 113 | + # Compute integral of rho over volume of the domain |
| 114 | + # (i.e. total charge of the plasma particles) |
| 115 | + rho_integral = 0.0 |
| 116 | + for k in range(1, nz-1): |
| 117 | + for i in range(1, nr-1): |
| 118 | + rho_integral += rho[i,k] * r[i] * dr * dz |
| 119 | + |
| 120 | + # Due to an oddity in WarpX (which will probably be solved later) |
| 121 | + # we need to multiply `rho` by `-epsilon_0` to get the correct charge |
| 122 | + rho_integral *= 2 * np.pi * -scc.epsilon_0 #does this oddity still exist? |
| 123 | + |
| 124 | + # Compute charge of the spacecraft, based on Gauss theorem |
| 125 | + q_spacecraft = - rho_integral - scc.epsilon_0 * grad_phi_integral |
| 126 | + print('Virtual charge on the spacecraft: %e' %q_spacecraft) |
| 127 | + return q_spacecraft |
| 128 | + |
| 129 | + |
| 130 | +def compute_actual_charge_on_spacecraft(): |
| 131 | + """ |
| 132 | + Compute the actual charge on the spacecraft, |
| 133 | + by counting how many electrons and protons |
| 134 | + were collected by the WarpX embedded boundary (EB) |
| 135 | + """ |
| 136 | + charge = {'electrons': -scc.e, 'protons': scc.e} |
| 137 | + q_spacecraft = 0 |
| 138 | + particle_buffer = ParticleBoundaryBufferWrapper() |
| 139 | + for species in charge.keys(): |
| 140 | + weights = particle_buffer.get_particle_boundary_buffer(species, 'eb', 'w', 0) |
| 141 | + sum_weights_over_tiles = sum([w.sum() for w in weights]) |
| 142 | + |
| 143 | + # Reduce across all MPI ranks |
| 144 | + ntot = float(mpi.COMM_WORLD.allreduce(sum_weights_over_tiles, op=mpi.SUM)) |
| 145 | + print('Total number of %s collected on spacecraft: %e'%(species, ntot)) |
| 146 | + q_spacecraft += ntot * charge[species] |
| 147 | + |
| 148 | + print('Actual charge on the spacecraft: %e' %q_spacecraft) |
| 149 | + return q_spacecraft |
| 150 | + |
| 151 | + |
| 152 | +########################## |
| 153 | +# numerics parameters |
| 154 | +########################## |
| 155 | + |
| 156 | +dt=1.27e-8 |
| 157 | + |
| 158 | +# --- Nb time steps |
| 159 | +max_steps = 1000 |
| 160 | +diagnostic_interval = 10 |
| 161 | + |
| 162 | +# --- grid |
| 163 | +nr = 40 |
| 164 | +nz= 80 |
| 165 | + |
| 166 | +rmin = 0.0 |
| 167 | +rmax = 3 |
| 168 | +zmin = -3 |
| 169 | +zmax = 3 |
| 170 | + |
| 171 | +number_per_cell =5 |
| 172 | +number_per_cell_each_dim = [10,1, 1] |
| 173 | + |
| 174 | + |
| 175 | +########################## |
| 176 | +# physics components |
| 177 | +########################## |
| 178 | + |
| 179 | +n = 7.0e9 #plasma density #particles/m^3 |
| 180 | +Te = 85 #Electron temp in eV |
| 181 | +Ti = 0.05 * Te #Ion temp in eV |
| 182 | +qe = picmi.constants.q_e #elementary charge |
| 183 | +m_e = picmi.constants.m_e #electron mass |
| 184 | +m_i = 1836.0 * m_e #mass of ion |
| 185 | +v_eth = (qe * Te / m_e) ** 0.5 |
| 186 | +v_pth = (qe * Ti / m_i) ** 0.5 |
| 187 | + |
| 188 | +# nothing to change in the distribution function? |
| 189 | +e_dist = picmi.UniformDistribution(density = n, rms_velocity=[v_eth, v_eth, v_eth] ) |
| 190 | +e_dist2 = picmi.UniformFluxDistribution( |
| 191 | + flux=n*v_eth/(2*np.pi)**.5, # Flux for Gaussian with vmean=0 |
| 192 | + surface_flux_position=3, |
| 193 | + flux_direction=-1, flux_normal_axis='r', |
| 194 | + gaussian_flux_momentum_distribution=True, |
| 195 | + rms_velocity=[v_eth, v_eth, v_eth] ) |
| 196 | +electrons = picmi.Species(particle_type='electron', |
| 197 | + name='electrons', |
| 198 | + initial_distribution=[e_dist,e_dist2], |
| 199 | + warpx_save_particles_at_eb=1) |
| 200 | + |
| 201 | +p_dist = picmi.UniformDistribution(density = n, rms_velocity=[v_pth, v_pth, v_pth] ) |
| 202 | +p_dist2 = picmi.UniformFluxDistribution( |
| 203 | + flux=n*v_pth/(2*np.pi)**.5, # Flux for Gaussian with vmean=0 |
| 204 | + surface_flux_position=3, |
| 205 | + flux_direction=-1, flux_normal_axis='r', |
| 206 | + gaussian_flux_momentum_distribution=True, |
| 207 | + rms_velocity=[v_pth, v_pth, v_pth] ) |
| 208 | +protons = picmi.Species(particle_type='proton', |
| 209 | + name='protons', |
| 210 | + initial_distribution=[p_dist,p_dist2], |
| 211 | + warpx_save_particles_at_eb=1) |
| 212 | + |
| 213 | + |
| 214 | +########################## |
| 215 | +# numerics components |
| 216 | +########################## |
| 217 | + |
| 218 | +grid = picmi.CylindricalGrid( |
| 219 | + number_of_cells = [nr, nz], |
| 220 | + n_azimuthal_modes = 1, |
| 221 | + lower_bound = [rmin, zmin], |
| 222 | + upper_bound = [rmax, zmax], |
| 223 | + lower_boundary_conditions = ['none', 'dirichlet'], |
| 224 | + upper_boundary_conditions = ['dirichlet', 'dirichlet'], |
| 225 | + lower_boundary_conditions_particles = ['absorbing', 'reflecting'], |
| 226 | + upper_boundary_conditions_particles = ['absorbing', 'reflecting'] |
| 227 | +) |
| 228 | + |
| 229 | +solver = picmi.ElectrostaticSolver( |
| 230 | + grid=grid, method='Multigrid', |
| 231 | + warpx_absolute_tolerance=1e-7 |
| 232 | +) |
| 233 | + |
| 234 | +embedded_boundary = picmi.EmbeddedBoundary( |
| 235 | + implicit_function="-(x**2+y**2+z**2-radius**2)", |
| 236 | + potential=1., # arbitrary value ; this will be corrected by a callback function |
| 237 | + radius = 0.3277 |
| 238 | +) |
| 239 | + |
| 240 | + |
| 241 | +########################## |
| 242 | +# diagnostics |
| 243 | +########################## |
| 244 | + |
| 245 | +field_diag = picmi.FieldDiagnostic( |
| 246 | + name = 'diag1', |
| 247 | + grid = grid, |
| 248 | + period = diagnostic_interval, |
| 249 | + data_list = ['Er', 'Ez', 'phi', 'rho', |
| 250 | + 'rho_electrons', 'rho_protons'], |
| 251 | + warpx_format = 'openpmd', |
| 252 | + write_dir = '.', |
| 253 | + warpx_file_prefix = 'spacecraft_charging_plt' |
| 254 | +) |
| 255 | + |
| 256 | +part_diag = picmi.ParticleDiagnostic(name = 'diag1', |
| 257 | + period = diagnostic_interval, |
| 258 | + species = [electrons, protons], |
| 259 | + warpx_format = 'openpmd', |
| 260 | + write_dir = '.', |
| 261 | + warpx_file_prefix = 'spacecraft_charging_plt' |
| 262 | +) |
| 263 | + |
| 264 | +########################## |
| 265 | +# simulation setup |
| 266 | +########################## |
| 267 | + |
| 268 | +sim = picmi.Simulation( |
| 269 | + solver = solver, |
| 270 | + time_step_size = dt, |
| 271 | + max_steps = max_steps, |
| 272 | + warpx_embedded_boundary=embedded_boundary, |
| 273 | + warpx_amrex_the_arena_is_managed=1, |
| 274 | + warpx_random_seed=1 |
| 275 | +) |
| 276 | + |
| 277 | +layout1=picmi.GriddedLayout(n_macroparticle_per_cell=number_per_cell_each_dim, |
| 278 | + grid=grid) |
| 279 | +layout2=picmi.PseudoRandomLayout(n_macroparticles_per_cell=number_per_cell, |
| 280 | + grid=grid) |
| 281 | +sim.add_species(electrons, |
| 282 | + layout = [layout1,layout2]) |
| 283 | + |
| 284 | +sim.add_species(protons, |
| 285 | + layout = [layout1,layout2]) |
| 286 | + |
| 287 | +sim.add_diagnostic(field_diag) |
| 288 | +sim.add_diagnostic(part_diag) |
| 289 | + |
| 290 | +########################## |
| 291 | +# simulation run |
| 292 | +########################## |
| 293 | + |
| 294 | +spc = SpaceChargeFieldCorrector() |
| 295 | + |
| 296 | +installafterInitEsolve( spc.save_normalized_vacuum_Efields ) |
| 297 | +installafterEsolve( spc.correct_space_charge_fields ) |
| 298 | + |
| 299 | +sim.step(max_steps) |
0 commit comments