Skip to content

Commit

Permalink
Merge pull request #41 from ExCALIBUR-NEPTUNE/tom/momentum_diagnostics
Browse files Browse the repository at this point in the history
Tom/momentum diagnostics
  • Loading branch information
TomGoffrey authored Mar 1, 2022
2 parents 154af99 + 6e59514 commit 9998a66
Show file tree
Hide file tree
Showing 8 changed files with 455 additions and 2 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ similar for `y_probes` and `z_probes`.
The field probes provide human readable data for the electric and magnetic field components as a function
of time, as well as some basic grid information to allow for convenient analysis.

Additionally the time history of particle momentum and Poynting vector summed over the computational domain
maybe output to a file `Data/momentum.dat` by setting

```
write_momentum = T,
```

in the `CONTROL` block of the `input.deck` file.

## Further Details

- Information regarding the low noise PIC algorithm can be found in ExCALIBUR-NEPTUNE report 2047355-TN-03
Expand Down
3 changes: 3 additions & 0 deletions example_decks/two_stream_neutral.deck
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
&control
problem = 'two_stream_neutral'
/
119 changes: 119 additions & 0 deletions minepoch_py/momentum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
import sys
import argparse
import matplotlib.pyplot as plt
import numpy as np


def parse_arguments():
"""Parse command line options.
"""

parser = argparse.ArgumentParser(
description="""Momentum conservation analysis for minEPOCH
""",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)

group = parser.add_argument_group("Tolerances")

group.add_argument(
"--momentum_conservation",
default=1e-4,
type=float,
help="Allowed fractional error in momentum conservation",
nargs="?",
)

parser.add_argument("--disable_plots", help="Disable plot generation",
action="store_true")

return parser.parse_args()


def get_momenta(fname='Data/momentum.dat'):
"""Calculate time history of field and particle momentum for an output file.
Args:
fname: Input filename. Default: Data/momentum.dat
Returns:
times, particle_momentum, field_momentum
"""

times = []
field_momenta = []
particle_momenta = []
with open(fname) as f:
# Skip header
_ = f.readline()
for line in f:
times.append(line.split()[1])
particle_momenta.append(line.split()[2:5])
field_momenta.append(line.split()[5:])

times = np.array(times, dtype=float)
particle_momenta = np.array(particle_momenta, dtype=float)
field_momenta = np.array(field_momenta, dtype=float)

return times, particle_momenta, field_momenta


def momentum_check(fname='Data/momentum.dat', tolerance=None, label=None,
plot=True, savefig=False, signedplot=True):
"""Check quality of momentum conservation from minEPOCH simulation
Args:
fname: Input filename. Default: Data/momentum.dat
tolerance: Maximum acceptable fractional error. Default: None (no check)
label: Label for data set when plotting. Default: None
plot: Control optional plotting. Default: True
savefig: Control saving of figure. Default: False
signedplot: Plot +/- errors, or just absolute values. Default: True
"""

# Calculate total energy as a function of time
times, pp, fp = get_momenta(fname)
total_p = np.sum((pp + fp)**2, axis=1) # Magnitude as a function of time


# If plotting requested produce plot of energy conservation error as
# a function of time
if plot:
if signedplot:
data = total_p - total_p[0]
else:
data = np.abs(total_p - total_p[0])
plt.plot(times, data / total_p[0], label=label)
plt.xlabel(r'$\mathrm{t (s)}$', fontsize=16)
plt.ylabel(r'$\frac{\Delta |P|}{|P|(t=0)}$', fontsize=16)
plt.xlim(left=times[0])

# Update legend if necessary
if label is not None:
plt.legend()

plt.tight_layout()

if savefig:
plt.savefig('momentum_conservation.png')

# If tolerance provided, check final energy conservation error
if tolerance is not None:
delta_p = np.abs(total_p[-1] - total_p[0]) / total_p[0]
if delta_p > tolerance:
print('Momentum conservation (fractional) error = %.4E' % delta_p)
return 1
return 0

return None


if __name__ == "__main__":
# Parse arguments
options = parse_arguments()
# Run error analysis
sys.exit(momentum_check(tolerance=options.momentum_conservation,
plot=not options.disable_plots,
savefig=True))
3 changes: 2 additions & 1 deletion src/deck/deck.f90
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ SUBROUTINE read_deck
stdout_frequency, particle_push_start_time, n_species, &
fixed_fields, global_substeps, use_esirkepov, n_field_probes, &
explicit_pic, linear_tolerance, nonlinear_tolerance, &
verbose_solver, use_pseudo_current, pseudo_current_fac
verbose_solver, use_pseudo_current, pseudo_current_fac, &
write_momentum
NAMELIST/field_probe_positions/ x_probes, y_probes, z_probes

IF (first) THEN
Expand Down
87 changes: 87 additions & 0 deletions src/io/calc_df.F90
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,93 @@ END SUBROUTINE calc_total_energy_sum



SUBROUTINE calc_total_momentum_sum

REAL(num) :: particle_px, particle_py, particle_pz
REAL(num) :: field_px, field_py, field_pz
REAL(num) :: part_w
REAL(num) :: ex_cc, ey_cc, ez_cc
REAL(num) :: bx_cc, by_cc, bz_cc
REAL(num) :: sum_out(6), sum_in(6)
REAL(num), PARAMETER :: c2 = c**2
INTEGER :: ispecies, i, j, k, im, jm, km
TYPE(particle), POINTER :: current
TYPE(particle_species), POINTER :: species, next_species

particle_px = 0.0_num
particle_py = 0.0_num
particle_pz = 0.0_num

! Sum over all particles to calculate total momentum
next_species => species_list
DO ispecies = 1, n_species
species => next_species
next_species => species%next

current => species%attached_list%head
part_w = species%weight

DO WHILE (ASSOCIATED(current))
IF (particles_uniformly_distributed) THEN
part_w = current%weight
ENDIF
particle_px = particle_px + current%part_p(1) * part_w
particle_py = particle_py + current%part_p(2) * part_w
particle_pz = particle_pz + current%part_p(3) * part_w

current => current%next
ENDDO
ENDDO

! Total EM field momentum
field_px = 0.0_num
field_py = 0.0_num
field_pz = 0.0_num

DO k = 1, nz
km = k - 1
DO j = 1, ny
jm = j - 1
DO i = 1, nx
im = i - 1
ex_cc = 0.5_num * (ex(im, j , k ) + ex(i , j , k ))
ey_cc = 0.5_num * (ey(i , jm, k ) + ey(i , j , k ))
ez_cc = 0.5_num * (ez(i , j , km) + ez(i , j , k ))

bx_cc = 0.25_num * (bx(i , jm, km) + bx(i , j , km) &
+ bx(i , jm, k ) + bx(i , j , k ))
by_cc = 0.25_num * (by(im, j , km) + by(i , j , km) &
+ by(im, j , k ) + by(i , j , k ))
bz_cc = 0.25_num * (bz(im, jm, k ) + bz(i , jm, k ) &
+ bz(im, j ,k ) + bz(i , j , k ))

field_px = field_px + (ey_cc * bz_cc - ez_cc * by_cc) / mu0
field_py = field_py + (ez_cc * bx_cc - ex_cc * bz_cc) / mu0
field_pz = field_pz + (ex_cc * by_cc - ey_cc * bx_cc) / mu0
ENDDO
ENDDO
ENDDO

sum_out(1) = particle_px
sum_out(2) = particle_py
sum_out(3) = particle_pz
sum_out(4) = field_px * dx * dy * dz / c2
sum_out(5) = field_py * dx * dy * dz / c2
sum_out(6) = field_pz * dx * dy * dz / c2

CALL MPI_REDUCE(sum_out, sum_in, 6, mpireal, MPI_SUM, 0, comm, errcode)

total_px_part = sum_in(1)
total_py_part = sum_in(2)
total_pz_part = sum_in(3)
total_px_field = sum_in(4)
total_py_field = sum_in(5)
total_pz_field = sum_in(6)

END SUBROUTINE calc_total_momentum_sum



SUBROUTINE calc_charge_density(charge_density)

REAL(num), INTENT(OUT), DIMENSION(1-ng:, 1-ng:, 1-ng:) :: charge_density
Expand Down
62 changes: 62 additions & 0 deletions src/io/diagnostics.F90
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ SUBROUTINE output_routines(step, force_write) ! step = step index

IF (n_field_probes > 0) CALL write_field_probes(step)

IF (write_momentum) CALL momentum_history(step)

END SUBROUTINE output_routines


Expand Down Expand Up @@ -298,4 +300,64 @@ SUBROUTINE time_history(step) ! step = step index

END SUBROUTINE time_history



SUBROUTINE momentum_history(step) ! step = step index

INTEGER, INTENT(INOUT) :: step
CHARACTER(LEN=11+data_dir_max_length) :: full_filename
LOGICAL, SAVE :: first_call = .TRUE.
INTEGER :: errcode
INTEGER, PARAMETER :: fu = 67
LOGICAL, PARAMETER :: do_flush = .TRUE.
INTEGER, PARAMETER :: dump_frequency = 10

full_filename = TRIM(data_dir) // '/momentum.dat'

IF (first_call) THEN
! open the file
IF (rank == 0) THEN
OPEN(unit=fu, status='REPLACE', file=TRIM(full_filename), &
iostat=errcode)
IF (errcode /= 0) THEN
PRINT*, 'Failed to open file: ', TRIM(full_filename)
CALL MPI_ABORT(MPI_COMM_WORLD, c_err_io, errcode)
END IF
WRITE(fu,'(''# '',a5,99a23)') 'step', &
'px_part', 'py_part', 'pz_part', &
'px_field', 'py_field', 'pz_field'
IF (do_flush) CLOSE(unit=fu)
ENDIF
first_call = .FALSE.
ENDIF

IF (MOD(step, dump_frequency) /= 0) RETURN

IF (timer_collect) THEN
IF (timer_walltime < 0.0_num) THEN
CALL timer_start(c_timer_io)
ELSE
CALL timer_start(c_timer_io, .TRUE.)
ENDIF
ENDIF

io_list => species_list

CALL calc_total_momentum_sum

IF (rank == 0) THEN
IF (do_flush) THEN
OPEN(unit=fu, status='OLD', position='APPEND', &
file=TRIM(full_filename), iostat=errcode)
ENDIF

WRITE(fu,'(i7,99e23.14)') step, time, &
total_px_part, total_py_part, total_pz_part, &
total_px_field, total_py_field, total_pz_field

IF (do_flush) CLOSE(unit=fu)
ENDIF

END SUBROUTINE momentum_history

END MODULE diagnostics
12 changes: 11 additions & 1 deletion src/shared_data.F90
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ MODULE shared_data

INTEGER :: n_field_probes = 0
INTEGER, DIMENSION(:,:), ALLOCATABLE :: field_probes
! Output momentum time history
LOGICAL :: write_momentum = .TRUE.

LOGICAL, DIMENSION(c_dir_x:c_dir_z,0:c_stagger_max) :: stagger
INTEGER(i8) :: push_per_field = 5
Expand All @@ -433,8 +435,16 @@ MODULE shared_data
REAL(num) :: laser_absorb_local = 0.0_num
REAL(num) :: laser_injected = 0.0_num
REAL(num) :: laser_absorbed = 0.0_num

! Energy diagnostics
REAL(num) :: total_particle_energy = 0.0_num
REAL(num) :: total_field_energy = 0.0_num
! Momentum diagnostics
REAL(num) :: total_px_part = 0.0_num
REAL(num) :: total_py_part = 0.0_num
REAL(num) :: total_pz_part = 0.0_num
REAL(num) :: total_px_field = 0.0_num
REAL(num) :: total_py_field = 0.0_num
REAL(num) :: total_pz_field = 0.0_num


END MODULE shared_data
Loading

0 comments on commit 9998a66

Please sign in to comment.