Skip to content

Commit 4077f8e

Browse files
EyaDammakRemiLehepre-commit-ci[bot]
authored
Add automated test for spacecraft charging (#4413)
* Adding of the bool `radially_weighted` as variable in the class Species * Remove error message * Fix bug with radially-weighted * Fix weighting of particles * ghost_cells problems fixed * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * modification of particle_containers to track the particle * Add PICMI interface for BoundaryScrapingDiagnostic * Add first iteration of the input script * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * second draft, < 2 min, good results * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * adding a draft of analysis code for spacecraft_charging * adding spacecraft_charging at the very end * adding spacecraft_charging analysis * Revert "Add PICMI interface for BoundaryScrapingDiagnostic" This reverts commit 521dc5d. * Revert changes in fields.py * Update PlasmaInjector.cpp Trying to fix the error during the test: /home/runner/work/WarpX/WarpX/Source/Initialization/PlasmaInjector.cpp:72:5: warning: 'zmax' should be initialized in a member initializer of the constructor [cppcoreguidelines-prefer-member-initializer] zmax = std::numeric_limits<amrex::Real>::max(); * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update PlasmaInjector.cpp Adding "amrex::Real" to define xmin,ymin,zmin,xmax,ymax,zmax * Update PlasmaInjector.cpp * modelisation code * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * new version gathering the electrons and the protons * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * deplaced in the Physics_applications file * not necessary * analysis file * changes in some syntaxes and adding checksum test * add png file * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * replaced by new syntax * change of syntax and keep just one version * Add checksum json file * Update PICMI_inputs_rz.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove changes related to radially-weighted * Update Regression/WarpX-tests.ini * Make script executable * Update format of automated test * Update test * adding description * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update PICMI_inputs_rz.py * Update analysis.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update PICMI_inputs_rz.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update PICMI_inputs_rz.py * Update PICMI_inputs_rz.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update PICMI_inputs_rz.py * analysis changed: I wish it works now * Update WarpX-tests.ini adding the outputFile * Update filename * Update checksum * Update Examples/Physics_applications/spacecraft_charging/analysis.py * Update PICMI_inputs_rz.py From a random to an identical test * Update analysis.py * Update spacecraft_charging.json * Update spacecraft_charging.json * Update analysis.py --------- Co-authored-by: Remi Lehe <remi.lehe@normalesup.org> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c9bfc7e commit 4077f8e

File tree

4 files changed

+431
-0
lines changed

4 files changed

+431
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
This script tests the potential-time profile on the surface of
5+
a conducting sphere (spacecraft) immersed in an initially static
6+
thermal plasma. The potential on the spacecraft decreases over
7+
the time to reach an equilibrium floating potential.
8+
9+
An input Python file PICMI_inputs_rz.py is used.
10+
11+
The test will check the curve fitting parameters v0 and tau defined
12+
by the following exponential function: phi(t)=v0(1-exp(-t/tau))
13+
"""
14+
15+
import os
16+
import sys
17+
18+
import matplotlib.pyplot as plt
19+
import numpy as np
20+
from openpmd_viewer import OpenPMDTimeSeries
21+
from scipy.optimize import curve_fit
22+
import yt
23+
24+
yt.funcs.mylog.setLevel(0)
25+
sys.path.insert(1, '../../../../warpx/Regression/Checksum/')
26+
import checksumAPI
27+
28+
# Open plotfile specified in command line
29+
filename = sys.argv[1]
30+
test_name = os.path.split(os.getcwd())[1]
31+
checksumAPI.evaluate_checksum(test_name, filename, output_format='openpmd')
32+
33+
ts = OpenPMDTimeSeries('./spacecraft_charging_plt')
34+
dt = 1.27e-8
35+
t=[]
36+
phi=[]
37+
it = ts.iterations
38+
39+
for i in it:
40+
phi_i = ts.get_field('phi',iteration=i,plot=False)
41+
# Find the minimum value among all grids for this iteration
42+
phi_min = np.min(phi_i[0])
43+
phi.append(phi_min)
44+
t.append(dt*i)
45+
46+
47+
def func(x, v0, tau):
48+
49+
return v0 * (1-np.exp(-np.array(x) / tau))
50+
51+
52+
53+
popt, pcov = curve_fit(func, t, phi)
54+
55+
plt.plot(t,phi, label='modelisation')
56+
plt.plot(t, func(t, *popt), 'r-',label='fit: v0=%5.3f, tau=%5.9f' % (popt[0], popt[1]))
57+
plt.legend()
58+
plt.savefig('min_phi_analysis.png')
59+
60+
print('fit parameters between the min(phi) curve over the time and the function v0(1-exp(-t/tau)):')
61+
print('v0=%5.3f, tau=%5.9f' % (popt[0], popt[1]))
62+
63+
64+
tolerance_v0=0.01
65+
tolerance_tau=0.01
66+
print("tolerance for v0 = "+ str(tolerance_v0 *100) + '%')
67+
print("tolerance for tau = "+ str(tolerance_tau*100) + '%')
68+
69+
mean_v0=-151.347
70+
mean_tau=0.000004351
71+
72+
diff_v0=np.abs((popt[0]-mean_v0)/mean_v0)
73+
diff_tau=np.abs((popt[1]-mean_tau)/mean_tau)
74+
75+
print("percentage error for v0 = "+ str(diff_v0 *100) + '%')
76+
print("percentage error for tau = "+ str(diff_tau*100) + '%')
77+
78+
assert (diff_v0 < tolerance_v0) and (diff_tau < tolerance_tau), 'Test spacecraft_charging did not pass'

0 commit comments

Comments
 (0)