diff --git a/README.md b/README.md index ebf53bb..2a61f9c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/example_decks/two_stream_neutral.deck b/example_decks/two_stream_neutral.deck new file mode 100644 index 0000000..5f23e7c --- /dev/null +++ b/example_decks/two_stream_neutral.deck @@ -0,0 +1,3 @@ +&control + problem = 'two_stream_neutral' +/ diff --git a/minepoch_py/momentum.py b/minepoch_py/momentum.py new file mode 100644 index 0000000..162308e --- /dev/null +++ b/minepoch_py/momentum.py @@ -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)) diff --git a/src/deck/deck.f90 b/src/deck/deck.f90 index 0de058a..9a58d4f 100644 --- a/src/deck/deck.f90 +++ b/src/deck/deck.f90 @@ -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 diff --git a/src/io/calc_df.F90 b/src/io/calc_df.F90 index 477f363..53e5754 100644 --- a/src/io/calc_df.F90 +++ b/src/io/calc_df.F90 @@ -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 diff --git a/src/io/diagnostics.F90 b/src/io/diagnostics.F90 index e9ac86a..f6eefd9 100644 --- a/src/io/diagnostics.F90 +++ b/src/io/diagnostics.F90 @@ -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 @@ -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 diff --git a/src/shared_data.F90 b/src/shared_data.F90 index 4104ec4..1231633 100644 --- a/src/shared_data.F90 +++ b/src/shared_data.F90 @@ -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 @@ -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 diff --git a/src/user_interaction/ic_module.f90 b/src/user_interaction/ic_module.f90 index b9d7f5d..9c6d259 100644 --- a/src/user_interaction/ic_module.f90 +++ b/src/user_interaction/ic_module.f90 @@ -27,6 +27,8 @@ SUBROUTINE custom_problem_setup(deck_state, problem) SELECT CASE (TRIM(problem)) CASE('two_stream') CALL two_stream_setup(deck_state) + CASE('two_stream_neutral') + CALL two_stream_neutral_setup(deck_state) CASE('one_stream') CALL one_stream_setup(deck_state) CASE('drift_kin_default') @@ -164,6 +166,166 @@ SUBROUTINE two_stream_setup(deck_state) END SUBROUTINE two_stream_setup + SUBROUTINE two_stream_neutral_setup(deck_state) + + INTEGER, INTENT(IN) :: deck_state + REAL(num), PARAMETER :: v_drift = 0.2_num * c + REAL(num), PARAMETER :: v_therm = 0.01_num * c + REAL(num), PARAMETER :: v_pert = 0.1_num * v_therm + REAL(num), PARAMETER :: n0 = 8e11 + INTEGER, PARAMETER :: ppc = 16 + REAL(num) :: gamma_drift, temp_x, omega + INTEGER :: ix + TYPE(particle_species), POINTER :: current_species + + IF (deck_state == c_ds_first) THEN + ! Set control variables here + nx_global = 64 + ny_global = 4 + nz_global = 4 + x_min = 0.0_num + x_max = 2.0_num * pi + y_min = x_min + ! Could do (x_max * ny_global) / nx_global, but be wary of compilers + ! which don't obey precedence implied by parentheses by default + ! (e.g. Intel) + y_max = x_max * REAL(ny_global, num) / REAL(nx_global, num) + z_min = x_min + z_max = x_max * REAL(nz_global, num) / REAL(nx_global, num) + + ! Plasma frequency + omega = SQRT(n0 * q0 * q0 / epsilon0 / m0) + t_end = 30.0_num / omega + stdout_frequency = 10 + + ! dt_multiplier = 0.5 + + + ! Need to set-up species here + NULLIFY(current_species) + CALL setup_species(current_species, 'Right') + + ! mass -- MANDATORY + current_species%mass = 1.1_num * m0 + + ! charge -- MANDATORY + current_species%charge = -1.0_num * q0 + + ! npart_per_cell + current_species%npart_per_cell = ppc + + ! MANDATORY + NULLIFY(current_species) + CALL setup_species(current_species, 'Left') + + ! mass -- MANDATORY + current_species%mass = 1.0_num * m0 + + ! charge -- MANDATORY + current_species%charge = -1.0_num * q0 + + ! npart_per_cell + current_species%npart_per_cell = ppc + + NULLIFY(current_species) + CALL setup_species(current_species, 'Balance') + + ! mass -- MANDATORY + current_species%mass = 100 * m0 + + ! charge -- MANDATORY + current_species%charge = 1.0_num * q0 + + ! npart_per_cell + current_species%npart_per_cell = ppc + + RETURN + END IF + + ! Calculate gamma_drift + ! Strictly should be function of x, but vpert << vdrift + gamma_drift = 1.0_num / SQRT(1.0_num - (v_drift / c)**2) + + ! Calculate (1 DoF) temperature + temp_x = v_therm**2 * m0 / kb + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + ! Species + + ! MANDATORY (Is the NULLIFY mandatory?) + NULLIFY(current_species) + CALL setup_species(current_species, 'Right') + + ! density + current_species%density = n0 + + ! drift_x + ! Add on perturbation to seed instability + DO ix = 1-ng, nx+ng + current_species%drift(ix,:,:,1) = gamma_drift * current_species%mass & + * (v_drift + v_pert * SIN(3.0_num * x(ix))) + END DO + + ! temp_x + current_species%temp(:,:,:,1) = temp_x + + IF (explicit_pic) THEN + current_species%is_implicit = .FALSE. + ELSE + current_species%is_implicit = .TRUE. + END IF + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + ! MANDATORY + NULLIFY(current_species) + CALL setup_species(current_species, 'Left') + + ! density + current_species%density = n0 + + ! drift_x + ! Add on perturbation to seed instability + DO ix = 1-ng, nx+ng + current_species%drift(ix,:,:,1) = gamma_drift * current_species%mass & + * (-v_drift + v_pert * SIN(3.0_num * x(ix))) + END DO + + ! temp_x + current_species%temp(:,:,:,1) = temp_x + + IF (explicit_pic) THEN + current_species%is_implicit = .FALSE. + ELSE + current_species%is_implicit = .TRUE. + END IF + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + ! MANDATORY + NULLIFY(current_species) + CALL setup_species(current_species, 'Balance') + + ! density + current_species%density = 2.0_num * n0 + + ! drift_x + DO ix = 1-ng, nx+ng + current_species%drift(ix,:,:,1) = 0.0_num + END DO + + ! temp_x + current_species%temp(:,:,:,1) = temp_x + + IF (explicit_pic) THEN + current_species%is_implicit = .FALSE. + ELSE + current_species%is_implicit = .TRUE. + END IF + + + END SUBROUTINE two_stream_neutral_setup SUBROUTINE em_wave_setup(deck_state)