Afivo  0.3
simple_streamer_2d.f90

A simplified model for ionization waves and/or streamers in 2D

1 
4 program simple_streamer_2d
5 
6  use m_a2_types
7  use m_a2_core
9  use m_a2_utils
10  use m_a2_restrict
11  use m_a2_multigrid
12  use m_a2_output
13  use m_write_silo
14  use m_a2_prolong
15 
16  implicit none
17 
18  integer :: i, n
19  character(len=100) :: fname
20  type(a2_t) :: tree ! This contains the full grid information
21  type(mg2_t) :: mg ! Multigrid option struct
22  type(ref_info_t) :: refine_info
23 
24  ! Indices of cell-centered variables
25  integer, parameter :: n_var_cell = 7 ! Number of variables
26  integer, parameter :: i_elec = 1 ! Electron density
27  integer, parameter :: i_pion = 2 ! Positive ion density
28  integer, parameter :: i_elec_old = 3 ! For time-stepping scheme
29  integer, parameter :: i_pion_old = 4 ! For time-stepping scheme
30  integer, parameter :: i_phi = 5 ! Electrical potential
31  integer, parameter :: i_fld = 6 ! Electric field norm
32  integer, parameter :: i_rhs = 7 ! Source term Poisson
33 
34  ! Indices of face-centered variables **
35  integer, parameter :: n_var_face = 2 ! Number of variables
36  integer, parameter :: f_elec = 1 ! Electron flux
37  integer, parameter :: f_fld = 2 ! Electric field vector
38 
39  ! Names of the cell-centered variables
40  character(len=10) :: st_cc_names(n_var_cell) = &
41  [character(len=10) :: "elec", "pion", "elec_old", &
42  "pion_old", "phi", "fld", "rhs"]
43 
44  ! Simulation parameters
45  real(dp), parameter :: end_time = 8e-9_dp
46  real(dp), parameter :: dt_output = 20e-11_dp
47  real(dp), parameter :: dt_max = 20e-11_dp
48  integer, parameter :: ref_per_steps = 2
49  integer, parameter :: box_size = 8
50 
51  ! Physical parameters
52  real(dp), parameter :: applied_field = -0.8e7_dp
53  real(dp), parameter :: mobility = 0.03_dp
54  real(dp), parameter :: diffusion_c = 0.2_dp
55 
56  ! Computational domain
57  real(dp), parameter :: domain_length = 10e-3_dp
58  real(dp), parameter :: refine_max_dx = 1e-3_dp
59  real(dp), parameter :: refine_min_dx = 1e-9_dp
60 
61  ! Settings for the initial conditions
62  real(dp), parameter :: init_density = 1e15_dp
63  real(dp), parameter :: init_y_min = 8.0e-3_dp
64  real(dp), parameter :: init_y_max = 9.0e-3_dp
65 
66  ! Simulation variables
67  real(dp) :: dt
68  real(dp) :: time
69  integer :: output_count
70 
71  ! To test charge conservation
72  real(dp) :: sum_elec, sum_pion
73 
74  ! Initialize the tree (which contains all the mesh information)
75  call init_tree(tree)
76 
77  ! Set the multigrid options. First define the variables to use
78  mg%i_phi = i_phi
79  mg%i_tmp = i_fld
80  mg%i_rhs = i_rhs
81 
82  ! Routines to use for ...
83  mg%sides_bc => sides_bc_pot ! Filling ghost cell on physical boundaries
84 
85  ! This routine always needs to be called when using multigrid
86  call mg2_init_mg(mg)
87 
88  call a2_set_cc_methods(tree, i_elec, a2_bc_dirichlet_zero, &
89  a2_gc_interp_lim, a2_prolong_limit)
90  call a2_set_cc_methods(tree, i_pion, a2_bc_dirichlet_zero, &
91  a2_gc_interp_lim, a2_prolong_limit)
92  call a2_set_cc_methods(tree, i_phi, mg%sides_bc, mg%sides_rb)
93 
94  output_count = 0 ! Number of output files written
95  time = 0 ! Simulation time (all times are in s)
96 
97  ! Set up the initial conditions
98  do
99  ! For each box in tree, set the initial conditions
100  call a2_loop_box(tree, set_initial_condition)
101 
102  ! Compute electric field on the tree.
103  ! First perform multigrid to get electric potential,
104  ! then take numerical gradient to geld field.
105  call compute_fld(tree, .false.)
106 
107  ! Adjust the refinement of a tree using refine_routine (see below) for grid
108  ! refinement.
109  ! Routine a2_adjust_refinement sets the bit af_bit_new_children for each box
110  ! that is refined. On input, the tree should be balanced. On output,
111  ! the tree is still balanced, and its refinement is updated (with at most
112  call a2_adjust_refinement(tree, & ! tree
113  refinement_routine, & ! Refinement function
114  refine_info) ! Information about refinement
115 
116  ! If no new boxes have been added or removed, exit the loop
117  if (refine_info%n_add == 0 .and. refine_info%n_rm == 0) exit
118  end do
119 
120  call a2_print_info(tree)
121 
122  do
123  ! Get a new time step, which is at most dt_max
124  call a2_reduction(tree, & ! Tree to do the reduction on
125  max_dt, & ! function
126  get_min, & ! function
127  dt_max, & ! Initial value for the reduction
128  dt) ! Result of the reduction
129 
130  if (dt < 1e-14) then
131  print *, "dt getting too small, instability?", dt
132  time = end_time + 1.0_dp
133  end if
134 
135  ! Every dt_output, write output
136  if (output_count * dt_output <= time) then
137  output_count = output_count + 1
138  write(fname, "(A,I6.6)") "simple_streamer_2d_", output_count
139 
140  ! Write the cell centered data of a tree to a Silo file. Only the
141  ! leaves of the tree are used
142  call a2_write_silo(tree, fname, output_count, time, dir="output")
143 
144  call a2_tree_sum_cc(tree, i_elec, sum_elec)
145  call a2_tree_sum_cc(tree, i_pion, sum_pion)
146  print *, "sum(pion-elec)/sum(pion)", (sum_pion - sum_elec)/sum_pion
147  end if
148 
149  if (time > end_time) exit
150 
151  ! We perform n_steps between mesh-refinements
152  do n = 1, ref_per_steps
153  time = time + dt
154 
155  ! Copy previous solution
156  call a2_tree_copy_cc(tree, i_elec, i_elec_old)
157  call a2_tree_copy_cc(tree, i_pion, i_pion_old)
158 
159  ! Two forward Euler steps over dt
160  do i = 1, 2
161  ! First calculate fluxes
162  call a2_loop_boxes(tree, fluxes_koren, leaves_only=.true.)
163  call a2_consistent_fluxes(tree, [f_elec])
164 
165  ! Update the solution
166  call a2_loop_box_arg(tree, update_solution, [dt], &
167  leaves_only=.true.)
168 
169  ! Compute new field on first iteration
170  if (i == 1) call compute_fld(tree, .true.)
171  end do
172 
173  ! Take average of phi_old and phi (explicit trapezoidal rule)
174  call a2_loop_box(tree, average_dens)
175 
176  ! Compute field with new density
177  call compute_fld(tree, .true.)
178  end do
179 
180  ! Fill ghost cells for i_pion and i_elec
181  call a2_gc_tree(tree, i_elec)
182  call a2_gc_tree(tree, i_pion)
183 
184  ! Adjust the refinement of a tree using refine_routine (see below) for grid
185  ! refinement.
186  ! Routine a2_adjust_refinement sets the bit af_bit_new_children for each box
187  ! that is refined. On input, the tree should be balanced. On output,
188  ! the tree is still balanced, and its refinement is updated (with at most
189  ! one level per call).
190  call a2_adjust_refinement(tree, & ! tree
191  refinement_routine, & ! Refinement function
192  refine_info, & ! Information about refinement
193  4) ! Buffer width (in cells)
194 
195  if (refine_info%n_add > 0 .or. refine_info%n_rm > 0) then
196  ! Compute the field on the new mesh
197  call compute_fld(tree, .true.)
198  end if
199  end do
200 
201 contains
202 
204  subroutine init_tree(tree)
205  type(a2_t), intent(inout) :: tree
206 
207  ! Variables used below to initialize tree
208  real(dp) :: dr
209  integer :: id
210  integer :: ix_list(2, 1) ! Spatial indices of initial boxes
211  integer :: nb_list(4, 1) ! Neighbors of initial boxes
212  integer :: n_boxes_init = 1000
213 
214  dr = domain_length / box_size
215 
216  ! Initialize tree
217  call a2_init(tree, & ! The tree to initialize
218  box_size, & ! Boxes have box_size^dim cells
219  n_var_cell, & ! Number of cell-centered variables
220  n_var_face, & ! Number of face-centered variables
221  dr, & ! spacing of a cell at lvl 1
222  coarsen_to=2, & ! Create additional coarse grids down to this size.
223  n_boxes = n_boxes_init, & ! Allocate initial storage for n_boxes.
224  cc_names=st_cc_names) ! Names of cell-centered variables
225 
226  ! Set up geometry
227  id = 1 ! One box ...
228  ix_list(:, id) = [1,1] ! With index 1,1 ...
229 
230  nb_list(a2_neighb_lowy, id) = -1 ! physical boundary
231  nb_list(a2_neighb_highy, id) = -1 ! idem
232  nb_list(a2_neighb_lowx, id) = id ! periodic boundary
233  nb_list(a2_neighb_highx, id) = id ! idem
234 
235  ! Create the base mesh
236  call a2_set_base(tree, 1, ix_list, nb_list)
237 
238  end subroutine init_tree
239 
241  subroutine refinement_routine(box, cell_flags)
242  type(box2_t), intent(in) :: box
243  integer, intent(out) :: cell_flags(box%n_cell, box%n_cell)
244  integer :: i, j, nc
245  real(dp) :: dx, dens, fld, adx, xy(2)
246 
247  nc = box%n_cell
248  dx = box%dr
249 
250  do j = 1, nc
251  do i = 1, nc
252  xy = a2_r_cc(box, [i,j])
253  dens = box%cc(i, j, i_elec)
254  fld = box%cc(i, j, i_fld)
255  adx = get_alpha(fld) * dx
256 
257  if (dens > 1e0_dp .and. adx > 0.8_dp) then
258  cell_flags(i, j) = af_do_ref
259  else if (dx < 1.25e-5_dp .and. adx < 0.1_dp) then
260  cell_flags(i, j) = af_rm_ref
261  else
262  cell_flags(i, j) = af_keep_ref
263  end if
264  end do
265  end do
266 
267  ! Make sure we don't have or get a too fine or too coarse grid
268  if (dx > refine_max_dx) then
269  cell_flags = af_do_ref
270  else if (dx < 2 * refine_min_dx) then
271  where(cell_flags == af_do_ref) cell_flags = af_keep_ref
272  else if (dx > 0.5_dp * refine_max_dx) then
273  where(cell_flags == af_rm_ref) cell_flags = af_keep_ref
274  end if
275 
276  end subroutine refinement_routine
277 
279  subroutine set_initial_condition(box)
280  type(box2_t), intent(inout) :: box
281  integer :: i, j, nc
282  real(dp) :: xy(2), normal_rands(2)
283 
284  nc = box%n_cell
285 
286  do j = 0, nc+1
287  do i = 0, nc+1
288  xy = a2_r_cc(box, [i,j])
289 
290  if (xy(2) > init_y_min .and. xy(2) < init_y_max) then
291  ! Approximate Poisson distribution with normal distribution
292  normal_rands = two_normals(box%dr**3 * init_density, &
293  sqrt(box%dr**3 * init_density))
294  ! Prevent negative numbers
295  box%cc(i, j, i_elec) = abs(normal_rands(1)) / box%dr**3
296  else
297  box%cc(i, j, i_elec) = 0
298  end if
299  end do
300  end do
301 
302  box%cc(:, :, i_pion) = box%cc(:, :, i_elec)
303  box%cc(:, :, i_phi) = 0 ! Inital potential set to zero
304 
305  end subroutine set_initial_condition
306 
309  function two_normals(mean, sigma) result(rands)
310  real(dp), intent(in) :: mean, sigma
311  real(dp) :: rands(2), sum_sq
312 
313  do
314  call random_number(rands)
315  rands = 2 * rands - 1
316  sum_sq = sum(rands**2)
317  if (sum_sq < 1.0_dp .and. sum_sq > 0.0_dp) exit
318  end do
319  rands = rands * sqrt(-2 * log(sum_sq) / sum_sq)
320  rands = mean + rands * sigma
321  end function two_normals
322 
324  real(dp) function get_min(a, b)
325  real(dp), intent(in) :: a, b
326  get_min = min(a, b)
327  end function get_min
328 
330  real(dp) function max_dt(box)
331  type(box2_t), intent(in) :: box
332  real(dp), parameter :: uc_eps0 = 8.8541878176d-12
333  real(dp), parameter :: uc_elem_charge = 1.6022d-19
334  integer :: i, j, nc
335  real(dp) :: fld(2)
336  real(dp) :: dt_cfl, dt_dif, dt_drt
337 
338  nc = box%n_cell
339  dt_cfl = dt_max
340 
341  do j = 1, nc
342  do i = 1, nc
343  fld(1) = 0.5_dp * (box%fc(i, j, 1, f_fld) + &
344  box%fc(i+1, j, 1, f_fld))
345  fld(2) = 0.5_dp * (box%fc(i, j, 2, f_fld) + &
346  box%fc(i, j+1, 2, f_fld))
347 
348  ! The 0.5 is here because of the explicit trapezoidal rule
349  dt_cfl = min(dt_cfl, 0.5_dp / sum(abs(fld * mobility) / box%dr))
350  end do
351  end do
352 
353  ! Dielectric relaxation time
354  dt_drt = uc_eps0 / (uc_elem_charge * mobility * &
355  maxval(box%cc(1:nc, 1:nc, i_elec)) + epsilon(1.0_dp))
356 
357  ! Diffusion condition
358  dt_dif = 0.25_dp * box%dr**2 / max(diffusion_c, epsilon(1.0_dp))
359 
360  max_dt = min(dt_cfl, dt_drt, dt_dif)
361  end function max_dt
362 
363 
369  elemental function get_alpha(fld) result(alpha)
370  real(dp), intent(in) :: fld
371  real(dp) :: alpha
372  real(dp), parameter :: c0 = 1.04e1_dp
373  real(dp), parameter :: c1 = 0.601_dp
374  real(dp), parameter :: c2 = 1.86e7_dp
375 
376  alpha = exp(c0) * (abs(fld) * 1e-5_dp)**c1 * exp(-c2 / abs(fld))
377  end function get_alpha
378 
379  ! Compute electric field on the tree. First perform multigrid to get electric
380  ! potential, then take numerical gradient to geld field.
381  subroutine compute_fld(tree, have_guess)
382  type(a2_t), intent(inout) :: tree
383  logical, intent(in) :: have_guess
384  real(dp), parameter :: fac = 1.6021766208e-19_dp / 8.8541878176e-12_dp
385  integer :: lvl, i, id, nc
386 
387  nc = tree%n_cell
388 
389  ! Set the source term (rhs)
390  !$omp parallel private(lvl, i, id)
391  do lvl = 1, tree%highest_lvl
392  !$omp do
393  do i = 1, size(tree%lvls(lvl)%leaves)
394  id = tree%lvls(lvl)%leaves(i)
395  tree%boxes(id)%cc(:, :, i_rhs) = fac * (&
396  tree%boxes(id)%cc(:, :, i_elec) - &
397  tree%boxes(id)%cc(:, :, i_pion))
398  end do
399  !$omp end do nowait
400  end do
401  !$omp end parallel
402 
403  ! Perform an FMG cycle (logicals: store residual, first call)
404  call mg2_fas_fmg(tree, mg, .false., have_guess)
405 
406  ! Compute field from potential
407  call a2_loop_box(tree, fld_from_pot)
408 
409  ! Set the field norm also in ghost cells
410  call a2_gc_tree(tree, i_fld, a2_gc_interp, a2_bc_neumann_zero)
411  end subroutine compute_fld
412 
413  ! Compute electric field from electrical potential
414  subroutine fld_from_pot(box)
415  type(box2_t), intent(inout) :: box
416  integer :: nc
417  real(dp) :: inv_dr
418 
419  nc = box%n_cell
420  inv_dr = 1 / box%dr
421 
422  box%fc(1:nc+1, 1:nc, 1, f_fld) = inv_dr * &
423  (box%cc(0:nc, 1:nc, i_phi) - box%cc(1:nc+1, 1:nc, i_phi))
424  box%fc(1:nc, 1:nc+1, 2, f_fld) = inv_dr * &
425  (box%cc(1:nc, 0:nc, i_phi) - box%cc(1:nc, 1:nc+1, i_phi))
426 
427  box%cc(1:nc, 1:nc, i_fld) = sqrt(&
428  0.25_dp * (box%fc(1:nc, 1:nc, 1, f_fld) + &
429  box%fc(2:nc+1, 1:nc, 1, f_fld))**2 + &
430  0.25_dp * (box%fc(1:nc, 1:nc, 2, f_fld) + &
431  box%fc(1:nc, 2:nc+1, 2, f_fld))**2)
432  end subroutine fld_from_pot
433 
434  ! Compute the electron fluxes due to drift and diffusion
435  subroutine fluxes_koren(boxes, id)
436  use m_flux_schemes
437  type(box2_t), intent(inout) :: boxes(:)
438  integer, intent(in) :: id
439  real(dp) :: inv_dr
440  real(dp) :: cc(-1:boxes(id)%n_cell+2, -1:boxes(id)%n_cell+2)
441  real(dp), allocatable :: v(:, :, :), dc(:, :, :)
442  integer :: nc
443 
444  nc = boxes(id)%n_cell
445  inv_dr = 1/boxes(id)%dr
446 
447  allocate(v(1:nc+1, 1:nc+1, 2))
448  allocate(dc(1:nc+1, 1:nc+1, 2))
449 
450  call a2_gc_box(boxes, id, i_elec, a2_gc_interp_lim, &
451  a2_bc_dirichlet_zero)
452  call a2_gc2_box(boxes, id, i_elec, a2_gc2_prolong_linear, &
453  a2_bc2_dirichlet_zero, cc, nc)
454 
455  v = -mobility * boxes(id)%fc(:, :, :, f_fld)
456  dc = diffusion_c
457 
458  call flux_koren_2d(cc, v, nc, 2)
459  call flux_diff_2d(cc, dc, inv_dr, nc, 2)
460 
461  boxes(id)%fc(:, :, :, f_elec) = v + dc
462  end subroutine fluxes_koren
463 
464  ! Take average of new and old electron/ion density for explicit trapezoidal rule
465  subroutine average_dens(box)
466  type(box2_t), intent(inout) :: box
467  box%cc(:, :, i_elec) = 0.5_dp * (box%cc(:, :, i_elec) + box%cc(:, :, i_elec_old))
468  box%cc(:, :, i_pion) = 0.5_dp * (box%cc(:, :, i_pion) + box%cc(:, :, i_pion_old))
469  end subroutine average_dens
470 
471  ! Advance solution over dt based on the fluxes / source term, using forward Euler
472  subroutine update_solution(box, dt)
473  type(box2_t), intent(inout) :: box
474  real(dp), intent(in) :: dt(:)
475  real(dp) :: inv_dr, src, sflux, fld
476  real(dp) :: alpha
477  integer :: i, j, nc
478 
479  nc = box%n_cell
480  inv_dr = 1/box%dr
481  do j = 1, nc
482  do i = 1, nc
483  fld = box%cc(i,j, i_fld)
484  alpha = get_alpha(fld)
485  src = box%cc(i, j, i_elec) * mobility * abs(fld) * alpha
486 
487  sflux = (sum(box%fc(i, j, :, f_elec)) - &
488  box%fc(i+1, j, 1, f_elec) - &
489  box%fc(i, j+1, 2, f_elec)) * inv_dr
490 
491  box%cc(i, j, i_elec) = box%cc(i, j, i_elec) + (src + sflux) * dt(1)
492  box%cc(i, j, i_pion) = box%cc(i, j, i_pion) + src * dt(1)
493  end do
494  end do
495 
496  end subroutine update_solution
497 
499  subroutine sides_bc_pot(box, nb, iv, bc_type)
500  type(box2_t), intent(inout) :: box
501  integer, intent(in) :: nb ! Direction for the boundary condition
502  integer, intent(in) :: iv ! Index of variable
503  integer, intent(out) :: bc_type ! Type of boundary condition
504  integer :: nc
505 
506  nc = box%n_cell
507 
508  select case (nb)
509  case (a2_neighb_lowy)
510  bc_type = af_bc_neumann
511  box%cc(1:nc, 0, iv) = applied_field
512  case (a2_neighb_highy)
513  bc_type = af_bc_dirichlet
514  box%cc(1:nc, nc+1, iv) = 0
515  case default
516  stop "sides_bc_pot: unspecified boundary"
517  end select
518  end subroutine sides_bc_pot
519 
520 end program simple_streamer_2d