From 0d3c970d7e7c57ecd8598173ddd154f8ca6ebc23 Mon Sep 17 00:00:00 2001 From: Owen Parry <101191772+oparry-ukaea@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:48:30 +0000 Subject: [PATCH] 3D coupled deliverable (#209) * Fix a CMake syntax error that meant the 'SOLVER_LIBS' list wasn't being assembled properly. * Skeleton for the Hermes-3 LAPD solver. * Rename solver source directory for consistency. * First attempt at a mesh for the H3LAPD problem - tries to match H3 resolution. * A much lower res mesh for testing the H3LAPD solver. * Add a convenience script for converting a .geo gmsh config to a Nektar xml. Force uncompressed xml for now. * First version of LAPD (low-res) config. * Add advection terms. * Add pressure gradient terms. * Add E_perp terms. * Reorder H3LAPDSystem variables and functions. * Store perp velocities at class level to allow use in multiple terms. * Add collision terms to momentum equations and polarisation drift term to vorticity equation. * Add some comments and docstrings. * Add explicit values to the low res config file, rather than relying on defaults. * Added a note on initial conditions. * More flexible PrintArrVals * Add density source term. * Label low, high z boundaries in config * Initial guess at ICs. * Comment on the density source term * Removed two places where Bvec=(0,0,B) and |Bvec| = 1 were assumed. * Comments and cosmetic * Fix domain cross section size in low, full res meshes. * Phi solve attempt 1 * Comments in config xml. * Set BCs. * Fix typo in config comment. * Fix error in config - IterativeSolverTolerance can be in GLOBALSYSSOLNINFO or PARAMETERS, but not SOLVERINFO. * Improvements to PrintArrVals debugging func. * Apply non-dimensionalisation factors to input params, BCs, ICs, density source. * ne, w BCs to Dirichlet, since we don't expect Neumann to be supported for discontinous fields (phi isn't discontinous here). * Modified timestep, number of steps for consistency with H3 and to reflect scaling of the ion cyclotron frequency. * Calculate density-dependent Coulomb logarithm and collision frequency for electron-ion interactions. * Rename a config file variable, for consistency, and explicitly include time scaling in the density source. * Changed some variable and function names to clarify collision frequency calculation. * Correction to nu_ei calc (equations doc is wrong). * More readable expression for nu_ei_const. * Correct electric potential scaling factor; rearrange temperature scaling factor expression. * Rename temperature scaling param; avoids clash with time scaling factor just in case param names aren't case sensitive. * Allow build dir to be supplied as a relative path in run_eg.sh * Correct m_u value. * Different scaling approach; set B0 and derive ts, rather than vice versa. * Rename dimensionless density and velocity params to match Hermes 3 docs. * Fix confusion between n_e and nRef in the phi solve. * Minor; xml formatting * Add an H3LAPD example that uses a cuboid mesh, works with PhiSolve(). * Correct function misnomer - AddEPerpTerms => AddEParTerms. * Correct misleading variable names. * Zero outarray. * Correct a number of misnamed variables. * Implement density floor when computing parallel velocity. * Add convenience function to zero outarray. * Implement polarisation drift term via an advection object to properly account for DG flux. * Fix call to AddAdvTerms for polarisation drift term and add some error checking. * Allow Helmsolve coefficients to be set by constant factors (not 'variable coefficients') that are read as parameters from the session file. Defaults to [1,1,1]. * Make array arg of PrintArrSize const. * Provide a virtual function to allow subclasses to define different RHSs for the phi solve. * Density source as separate function. * Added Ed's implementation of the 2D-in-3D Hasegawa-Wakatani equations. * Modified geo to xml script to allow output_basename != input_basename. * Renamed hw config xml to distinguish clearly from full LAPD config. * Strip LAPD-specific parameters from HW example and rename others for clarity. * Modified H3LAPDSystem::CalcEAndAdvVels to avoid referencing momentum fields if the derived system doesn't use them. * Remove unused momentum fields from HW example. * Remove unused class. * Rename all .h extensions to .hpp. * Remove unused array of forcing objects. * Remove superfluous function in HW session file. * Copy neutral_particles header from SimpleSOL and add particle system to H3LAPDSystem. Builds. * First go at adapting NeutralParticleSystem; no particle init yet. * Add particle source to density field. * Add background density via a session param. * Reuse Te_eV param in particle system. * Fix get_point_in_subdomain() for 3D (#211) * added add more generic implementation to get a point in the domain * added missing memory cleanup * added missing api docstrings to get_element functions * Add particle initialisation. * Reduce to 100 steps for debugging * track particle id offset between calls to add_particles * updated NESO-Particles submodule commit to include RNG updates * Fix bug with assumed variable order. * Initialise ne, w, phi to zero. * Tweak params - makes it stable (ne doesn't NaN), but ionisation near zero. * added lower to higher order projection * added drift velocity * tidy up of ionisation kernel * removed ErrorPropagate instance that is dead code * 7 modes => 4 modes * Set a particle_drift_velocity in the session file. * Tweak timestep, other params to get a stable example that demonstrates coupling. * Increase mesh resolution slightly. * Typo. * Less particle output. * Set source width via a param. * Handle peralign args to NekMesh in geo_to_xml script. * Revert mesh extent, particle source width to more stable vals. * Increase particle_number_density to 1e16. * Reduce num_particles_total to 1e4 * Triple sim time limit. * Double sim length to try and see blob movement. * 5 modes * Fix a (non-critical) error in the geo_to_xml script and document the periodic BC options in the usage message. * Make NeutralParticleSystem::integrate() behave when num_particles_total=0. * Bugfix - actually apply the kappa value set in the input file. * num_particles_total back to 1e5, NUMMODES to 6. * Set alpha=0.1, kappa=10. * Restore Ed's example as 'hw_fluid-only'. * Set alpha=0.1, kappa=10 in hw_fluid-only e.g. * Tweak fluid-only e.g to get stable sim that produces turbulence. kappa=3.5, 6x large init ne amplitude, half dt, double sim length. * Make coupled sim's length, output times, and kappa value the same as the fluid-only e.g. * Automatically pick up RelWithDebInfo version of NekMesh in geo_to_xml script. * Various tweaks to make it easier to run fluid-only sims, including removing requirements for ne_src field, num_particles_per_cell param to be defined when not using particles. * Modify NektarSolverTest to allow solver name to be set independently of test fixture name. * Add test for growth rate of energy and enstrophy. * Remove some debugging messages. * Rename 2D HW equation system, tests and examples in anticipation of 3D HW solver. * Refactor equation system classes. * Fix gamma_alpha def in GrowthRatesRecorder. * Tweak E definition in GrowthRatesRecorder. * Modify growth rates test to give non-zero gamma_alpha (initial n != phi). * Tighten up growth rate tolerances. * var rename * Move a header. * Add a mass conservation test for the coupled 2Din3DHW solver (alpha=kappa=0). * Rename particle system source file. * Set a more suitable namespace for all H3LAPD solver components. * Remove superfluous variables from GrowthRatesRecorder. * Consistent variable naming, ordering and doxygen markup. * Remove superfluous variable. * Make non-zero return code a fatal error in HW tests. * Various tweaks to geo_to_xml script. Added description and example usage at the top of the file. * Remove old Nektar licence headers from equation system source files and add class-level doxygen markup. * Add some links to initial markdown for lapd e.g. * Assert that all required fields have the same number of quadrature points. * Changes to address various review comments. * Handle B_xy = 0 correctly. * Update neso-particles to use NESOASSERT fix and modify some calls to allow for namespace change. * Use NESOASSERT for NaN check. * Small increase to mass conservation test tolerance. --------- Co-authored-by: Will Saunders <77331509+will-saunders-ukaea@users.noreply.github.com> Co-authored-by: Will Saunders --- CMakeLists.txt | 3 +- examples/H3LAPD/.gitignore | 4 + .../2Din3D-hw/cuboid_periodic_8x8x16.geo | 37 + examples/H3LAPD/2Din3D-hw/hw.xml | 128 +++ .../H3LAPD/2Din3D-hw/run_cmd_template.txt | 1 + .../cuboid_periodic_5x5x10.geo | 37 + examples/H3LAPD/2Din3D-hw_fluid-only/hw.xml | 103 +++ .../2Din3D-hw_fluid-only/run_cmd_template.txt | 1 + examples/H3LAPD/cuboid/cuboid.geo | 31 + examples/H3LAPD/cuboid/lapd.xml | 142 ++++ examples/H3LAPD/cuboid/run_cmd_template.txt | 1 + examples/H3LAPD/full_res/full_res.geo | 48 ++ examples/H3LAPD/lapd.md | 40 + examples/H3LAPD/low_res/lapd.xml | 144 ++++ examples/H3LAPD/low_res/low_res.geo | 47 ++ examples/H3LAPD/low_res/run_cmd_template.txt | 1 + include/atomic_data_reader.hpp | 8 +- include/interpolator.hpp | 4 +- .../geometry_transport_2d.hpp | 22 +- .../geometry_transport_3d.hpp | 9 + .../particle_mesh_interface.hpp | 85 +- neso-particles | 2 +- scripts/geo_to_xml.sh | 204 +++++ scripts/run_eg.sh | 2 +- solvers/H3LAPD/CMakeLists.txt | 45 ++ .../Diagnostics/GrowthRatesRecorder.hpp | 164 ++++ solvers/H3LAPD/Diagnostics/MassRecorder.hpp | 184 +++++ .../EquationSystems/DriftReducedSystem.cpp | 606 ++++++++++++++ .../EquationSystems/DriftReducedSystem.hpp | 174 ++++ .../H3LAPD/EquationSystems/HW2Din3DSystem.cpp | 189 +++++ .../H3LAPD/EquationSystems/HW2Din3DSystem.hpp | 90 +++ solvers/H3LAPD/EquationSystems/LAPDSystem.cpp | 375 +++++++++ solvers/H3LAPD/EquationSystems/LAPDSystem.hpp | 112 +++ solvers/H3LAPD/H3LAPD.cpp | 51 ++ solvers/H3LAPD/H3LAPD.hpp | 18 + .../ParticleSystems/NeutralParticleSystem.hpp | 744 ++++++++++++++++++ solvers/H3LAPD/main.cpp | 31 + solvers/SimpleSOL/CMakeLists.txt | 2 +- .../geometry_transport_2d.cpp | 45 ++ .../geometry_transport_3d.cpp | 31 + test/CMakeLists.txt | 2 +- .../2Din3DHWGrowthRates_config.xml | 107 +++ .../2Din3DHWGrowthRates_mesh.xml | 35 + .../Coupled2Din3DHWMassCons_config.xml | 130 +++ .../Coupled2Din3DHWMassCons_mesh.xml | 35 + .../solvers/H3LAPD/common/.gitkeep | 0 .../solvers/H3LAPD/test_H3LAPD.cpp | 15 + test/integration/solvers/H3LAPD/test_H3LAPD.h | 150 ++++ .../integration/solvers/solver_test_utils.cpp | 11 +- test/integration/solvers/solver_test_utils.h | 3 + .../test_particle_geometry_interface.cpp | 50 ++ .../test_particle_geometry_interface_3d.cpp | 52 ++ 52 files changed, 4486 insertions(+), 69 deletions(-) create mode 100644 examples/H3LAPD/.gitignore create mode 100644 examples/H3LAPD/2Din3D-hw/cuboid_periodic_8x8x16.geo create mode 100644 examples/H3LAPD/2Din3D-hw/hw.xml create mode 100644 examples/H3LAPD/2Din3D-hw/run_cmd_template.txt create mode 100644 examples/H3LAPD/2Din3D-hw_fluid-only/cuboid_periodic_5x5x10.geo create mode 100644 examples/H3LAPD/2Din3D-hw_fluid-only/hw.xml create mode 100644 examples/H3LAPD/2Din3D-hw_fluid-only/run_cmd_template.txt create mode 100644 examples/H3LAPD/cuboid/cuboid.geo create mode 100644 examples/H3LAPD/cuboid/lapd.xml create mode 100644 examples/H3LAPD/cuboid/run_cmd_template.txt create mode 100644 examples/H3LAPD/full_res/full_res.geo create mode 100644 examples/H3LAPD/lapd.md create mode 100644 examples/H3LAPD/low_res/lapd.xml create mode 100644 examples/H3LAPD/low_res/low_res.geo create mode 100644 examples/H3LAPD/low_res/run_cmd_template.txt create mode 100755 scripts/geo_to_xml.sh create mode 100644 solvers/H3LAPD/CMakeLists.txt create mode 100644 solvers/H3LAPD/Diagnostics/GrowthRatesRecorder.hpp create mode 100644 solvers/H3LAPD/Diagnostics/MassRecorder.hpp create mode 100644 solvers/H3LAPD/EquationSystems/DriftReducedSystem.cpp create mode 100644 solvers/H3LAPD/EquationSystems/DriftReducedSystem.hpp create mode 100644 solvers/H3LAPD/EquationSystems/HW2Din3DSystem.cpp create mode 100644 solvers/H3LAPD/EquationSystems/HW2Din3DSystem.hpp create mode 100644 solvers/H3LAPD/EquationSystems/LAPDSystem.cpp create mode 100644 solvers/H3LAPD/EquationSystems/LAPDSystem.hpp create mode 100644 solvers/H3LAPD/H3LAPD.cpp create mode 100644 solvers/H3LAPD/H3LAPD.hpp create mode 100644 solvers/H3LAPD/ParticleSystems/NeutralParticleSystem.hpp create mode 100644 solvers/H3LAPD/main.cpp create mode 100644 src/nektar_interface/geometry_transport/geometry_transport_2d.cpp create mode 100644 test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_config.xml create mode 100644 test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_mesh.xml create mode 100644 test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_config.xml create mode 100644 test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_mesh.xml create mode 100644 test/integration/solvers/H3LAPD/common/.gitkeep create mode 100644 test/integration/solvers/H3LAPD/test_H3LAPD.cpp create mode 100644 test/integration/solvers/H3LAPD/test_H3LAPD.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a156e390..08590c64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ set(LIB_SRC_FILES ${SRC_DIR}/nektar_interface/basis_reference.cpp ${SRC_DIR}/nektar_interface/expansion_looping/jacobi_coeff_mod_basis.cpp ${SRC_DIR}/nektar_interface/function_evaluation.cpp + ${SRC_DIR}/nektar_interface/geometry_transport/geometry_transport_2d.cpp ${SRC_DIR}/nektar_interface/geometry_transport/geometry_transport_3d.cpp ${SRC_DIR}/nektar_interface/geometry_transport/halo_extension.cpp ${SRC_DIR}/nektar_interface/particle_cell_mapping/map_particles_2d.cpp @@ -114,7 +115,7 @@ set(LIB_SRC_FILES ${SRC_DIR}/run_info.cpp ${SRC_DIR}/simulation.cpp ${SRC_DIR}/species.cpp) - + set(LIB_SRC_FILES_IGNORE ${SRC_DIR}/main.cpp) check_file_list(${SRC_DIR} cpp "${LIB_SRC_FILES}" "${LIB_SRC_FILES_IGNORE}") diff --git a/examples/H3LAPD/.gitignore b/examples/H3LAPD/.gitignore new file mode 100644 index 00000000..41305542 --- /dev/null +++ b/examples/H3LAPD/.gitignore @@ -0,0 +1,4 @@ +# Ignore xml files generated from .geos +cuboid*.xml +low_res.xml +full_res.xml \ No newline at end of file diff --git a/examples/H3LAPD/2Din3D-hw/cuboid_periodic_8x8x16.geo b/examples/H3LAPD/2Din3D-hw/cuboid_periodic_8x8x16.geo new file mode 100644 index 00000000..a0729c63 --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw/cuboid_periodic_8x8x16.geo @@ -0,0 +1,37 @@ +//=============================== Parameters ================================== +// Lengths and resolutions in each dimension +xsize = 5; +ysize = 5; +zsize = 10; +nx = 8; +ny = 8; +nz = 16; +//============================================================================= + +// Create a line in the x-direction of length , with divisions +Point(1) = {-xsize/2, -ysize/2, 0, 0.01}; +Point(2) = {xsize/2, -ysize/2, 0, 0.01}; +Line(1) = {1, 2}; +Transfinite Line(1) = nx+1; + +// Extrude split line into meshed square/rectangle +sq = Extrude {0,ysize,0} {Curve{1}; Layers{ny}; Recombine;}; + +// Extrude square/rectangle into a cuboid +cbd = Extrude {0,0,zsize} {Surface{sq[1]}; Layers{nz}; Recombine;}; + +// Define physical volume, surfaces for BCs +// Domain +Physical Volume(0) = {cbd[1]}; +// Low-x side +Physical Surface(1) = {cbd[5]}; +// High-x side +Physical Surface(2) = {cbd[3]}; +// Low-y side +Physical Surface(3) = {cbd[2]}; +// High-y side +Physical Surface(4) = {cbd[4]}; +// Low-z side +Physical Surface(5) = {sq[1]}; +// High-z side +Physical Surface(6) = {cbd[0]}; \ No newline at end of file diff --git a/examples/H3LAPD/2Din3D-hw/hw.xml b/examples/H3LAPD/2Din3D-hw/hw.xml new file mode 100644 index 00000000..5c4f96a8 --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw/hw.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 0.000625

+

NumSteps = 64000

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps/1600

+

IO_CheckSteps = NumSteps/160

+ +

Bxy = 1.0

+ +

d22 = 0.0

+ + +

HW_alpha = 0.1

+ +

HW_kappa = 3.5

+ +

s = 0.5

+ +

num_particles_per_cell = -1

+

num_particle_steps_per_fluid_step = 1

+

num_particles_total = 100000

+

particle_num_write_particle_steps = IO_CheckSteps

+

particle_number_density = 1e16

+

particle_position_seed = 1

+

particle_thermal_velocity = 1.0

+

particle_drift_velocity = 2.0

+

particle_source_width = 0.2

+ +

Te_eV = 10.0

+ +

n_bg_SI = 1e18

+ +

t_to_SI = 2e-4

+

n_to_SI = 1e17

+
+ + + ne + w + phi + ne_src + + + + C[1] + C[2] + C[3] + C[4] + C[5] + C[6] + + + + + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + + + + + + + + + diff --git a/examples/H3LAPD/2Din3D-hw/run_cmd_template.txt b/examples/H3LAPD/2Din3D-hw/run_cmd_template.txt new file mode 100644 index 00000000..991b42d1 --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw/run_cmd_template.txt @@ -0,0 +1 @@ +mpirun -np hw.xml cuboid.xml \ No newline at end of file diff --git a/examples/H3LAPD/2Din3D-hw_fluid-only/cuboid_periodic_5x5x10.geo b/examples/H3LAPD/2Din3D-hw_fluid-only/cuboid_periodic_5x5x10.geo new file mode 100644 index 00000000..e467e3aa --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw_fluid-only/cuboid_periodic_5x5x10.geo @@ -0,0 +1,37 @@ +//=============================== Parameters ================================== +// Lengths and resolutions in each dimension +xsize = 5; +ysize = 5; +zsize = 10; +nx = 5; +ny = 5; +nz = 10; +//============================================================================= + +// Create a line in the x-direction of length , with divisions +Point(1) = {-xsize/2, -ysize/2, 0, 0.01}; +Point(2) = {xsize/2, -ysize/2, 0, 0.01}; +Line(1) = {1, 2}; +Transfinite Line(1) = nx+1; + +// Extrude split line into meshed square/rectangle +sq = Extrude {0,ysize,0} {Curve{1}; Layers{ny}; Recombine;}; + +// Extrude square/rectangle into a cuboid +cbd = Extrude {0,0,zsize} {Surface{sq[1]}; Layers{nz}; Recombine;}; + +// Define physical volume, surfaces for BCs +// Domain +Physical Volume(0) = {cbd[1]}; +// Low-x side +Physical Surface(1) = {cbd[5]}; +// High-x side +Physical Surface(2) = {cbd[3]}; +// Low-y side +Physical Surface(3) = {cbd[2]}; +// High-y side +Physical Surface(4) = {cbd[4]}; +// Low-z side +Physical Surface(5) = {sq[1]}; +// High-z side +Physical Surface(6) = {cbd[0]}; \ No newline at end of file diff --git a/examples/H3LAPD/2Din3D-hw_fluid-only/hw.xml b/examples/H3LAPD/2Din3D-hw_fluid-only/hw.xml new file mode 100644 index 00000000..64fa3ef7 --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw_fluid-only/hw.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 0.00125

+

NumSteps = 32000

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps/1600

+

IO_CheckSteps = NumSteps/160

+ +

Bxy = 1.0

+ +

d22 = 0.0

+ +

HW_alpha = 0.1

+

HW_kappa = 3.5

+ +

s = 0.5

+ +

num_particles_total = 0

+ + + + ne + w + phi + + + + C[1] + C[2] + C[3] + C[4] + C[5] + C[6] + + + + + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + + + + + + + + diff --git a/examples/H3LAPD/2Din3D-hw_fluid-only/run_cmd_template.txt b/examples/H3LAPD/2Din3D-hw_fluid-only/run_cmd_template.txt new file mode 100644 index 00000000..991b42d1 --- /dev/null +++ b/examples/H3LAPD/2Din3D-hw_fluid-only/run_cmd_template.txt @@ -0,0 +1 @@ +mpirun -np hw.xml cuboid.xml \ No newline at end of file diff --git a/examples/H3LAPD/cuboid/cuboid.geo b/examples/H3LAPD/cuboid/cuboid.geo new file mode 100644 index 00000000..b552f051 --- /dev/null +++ b/examples/H3LAPD/cuboid/cuboid.geo @@ -0,0 +1,31 @@ +//=============================== Parameters ================================== +// Lengths and resolutions in each dimension +xsize = 5; +ysize = 5; +zsize = 10; +nx = 5; +ny = 5; +nz = 10; +//============================================================================= + +// Create a line in the x-direction of length , with divisions +Point(1) = {-xsize/2, -ysize/2, 0, 0.01}; +Point(2) = {xsize/2, -ysize/2, 0, 0.01}; +Line(1) = {1, 2}; +Transfinite Line(1) = nx+1; + +// Extrude split line into meshed square +sq = Extrude {0,ysize,0} {Curve{1}; Layers{ny}; Recombine;}; + +// Extrude square into a cuboid +cbd = Extrude {0,0,zsize} {Surface{sq[1]}; Layers{nz}; Recombine;}; + +// Define physical volume, surfaces for BCs +// Domain +Physical Volume(0) = {cbd[1]}; +// Long sides, parallel to z-axis +Physical Surface(1) = {cbd[2],cbd[3],cbd[4],cbd[5]}; +// Low-z square +Physical Surface(2) = {sq[1]}; +// High-z square +Physical Surface(3) = {cbd[0]}; \ No newline at end of file diff --git a/examples/H3LAPD/cuboid/lapd.xml b/examples/H3LAPD/cuboid/lapd.xml new file mode 100644 index 00000000..9ecbb1c9 --- /dev/null +++ b/examples/H3LAPD/cuboid/lapd.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 1e3

+

NumSteps = 1e4

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps+1

+

IO_CheckSteps = 1

+ +

Te_eV = 5.0

+

Td_eV = 0.1

+

B_T = 0.1

+ +

B0 = 0.1

+

Ls = 1

+

Nref = 1e18

+ +

kJ = 1.38e-23

+

keV = 8.62e-5

+

m_u = 1.67e-27

+

q_e = 1.60e-19

+

eps0_SI = 8.85e-12

+ +

ts = m_u/B0/q_e

+

ns = Nref/Ls/Ls/Ls

+

ps = Nref*m_u/Ls/ts/ts

+

Us = Ls/ts

+

Tmps = m_u*Us*Us/kJ

+

phis = m_u*Us*Us/q_e

+

wci = 1./ts

+ +

Bxy = B_T/B0

+

e = 1.0

+

eps0 = eps0_SI*phis*Ls/q_e

+

md = 2.0

+

me = 60./1836

+

nRef = 1e18/ns

+

qd = e

+

qelec = -e

+

Rmax = 0.4/Ls

+

Te = Te_eV/keV/Tmps

+

Td = Td_eV/keV/Tmps

+

cs = sqrt(e*(Td+Te)/(md+me))

+

vRef = cs

+ +

sumvsq = 2*(Te/me+Td/md)

+

nu_ei_const = qelec^2*qd^2*(1+me/md)/(3*(PI*sumvsq)^1.5*eps0^2*me^2)

+

logLambda_const = 30-log(qd/e)+1.5*log(Te_eV)

+ + + + ne + Ge + Gd + w + phi + + + + C[1] + C[2] + C[3] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/examples/H3LAPD/cuboid/run_cmd_template.txt b/examples/H3LAPD/cuboid/run_cmd_template.txt new file mode 100644 index 00000000..2e61c35b --- /dev/null +++ b/examples/H3LAPD/cuboid/run_cmd_template.txt @@ -0,0 +1 @@ + lapd.xml cuboid.xml \ No newline at end of file diff --git a/examples/H3LAPD/full_res/full_res.geo b/examples/H3LAPD/full_res/full_res.geo new file mode 100644 index 00000000..a858a518 --- /dev/null +++ b/examples/H3LAPD/full_res/full_res.geo @@ -0,0 +1,48 @@ +//=================== Parameter names, chosen to match H3 ===================== +// Physical sizes / m +Rmax = 0.4; +length = 17; +// # cells in each dimension +nx = 85; // Radial resolution +ny = 16; // Parallel (on-axis) resolution +nz_target = 64; // Target Azimuthal resolution +// N.B. +// nx = 64 for H3, but they use Rmin=0.1; match approximately by increasing to nint[64 * 0.4 / (0.4-0.1)] +// nz is only a target - the actual value used is 3*Floor(nz_target,3) +//============================================================================= + +// Create a line in the x-direction of length , with divisions +Point(1) = {0, 0, 0, 0.01}; +Point(2) = {Rmax, 0, 0, 0.01}; +Line(1) = {1, 2}; +Transfinite Line(1) = nx+1; + +// Extrude split line into meshed circle +// (Rotation extrusion can only cope with angles < pi, so has to be done in three parts...) +nz_over3 = Floor(nz_target/3); +Printf("Using nz = %g",3*nz_over3); +c1[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{1}; Layers{nz_over3}; Recombine; +}; + +c2[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{c1[0]}; Layers{nz_over3}; Recombine; +}; + +c3[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{c2[0]}; Layers{nz_over3}; Recombine; +}; + +// Extrude each 1/3 of a circle to 1/3 of a cylinder +v1[] = Extrude {0,0,length} {Surface{c1[1]}; Layers{ny}; Recombine;}; +v2[] = Extrude {0,0,length} {Surface{c2[1]}; Layers{ny}; Recombine;}; +v3[] = Extrude {0,0,length} {Surface{c3[1]}; Layers{ny}; Recombine;}; + +// Label volumes and surfaces (combining the 3 azimuthally-split sections in each case) +// Whole domain volume +Physical Volume(0) = {v1[1],v2[1],v3[1]}; +// Curved boundary surface +Physical Surface(1) = {v1[3],v2[3],v3[3]}; +// Two circular boundary surfaces +Physical Surface(2) = {c1[1],c2[1],c3[1]}; +Physical Surface(3) = {v1[0],v2[0],v3[0]}; \ No newline at end of file diff --git a/examples/H3LAPD/lapd.md b/examples/H3LAPD/lapd.md new file mode 100644 index 00000000..a3d7f1ca --- /dev/null +++ b/examples/H3LAPD/lapd.md @@ -0,0 +1,40 @@ +## LAPD e.g. + +### Initial conditions + +[Herme-3](https://github.com/bendudson/hermes-3) initialises $n_e$ with +$$ + 0.1~e^{-x^2} + 10^{-5}~(\mathrm{mixmode}(z) + \mathrm{mixmode}(4*z - x)) +$$ + +From the [BOUT++ docs](https://bout-dev.readthedocs.io/en/latest/user_docs/variable_init.html#initialisation-of-time-evolved-variables): + + The ``mixmode(x)`` function is a mixture of Fourier modes of the form: + + $$ + \mathrm{mixmode}(x) = \sum_{i=1}^{14} \frac{1}{(1 + + |i-4|)^2}\cos[ix + \phi(i, \mathrm{seed})] + $$ + + where $\phi$ is a random phase between $-\pi$ and + $+\pi$, which depends on the seed. The factor in front of each + term is chosen so that the 4th harmonic ($i=4$) has the highest + amplitude. This is useful mainly for initialising turbulence + simulations, where a mixture of mode numbers is desired. + +OP: Not clear what ICs for fields other than $n_e$ are... + +--- + +### Electron-ion collision frequency +From eqns 133,134 in the equations doc: +$$ +\nu_{e,i} = \frac{|q_e||q_i|n_{i}{\rm log}\Lambda(1+m_e/m_i)}{3\pi^{3/2}\epsilon_0^{2}m_e^2(v_e^2+v_i^2)^{3/2}} +$$ +with +$$ +\begin{align} +{\rm log}\Lambda =&~30 − 0.5 \ln n_e − \ln Z_i + 1.5 \ln Te \\ +{\rm log}\Lambda =&~34.14 − 0.5 \ln n_e +\end{align} +$$ \ No newline at end of file diff --git a/examples/H3LAPD/low_res/lapd.xml b/examples/H3LAPD/low_res/lapd.xml new file mode 100644 index 00000000..b209fba0 --- /dev/null +++ b/examples/H3LAPD/low_res/lapd.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 1e3

+

NumSteps = 1e4

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps+1

+

IO_CheckSteps = 1

+ +

Te_eV = 5.0

+

Td_eV = 0.1

+

B_T = 0.1

+ +

B0 = 0.1

+

Ls = 1

+

Nref = 1e18

+ +

kJ = 1.38e-23

+

keV = 8.62e-5

+

m_u = 1.67e-27

+

q_e = 1.60e-19

+

eps0_SI = 8.85e-12

+ +

ts = m_u/B0/q_e

+

ns = Nref/Ls/Ls/Ls

+

ps = Nref*m_u/Ls/ts/ts

+

Us = Ls/ts

+

Tmps = m_u*Us*Us/kJ

+

phis = m_u*Us*Us/q_e

+

wci = 1./ts

+ +

Bxy = B_T/B0

+

e = 1.0

+

eps0 = eps0_SI*phis*Ls/q_e

+

md = 2.0

+

me = 60./1836

+

n_floor_fac = 1e-5

+

nRef = 1e18/ns

+

qd = e

+

qelec = -e

+

Rmax = 0.4/Ls

+

Te = Te_eV/keV/Tmps

+

Td = Td_eV/keV/Tmps

+

cs = sqrt(e*(Td+Te)/(md+me))

+

vRef = cs

+ +

sumvsq = 2*(Te/me+Td/md)

+

nu_ei_const = qelec^2*qd^2*(1+me/md)/(3*(PI*sumvsq)^1.5*eps0^2*me^2)

+

logLambda_const = 30-log(qd/e)+1.5*log(Te_eV)

+
+ + + ne + Ge + Gd + w + phi + + + + C[1] + C[2] + C[3] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/examples/H3LAPD/low_res/low_res.geo b/examples/H3LAPD/low_res/low_res.geo new file mode 100644 index 00000000..36f4a49a --- /dev/null +++ b/examples/H3LAPD/low_res/low_res.geo @@ -0,0 +1,47 @@ +//=================== Parameter names, chosen to match H3 ===================== +// Physical sizes / m +Rmax = 0.4; +length = 17; +// # cells in each dimension +nx = 6; // Radial resolution +ny = 3; // Parallel (on-axis) resolution +nz_target = 6; // Target Azimuthal resolution +// N.B. +// nz is only a target - the actual value used is 3*Floor(nz_target,3) +//============================================================================= + +// Create a line in the x-direction of length , with divisions +Point(1) = {0, 0, 0, 0.01}; +Point(2) = {Rmax, 0, 0, 0.01}; +Line(1) = {1, 2}; +Transfinite Line(1) = nx+1; + +// Extrude split line into meshed circle +// (Rotation extrusion can only cope with angles < pi, so has to be done in three parts...) +nz_over3 = Floor(nz_target/3); +Printf("Using nz = %g",3*nz_over3); +c1[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{1}; Layers{nz_over3}; Recombine; +}; + +c2[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{c1[0]}; Layers{nz_over3}; Recombine; +}; + +c3[] = Extrude {{0, 0, 1}, {0, 0, 0}, 2*Pi/3} { + Curve{c2[0]}; Layers{nz_over3}; Recombine; +}; + +// Extrude each 1/3 of a circle to 1/3 of a cylinder +v1[] = Extrude {0,0,length} {Surface{c1[1]}; Layers{ny}; Recombine;}; +v2[] = Extrude {0,0,length} {Surface{c2[1]}; Layers{ny}; Recombine;}; +v3[] = Extrude {0,0,length} {Surface{c3[1]}; Layers{ny}; Recombine;}; + +// Label volumes and surfaces (combining the 3 azimuthally-split sections in each case) +// Whole domain volume +Physical Volume(0) = {v1[1],v2[1],v3[1]}; +// Curved boundary surface +Physical Surface(1) = {v1[3],v2[3],v3[3]}; +// Two circular boundary surfaces +Physical Surface(2) = {c1[1],c2[1],c3[1]}; +Physical Surface(3) = {v1[0],v2[0],v3[0]}; \ No newline at end of file diff --git a/examples/H3LAPD/low_res/run_cmd_template.txt b/examples/H3LAPD/low_res/run_cmd_template.txt new file mode 100644 index 00000000..8466450b --- /dev/null +++ b/examples/H3LAPD/low_res/run_cmd_template.txt @@ -0,0 +1 @@ +mpirun -np lapd.xml low_res.xml \ No newline at end of file diff --git a/include/atomic_data_reader.hpp b/include/atomic_data_reader.hpp index 82dd5f7a..0b0766b7 100644 --- a/include/atomic_data_reader.hpp +++ b/include/atomic_data_reader.hpp @@ -24,14 +24,14 @@ class AtomicDataReader { AtomicDataReader() = delete; std::vector> get_data() { return m_data; } std::vector get_rates() { - NP::NESOASSERT(m_rate_idx >= 0 && m_rate_idx < m_data.size(), - "AtomicDataReader: data rate index isn't defined."); + NESOASSERT(m_rate_idx >= 0 && m_rate_idx < m_data.size(), + "AtomicDataReader: data rate index isn't defined."); return m_data[m_rate_idx]; } std::vector get_temps() { - NP::NESOASSERT(m_T_idx >= 0 && m_T_idx < m_data.size(), - "AtomicDataReader: data temperature index isn't defined."); + NESOASSERT(m_T_idx >= 0 && m_T_idx < m_data.size(), + "AtomicDataReader: data temperature index isn't defined."); return m_data[m_T_idx]; } diff --git a/include/interpolator.hpp b/include/interpolator.hpp index b5f23def..82a31749 100644 --- a/include/interpolator.hpp +++ b/include/interpolator.hpp @@ -30,8 +30,8 @@ class Interpolator { NP::SYCLTargetSharedPtr sycl_target) : m_sycl_target(sycl_target), m_x_data(x_data), m_y_data(y_data) { - NP::NESOASSERT(m_x_data.size() == m_y_data.size(), - "size of m_x_data vector doesn't equal m_y_data vector"); + NESOASSERT(m_x_data.size() == m_y_data.size(), + "size of m_x_data vector doesn't equal m_y_data vector"); }; Interpolator() = delete; diff --git a/include/nektar_interface/geometry_transport/geometry_transport_2d.hpp b/include/nektar_interface/geometry_transport/geometry_transport_2d.hpp index cb7dd82b..1f9c5a32 100644 --- a/include/nektar_interface/geometry_transport/geometry_transport_2d.hpp +++ b/include/nektar_interface/geometry_transport/geometry_transport_2d.hpp @@ -87,20 +87,18 @@ get_all_remote_geoms_2d(MPI_Comm comm, * @param[in] graph MeshGraph instance. * @param[in,out] std::map of Nektar++ Geometry2D pointers. */ -inline void get_all_elements_2d( +void get_all_elements_2d( Nektar::SpatialDomains::MeshGraphSharedPtr &graph, - std::map> &geoms) { - geoms.clear(); + std::map> &geoms); - for (auto &e : graph->GetAllTriGeoms()) { - geoms[e.first] = - std::dynamic_pointer_cast(e.second); - } - for (auto &e : graph->GetAllQuadGeoms()) { - geoms[e.first] = - std::dynamic_pointer_cast(e.second); - } -} +/** + * Get a local 2D geometry object from a Nektar++ MeshGraph + * + * @param graph Nektar++ MeshGraph to return geometry object from. + * @returns Local 2D geometry object. + */ +Geometry2DSharedPtr +get_element_2d(Nektar::SpatialDomains::MeshGraphSharedPtr &graph); /** * Add remote 2D objects to a map from geometry ids to shared pointers. diff --git a/include/nektar_interface/geometry_transport/geometry_transport_3d.hpp b/include/nektar_interface/geometry_transport/geometry_transport_3d.hpp index d5dc70d9..bfd3b279 100644 --- a/include/nektar_interface/geometry_transport/geometry_transport_3d.hpp +++ b/include/nektar_interface/geometry_transport/geometry_transport_3d.hpp @@ -30,6 +30,15 @@ void get_all_elements_3d( Nektar::SpatialDomains::MeshGraphSharedPtr &graph, std::map> &geoms); +/** + * Get a local 3D geometry object from a Nektar++ MeshGraph + * + * @param graph Nektar++ MeshGraph to return geometry object from. + * @returns Local 3D geometry object. + */ +Geometry3DSharedPtr +get_element_3d(Nektar::SpatialDomains::MeshGraphSharedPtr &graph); + /** * Categorise geometry types by shape, local or remote and X-map type. * diff --git a/include/nektar_interface/particle_mesh_interface.hpp b/include/nektar_interface/particle_mesh_interface.hpp index 5cfca503..00a02772 100644 --- a/include/nektar_interface/particle_mesh_interface.hpp +++ b/include/nektar_interface/particle_mesh_interface.hpp @@ -1012,55 +1012,54 @@ class ParticleMeshInterface : public HMesh { inline void get_point_in_subdomain(double *point) { auto graph = this->graph; - NESOASSERT(this->ndim == 2, "Expected 2 position components"); - - auto trigeoms = graph->GetAllTriGeoms(); - if (trigeoms.size() > 0) { - - auto tri = trigeoms.begin()->second; - auto v0 = tri->GetVertex(0); - auto v1 = tri->GetVertex(1); - auto v2 = tri->GetVertex(2); - - double mid[2]; - mid[0] = 0.5 * ((*v1)[0] - (*v0)[0]); - mid[1] = 0.5 * ((*v1)[1] - (*v0)[1]); - mid[0] += (*v0)[0]; - mid[1] += (*v0)[1]; - - point[0] = 0.5 * ((*v2)[0] - mid[0]); - point[1] = 0.5 * ((*v2)[1] - mid[1]); - point[0] += mid[0]; - point[1] += mid[1]; - - Array test(3); - test[0] = point[0]; - test[1] = point[1]; - test[2] = 0.0; - NESOASSERT(tri->ContainsPoint(test), - "Triangle should contain this point"); + NESOASSERT(this->ndim == 2 || this->ndim == 3, + "Expected 2 or 3 position components"); + // Find a local geometry object + GeometrySharedPtr geom; + if (this->ndim == 2) { + geom = std::dynamic_pointer_cast(get_element_2d(graph)); } else { - auto quadgeoms = graph->GetAllQuadGeoms(); - NESOASSERT(quadgeoms.size() > 0, - "could not find any 2D geometry objects"); - - auto quad = quadgeoms.begin()->second; - auto v0 = quad->GetVertex(0); - auto v2 = quad->GetVertex(2); + geom = std::dynamic_pointer_cast(get_element_3d(graph)); + } + NESOASSERT(geom != nullptr, "Geom pointer is null."); - Array mid(3); - mid[0] = 0.5 * ((*v2)[0] - (*v0)[0]); - mid[1] = 0.5 * ((*v2)[1] - (*v0)[1]); - mid[2] = 0.0; - mid[0] += (*v0)[0]; - mid[1] += (*v0)[1]; + // Get the average of the geoms vertices as a point in the domain + const int num_verts = geom->GetNumVerts(); + auto v0 = geom->GetVertex(0); + Array coords(3); + v0->GetCoords(coords); + for (int dimx = 0; dimx < this->ndim; dimx++) { + point[dimx] = coords[dimx]; + } + for (int vx = 1; vx < num_verts; vx++) { + auto v = geom->GetVertex(vx); + v->GetCoords(coords); + for (int dimx = 0; dimx < this->ndim; dimx++) { + point[dimx] += coords[dimx]; + } + } + for (int dimx = 0; dimx < this->ndim; dimx++) { + point[dimx] /= ((double)num_verts); + } - NESOASSERT(quad->ContainsPoint(mid), "Quad should contain this point"); + Array mid(3); + mid[2] = 0; + for (int dimx = 0; dimx < this->ndim; dimx++) { + mid[dimx] = point[dimx]; + } - point[0] = mid[0]; - point[1] = mid[1]; + // If somehow the average is not in the domain use the first vertex + if (!geom->ContainsPoint(mid)) { + v0->GetCoords(coords); + for (int dimx = 0; dimx < this->ndim; dimx++) { + const auto p = coords[dimx]; + point[dimx] = p; + mid[dimx] = p; + } } + + NESOASSERT(geom->ContainsPoint(mid), "Geom should contain this point"); }; }; diff --git a/neso-particles b/neso-particles index 47d91880..9c6b4626 160000 --- a/neso-particles +++ b/neso-particles @@ -1 +1 @@ -Subproject commit 47d91880b3047cacf608cd87e07e27af569afd7b +Subproject commit 9c6b4626645f6aaaca478e4798f2fdee5dd2675b diff --git a/scripts/geo_to_xml.sh b/scripts/geo_to_xml.sh new file mode 100755 index 00000000..999790c3 --- /dev/null +++ b/scripts/geo_to_xml.sh @@ -0,0 +1,204 @@ +#!/bin/env bash +# +# Script to convert a gmsh .geo file to a Nektar++ xml mesh. +# Requires gmsh and NekMesh. +# +# Usage: +# ./scripts/geo_to_xml.sh [path to .geo file] <-g gmsh_path> <-m NekMesh_path> <-o output_filename> <-x x_bndry_compIDs> <-y y_bndry_compIDs> <-z z_bndry_compIDs> +# +# If 'gmsh' or 'NekMesh' ('NekMesh-rg' also valid) aren't on your path, locations can be supplied with '-g' and '-m' respectively. +# Output is generated next to the input file being converted. +# By default, output_filename = input_filename[.geo => .xml]. Use '-o' to specify something else. +# If periodic BCs will be used, provide boundary composite IDs via the -x, -y and -z args; e.g. -x 1,2 -y 3,4 -z 5,6 +# +# Example: +# ./scripts/geo_to_xml.sh examples/H3LAPD/hw/cuboid_periodic_8x8x16.geo -o cuboid.xml --xbids 1,2 --ybids 3,4 --zbids 5,6 +# +# Converts .geo file, ensuring Nektar composites are correctly aligned for periodic BCs in x,y and z directions. Output to 'cuboid.xml'. + +#------------------------------------------------------------------------------ +# Helper functions +check_exec() { + local cmd="$1" + local flag="$2" + if ! command -v $cmd &> /dev/null + then + echo "[$cmd] doesn't seem to be a valid executable" + if [ -n "$flag" ]; then + echo "Override location with -$flag " + fi + exit + fi +} + +echo_usage() { + echo "Usage:" + echo " $0 [path_to_geo_file] <-g gmsh_path> <-m NekMesh_path> <-o output_filename> <-x x_bndry_compIDs> <-y y_bndry_compIDs> <-z z_bndry_compIDs>" + echo " If periodic BCs will be used, provide boundary composite IDs via the -x, -y and -z args; e.g. -x 1,2 -y 3,4 -z 5,6" +} + +parse_args() { + POSITIONAL_ARGS=() + while [[ $# -gt 0 ]]; do + case $1 in + -g|--gmsh) + gm_exec="$2" + shift 2 + ;; + -m|--nekmesh) + nm_exec="$2" + shift 2 + ;; + -n|--ndims) + ndims="$2" + case $ndims in + 1|2|3);; + *) + echo "Invalid number of dimensions [$ndims]; must be 1, 2 or 3" + exit 3 + ;; + esac + shift 2 + ;; + -o|--output) + output_fname="$2" + shift 2 + ;; + -x|--xbids) + xbids="$2" + shift 2 + ;; + -y|--ybids) + ybids="$2" + shift 2 + ;; + -z|--zbids) + zbids="$2" + shift 2 + ;; + -h|--help) + echo_usage + exit 8 + ;; + -*) + echo "Unknown option $1" + exit 2 + ;; + *) + # Save positional args in an array + POSITIONAL_ARGS+=("$1") + shift + ;; + esac + done + + # Restore and extract positional args + set -- "${POSITIONAL_ARGS[@]}" + if [ $# -lt 1 ]; then + echo_usage + exit 1 + fi + geo_path=$1 + msh_path="${geo_path%.geo}.msh" + if [ -n "$output_fname" ]; then + xml_path=$(dirname "$geo_path")/${output_fname%.xml}.xml + else + xml_path="${geo_path%.geo}.xml" + fi +} + +# Try release version of NekMesh first; fallback to RelWithDebInfo version +set_nm_default_exec() { + nm_exec="NekMesh" + if ! command -v $nm_exec &> /dev/null; then + nm_exec="NekMesh-rg" + if ! command -v $nm_exec &> /dev/null; then + # Neither version found; reset nm_exec and leave it to check_exec to inform the user + nm_exec="NekMesh" + fi + fi +} + +# if physical surface/composite IDs have been passed, assemble appropriate 'peralign' argument strings for NekMesh +set_peralign_opts() { + for dim in x y z; do + vn="${dim}bids" + if [ -n "${!vn}" ]; then + arr=(${!vn//,/ }) + if [ ${#arr[@]} -ne 2 ]; then + echo "Failed to parse boundary IDs string [${!vn}] for dimension ${dim}" + echo "Strings should be two integers separated by a comma, with no spaces " + exit 6 + fi + nm_args="$nm_args -m peralign:surf1=${arr[0]}:surf2=${arr[1]}:dir=${dim}:orient" + fi + done +} + +report_options() { + echo "Options:" + echo " path to .geo : $geo_path" + echo " output path : $xml_path" + echo " n dims : $ndims" + echo " gmsh exec : $gm_exec" + echo " NekMesh exec : $nm_exec" + echo "" +} +#------------------------------------------------------------------------------ + +# Default options +geo_path="Not set" +gm_exec="gmsh" +ndims="3" +set_nm_default_exec +nm_args="-v" +# Parse command line args and report resulting options +parse_args "$@" +report_options + +set_peralign_opts + +# Check gmsh, NekMesh can be found +check_exec "$gm_exec" "g" +check_exec "$nm_exec" "m" + +# Run gmsh +gmsh_cmd="$gm_exec -$ndims $geo_path" +echo "Running [$gmsh_cmd]" +gmsh_output=$($gmsh_cmd) +gmsh_ret_code=$? +if [ $gmsh_ret_code -ne 0 ] +then + echo "gmsh returned $gmsh_ret_code. Output was: " + echo "$gmsh_output" + exit 4 +fi +echo Done +echo + + + +# Remove any existing .xml file +\rm -f "$xml_path" + +# Run NekMesh +nm_cmd="$nm_exec $nm_args $msh_path $xml_path:xml:uncompress" +echo "Running [$nm_cmd]" +nm_output=$($nm_cmd) +nm_ret_code=$? +if [ $nm_ret_code -ne 0 ] +then + echo "NekMesh returned $nm_ret_code. Output was: " + echo "$nm_output" + exit 5 +fi +echo Done +echo + +# Remove intermediate .msh file +\rm -f "$msh_path" + +# Remove EXPANSIONS node from mesh xml +sed -i '//,/<\/EXPANSIONS>/d' "$xml_path" + +echo "Generated Nektar xml mesh at $xml_path" \ No newline at end of file diff --git a/scripts/run_eg.sh b/scripts/run_eg.sh index ae1592ad..5f65508c 100755 --- a/scripts/run_eg.sh +++ b/scripts/run_eg.sh @@ -43,7 +43,7 @@ parse_args() { while [[ $# -gt 0 ]]; do case $1 in -b|--build-dir) - build_dir="$2" + build_dir=$(realpath "$2") shift 2 ;; -n|--num_mpi) diff --git a/solvers/H3LAPD/CMakeLists.txt b/solvers/H3LAPD/CMakeLists.txt new file mode 100644 index 00000000..885f7750 --- /dev/null +++ b/solvers/H3LAPD/CMakeLists.txt @@ -0,0 +1,45 @@ +# Identify source files +set(H3LAPD_SRC_FILES + EquationSystems/DriftReducedSystem.cpp EquationSystems/HW2Din3DSystem.cpp + EquationSystems/LAPDSystem.cpp H3LAPD.cpp) + +# ============================== Object library =============================== +# Put solver specific source in an object library so that tests can use it +set(LIBRARY_NAME H3LAPD_ObjLib) +set(SOLVER_LIBS + ${SOLVER_LIBS} ${LIBRARY_NAME} + CACHE INTERNAL "") +add_library(${LIBRARY_NAME} OBJECT ${H3LAPD_SRC_FILES}) +target_compile_options(${LIBRARY_NAME} PRIVATE ${BUILD_TYPE_COMPILE_FLAGS}) +target_link_libraries(${LIBRARY_NAME} PRIVATE Nektar++::nektar++) +# Top-level include dir for nektar-interface headers +set(TOPLEVEL_INCLUDE_DIR "${INC_DIR}") +target_include_directories( + ${LIBRARY_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR} + ${NESO_PARTICLES_INCLUDE_PATH} ${TOPLEVEL_INCLUDE_DIR}) + +add_sycl_to_target(TARGET ${LIBRARY_NAME} SOURCES ${H3LAPD_SRC_FILES}) + +# =================================== Exec ==================================== +set(EXEC_TARGET_NAME H3LAPD) + +add_executable(${EXEC_TARGET_NAME} main.cpp $) + +# Compile options +target_compile_options(${EXEC_TARGET_NAME} PRIVATE ${BUILD_TYPE_COMPILE_FLAGS}) + +# Linker options, target libs +target_link_options(${EXEC_TARGET_NAME} PRIVATE ${BUILD_TYPE_LINK_FLAGS}) +target_link_libraries( + ${EXEC_TARGET_NAME} PRIVATE Nektar++::nektar++ ${NESO_PARTICLES_LIBRARIES} + ${NESO_LIBRARY_NAME}) + +add_sycl_to_target(TARGET ${EXEC_TARGET_NAME} SOURCES main.cpp) + +# Install location +install(TARGETS ${EXEC_TARGET_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) + +# Exec rpath +set_target_properties( + ${EXEC_TARGET_NAME} PROPERTIES INSTALL_RPATH ${INSTALL_RPATH} + ${NEKTAR++_LIBRARY_DIRS}) diff --git a/solvers/H3LAPD/Diagnostics/GrowthRatesRecorder.hpp b/solvers/H3LAPD/Diagnostics/GrowthRatesRecorder.hpp new file mode 100644 index 00000000..8f29b178 --- /dev/null +++ b/solvers/H3LAPD/Diagnostics/GrowthRatesRecorder.hpp @@ -0,0 +1,164 @@ +#ifndef H3LAPD_GROWTH_RATES_RECORDER_H +#define H3LAPD_GROWTH_RATES_RECORDER_H + +#include +#include +#include +#include +#include + +#include "../ParticleSystems/NeutralParticleSystem.hpp" +#include + +namespace LU = Nektar::LibUtilities; + +namespace NESO::Solvers::H3LAPD { +/** + * @brief Class to manage recording of energy and enstrophy growth rates + * for Hasegawa-Wakatani-based equation systems. + * + */ +template class GrowthRatesRecorder { +protected: + /// Pointer to number density field + std::shared_ptr m_n; + /// Pointer to electric potential field + std::shared_ptr m_phi; + /// Pointer to vorticity field + std::shared_ptr m_w; + + /// HW α constant + double m_alpha; + /// File handle for recording output + std::ofstream m_fh; + /// HW κ constant + double m_kappa; + /// Number of quad points associated with fields m_n, m_phi and m_w + int m_npts; + /// MPI rank + int m_rank; + /// Sets recording frequency (value of 0 disables recording) + int m_recording_step; + /// Pointer to session object + const LU::SessionReaderSharedPtr m_session; + +public: + GrowthRatesRecorder(const LU::SessionReaderSharedPtr session, + std::shared_ptr n, std::shared_ptr w, + std::shared_ptr phi, int npts, double alpha, + double kappa) + : m_session(session), m_n(n), m_w(w), m_phi(phi), m_alpha(alpha), + m_kappa(kappa), m_npts(npts) { + + // Store recording frequency for convenience + m_session->LoadParameter("growth_rates_recording_step", m_recording_step, + 0); + + // Store MPI rank for convenience + m_rank = m_session->GetComm()->GetRank(); + + // Write file header + if ((m_rank == 0) && (m_recording_step > 0)) { + m_fh.open("growth_rates.csv"); + m_fh << "step,E,W,dEdt_exp,dWdt_exp\n"; + } + }; + + ~GrowthRatesRecorder() { + // Close file on destruct + if ((m_rank == 0) && (m_recording_step > 0)) { + m_fh.close(); + } + } + + /** + * Calculate Energy = 0.5 ∫ (n^2+|∇ϕ|^2) dV + */ + inline double compute_energy() { + Array integrand(m_npts); + // First, set integrand = n^2 + Vmath::Vmul(m_npts, m_n->GetPhys(), 1, m_n->GetPhys(), 1, integrand, 1); + + // Compute phi derivs, square them and add to integrand + Array xderiv(m_npts), yderiv(m_npts), zderiv(m_npts); + m_phi->PhysDeriv(m_phi->GetPhys(), xderiv, yderiv, zderiv); + Vmath::Vvtvp(m_npts, xderiv, 1, xderiv, 1, integrand, 1, integrand, 1); + Vmath::Vvtvp(m_npts, yderiv, 1, yderiv, 1, integrand, 1, integrand, 1); + Vmath::Vvtvp(m_npts, zderiv, 1, zderiv, 1, integrand, 1, integrand, 1); + + // integrand *= 0.5 + Vmath::Smul(m_npts, 0.5, integrand, 1, integrand, 1); + return m_n->Integral(integrand); + } + + /** + * Calculate Enstrophy = 0.5 ∫ (n-w)^2 dV + */ + inline double compute_enstrophy() { + Array integrand(m_npts); + // Set integrand = n-w + Vmath::Vsub(m_npts, m_n->GetPhys(), 1, m_w->GetPhys(), 1, integrand, 1); + // Set integrand = (n-w)^2 + Vmath::Vmul(m_npts, integrand, 1, integrand, 1, integrand, 1); + // Set integrand = 0.5*(n-w)^2 + Vmath::Smul(m_npts, 0.5, integrand, 1, integrand, 1); + return m_n->Integral(integrand); + } + + /** + * Calculate Gamma_alpha = alpha ∫ (n-phi)^2 dV + */ + inline double compute_Gamma_a() { + Array integrand(m_npts); + // Set integrand = n - phi + Vmath::Vsub(m_npts, m_n->GetPhys(), 1, m_phi->GetPhys(), 1, integrand, 1); + // Set integrand = (n - phi)^2 + Vmath::Vmul(m_npts, integrand, 1, integrand, 1, integrand, 1); + // Set integrand = alpha*(n - phi)^2 + Vmath::Smul(m_npts, m_alpha, integrand, 1, integrand, 1); + return m_n->Integral(integrand); + } + + /** + * Calculate Gamma_n = -kappa ∫ n * dphi/dy dV + */ + inline double compute_Gamma_n() { + Array integrand(m_npts); + + // Set integrand = n * dphi/dy + m_phi->PhysDeriv(1, m_phi->GetPhys(), integrand); + Vmath::Vmul(m_npts, integrand, 1, m_n->GetPhys(), 1, integrand, 1); + + // Set integrand = -kappa * n * dphi/dy + Vmath::Smul(m_npts, -1 * m_kappa, integrand, 1, integrand, 1); + return m_n->Integral(integrand); + } + + /** + * Compute energy, enstrophy and gamma values and output to file + */ + inline void compute(int step) { + if (m_recording_step > 0) { + if (step % m_recording_step == 0) { + + const double energy = compute_energy(); + const double enstrophy = compute_enstrophy(); + const double Gamma_n = compute_Gamma_n(); + const double Gamma_a = compute_Gamma_a(); + + // Write values to file. In Debug, print to stdout too. + if (m_rank == 0) { + nprint(step, ",", energy, ",", enstrophy, ",", Gamma_n - Gamma_a, ",", + Gamma_n); + m_fh << step << "," << std::setprecision(9) << energy << "," + << enstrophy << "," << Gamma_n - Gamma_a << "," << Gamma_n + << "\n"; + } + } + } + } +}; + +} // namespace NESO::Solvers::H3LAPD + +#endif // H3LAPD_GROWTH_RATES_RECORDER_H \ No newline at end of file diff --git a/solvers/H3LAPD/Diagnostics/MassRecorder.hpp b/solvers/H3LAPD/Diagnostics/MassRecorder.hpp new file mode 100644 index 00000000..523ef2e2 --- /dev/null +++ b/solvers/H3LAPD/Diagnostics/MassRecorder.hpp @@ -0,0 +1,184 @@ +#ifndef H3LAPD_MASS_RECORDER_H +#define H3LAPD_MASS_RECORDER_H + +#include +#include +#include +#include +#include +#include + +#include "../ParticleSystems/NeutralParticleSystem.hpp" + +namespace LU = Nektar::LibUtilities; + +namespace NESO::Solvers::H3LAPD { +/** + * @brief Class to manage recording of masses in a coupled fluid-particle + * system. + */ +template class MassRecorder { +protected: + /// Buffer to store total particle weight + BufferDeviceHost m_dh_particle_total_weight; + /// File handle for recording output + std::ofstream m_fh; + /// Flag to track whether initial fluid mass has been computed + bool m_initial_fluid_mass_computed; + /// The initial fluid mass + double m_initial_mass_fluid; + /// Pointer to number density field + std::shared_ptr m_n; + /// Pointer to particle system + std::shared_ptr m_particle_sys; + /// MPI rank + int m_rank; + /// Sets recording frequency (value of 0 disables recording) + int m_recording_step; + /// Pointer to session object + const LU::SessionReaderSharedPtr m_session; + /// Pointer to sycl target + SYCLTargetSharedPtr m_sycl_target; + +public: + MassRecorder(const LU::SessionReaderSharedPtr session, + std::shared_ptr particle_sys, + std::shared_ptr n) + : m_session(session), m_particle_sys(particle_sys), m_n(n), + m_sycl_target(particle_sys->m_sycl_target), + m_dh_particle_total_weight(particle_sys->m_sycl_target, 1), + m_initial_fluid_mass_computed(false) { + + m_session->LoadParameter("mass_recording_step", m_recording_step, 0); + m_rank = m_sycl_target->comm_pair.rank_parent; + if ((m_rank == 0) && (m_recording_step > 0)) { + m_fh.open("mass_recording.csv"); + m_fh << "step,relative_error,mass_particles,mass_fluid\n"; + } + }; + + ~MassRecorder() { + if ((m_rank == 0) && (m_recording_step > 0)) { + m_fh.close(); + } + } + + /** + * Integrate the Nektar number density field and convert the result to SI + */ + inline double compute_fluid_mass() { + return m_n->Integral(m_n->GetPhys()) * m_particle_sys->m_n_to_SI; + } + + /** + * Compute and store the integral of the initial number density field. + */ + inline void compute_initial_fluid_mass() { + if (m_recording_step > 0) { + if (!m_initial_fluid_mass_computed) { + m_initial_mass_fluid = compute_fluid_mass(); + m_initial_fluid_mass_computed = true; + } + } + } + + /** + * Get the initial fluid mass + */ + inline double get_initial_mass() { + NESOASSERT(m_initial_fluid_mass_computed == true, + "initial fluid mass not computed"); + return m_initial_mass_fluid; + } + + /** + * Compute total mass (computational weight) in the particle system across all + * MPI tasks. + */ + inline double compute_particle_mass() { + auto particle_group = m_particle_sys->m_particle_group; + auto k_W = (*particle_group)[Sym("COMPUTATIONAL_WEIGHT")] + ->cell_dat.device_ptr(); + + m_dh_particle_total_weight.h_buffer.ptr[0] = 0.0; + m_dh_particle_total_weight.host_to_device(); + auto k_particle_weight = m_dh_particle_total_weight.d_buffer.ptr; + + const auto pl_iter_range = + particle_group->mpi_rank_dat->get_particle_loop_iter_range(); + const auto pl_stride = + particle_group->mpi_rank_dat->get_particle_loop_cell_stride(); + const auto pl_npart_cell = + particle_group->mpi_rank_dat->get_particle_loop_npart_cell(); + + m_sycl_target->queue + .submit([&](sycl::handler &cgh) { + cgh.parallel_for<>( + sycl::range<1>(pl_iter_range), [=](sycl::id<1> idx) { + NESO_PARTICLES_KERNEL_START + const INT cellx = NESO_PARTICLES_KERNEL_CELL; + const INT layerx = NESO_PARTICLES_KERNEL_LAYER; + + const double contrib = k_W[cellx][0][layerx]; + + sycl::atomic_ref + energy_atomic(k_particle_weight[0]); + energy_atomic.fetch_add(contrib); + + NESO_PARTICLES_KERNEL_END + }); + }) + .wait_and_throw(); + + m_dh_particle_total_weight.device_to_host(); + const double tmp_weight = m_dh_particle_total_weight.h_buffer.ptr[0]; + double total_particle_weight; + MPICHK(MPI_Allreduce(&tmp_weight, &total_particle_weight, 1, MPI_DOUBLE, + MPI_SUM, m_sycl_target->comm_pair.comm_parent)); + + return total_particle_weight; + } + + /** + * Compute the total mass that has been added to the particle system (ignoring + * subsequent changes to the computational weights) + */ + inline double compute_total_added_mass() { + // N.B. in this case, total_num_particles_added already accounts for all MPI + // ranks - no need for an Allreduce + double added_mass = ((double)m_particle_sys->m_total_num_particles_added) * + m_particle_sys->m_particle_init_weight; + return added_mass; + } + + /*** + * Compute the masses of the fluid and particle systems, and the fractional + * error in the total mass relative to that expected. Output to file and to + * stdout in Debug mode. + */ + inline void compute(int step) { + if (m_recording_step > 0) { + if (step % m_recording_step == 0) { + const double mass_particles = compute_particle_mass(); + const double mass_fluid = compute_fluid_mass(); + const double mass_total = mass_particles + mass_fluid; + const double mass_added = compute_total_added_mass(); + const double correct_total = mass_added + m_initial_mass_fluid; + + // Write values to file + if (m_rank == 0) { + nprint(step, ",", + abs(correct_total - mass_total) / abs(correct_total), ",", + mass_particles, ",", mass_fluid, ","); + m_fh << step << "," + << abs(correct_total - mass_total) / abs(correct_total) << "," + << mass_particles << "," << mass_fluid << "\n"; + } + } + } + }; +}; +} // namespace NESO::Solvers::H3LAPD + +#endif diff --git a/solvers/H3LAPD/EquationSystems/DriftReducedSystem.cpp b/solvers/H3LAPD/EquationSystems/DriftReducedSystem.cpp new file mode 100644 index 00000000..d409e723 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/DriftReducedSystem.cpp @@ -0,0 +1,606 @@ +#include +#include +#include + +#include "DriftReducedSystem.hpp" + +namespace NESO::Solvers::H3LAPD { +DriftReducedSystem::DriftReducedSystem( + const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph) + : UnsteadySystem(session, graph), AdvectionSystem(session, graph), + m_field_to_index(session->GetVariables()), + m_adv_vel_elec(graph->GetSpaceDimension()), + m_ExB_vel(graph->GetSpaceDimension()), m_E(graph->GetSpaceDimension()) { + // Construct particle system + m_particle_sys = std::make_shared(session, graph); +} + +/** + * @brief Compute advection terms and add them to an output array + * @details For each field listed in @p field_names copy field pointers from + * m_fields and physical values from @p in_arr to temporary arrays and create a + * temporary output array of the same size. Call Advect() on @p adv_obj passing + * the temporary arrays. Finally, loop over @p eqn_labels to determine which + * element(s) of @p out_arr to subtract the results from. + * + * N.B. The creation of temporary arrays is necessary to bypass restrictions in + * the Nektar advection API. + * + * @param field_names List of field names to compute advection terms for + * @param adv_obj Nektar advection object + * @param adv_vel Array of advection velocities (outer dim size = nfields) + * @param in_arr Physical values for *all* fields + * @param[out] out_arr RHS array (for *all* fields) + * @param time Simulation time + * @param eqn_labels List of field names identifying indices in out_arr to add + * the result to. Defaults to @p field_names + */ +void DriftReducedSystem::add_adv_terms( + std::vector field_names, const SU::AdvectionSharedPtr adv_obj, + const Array> &adv_vel, + const Array> &in_arr, + Array> &out_arr, const NekDouble time, + std::vector eqn_labels) { + + // Default is to add result of advecting field f to the RHS of df/dt equation + if (eqn_labels.empty()) { + eqn_labels = std::vector(field_names); + } else { + ASSERTL1(field_names.size() == eqn_labels.size(), + "add_adv_terms: Number of quantities being advected must match " + "the number of equation labels."); + } + + int nfields = field_names.size(); + int npts = GetNpoints(); + + /* Make temporary copies of target fields, in_arr vals and initialise a + * temporary output array + */ + Array tmp_fields(nfields); + Array> tmp_inarray(nfields); + Array> tmp_outarray(nfields); + for (auto ii = 0; ii < nfields; ii++) { + int idx = m_field_to_index.get_idx(field_names[ii]); + tmp_fields[ii] = m_fields[idx]; + tmp_inarray[ii] = Array(npts); + Vmath::Vcopy(npts, in_arr[idx], 1, tmp_inarray[ii], 1); + tmp_outarray[ii] = Array(out_arr[idx].size()); + } + // Compute advection terms; result is returned in temporary output array + adv_obj->Advect(tmp_fields.size(), tmp_fields, adv_vel, tmp_inarray, + tmp_outarray, time); + + // Subtract temporary output array from the appropriate indices of out_arr + for (auto ii = 0; ii < nfields; ii++) { + int idx = m_field_to_index.get_idx(eqn_labels[ii]); + Vmath::Vsub(out_arr[idx].size(), out_arr[idx], 1, tmp_outarray[ii], 1, + out_arr[idx], 1); + } +} + +/** + * @brief Add (density) source term via a Nektar session function. + * + * @details Looks for a function called "dens_src", evaluates it, and adds the + * result to @p out_arr + * + * @param[out] out_arr RHS array to add the source too + * @todo Check function exists, rather than relying on Nektar ASSERT + */ +void DriftReducedSystem::add_density_source( + Array> &out_arr) { + + int ne_idx = m_field_to_index.get_idx("ne"); + int npts = GetNpoints(); + Array tmpx(npts), tmpy(npts), tmpz(npts); + m_fields[ne_idx]->GetCoords(tmpx, tmpy, tmpz); + Array dens_src(npts, 0.0); + LU::EquationSharedPtr dens_src_func = + m_session->GetFunction("dens_src", ne_idx); + dens_src_func->Evaluate(tmpx, tmpy, tmpz, dens_src); + Vmath::Vadd(npts, out_arr[ne_idx], 1, dens_src, 1, out_arr[ne_idx], 1); +} + +/** + * @brief Adds particle sources. + * @details For each in @p target_fields , look for another field + * called _src. If it exists, add the physical values of + * field_name_src to the appropriate index of @p out_arr. + * + * @param target_fields list of Nektar field names for which to look for a + * '_src' counterpart + * @param[out] out_arr the RHS array + * + */ +void DriftReducedSystem::add_particle_sources( + std::vector target_fields, + Array> &out_arr) { + for (auto target_field : target_fields) { + int src_field_idx = m_field_to_index.get_idx(target_field + "_src"); + + if (src_field_idx >= 0) { + // Check that the target field is one that is time integrated + auto tmp_it = std::find(m_int_fld_names.cbegin(), m_int_fld_names.cend(), + target_field); + ASSERTL0(tmp_it != m_int_fld_names.cend(), + "Target for particle source ['" + target_field + + "'] does not appear in the list of time-integrated fields " + "(m_int_fld_names).") + /* + N.B. out_arr can be smaller than m_fields if any fields aren't + time-integrated, so can't just use out_arr_idx = + m_field_to_index.get_idx(target_field) + */ + auto out_arr_idx = std::distance(m_int_fld_names.cbegin(), tmp_it); + Vmath::Vadd(out_arr[out_arr_idx].size(), out_arr[out_arr_idx], 1, + m_fields[src_field_idx]->GetPhys(), 1, out_arr[out_arr_idx], + 1); + } + } +} + +/** + * @brief Compute E = \f$ -\nabla\phi\f$, \f$ v_{E\times B}\f$ and the + * advection velocities used in the ne/Ge, Gd equations. + * + * @param in_arr array of field phys vals + */ +void DriftReducedSystem::calc_E_and_adv_vels( + const Array> &in_arr) { + int phi_idx = m_field_to_index.get_idx("phi"); + int npts = GetNpoints(); + m_fields[phi_idx]->PhysDeriv(m_fields[phi_idx]->GetPhys(), m_E[0], m_E[1], + m_E[2]); + Vmath::Neg(npts, m_E[0], 1); + Vmath::Neg(npts, m_E[1], 1); + Vmath::Neg(npts, m_E[2], 1); + + // v_ExB = Evec x Bvec / B^2 + Vmath::Svtsvtp(npts, m_B[2] / m_Bmag / m_Bmag, m_E[1], 1, + -m_B[1] / m_Bmag / m_Bmag, m_E[2], 1, m_ExB_vel[0], 1); + Vmath::Svtsvtp(npts, m_B[0] / m_Bmag / m_Bmag, m_E[2], 1, + -m_B[2] / m_Bmag / m_Bmag, m_E[0], 1, m_ExB_vel[1], 1); + Vmath::Svtsvtp(npts, m_B[1] / m_Bmag / m_Bmag, m_E[0], 1, + -m_B[0] / m_Bmag / m_Bmag, m_E[1], 1, m_ExB_vel[2], 1); +} + +/** + * @brief Perform projection into correct polynomial space. + * + * @details This routine projects the @p in_arr input and ensures the @p + * out_arr output lives in the correct space. Since we are hard-coding DG, this + * corresponds to a simple copy from in to out, since no elemental + * connectivity is required and the output of the RHS function is + * polynomial. + * + * @param in_arr Unprojected values + * @param[out] out_arr Projected values + * @param time Current simulation time + * + */ +void DriftReducedSystem::do_ode_projection( + const Array> &in_arr, + Array> &out_arr, const NekDouble time) { + int num_vars = in_arr.size(); + int npoints = in_arr[0].size(); + + for (int i = 0; i < num_vars; ++i) { + Vmath::Vcopy(npoints, in_arr[i], 1, out_arr[i], 1); + } +} + +/** + * @brief Compute components of advection velocities normal to trace elements + * (faces, in 3D). + * + * @param[in,out] trace_vel_norm Trace normal velocities for each field + * @param adv_vel Advection velocities for each field + */ +Array &DriftReducedSystem::get_adv_vel_norm( + Array &trace_vel_norm, + const Array> &adv_vel) { + // Number of trace (interface) points + int num_trace_pts = GetTraceNpoints(); + // Auxiliary variable to compute normal velocities + Array tmp(num_trace_pts); + + // Zero previous values + Vmath::Zero(num_trace_pts, trace_vel_norm, 1); + + // Compute dot product of advection velocity with the trace normals and store + for (int i = 0; i < adv_vel.size(); ++i) { + m_fields[0]->ExtractTracePhys(adv_vel[i], tmp); + Vmath::Vvtvp(num_trace_pts, m_traceNormals[i], 1, tmp, 1, trace_vel_norm, 1, + trace_vel_norm, 1); + } + return trace_vel_norm; +} + +/** + * @brief Compute trace-normal advection velocities for the electron density. + */ +Array &DriftReducedSystem::get_adv_vel_norm_elec() { + return get_adv_vel_norm(m_norm_vel_elec, m_adv_vel_elec); +} + +/** + * @brief Compute trace-normal advection velocities for the vorticity equation. + */ +Array &DriftReducedSystem::get_adv_vel_norm_vort() { + return get_adv_vel_norm(m_norm_vel_vort, m_ExB_vel); +} + +/** + * @brief Construct flux array. + * + * @param field_vals Physical values for each advection field + * @param adv_vel Advection velocities for each advection field + * @param[out] flux Flux array + */ +void DriftReducedSystem::get_flux_vector( + const Array> &field_vals, + const Array> &adv_vel, + Array>> &flux) { + ASSERTL1(flux[0].size() == adv_vel.size(), + "Dimension of flux array and advection velocity array do not match"); + int npts = field_vals[0].size(); + + for (auto i = 0; i < flux.size(); ++i) { + for (auto j = 0; j < flux[0].size(); ++j) { + Vmath::Vmul(npts, field_vals[i], 1, adv_vel[j], 1, flux[i][j], 1); + } + } +} + +/** + * @brief Construct the flux vector for the diffusion problem. + * @todo not implemented + */ +void DriftReducedSystem::get_flux_vector_diff( + const Array> &in_arr, + const Array>> &q_field, + Array>> &viscous_tensor) { + std::cout << "*** GetFluxVectorDiff not defined! ***" << std::endl; +} + +/** + * @brief Compute the flux vector for advection in the electron density and + * momentum equations. + * + * @param field_vals Array of Fields ptrs + * @param[out] flux Resulting flux array + */ +void DriftReducedSystem::get_flux_vector_elec( + const Array> &field_vals, + Array>> &flux) { + get_flux_vector(field_vals, m_adv_vel_elec, flux); +} + +/** + * @brief Compute the flux vector for advection in the vorticity equation. + * + * @param field_vals Array of Fields ptrs + * @param[out] flux Resulting flux array + */ +void DriftReducedSystem::get_flux_vector_vort( + const Array> &field_vals, + Array>> &flux) { + // Advection velocity is v_ExB in the vorticity equation + get_flux_vector(field_vals, m_ExB_vel, flux); +} + +/** + * @brief Load all required session parameters into member variables. + */ +void DriftReducedSystem::load_params() { + // Type of advection to use -- in theory we also support flux reconstruction + // for quad-based meshes, or you can use a standard convective term if you + // were fully continuous in space. Default is DG. + m_session->LoadSolverInfo("AdvectionType", m_adv_type, "WeakDG"); + + // ***Assumes field aligned with z-axis*** + // Magnetic field strength. Fix B = [0, 0, Bxy] for now + m_B = std::vector(m_graph->GetSpaceDimension(), 0); + m_session->LoadParameter("Bxy", m_B[2], 0.1); + + // Coefficient factors for potential solve + m_session->LoadParameter("d00", m_d00, 1); + m_session->LoadParameter("d11", m_d11, 1); + m_session->LoadParameter("d22", m_d22, 1); + + // Factor to set density floor; default to 1e-5 (Hermes-3 default) + m_session->LoadParameter("n_floor_fac", m_n_floor_fac, 1e-5); + + // Reference number density + m_session->LoadParameter("nRef", m_n_ref, 1.0); + + // Type of Riemann solver to use. Default = "Upwind" + m_session->LoadSolverInfo("UpwindType", m_riemann_solver_type, "Upwind"); + + // Particle-related parameters + m_session->LoadParameter("num_particle_steps_per_fluid_step", + m_num_part_substeps, 1); + m_session->LoadParameter("particle_num_write_particle_steps", + m_num_write_particle_steps, 0); + m_part_timestep = m_timestep / m_num_part_substeps; +} + +/** + * @brief Utility function to print the size of a 1D Nektar array. + * @param arr Array to print the size of + * @param label Label to include in the output message + * @param all_tasks If true, print message on all tasks, else print only on task + * 0 (default=false) + */ +void DriftReducedSystem::print_arr_size(const Array &arr, + std::string label, bool all_tasks) { + if (m_session->GetComm()->TreatAsRankZero() || all_tasks) { + if (!label.empty()) { + std::cout << label << " "; + } + std::cout << "size = " << arr.size() << std::endl; + } +} + +/** + * @brief Utility function to print values in a 1D Nektar array. + * + * @param arr Nektar array from which to extract values + * @param num number of values to report + * @param stride stride between indices (first value has index 0) + * @param label label to use for the array when reporting values + * @param all_tasks flag to output the result on all tasks (default is just task + * 0) + */ +void DriftReducedSystem::print_arr_vals(const Array &arr, + int num, int stride, std::string label, + bool all_tasks) { + if (m_session->GetComm()->TreatAsRankZero() || all_tasks) { + if (!label.empty()) { + std::cout << "[" << label << "]" << std::endl; + } + int ii_max = std::min(static_cast(arr.size()), num * stride); + for (auto ii = 0; ii < ii_max; ii = ii + stride) { + std::cout << " " << std::setprecision(12) << arr[ii] << std::endl; + } + } +} + +/** + * @brief Calls HelmSolve to solve for the electric potential, given the + * right-hand-side returned by get_phi_solve_rhs + * + * @param in_arr Array of physical field values + */ +void DriftReducedSystem::solve_phi( + const Array> &in_arr) { + + // Field indices + int npts = GetNpoints(); + int phi_idx = m_field_to_index.get_idx("phi"); + + // Define rhs + Array rhs(npts); + get_phi_solve_rhs(in_arr, rhs); + + // Set up factors for electrostatic potential solve + StdRegions::ConstFactorMap factors; + // Helmholtz => Poisson (lambda = 0) + factors[StdRegions::eFactorLambda] = 0.0; + // Set coefficient factors + factors[StdRegions::eFactorCoeffD00] = m_d00; + factors[StdRegions::eFactorCoeffD11] = m_d11; + factors[StdRegions::eFactorCoeffD22] = m_d22; + + // Solve for phi. Output of this routine is in coefficient (spectral) + // space, so backwards transform to physical space since we'll need that + // for the advection step & computing drift velocity. + m_fields[phi_idx]->HelmSolve(rhs, m_fields[phi_idx]->UpdateCoeffs(), factors); + m_fields[phi_idx]->BwdTrans(m_fields[phi_idx]->GetCoeffs(), + m_fields[phi_idx]->UpdatePhys()); +} + +/** + * @brief Check required fields are all defined and have the same number of quad + * points + */ +void DriftReducedSystem::validate_fields() { + int npts_exp = GetNpoints(); + for (auto &fld_name : m_required_flds) { + int idx = m_field_to_index.get_idx(fld_name); + // Check field exists + ASSERTL0(idx >= 0, "Required field [" + fld_name + "] is not defined."); + // Check fields all have the same number of quad points + int npts = m_fields[idx]->GetNpoints(); + ASSERTL0(npts == npts_exp, + "Expecting " + std::to_string(npts_exp) + + " quad points, but field '" + fld_name + "' has " + + std::to_string(npts) + + ". Check NUMMODES is the same for all required fields."); + } +} + +/** + * @brief Post-construction class initialisation. + * + * @param create_field if true, create a new field object and add it to + * m_fields. Optional, defaults to true. + */ +void DriftReducedSystem::v_InitObject(bool create_field) { + // If particle-coupling is enabled, + if (this->m_particle_sys->m_num_particles > 0) { + m_required_flds.push_back("ne_src"); + } + + AdvectionSystem::v_InitObject(create_field); + + // Ensure that the session file defines all required variables and that they + // have the same order + validate_fields(); + + // Load parameters + load_params(); + + // Compute some properties derived from params + m_Bmag = std::sqrt(m_B[0] * m_B[0] + m_B[1] * m_B[1] + m_B[2] * m_B[2]); + m_b_unit = std::vector(m_graph->GetSpaceDimension()); + for (auto idim = 0; idim < m_b_unit.size(); idim++) { + m_b_unit[idim] = (m_Bmag > 0) ? m_B[idim] / m_Bmag : 0.0; + } + + // Tell UnsteadySystem to only integrate a subset of fields in time + // (Ignore fields that don't have a time derivative) + m_intVariables.resize(m_int_fld_names.size()); + for (auto ii = 0; ii < m_int_fld_names.size(); ii++) { + int var_idx = m_field_to_index.get_idx(m_int_fld_names[ii]); + ASSERTL0(var_idx >= 0, "Setting time integration vars - GetIntFieldNames() " + "returned an invalid field name."); + m_intVariables[ii] = var_idx; + } + + // Since we are starting from a setup where each field is defined to be a + // discontinuous field (and thus support DG), the first thing we do is to + // recreate the phi field so that it is continuous, in order to support the + // Poisson solve. Note that you can still perform a Poisson solve using a + // discontinuous field, which is done via the hybridisable discontinuous + // Galerkin (HDG) approach. + int phi_idx = m_field_to_index.get_idx("phi"); + m_fields[phi_idx] = MemoryManager::AllocateSharedPtr( + m_session, m_graph, m_session->GetVariable(phi_idx), true, true); + + // Create storage for advection velocities, parallel velocity difference,ExB + // drift velocity, E field + int npts = GetNpoints(); + for (int i = 0; i < m_graph->GetSpaceDimension(); ++i) { + m_adv_vel_elec[i] = Array(npts); + m_ExB_vel[i] = Array(npts); + m_E[i] = Array(npts); + } + // Create storage for electron parallel velocities + m_par_vel_elec = Array(npts); + + // Type of advection class to be used. By default, we only support the + // discontinuous projection, since this is the only approach we're + // considering for this solver. + ASSERTL0(m_projectionType == MR::eDiscontinuous, + "Unsupported projection type: only discontinuous" + " projection supported."); //// + + // Do not forwards transform initial condition. + m_homoInitialFwd = false; //// + + // Define the normal velocity fields. + // These are populated at each step (by reference) in calls to GetVnAdv() + if (m_fields[0]->GetTrace()) { + auto nTrace = GetTraceNpoints(); + m_norm_vel_elec = Array(nTrace); + m_norm_vel_vort = Array(nTrace); + } + + // Advection objects + // Need one per advection velocity + m_adv_elec = SU::GetAdvectionFactory().CreateInstance(m_adv_type, m_adv_type); + m_adv_vort = SU::GetAdvectionFactory().CreateInstance(m_adv_type, m_adv_type); + + // Set callback functions to compute flux vectors + m_adv_elec->SetFluxVector(&DriftReducedSystem::get_flux_vector_elec, this); + m_adv_vort->SetFluxVector(&DriftReducedSystem::get_flux_vector_vort, this); + + // Create Riemann solvers (one per advection object) and set normal velocity + // callback functions + m_riemann_elec = SU::GetRiemannSolverFactory().CreateInstance( + m_riemann_solver_type, m_session); + m_riemann_elec->SetScalar("Vn", &DriftReducedSystem::get_adv_vel_norm_elec, + this); + m_riemann_vort = SU::GetRiemannSolverFactory().CreateInstance( + m_riemann_solver_type, m_session); + m_riemann_vort->SetScalar("Vn", &DriftReducedSystem::get_adv_vel_norm_vort, + this); + + // Tell advection objects about the Riemann solvers and finish init + m_adv_elec->SetRiemannSolver(m_riemann_elec); + m_adv_elec->InitObject(m_session, m_fields); + m_adv_vort->SetRiemannSolver(m_riemann_vort); + m_adv_vort->InitObject(m_session, m_fields); + + // Bind projection function for time integration object + m_ode.DefineProjection(&DriftReducedSystem::do_ode_projection, this); + + ASSERTL0(m_explicitAdvection, + "This solver only supports explicit-in-time advection."); + + // Store DisContFieldSharedPtr casts of fields in a map, indexed by name, for + // use in particle project,evaluate operations + int idx = 0; + for (auto &field_name : m_session->GetVariables()) { + m_discont_fields[field_name] = + std::dynamic_pointer_cast(m_fields[idx]); + idx++; + } + + if (m_particle_sys->m_num_particles > 0) { + // Set up object to project onto density source field + int low_order_project; + m_session->LoadParameter("low_order_project", low_order_project, 0); + if (low_order_project) { + ASSERTL0( + m_discont_fields.count("ne_src_interp"), + "Intermediate, lower order interpolation field not found in config."); + m_particle_sys->setup_project(m_discont_fields["ne_src_interp"], + m_discont_fields["ne_src"]); + } else { + m_particle_sys->setup_project(m_discont_fields["ne_src"]); + } + } + + // Set up object to evaluate density field + m_particle_sys->setup_evaluate_ne(m_discont_fields["ne"]); +} + +/** + * @brief Override v_PostIntegrate to do particle output + * + * @param step Time step number + */ +bool DriftReducedSystem::v_PostIntegrate(int step) { + // Writes a step of the particle trajectory. + if (m_num_write_particle_steps > 0 && + (step % m_num_write_particle_steps) == 0) { + m_particle_sys->write(step); + m_particle_sys->write_source_fields(); + } + return AdvectionSystem::v_PostIntegrate(step); +} + +/** + * @brief Override v_PreIntegrate to do particle system integration, projection + * onto source terms. + * + * @param step Time step number + */ +bool DriftReducedSystem::v_PreIntegrate(int step) { + if (m_particle_sys->m_num_particles > 0) { + // Integrate the particle system to the requested time. + m_particle_sys->integrate(m_time + m_timestep, m_part_timestep); + // Project onto the source fields + m_particle_sys->project_source_terms(); + } + + return AdvectionSystem::v_PreIntegrate(step); +} + +/** + * @brief Convenience function to zero a Nektar Array of 1D Arrays. + * + * @param out_arr Array of 1D arrays to be zeroed + * + */ +void DriftReducedSystem::zero_out_array( + Array> &out_arr) { + for (auto ifld = 0; ifld < out_arr.size(); ifld++) { + Vmath::Zero(out_arr[ifld].size(), out_arr[ifld], 1); + } +} +} // namespace NESO::Solvers::H3LAPD diff --git a/solvers/H3LAPD/EquationSystems/DriftReducedSystem.hpp b/solvers/H3LAPD/EquationSystems/DriftReducedSystem.hpp new file mode 100644 index 00000000..8c8c64b0 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/DriftReducedSystem.hpp @@ -0,0 +1,174 @@ +#ifndef H3LAPD_DRIFT_REDUCED_SYSTEM_H +#define H3LAPD_DRIFT_REDUCED_SYSTEM_H + +#include "../ParticleSystems/NeutralParticleSystem.hpp" + +#include "nektar_interface/utilities.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace LU = Nektar::LibUtilities; +namespace MR = Nektar::MultiRegions; +namespace SD = Nektar::SpatialDomains; +namespace SU = Nektar::SolverUtils; + +namespace NESO::Solvers::H3LAPD { + +/** + * @brief Abstract base class for drift-reduced equation systems, including + * Hasegawa-Wakatani and LAPD. + * + */ +class DriftReducedSystem : virtual public SU::AdvectionSystem { +public: + friend class MemoryManager; + + /// Name of class + static std::string class_name; + + /// Free particle system memory on destruction + virtual ~DriftReducedSystem() { m_particle_sys->free(); } + +protected: + DriftReducedSystem(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph); + + /// Advection object used in the electron density equation + SU::AdvectionSharedPtr m_adv_elec; + /// Storage for ne advection velocities + Array> m_adv_vel_elec; + /// Advection type + std::string m_adv_type; + /// Advection object used in the vorticity equation + SU::AdvectionSharedPtr m_adv_vort; + /// Magnetic field vector + std::vector m_B; + /// Magnitude of the magnetic field + NekDouble m_Bmag; + /// Normalised magnetic field vector + std::vector m_b_unit; + /** Source fields cast to DisContFieldSharedPtr, indexed by name, for use in + * particle evaluation/projection methods + */ + std::map m_discont_fields; + /// Storage for physical values of the electric field + Array> m_E; + /// Storage for ExB drift velocity + Array> m_ExB_vel; + /// Field name => index mapper + NESO::NektarFieldIndexMap m_field_to_index; + /// Names of fields that will be time integrated + std::vector m_int_fld_names; + /// Factor used to set the density floor (n_floor = m_n_floor_fac * m_n_ref) + NekDouble m_n_floor_fac; + /// Reference number density + NekDouble m_n_ref; + /// Storage for electron parallel velocities + Array m_par_vel_elec; + /// Particles system + std::shared_ptr m_particle_sys; + /// List of field names required by the solver + std::vector m_required_flds; + /// Riemann solver type (used for all advection terms) + std::string m_riemann_solver_type; + + void add_adv_terms( + std::vector field_names, + const SU::AdvectionSharedPtr adv_obj, + const Array> &adv_vel, + const Array> &in_arr, + Array> &out_arr, const NekDouble time, + std::vector eqn_labels = std::vector()); + + void add_density_source(Array> &out_arr); + + void add_particle_sources(std::vector target_fields, + Array> &out_arr); + + virtual void + calc_E_and_adv_vels(const Array> &in_arr); + + virtual void + explicit_time_int(const Array> &in_arr, + Array> &out_arr, + const NekDouble time) = 0; + + Array & + get_adv_vel_norm(Array &trace_vel_norm, + const Array> &adv_vel); + + void get_flux_vector(const Array> &fields_vals, + const Array> &adv_vel, + Array>> &flux); + + virtual void + get_phi_solve_rhs(const Array> &in_arr, + Array &rhs) = 0; + virtual void load_params(); + void solve_phi(const Array> &in_arr); + + virtual void v_InitObject(bool DeclareField) override; + virtual bool v_PostIntegrate(int step) override; + virtual bool v_PreIntegrate(int step) override; + + void zero_out_array(Array> &out_arr); + + //--------------------------------------------------------------------------- + // Debugging + void print_arr_vals(const Array &arr, int num, + int stride = 1, std::string label = "", + bool all_tasks = false); + void print_arr_size(const Array &arr, std::string label = "", + bool all_tasks = false); + +private: + /// d00 coefficient for Helmsolve + NekDouble m_d00; + /// d11 coefficient for Helmsolve + NekDouble m_d11; + /// d22 coefficient for Helmsolve + NekDouble m_d22; + /// Storage for component of ne advection velocity normal to trace elements + Array m_norm_vel_elec; + /// Storage for component of w advection velocity normal to trace elements + Array m_norm_vel_vort; + /// Number of particle timesteps per fluid timestep. + int m_num_part_substeps; + /// Number of time steps between particle trajectory step writes. + int m_num_write_particle_steps; + /// Particle timestep size. + double m_part_timestep; + /// Riemann solver object used in electron advection + SU::RiemannSolverSharedPtr m_riemann_elec; + /// Riemann solver object used in vorticity advection + SU::RiemannSolverSharedPtr m_riemann_vort; + + void + do_ode_projection(const Array> &in_arr, + Array> &out_arr, + const NekDouble time); + Array &get_adv_vel_norm_elec(); + Array &get_adv_vel_norm_vort(); + + void get_flux_vector_diff( + const Array> &in_arr, + const Array>> &q_field, + Array>> &viscous_tensor); + void + get_flux_vector_elec(const Array> &fields_vals, + Array>> &flux); + void + get_flux_vector_vort(const Array> &fields_vals, + Array>> &flux); + + void validate_fields(); +}; + +} // namespace NESO::Solvers::H3LAPD +#endif // H3LAPD_DRIFT_REDUCED_SYSTEM_H diff --git a/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.cpp b/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.cpp new file mode 100644 index 00000000..b16de594 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.cpp @@ -0,0 +1,189 @@ +#include "HW2Din3DSystem.hpp" +#include "neso_particles.hpp" +#include +#include +#include + +namespace NESO::Solvers::H3LAPD { +std::string HW2Din3DSystem::class_name = + SU::GetEquationSystemFactory().RegisterCreatorFunction( + "2Din3DHW", HW2Din3DSystem::create, + "(2D) Hasegawa-Wakatani equation system as an intermediate step " + "towards the full H3-LAPD problem"); + +HW2Din3DSystem::HW2Din3DSystem(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph) + : UnsteadySystem(session, graph), AdvectionSystem(session, graph), + DriftReducedSystem(session, graph) { + m_required_flds = {"ne", "w", "phi"}; + m_int_fld_names = {"ne", "w"}; + + // Frequency of growth rate recording. Set zero to disable. + m_diag_growth_rates_recording_enabled = + session->DefinesParameter("growth_rates_recording_step"); + + // Frequency of mass recording. Set zero to disable. + m_diag_mass_recording_enabled = + session->DefinesParameter("mass_recording_step"); +} + +/** + * @brief Override DriftReducedSystem::calc_E_and_adv_vels in order to set + * electron advection veloctity in v_ExB + * + * @param in_arr array of field phys vals + */ +void HW2Din3DSystem::calc_E_and_adv_vels( + const Array> &in_arr) { + DriftReducedSystem::calc_E_and_adv_vels(in_arr); + int npts = GetNpoints(); + + Vmath::Zero(npts, m_par_vel_elec, 1); + // vAdv[iDim] = b[iDim]*v_par + v_ExB[iDim] for each species + for (auto iDim = 0; iDim < m_graph->GetSpaceDimension(); iDim++) { + Vmath::Svtvp(npts, m_b_unit[iDim], m_par_vel_elec, 1, m_ExB_vel[iDim], 1, + m_adv_vel_elec[iDim], 1); + } +} + +/** + * @brief Populate rhs array ( @p out_arr ) for explicit time integration of + * the 2D Hasegawa Wakatani equations. + * + * @param in_arr physical values of all fields + * @param[out] out_arr output array (RHSs of time integration equations) + */ +void HW2Din3DSystem::explicit_time_int( + const Array> &in_arr, + Array> &out_arr, const NekDouble time) { + + // Check in_arr for NaNs + for (auto &var : {"ne", "w"}) { + auto fidx = m_field_to_index.get_idx(var); + for (auto ii = 0; ii < in_arr[fidx].size(); ii++) { + std::stringstream err_msg; + err_msg << "Found NaN in field " << var; + NESOASSERT(std::isfinite(in_arr[fidx][ii]), err_msg.str().c_str()); + } + } + + zero_out_array(out_arr); + + // Solve for electrostatic potential + solve_phi(in_arr); + + // Calculate electric field from Phi, as well as corresponding drift velocity + calc_E_and_adv_vels(in_arr); + + // Get field indices + int npts = GetNpoints(); + int ne_idx = m_field_to_index.get_idx("ne"); + int phi_idx = m_field_to_index.get_idx("phi"); + int w_idx = m_field_to_index.get_idx("w"); + + // Advect ne and w (m_adv_vel_elec === m_ExB_vel for HW) + add_adv_terms({"ne"}, m_adv_elec, m_adv_vel_elec, in_arr, out_arr, time); + add_adv_terms({"w"}, m_adv_vort, m_ExB_vel, in_arr, out_arr, time); + + // Add \alpha*(\phi-n_e) to RHS + Array HWterm_2D_alpha(npts); + Vmath::Vsub(npts, m_fields[phi_idx]->GetPhys(), 1, + m_fields[ne_idx]->GetPhys(), 1, HWterm_2D_alpha, 1); + Vmath::Smul(npts, m_alpha, HWterm_2D_alpha, 1, HWterm_2D_alpha, 1); + Vmath::Vadd(npts, out_arr[w_idx], 1, HWterm_2D_alpha, 1, out_arr[w_idx], 1); + Vmath::Vadd(npts, out_arr[ne_idx], 1, HWterm_2D_alpha, 1, out_arr[ne_idx], 1); + + // Add \kappa*\dpartial\phi/\dpartial y to RHS + Array HWterm_2D_kappa(npts); + m_fields[phi_idx]->PhysDeriv(1, m_fields[phi_idx]->GetPhys(), + HWterm_2D_kappa); + Vmath::Smul(npts, m_kappa, HWterm_2D_kappa, 1, HWterm_2D_kappa, 1); + Vmath::Vsub(npts, out_arr[ne_idx], 1, HWterm_2D_kappa, 1, out_arr[ne_idx], 1); + + // Add particle sources + add_particle_sources({"ne"}, out_arr); +} + +/** + * @brief Choose phi solve RHS = w + * + * @param in_arr physical values of all fields + * @param[out] rhs RHS array to pass to Helmsolve + */ +void HW2Din3DSystem::get_phi_solve_rhs( + const Array> &in_arr, + Array &rhs) { + int npts = GetNpoints(); + int w_idx = m_field_to_index.get_idx("w"); + Vmath::Vcopy(npts, in_arr[w_idx], 1, rhs, 1); +} + +/** + * @brief Read base class params then extra params required for 2D-in-3D HW. + */ +void HW2Din3DSystem::load_params() { + DriftReducedSystem::load_params(); + + // alpha + m_session->LoadParameter("HW_alpha", m_alpha, 2); + + // kappa + m_session->LoadParameter("HW_kappa", m_kappa, 1); +} + +/** + * @brief Post-construction class-initialisation. + */ +void HW2Din3DSystem::v_InitObject(bool DeclareField) { + DriftReducedSystem::v_InitObject(DeclareField); + + // Bind RHS function for time integration object + m_ode.DefineOdeRhs(&HW2Din3DSystem::explicit_time_int, this); + + // Create diagnostic for recording growth rates + if (m_diag_growth_rates_recording_enabled) { + m_diag_growth_rates_recorder = + std::make_shared>( + m_session, m_discont_fields["ne"], m_discont_fields["w"], + m_discont_fields["phi"], GetNpoints(), m_alpha, m_kappa); + } + + // Create diagnostic for recording fluid and particles masses + if (m_diag_mass_recording_enabled) { + m_diag_mass_recorder = + std::make_shared>( + m_session, m_particle_sys, m_discont_fields["ne"]); + } +} + +/** + * @brief Compute diagnostics, if enabled, then call base class member func. + */ +bool HW2Din3DSystem::v_PostIntegrate(int step) { + if (m_diag_growth_rates_recording_enabled) { + m_diag_growth_rates_recorder->compute(step); + } + + if (m_diag_mass_recording_enabled) { + m_diag_mass_recorder->compute(step); + } + + m_solver_callback_handler.call_post_integrate(this); + return DriftReducedSystem::v_PostIntegrate(step); +} + +/** + * @brief Do initial set up for mass recording diagnostic (first call only), if + * enabled, then call base class member func. + */ +bool HW2Din3DSystem::v_PreIntegrate(int step) { + m_solver_callback_handler.call_pre_integrate(this); + + if (m_diag_mass_recording_enabled) { + m_diag_mass_recorder->compute_initial_fluid_mass(); + } + + return DriftReducedSystem::v_PreIntegrate(step); +} + +} // namespace NESO::Solvers::H3LAPD diff --git a/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.hpp b/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.hpp new file mode 100644 index 00000000..ee6097c6 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/HW2Din3DSystem.hpp @@ -0,0 +1,90 @@ +#ifndef H3LAPD_HW2DIN3D_SYSTEM_H +#define H3LAPD_HW2DIN3D_SYSTEM_H + +#include "nektar_interface/utilities.hpp" + +#include +#include +#include +#include +#include +#include + +#include "../Diagnostics/GrowthRatesRecorder.hpp" +#include "../Diagnostics/MassRecorder.hpp" +#include "DriftReducedSystem.hpp" + +namespace LU = Nektar::LibUtilities; +namespace MR = Nektar::MultiRegions; +namespace SD = Nektar::SpatialDomains; +namespace SU = Nektar::SolverUtils; + +namespace NESO::Solvers::H3LAPD { + +/** + * @brief 2D Hasegawa-Wakatani equation system designed to work in a 3D domain. + * @details Intended as an intermediate step towards the full LAPD equation + * system. Evolves ne, w, phi only, no momenta, no ions. + */ +class HW2Din3DSystem : virtual public DriftReducedSystem { +public: + friend class MemoryManager; + + /** + * @brief Creates an instance of this class. + */ + static SU::EquationSystemSharedPtr + create(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph) { + SU::EquationSystemSharedPtr p = + MemoryManager::AllocateSharedPtr(session, graph); + p->InitObject(); + return p; + } + + /// Name of class + static std::string class_name; + /// Object that allows optional recording of energy and enstrophy growth rates + std::shared_ptr> + m_diag_growth_rates_recorder; + /// Object that allows optional recording of total fluid, particle masses + std::shared_ptr> m_diag_mass_recorder; + /// Callback handler to call user defined callbacks. + SolverCallbackHandler m_solver_callback_handler; + +protected: + HW2Din3DSystem(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph); + + virtual void calc_E_and_adv_vels( + const Array> &inarray) override; + + void + explicit_time_int(const Array> &inarray, + Array> &outarray, + const NekDouble time) override; + + void + get_phi_solve_rhs(const Array> &inarray, + Array &rhs) override; + + void load_params() override; + + virtual void v_InitObject(bool DeclareField) override; + + virtual bool v_PostIntegrate(int step) override; + virtual bool v_PreIntegrate(int step) override; + +private: + /// Hasegawa-Wakatani α + NekDouble m_alpha; + /// Bool to enable/disable growth rate recordings + bool m_diag_growth_rates_recording_enabled; + /// Bool to enable/disable mass recordings + bool m_diag_mass_recording_enabled; + /// Hasegawa-Wakatani κ + NekDouble m_kappa; +}; + +} // namespace NESO::Solvers::H3LAPD +#endif // H3LAPD_HW2DIN3D_SYSTEM_H \ No newline at end of file diff --git a/solvers/H3LAPD/EquationSystems/LAPDSystem.cpp b/solvers/H3LAPD/EquationSystems/LAPDSystem.cpp new file mode 100644 index 00000000..26275d43 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/LAPDSystem.cpp @@ -0,0 +1,375 @@ +#include "LAPDSystem.hpp" +#include + +namespace NESO::Solvers::H3LAPD { +std::string LAPDSystem::class_name = + SU::GetEquationSystemFactory().RegisterCreatorFunction( + "LAPD", LAPDSystem::create, "LAPD equation system"); + +LAPDSystem::LAPDSystem(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph) + : UnsteadySystem(session, graph), AdvectionSystem(session, graph), + DriftReducedSystem(session, graph), + m_adv_vel_PD(graph->GetSpaceDimension()), + m_adv_vel_ions(graph->GetSpaceDimension()) { + m_required_flds = {"ne", "Ge", "Gd", "w", "phi"}; + m_int_fld_names = {"ne", "Ge", "Gd", "w"}; + // Construct particle system + m_particle_sys = std::make_shared(session, graph); +} + +/** + * @brief Add collision terms to an output array + * @param in_arr physical values of all fields + * @param[out] out_arr array to which collision terms should be added + */ +void LAPDSystem::add_collision_terms( + const Array> &in_arr, + Array> &out_arr) { + + int npts = in_arr[0].size(); + + // Field indices + int Gd_idx = m_field_to_index.get_idx("Gd"); + int Ge_idx = m_field_to_index.get_idx("Ge"); + int ne_idx = m_field_to_index.get_idx("ne"); + + /* + Calculate collision term + This is the momentum(-density) tranferred from electrons to ions by + collisions, so add it to Gd rhs, but subtract it from Ge rhs + */ + Array collision_freqs(npts), collision_term(npts), + vDiffne(npts); + Vmath::Vmul(npts, in_arr[ne_idx], 1, m_adv_vel_PD[2], 1, vDiffne, 1); + calc_collision_freqs(in_arr[ne_idx], collision_freqs); + for (auto ii = 0; ii < npts; ii++) { + collision_term[ii] = m_me * collision_freqs[ii] * vDiffne[ii]; + } + + // Subtract collision term from Ge rhs + Vmath::Vsub(npts, out_arr[Ge_idx], 1, collision_term, 1, out_arr[Ge_idx], 1); + + // Add collision term to Gd rhs + Vmath::Vadd(npts, out_arr[Gd_idx], 1, collision_term, 1, out_arr[Gd_idx], 1); +} + +/** + * @brief Add E_\par terms to an output array + * @param in_arr physical values of all fields + * @param[out] out_arr array to which terms should be added + */ +void LAPDSystem::add_E_par_terms( + const Array> &in_arr, + Array> &out_arr) { + + int npts = GetNpoints(); + + // Field indices + int ne_idx = m_field_to_index.get_idx("ne"); + int Ge_idx = m_field_to_index.get_idx("Ge"); + int Gd_idx = m_field_to_index.get_idx("Gd"); + + // Calculate EParTerm = e*n_e*EPar (=== e*n_d*EPar) + // ***Assumes field aligned with z-axis*** + Array E_Par_term(npts); + Vmath::Vmul(npts, in_arr[ne_idx], 1, m_E[2], 1, E_Par_term, 1); + Vmath::Smul(npts, m_charge_e, E_Par_term, 1, E_Par_term, 1); + + // Subtract E_Par_term from out_arr[Ge_idx] + Vmath::Vsub(npts, out_arr[Ge_idx], 1, E_Par_term, 1, out_arr[Ge_idx], 1); + + // Add E_Par_term to out_arr[Gd_idx] + Vmath::Vadd(npts, out_arr[Gd_idx], 1, E_Par_term, 1, out_arr[Gd_idx], 1); +} + +/** + * @brief Add \nabla P terms to an output array + * @param in_arr physical values of all fields + * @param[out] out_arr array to which terms should be added + */ +void LAPDSystem::add_grad_P_terms( + const Array> &in_arr, + Array> &out_arr) { + + int npts = in_arr[0].size(); + + // Field indices + int ne_idx = m_field_to_index.get_idx("ne"); + int Ge_idx = m_field_to_index.get_idx("Ge"); + int Gd_idx = m_field_to_index.get_idx("Gd"); + + // Subtract parallel pressure gradient for Electrons from out_arr[Ge_idx] + Array P_elec(npts), par_gradP_elec(npts); + Vmath::Smul(npts, m_Te, in_arr[ne_idx], 1, P_elec, 1); + // ***Assumes field aligned with z-axis*** + m_fields[ne_idx]->PhysDeriv(2, P_elec, par_gradP_elec); + Vmath::Vsub(npts, out_arr[Ge_idx], 1, par_gradP_elec, 1, out_arr[Ge_idx], 1); + + // Subtract parallel pressure gradient for Ions from out_arr[Ge_idx] + // N.B. ne === nd + Array P_ions(npts), par_GradP_ions(npts); + Vmath::Smul(npts, m_Td, in_arr[ne_idx], 1, P_ions, 1); + // ***Assumes field aligned with z-axis*** + m_fields[ne_idx]->PhysDeriv(2, P_ions, par_GradP_ions); + Vmath::Vsub(npts, out_arr[Gd_idx], 1, par_GradP_ions, 1, out_arr[Gd_idx], 1); +} + +/** + * @brief Calculate collision frequencies + * + * @param ne Array of electron densities + * @param[out][out] nu_ei Output array for collision frequencies + */ +void LAPDSystem::calc_collision_freqs(const Array &ne, + Array &nu_ei) { + Array log_lambda(ne.size()); + calc_coulomb_logarithm(ne, log_lambda); + for (auto ii = 0; ii < ne.size(); ii++) { + nu_ei[ii] = m_nu_ei_const * ne[ii] * log_lambda[ii]; + } +} + +/** + * @brief Calculate the Coulomb logarithm + * + * @param ne Array of electron densities + * @param[out] log_lambda Output array for Coulomb logarithm values + */ +void LAPDSystem::calc_coulomb_logarithm(const Array &ne, + Array &log_lambda) { + /* log_lambda = m_coulomb_log_const - 0.5\ln n_e + where: + m_coulomb_log_const = 30 − \ln Z_i +1.5\ln T_e + n_e in SI units + */ + for (auto ii = 0; ii < log_lambda.size(); ii++) { + log_lambda[ii] = m_coulomb_log_const - 0.5 * std::log(m_n_to_SI * ne[ii]); + } +} + +/** + * @brief Compute E = \f$ -\nabla\phi\f$, \f$ v_{E\times B}\f$ and the advection + * velocities used in the ne/Ge, Gd equations. + * + * @param in_arr physical values of all fields + */ +void LAPDSystem::calc_E_and_adv_vels( + const Array> &in_arr) { + DriftReducedSystem::calc_E_and_adv_vels(in_arr); + int npts = GetNpoints(); + + int ne_idx = m_field_to_index.get_idx("ne"); + int Gd_idx = m_field_to_index.get_idx("Gd"); + int Ge_idx = m_field_to_index.get_idx("Ge"); + + // v_par,d = Gd / max(ne,n_floor) / md (N.B. ne === nd) + for (auto ii = 0; ii < npts; ii++) { + m_par_vel_ions[ii] = in_arr[Gd_idx][ii] / + std::max(in_arr[ne_idx][ii], m_n_ref * m_n_floor_fac); + } + Vmath::Smul(npts, 1.0 / m_md, m_par_vel_ions, 1, m_par_vel_ions, 1); + + // v_par,e = Ge / max(ne,n_floor) / me + for (auto ii = 0; ii < npts; ii++) { + m_par_vel_elec[ii] = in_arr[Ge_idx][ii] / + std::max(in_arr[ne_idx][ii], m_n_ref * m_n_floor_fac); + } + Vmath::Smul(npts, 1.0 / m_me, m_par_vel_elec, 1, m_par_vel_elec, 1); + + /* + Store difference in parallel velocities in m_vAdvDiffPar + N.B. Outer dimension of storage has size ndim to allow it to be used in + advection operation later + */ + Vmath::Vsub(npts, m_par_vel_elec, 1, m_par_vel_ions, 1, m_adv_vel_PD[2], 1); + + // vAdv[iDim] = b[iDim]*v_par + v_ExB[iDim] for each species + for (auto iDim = 0; iDim < m_graph->GetSpaceDimension(); iDim++) { + Vmath::Svtvp(npts, m_b_unit[iDim], m_par_vel_elec, 1, m_ExB_vel[iDim], 1, + m_adv_vel_elec[iDim], 1); + Vmath::Svtvp(npts, m_b_unit[iDim], m_par_vel_ions, 1, m_ExB_vel[iDim], 1, + m_adv_vel_ions[iDim], 1); + } +} + +/** + * @brief Populate rhs array ( @p out_arr ) for explicit time integration of + * the LAPD equations. + * + * @param in_arr physical values of all fields + * @param[out] out_arr output array (RHSs of time integration equations) + */ +void LAPDSystem::explicit_time_int( + const Array> &in_arr, + Array> &out_arr, const NekDouble time) { + + // Zero out_arr + for (auto ifld = 0; ifld < out_arr.size(); ifld++) { + Vmath::Zero(out_arr[ifld].size(), out_arr[ifld], 1); + } + + // Solver for electrostatic potential. + solve_phi(in_arr); + + // Calculate electric field from Phi, as well as corresponding velocities for + // all advection operations + calc_E_and_adv_vels(in_arr); + + // Add advection terms to out_arr, handling (ne, Ge), Gd and w separately + add_adv_terms({"ne", "Ge"}, m_adv_elec, m_adv_vel_elec, in_arr, out_arr, + time); + add_adv_terms({"Gd"}, m_adv_ions, m_adv_vel_ions, in_arr, out_arr, time); + add_adv_terms({"w"}, m_adv_vort, m_ExB_vel, in_arr, out_arr, time); + + add_grad_P_terms(in_arr, out_arr); + + add_E_par_terms(in_arr, out_arr); + + // Add collision terms to RHS of Ge, Gd eqns + add_collision_terms(in_arr, out_arr); + // Add polarisation drift term to vorticity eqn RHS + add_adv_terms({"ne"}, m_adv_PD, m_adv_vel_PD, in_arr, out_arr, time, {"w"}); + + // Add density source via xml-defined function + add_density_source(out_arr); +} + +/** + * @brief Compute the normal advection velocities for the ion momentum equation + */ +Array &LAPDSystem::get_adv_vel_norm_ions() { + return get_adv_vel_norm(m_norm_vel_ions, m_adv_vel_ions); +} + +/** + * @brief Compute the normal advection velocities for the polarisation drift + * term. + */ +Array &LAPDSystem::get_adv_vel_norm_PD() { + return get_adv_vel_norm(m_norm_vel_PD, m_adv_vel_PD); +} + +/** + * @brief Compute the flux vector for advection in the ion momentum equation. + * + * @param field_vals physical values of all fields + * @param[out] flux Resulting flux array + */ +void LAPDSystem::get_flux_vector_ions( + const Array> &field_vals, + Array>> &flux) { + get_flux_vector(field_vals, m_adv_vel_ions, flux); +} + +/** + * @brief Compute the flux vector for the polarisation drift term. + * + * @param field_vals physical values of all fields + * @param[out] flux Resulting flux array + */ +void LAPDSystem::get_flux_vector_PD( + const Array> &field_vals, + Array>> &flux) { + get_flux_vector(field_vals, m_adv_vel_PD, flux); +} + +/** + * @brief Choose phi solve RHS = w * B^2 / (m_d * m_nRef) + * + * @param in_arr physical values of all fields + * @param[out] rhs RHS array to pass to Helmsolve + */ +void LAPDSystem::get_phi_solve_rhs( + const Array> &in_arr, + Array &rhs) { + + int npts = GetNpoints(); + int w_idx = m_field_to_index.get_idx("w"); + Vmath::Smul(npts, m_Bmag * m_Bmag / m_n_ref / m_md, in_arr[w_idx], 1, rhs, 1); +} + +/** + * @brief Read base class params then extra params required for LAPD. + */ +void LAPDSystem::load_params() { + DriftReducedSystem::load_params(); + + // Factor to convert densities back to SI; used in the Coulomb logarithm calc + m_session->LoadParameter("ns", m_n_to_SI, 1.0); + + // Charge + m_session->LoadParameter("e", m_charge_e, 1.0); + + // Ion mass + m_session->LoadParameter("md", m_md, 2.0); + + // Electron mass - default val is multiplied by 60 to improve convergence + m_session->LoadParameter("me", m_me, 60. / 1836); + + // Electron temperature in eV + m_session->LoadParameter("Te", m_Te, 5.0); + + // Ion temperature in eV + m_session->LoadParameter("Td", m_Td, 0.1); + + // Density independent part of the coulomb logarithm + m_session->LoadParameter("logLambda_const", m_coulomb_log_const); + + // Pre-factor used when calculating collision frequencies; read from config + m_session->LoadParameter("nu_ei_const", m_nu_ei_const); +} + +/** + * @brief Post-construction class-initialisation. + */ +void LAPDSystem::v_InitObject(bool DeclareField) { + DriftReducedSystem::v_InitObject(DeclareField); + // Create storage for advection velocities, parallel velocity difference, ExB + // drift velocity, E field + int npts = GetNpoints(); + for (int i = 0; i < m_graph->GetSpaceDimension(); ++i) { + m_adv_vel_ions[i] = Array(npts); + m_adv_vel_PD[i] = Array(npts); + Vmath::Zero(npts, m_adv_vel_PD[i], 1); + } + // Create storage for ion parallel velocities + m_par_vel_ions = Array(npts); + + // Define the normal velocity fields. + // These are populated at each step (by reference) in calls to + // get_adv_vel_norm() + if (m_fields[0]->GetTrace()) { + auto num_trace_pts = GetTraceNpoints(); + m_norm_vel_ions = Array(num_trace_pts); + m_norm_vel_PD = Array(num_trace_pts); + } + + // Advection objects + m_adv_ions = SU::GetAdvectionFactory().CreateInstance(m_adv_type, m_adv_type); + m_adv_PD = SU::GetAdvectionFactory().CreateInstance(m_adv_type, m_adv_type); + + // Set callback functions to compute flux vectors + m_adv_ions->SetFluxVector(&LAPDSystem::get_flux_vector_ions, this); + m_adv_PD->SetFluxVector(&LAPDSystem::get_flux_vector_PD, this); + + // Create Riemann solvers (one per advection object) and set normal velocity + // callback functions + m_riemann_ions = SU::GetRiemannSolverFactory().CreateInstance( + m_riemann_solver_type, m_session); + m_riemann_ions->SetScalar("Vn", &LAPDSystem::get_adv_vel_norm_ions, this); + m_riemann_PD = SU::GetRiemannSolverFactory().CreateInstance( + m_riemann_solver_type, m_session); + m_riemann_PD->SetScalar("Vn", &LAPDSystem::get_adv_vel_norm_PD, this); + + // Tell advection objects about the Riemann solvers and finish init + m_adv_ions->SetRiemannSolver(m_riemann_ions); + m_adv_ions->InitObject(m_session, m_fields); + m_adv_PD->InitObject(m_session, m_fields); + m_adv_PD->SetRiemannSolver(m_riemann_PD); + + // Bind RHS function for time integration object + m_ode.DefineOdeRhs(&LAPDSystem::explicit_time_int, this); +} + +} // namespace NESO::Solvers::H3LAPD diff --git a/solvers/H3LAPD/EquationSystems/LAPDSystem.hpp b/solvers/H3LAPD/EquationSystems/LAPDSystem.hpp new file mode 100644 index 00000000..4b691021 --- /dev/null +++ b/solvers/H3LAPD/EquationSystems/LAPDSystem.hpp @@ -0,0 +1,112 @@ + +#ifndef H3LAPD_LAPD_SYSTEM_H +#define H3LAPD_LAPD_SYSTEM_H + +#include "DriftReducedSystem.hpp" +#include +#include + +namespace LU = Nektar::LibUtilities; +namespace SD = Nektar::SpatialDomains; +namespace SU = Nektar::SolverUtils; + +namespace NESO::Solvers::H3LAPD { + +/** + * @brief Initial version of full LAPD equation system. + */ +class LAPDSystem : virtual public DriftReducedSystem { +public: + friend class MemoryManager; + + /** + * @brief Creates an instance of this class. + */ + static SU::EquationSystemSharedPtr + create(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph) { + SU::EquationSystemSharedPtr p = + MemoryManager::AllocateSharedPtr(session, graph); + p->InitObject(); + return p; + } + + /// Name of class + static std::string class_name; + +protected: + LAPDSystem(const LU::SessionReaderSharedPtr &session, + const SD::MeshGraphSharedPtr &graph); + virtual void + explicit_time_int(const Array> &in_arr, + Array> &out_arr, + const NekDouble time) override; + virtual void + get_phi_solve_rhs(const Array> &in_arr, + Array &rhs) override; + virtual void load_params() override; + virtual void v_InitObject(bool DeclareField) override; + +private: + /// Advection object used in the ion momentum equation + SU::AdvectionSharedPtr m_adv_ions; + /// Advection object used for polarisation drift advection + SU::AdvectionSharedPtr m_adv_PD; + /// Storage for ion advection velocities + Array> m_adv_vel_ions; + /** Storage for difference between elec, ion parallel velocities. Has size + ndim so that it can be used in advection operation */ + Array> m_adv_vel_PD; + /// Charge unit + NekDouble m_charge_e; + /// Density-independent part of the Coulomb logarithm; read from config + NekDouble m_coulomb_log_const; + /// Ion mass; + NekDouble m_md; + /// Electron mass; + NekDouble m_me; + /// Factor to convert densities (back) to SI; used in Coulomb logarithm calc + NekDouble m_n_to_SI; + /// Storage for component of Gd advection velocity normal to trace elements + Array m_norm_vel_ions; + /// Storage for component of polarisation drift velocity normal to trace + /// elements + Array m_norm_vel_PD; + /// Pre-factor used when calculating collision frequencies; read from config + NekDouble m_nu_ei_const; + /// Storage for ion parallel velocities + Array m_par_vel_ions; + /// Riemann solver object used in ion advection + SU::RiemannSolverSharedPtr m_riemann_ions; + /// Riemann solver object used in polarisation drift advection + SU::RiemannSolverSharedPtr m_riemann_PD; + /// Ion temperature in eV + NekDouble m_Td; + /// Electron temperature in eV + NekDouble m_Te; + + void + add_collision_terms(const Array> &in_arr, + Array> &out_arr); + void add_E_par_terms(const Array> &in_arr, + Array> &out_arr); + void add_grad_P_terms(const Array> &in_arr, + Array> &out_arr); + void calc_collision_freqs(const Array &ne, + Array &coeffs); + void calc_coulomb_logarithm(const Array &ne, + Array &LogLambda); + virtual void calc_E_and_adv_vels( + const Array> &in_arr) override; + Array &get_adv_vel_norm_ions(); + Array &get_adv_vel_norm_PD(); + void + get_flux_vector_ions(const Array> &physfield, + Array>> &flux); + void + get_flux_vector_PD(const Array> &physfield, + Array>> &flux); +}; + +} // namespace NESO::Solvers::H3LAPD +#endif // H3LAPD_LAPD_SYSTEM_H \ No newline at end of file diff --git a/solvers/H3LAPD/H3LAPD.cpp b/solvers/H3LAPD/H3LAPD.cpp new file mode 100644 index 00000000..95daa529 --- /dev/null +++ b/solvers/H3LAPD/H3LAPD.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// File: H3LAPD.cpp +// +// +// Description: Solver function for H3LAPD. +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "H3LAPD.hpp" + +namespace LU = Nektar::LibUtilities; +namespace SD = Nektar::SpatialDomains; +namespace SU = Nektar::SolverUtils; + +namespace NESO::Solvers::H3LAPD { + +int run_H3LAPD(int argc, char *argv[]) { + try { + // Create session reader. + auto session = LU::SessionReader::CreateInstance(argc, argv); + + // Read the mesh and create a MeshGraph object. + auto graph = SD::MeshGraph::Read(session); + + // Create driver. + std::string driverName; + session->LoadSolverInfo("Driver", driverName, "Standard"); + auto drv = + SU::GetDriverFactory().CreateInstance(driverName, session, graph); + + // Execute driver + drv->Execute(); + + // Finalise session + session->Finalise(); + } catch (const std::runtime_error &e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } catch (const std::string &eStr) { + std::cerr << "Error: " << eStr << std::endl; + return 2; + } + + return 0; +} + +} // namespace NESO::Solvers::H3LAPD diff --git a/solvers/H3LAPD/H3LAPD.hpp b/solvers/H3LAPD/H3LAPD.hpp new file mode 100644 index 00000000..73220056 --- /dev/null +++ b/solvers/H3LAPD/H3LAPD.hpp @@ -0,0 +1,18 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// File: H3LAPD.hpp +// +// +// Description: Header for the H3LAPD solver function. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef H3LAPD_H +#define H3LAPD_H +namespace NESO::Solvers::H3LAPD { + +int run_H3LAPD(int argc, char *argv[]); + +} // namespace NESO::Solvers::H3LAPD + +#endif \ No newline at end of file diff --git a/solvers/H3LAPD/ParticleSystems/NeutralParticleSystem.hpp b/solvers/H3LAPD/ParticleSystems/NeutralParticleSystem.hpp new file mode 100644 index 00000000..ee0fe118 --- /dev/null +++ b/solvers/H3LAPD/ParticleSystems/NeutralParticleSystem.hpp @@ -0,0 +1,744 @@ +#ifndef H3LAPD_NEUTRAL_PARTICLE_SYSTEM_H +#define H3LAPD_NEUTRAL_PARTICLE_SYSTEM_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace LU = Nektar::LibUtilities; +namespace NP = NESO::Particles; +namespace SD = Nektar::SpatialDomains; + +namespace NESO::Solvers::H3LAPD { + +// TODO move this to the correct place +/** + * @brief Evaluate the Barry et al approximation to the exponential integral + * function https://en.wikipedia.org/wiki/Exponential_integral E_1(x) + */ +inline double expint_barry_approx(const double x) { + constexpr double gamma_Euler_Mascheroni = 0.5772156649015329; + const double G = std::exp(-gamma_Euler_Mascheroni); + const double b = std::sqrt(2 * (1 - G) / G / (2 - G)); + const double h_inf = (1 - G) * (std::pow(G, 2) - 6 * G + 12) / + (3 * G * std::pow(2 - G, 2) * b); + const double q = 20.0 / 47.0 * std::pow(x, std::sqrt(31.0 / 26.0)); + const double h = 1 / (1 + x * std::sqrt(x)) + h_inf * q / (1 + q); + const double logfactor = + std::log(1 + G / x - (1 - G) / std::pow(h + b * x, 2)); + return std::exp(-x) / (G + (1 - G) * std::exp(-(x / (1 - G)))) * logfactor; +} + +/** + * @brief System of Neutral particles that can be coupled to equation systems + * inheriting from NESO::Solvers::H3LAPD::LAPDSystem. + */ +class NeutralParticleSystem { +public: + /** + * Create a new instance. + * + * @param session Nektar++ session to use for parameters and simulation + * specification. + * @param graph Nektar++ MeshGraph on which particles exist. + * @param comm (optional) MPI communicator to use - default MPI_COMM_WORLD. + * + */ + NeutralParticleSystem(LU::SessionReaderSharedPtr session, + SD::MeshGraphSharedPtr graph, + MPI_Comm comm = MPI_COMM_WORLD) + : m_session(session), m_graph(graph), m_comm(comm), + m_ndim(graph->GetSpaceDimension()), m_h5part_exists(false), + m_simulation_time(0.0), m_total_num_particles_added(0) { + + m_debug_write_fields_count = 0; + + // Set plasma temperature from session param + get_from_session(m_session, "Te_eV", m_TeV, 10.0); + // Set background density from session param + get_from_session(m_session, "n_bg_SI", m_n_bg_SI, 1e18); + + // Read the number of particles per cell / total number of particles + int tmp_int; + m_session->LoadParameter("num_particles_per_cell", tmp_int, -1); + m_num_particles_per_cell = tmp_int; + m_session->LoadParameter("num_particles_total", tmp_int, -1); + m_num_particles = tmp_int; + + if (m_num_particles > 0) { + if (m_num_particles_per_cell > 0) { + nprint("Ignoring value of 'num_particles_per_cell' because " + "'num_particles_total' was specified."); + m_num_particles_per_cell = -1; + } + } else { + if (m_num_particles_per_cell > 0) { + // Reduce the global number of elements + const int num_elements_local = m_graph->GetNumElements(); + int num_elements_global; + MPICHK(MPI_Allreduce(&num_elements_local, &num_elements_global, 1, + MPI_INT, MPI_SUM, m_comm)); + + // compute the global number of particles + m_num_particles = + ((int64_t)num_elements_global) * m_num_particles_per_cell; + } else { + nprint("Neutral particles disabled (Neither 'num_particles_total' or " + "'num_particles_per_cell' are set)"); + } + } + + // Create interface between particles and nektar++ + m_particle_mesh_interface = + std::make_shared(m_graph, 0, m_comm); + m_sycl_target = + std::make_shared(0, m_particle_mesh_interface->get_comm()); + m_nektar_graph_local_mapper = std::make_shared( + m_sycl_target, m_particle_mesh_interface); + m_domain = std::make_shared(m_particle_mesh_interface, + m_nektar_graph_local_mapper); + + // SI scaling factors required by ionise() + m_session->LoadParameter("n_to_SI", m_n_to_SI, 1e17); + m_session->LoadParameter("t_to_SI", m_t_to_SI, 1e-3); + + // Create ParticleGroup + ParticleSpec particle_spec{ + ParticleProp(Sym("POSITION"), 3, true), + ParticleProp(Sym("CELL_ID"), 1, true), + ParticleProp(Sym("PARTICLE_ID"), 1), + ParticleProp(Sym("COMPUTATIONAL_WEIGHT"), 1), + ParticleProp(Sym("SOURCE_DENSITY"), 1), + ParticleProp(Sym("ELECTRON_DENSITY"), 1), + ParticleProp(Sym("MASS"), 1), + ParticleProp(Sym("VELOCITY"), 3)}; + + m_particle_group = + std::make_shared(m_domain, particle_spec, m_sycl_target); + + m_particle_remover = std::make_shared(m_sycl_target); + + // Set up periodic boundary conditions. + m_periodic_bc = std::make_shared( + m_sycl_target, m_graph, m_particle_group->position_dat); + + // Set up map between cell indices + m_cell_id_translation = std::make_shared( + m_sycl_target, m_particle_group->cell_id_dat, + m_particle_mesh_interface); + + // Set properties that affect the behaviour of add_particles() + get_from_session(m_session, "particle_thermal_velocity", + m_particle_thermal_velocity, 1.0); + get_from_session(m_session, "particle_drift_velocity", + m_particle_drift_velocity, 0.0); + + // Set particle region = domain volume for now + double particle_region_volume = m_periodic_bc->global_extent[0]; + for (auto idim = 1; idim < m_ndim; idim++) { + particle_region_volume *= m_periodic_bc->global_extent[idim]; + } + + // read or deduce a number density from the configuration file + get_from_session(m_session, "particle_number_density", + m_particle_number_density, -1.0); + if (m_particle_number_density < 0.0) { + m_particle_init_weight = 1.0; + m_particle_number_density = m_num_particles / particle_region_volume; + } else { + const double num_phys_particles = + m_particle_number_density * particle_region_volume; + m_particle_init_weight = + (m_num_particles == 0) ? 0.0 : num_phys_particles / m_num_particles; + } + + // get seed from file + std::srand(std::time(nullptr)); + + get_from_session(m_session, "particle_position_seed", m_seed, std::rand()); + + const long rank = m_sycl_target->comm_pair.rank_parent; + m_rng_phasespace = std::mt19937(m_seed + rank); + }; + + /// Disable (implicit) copies. + NeutralParticleSystem(const NeutralParticleSystem &st) = delete; + /// Disable (implicit) copies. + NeutralParticleSystem &operator=(NeutralParticleSystem const &a) = delete; + + /// Factor to convert nektar density units to SI (required by ionisation calc) + double m_n_to_SI; + /// Global number of particles in the simulation. + int64_t m_num_particles; + /// NESO-Particles ParticleGroup containing charged particles. + ParticleGroupSharedPtr m_particle_group; + /// Initial particle weight. + double m_particle_init_weight; + /// Compute target. + SYCLTargetSharedPtr m_sycl_target; + /// Total number of particles added on this MPI rank. + uint64_t m_total_num_particles_added; + + /** + * Free the object before MPI_Finalize is called. + */ + inline void free() { + if (m_h5part_exists) { + m_h5part->close(); + } + m_particle_group->free(); + m_particle_mesh_interface->free(); + m_sycl_target->free(); + }; + + /** + * Integrate the particle system forward to the requested time using + * (at most) the requested time step. + * + * @param time_end Target time to integrate to. + * @param dt Time step size. + */ + inline void integrate(const double time_end, const double dt) { + + // Get the current simulation time. + NESOASSERT(time_end >= m_simulation_time, + "Cannot integrate backwards in time."); + if (time_end == m_simulation_time || m_num_particles == 0) { + return; + } + if (m_total_num_particles_added == 0) { + this->add_particles(1.0); + } + + double time_tmp = m_simulation_time; + while (time_tmp < time_end) { + const double dt_inner = std::min(dt, time_end - time_tmp); + this->forward_euler(dt_inner); + this->ionise(dt_inner); + time_tmp += dt_inner; + } + + m_simulation_time = time_end; + } + + /** + * Project particle source terms onto nektar fields. + */ + inline void project_source_terms() { + NESOASSERT(m_field_project != nullptr, + "Field project object is null. Was setup_project called?"); + + std::vector> syms = {Sym("SOURCE_DENSITY")}; + std::vector components = {0}; + m_field_project->project(syms, components); + if (m_low_order_project) { + FieldUtils::Interpolator interpolator{}; + std::vector in_exp = { + m_fields["ne_src_interp"]}; + std::vector out_exp = { + m_fields["ne_src"]}; + interpolator.Interpolate(in_exp, out_exp); + } + // remove fully ionised particles from the simulation + remove_marked_particles(); + } + + /** + * Set up the evaluation of a number density field. + * + * @param n Nektar++ field storing fluid number density. + */ + inline void setup_evaluate_ne(std::shared_ptr n) { + m_field_evaluate_ne = std::make_shared>( + n, m_particle_group, m_cell_id_translation); + m_fields["ne"] = n; + } + + /** + * Set up the projection object + * + * @param ne_src Nektar++ field to project particle source terms onto. + */ + inline void setup_project(std::shared_ptr ne_src) { + std::vector> fields = {ne_src}; + m_field_project = std::make_shared>( + fields, m_particle_group, m_cell_id_translation); + + // Add to local map + m_fields["ne_src"] = ne_src; + m_low_order_project = false; + } + + /** + * Alternative projection set up which first projects number density onto + * @p ne_src_interp then interpolates onto @p ne_src. + * + * @param ne_src_interp Nektar++ field to project particle source terms onto. + * @param ne_src Nektar++ field to interpolate the projected source terms + * onto. + */ + inline void setup_project(std::shared_ptr ne_src_interp, + std::shared_ptr ne_src) { + std::vector> fields = {ne_src_interp}; + m_field_project = std::make_shared>( + fields, m_particle_group, m_cell_id_translation); + + // Add to local map + m_fields["ne_src_interp"] = ne_src_interp; + m_fields["ne_src"] = ne_src; + m_low_order_project = true; + } + + /** + * Write current particle state to trajectory output file. + * + * @param step Time step number. + */ + inline void write(const int step) { + + if (m_sycl_target->comm_pair.rank_parent == 0) { + nprint("Writing particle trajectories at step", step); + } + + if (!m_h5part_exists) { + // Create instance to write particle data to h5 file + m_h5part = std::make_shared( + "particle_trajectory.h5part", m_particle_group, Sym("POSITION"), + Sym("CELL_ID"), Sym("COMPUTATIONAL_WEIGHT"), + Sym("VELOCITY"), Sym("PARTICLE_ID")); + m_h5part_exists = true; + } + + m_h5part->write(); + } + + /** + * Write the projection fields to vtu for debugging. + */ + inline void write_source_fields() { + for (auto entry : m_fields) { + std::string filename = "debug_" + entry.first + "_" + + std::to_string(m_debug_write_fields_count++) + + ".vtu"; + write_vtu(entry.second, filename, entry.first); + } + } + +protected: + /// Object used to map to/from nektar geometry ids to 0,N-1 + std::shared_ptr m_cell_id_translation; + /// NESO-Particles domain. + DomainSharedPtr m_domain; + /// Trajectory writer for particles + std::shared_ptr m_h5part; + /// Assumed background density in SI units, read from session + double m_n_bg_SI; + /// Mapping instance to map particles into nektar++ elements. + std::shared_ptr m_nektar_graph_local_mapper; + /// Average number of particles per cell (element) in the simulation. + int64_t m_num_particles_per_cell; + /// Particle drift velocity + double m_particle_drift_velocity; + /// Initial particle velocity. + double m_particle_init_vel; + /// Mass of particles + const double m_particle_mass = 1.0; + /// HMesh instance that allows particles to move over nektar++ meshes. + ParticleMeshInterfaceSharedPtr m_particle_mesh_interface; + /// Number density in simulation domain + double m_particle_number_density; + /// PARTICLE_ID value used to flag particles for removal from the simulation + const int m_particle_remove_key = -1; + /// Particle thermal velocity + double m_particle_thermal_velocity; + /// Object used to apply particle boundary conditions + std::shared_ptr m_periodic_bc; + // Random seed used in particle initialisation + int m_seed; + /// Factor to convert nektar time units to SI (required by ionisation calc) + double m_t_to_SI; + /// Temperature assumed for ionisation rate, read from session + double m_TeV; + + /// MPI communicator + MPI_Comm m_comm; + /// Counter used to name debugging output files + int m_debug_write_fields_count; + /// Object used to evaluate Nektar number density field + std::shared_ptr> m_field_evaluate_ne; + /// Object used to project onto Nektar number density field + std::shared_ptr> m_field_project; + /// Map of pointers to Nektar fields coupled via evaluation and/or projection + std::map> m_fields; + /// Pointer to Nektar Meshgraph object + SD::MeshGraphSharedPtr m_graph; + /// Variable to track existence of output data file + bool m_h5part_exists; + /// Variable to toggle use of low order projection + bool m_low_order_project; + /// Number of spatial dimensions being used + const int m_ndim; + /// Object to handle particle removal + std::shared_ptr m_particle_remover; + /// Random number generator + std::mt19937 m_rng_phasespace; + /// Pointer to Session object + LU::SessionReaderSharedPtr m_session; + /// Simulation time + double m_simulation_time; + + /** + * Add particles to the simulation. + * + * @param add_proportion Specifies the proportion of the number of particles + * added in a time step. + */ + inline void add_particles(const double add_proportion) { + long rstart, rend; + const long size = m_sycl_target->comm_pair.size_parent; + const long rank = m_sycl_target->comm_pair.rank_parent; + + const long num_particles_to_add = + std::round(add_proportion * ((double)m_num_particles)); + + get_decomp_1d(size, num_particles_to_add, rank, &rstart, &rend); + const long N = rend - rstart; + const int cell_count = m_domain->mesh->get_cell_count(); + + if (N > 0) { + // Generate N particles + ParticleSet initial_distribution(N, + m_particle_group->get_particle_spec()); + + // Generate particle positions and velocities + std::vector> positions, velocities; + + // Positions are Gaussian, centred at origin, same width in all dims + double mu = 0.0; + double sigma; + get_from_session(m_session, "particle_source_width", sigma, 0.5); + positions = NESO::Particles::normal_distribution(N, m_ndim, mu, sigma, + m_rng_phasespace); + // Centre of distribution + std::vector offsets = { + 0.0, 0.0, + (m_periodic_bc->global_extent[2] - m_periodic_bc->global_origin[2]) / + 2}; + + velocities = NESO::Particles::normal_distribution( + N, m_ndim, m_particle_drift_velocity, m_particle_thermal_velocity, + m_rng_phasespace); + + // Set positions, velocities + for (int ipart = 0; ipart < N; ipart++) { + for (int idim = 0; idim < m_ndim; idim++) { + initial_distribution[Sym("POSITION")][ipart][idim] = + positions[idim][ipart] + offsets[idim]; + initial_distribution[Sym("VELOCITY")][ipart][idim] = + velocities[idim][ipart]; + } + } + + // Set remaining properties + for (int ipart = 0; ipart < N; ipart++) { + initial_distribution[Sym("CELL_ID")][ipart][0] = + ipart % cell_count; + initial_distribution[Sym("COMPUTATIONAL_WEIGHT")][ipart][0] = + m_particle_init_weight; + initial_distribution[Sym("MASS")][ipart][0] = m_particle_mass; + initial_distribution[Sym("PARTICLE_ID")][ipart][0] = + ipart + rstart + m_total_num_particles_added; + } + m_particle_group->add_particles_local(initial_distribution); + } + m_total_num_particles_added += num_particles_to_add; + + parallel_advection_initialisation(m_particle_group); + parallel_advection_store(m_particle_group); + + const int num_steps = 20; + for (int stepx = 0; stepx < num_steps; stepx++) { + parallel_advection_step(m_particle_group, num_steps, stepx); + this->transfer_particles(); + } + parallel_advection_restore(m_particle_group); + + // Move particles to the owning ranks and correct cells. + this->transfer_particles(); + } + + /** + * Apply the boundary conditions to the particle system. + */ + inline void boundary_conditions() { + NESOASSERT(this->is_fully_periodic(), + "NeutralParticleSystem: Only fully periodic BCs are supported."); + m_periodic_bc->execute(); + } + + /** + * Evaluate fields at the particle locations. + */ + inline void evaluate_fields() { + + NESOASSERT(m_field_evaluate_ne != nullptr, + "FieldEvaluate object is null. Was setup_evaluate_ne called?"); + + m_field_evaluate_ne->evaluate(Sym("ELECTRON_DENSITY")); + + // Particle property to update + auto k_n = (*m_particle_group)[Sym("ELECTRON_DENSITY")] + ->cell_dat.device_ptr(); + + auto k_n_bg_SI = m_n_bg_SI; + + // Unit conversion factors + double k_n_to_SI = m_n_to_SI; + + const auto pl_iter_range = + m_particle_group->mpi_rank_dat->get_particle_loop_iter_range(); + const auto pl_stride = + m_particle_group->mpi_rank_dat->get_particle_loop_cell_stride(); + const auto pl_npart_cell = + m_particle_group->mpi_rank_dat->get_particle_loop_npart_cell(); + m_sycl_target->queue + .submit([&](sycl::handler &cgh) { + cgh.parallel_for<>( + sycl::range<1>(pl_iter_range), [=](sycl::id<1> idx) { + NESO_PARTICLES_KERNEL_START + const INT cellx = NESO_PARTICLES_KERNEL_CELL; + const INT layerx = NESO_PARTICLES_KERNEL_LAYER; + + k_n[cellx][0][layerx] = + k_n_bg_SI + k_n[cellx][0][layerx] * k_n_to_SI; + NESO_PARTICLES_KERNEL_END + }); + }) + .wait_and_throw(); + } + + /** + * Apply Forward-Euler, which with no forces is trivial. + * + * @param dt Time step size. + */ + inline void forward_euler(const double dt) { + + const double k_dt = dt; + + auto t0 = profile_timestamp(); + + auto k_P = + (*m_particle_group)[Sym("POSITION")]->cell_dat.device_ptr(); + auto k_V = + (*m_particle_group)[Sym("VELOCITY")]->cell_dat.device_ptr(); + + const auto pl_iter_range = + m_particle_group->mpi_rank_dat->get_particle_loop_iter_range(); + const auto pl_stride = + m_particle_group->mpi_rank_dat->get_particle_loop_cell_stride(); + const auto pl_npart_cell = + m_particle_group->mpi_rank_dat->get_particle_loop_npart_cell(); + + m_sycl_target->profile_map.inc("NeutralParticleSystem", + "ForwardEuler_Prepare", 1, + profile_elapsed(t0, profile_timestamp())); + + m_sycl_target->queue + .submit([&](sycl::handler &cgh) { + cgh.parallel_for<>( + sycl::range<1>(pl_iter_range), [=](sycl::id<1> idx) { + NESO_PARTICLES_KERNEL_START + const INT cellx = NESO_PARTICLES_KERNEL_CELL; + const INT layerx = NESO_PARTICLES_KERNEL_LAYER; + + k_P[cellx][0][layerx] += k_dt * k_V[cellx][0][layerx]; + k_P[cellx][1][layerx] += k_dt * k_V[cellx][1][layerx]; + k_P[cellx][2][layerx] += k_dt * k_V[cellx][2][layerx]; + + NESO_PARTICLES_KERNEL_END + }); + }) + .wait_and_throw(); + m_sycl_target->profile_map.inc("NeutralParticleSystem", + "ForwardEuler_Execute", 1, + profile_elapsed(t0, profile_timestamp())); + + // positions were written so we apply boundary conditions and move + // particles between ranks + this->transfer_particles(); + } + + /** + * Helper function to get values from the session file. + * + * @param session Session object. + * @param name Name of the parameter. + * @param[out] output Reference to the output variable. + * @param default_value Default value if name not found in the session file. + */ + template + inline void get_from_session(LU::SessionReaderSharedPtr session, + std::string name, T &output, T default_value) { + if (session->DefinesParameter(name)) { + session->LoadParameter(name, output); + } else { + output = default_value; + } + } + + /** + * Apply ionisation + * + * @param dt Time step size. + */ + inline void ionise(const double dt) { + + // Evaluate the density and temperature fields at the particle locations + this->evaluate_fields(); + + const double k_dt = dt; + const double k_dt_SI = dt * m_t_to_SI; + const double k_n_scale = 1 / m_n_to_SI; + + const double k_a_i = 4.0e-14; // a_i constant for hydrogen (a_1) + const double k_b_i = 0.6; // b_i constant for hydrogen (b_1) + const double k_c_i = 0.56; // c_i constant for hydrogen (c_1) + const double k_E_i = + 13.6; // E_i binding energy for most bound electron in hydrogen (E_1) + const double k_q_i = 1.0; // Number of electrons in inner shell for hydrogen + const double k_b_i_expc_i = + k_b_i * std::exp(k_c_i); // exp(c_i) constant for hydrogen (c_1) + + const double k_rate_factor = + -k_q_i * 6.7e7 * k_a_i * 1e-6; // 1e-6 to go from cm^3 to m^3 + + const INT k_remove_key = m_particle_remove_key; + + auto t0 = profile_timestamp(); + + auto k_ID = + (*m_particle_group)[Sym("PARTICLE_ID")]->cell_dat.device_ptr(); + auto k_n = (*m_particle_group)[Sym("ELECTRON_DENSITY")] + ->cell_dat.device_ptr(); + auto k_SD = + (*m_particle_group)[Sym("SOURCE_DENSITY")]->cell_dat.device_ptr(); + + auto k_V = + (*m_particle_group)[Sym("VELOCITY")]->cell_dat.device_ptr(); + auto k_W = (*m_particle_group)[Sym("COMPUTATIONAL_WEIGHT")] + ->cell_dat.device_ptr(); + + const auto pl_iter_range = + m_particle_group->mpi_rank_dat->get_particle_loop_iter_range(); + const auto pl_stride = + m_particle_group->mpi_rank_dat->get_particle_loop_cell_stride(); + const auto pl_npart_cell = + m_particle_group->mpi_rank_dat->get_particle_loop_npart_cell(); + + m_sycl_target->profile_map.inc("NeutralParticleSystem", + "Ionisation_Prepare", 1, + profile_elapsed(t0, profile_timestamp())); + + const REAL invratio = k_E_i / m_TeV; + const REAL rate = -k_rate_factor / (m_TeV * std::sqrt(m_TeV)) * + (expint_barry_approx(invratio) / invratio + + (k_b_i_expc_i / (invratio + k_c_i)) * + expint_barry_approx(invratio + k_c_i)); + + m_sycl_target->queue + .submit([&](sycl::handler &cgh) { + cgh.parallel_for<>( + sycl::range<1>(pl_iter_range), [=](sycl::id<1> idx) { + NESO_PARTICLES_KERNEL_START + const INT cellx = NESO_PARTICLES_KERNEL_CELL; + const INT layerx = NESO_PARTICLES_KERNEL_LAYER; + const REAL n_SI = k_n[cellx][0][layerx]; + + const REAL weight = k_W[cellx][0][layerx]; + // note that the rate will be a positive number, so minus sign + // here + REAL deltaweight = -rate * weight * k_dt_SI * n_SI; + + /* Check whether weight is about to drop below zero. + If so, flag particle for removal and adjust deltaweight. + These particles are removed after the project call. + */ + if ((weight + deltaweight) <= 0) { + k_ID[cellx][0][layerx] = k_remove_key; + deltaweight = -weight; + } + + // Mutate the weight on the particle + k_W[cellx][0][layerx] += deltaweight; + // Set value for fluid density source (num / Nektar unit time) + k_SD[cellx][0][layerx] = -deltaweight * k_n_scale / k_dt; + + NESO_PARTICLES_KERNEL_END + }); + }) + .wait_and_throw(); + + m_sycl_target->profile_map.inc("NeutralParticleSystem", + "Ionisation_Execute", 1, + profile_elapsed(t0, profile_timestamp())); + } + + /** + * Returns true if all boundary conditions on the density field are + * periodic. + */ + inline bool is_fully_periodic() { + NESOASSERT(m_fields.count("ne") == 1, "ne field not found in fields."); + auto bcs = m_fields["ne"]->GetBndConditions(); + bool is_pbc = true; + for (auto &bc : bcs) { + is_pbc &= (bc->GetBoundaryConditionType() == ePeriodic); + } + return is_pbc; + } + + /** + * Remove particles from the system whose ID has been flagged with a + * particular key + */ + inline void remove_marked_particles() { + m_particle_remover->remove(m_particle_group, + (*m_particle_group)[Sym("PARTICLE_ID")], + m_particle_remove_key); + } + + /** + * Apply boundary conditions and transfer particles between MPI ranks. + */ + inline void transfer_particles() { + auto t0 = profile_timestamp(); + this->boundary_conditions(); + m_particle_group->hybrid_move(); + m_cell_id_translation->execute(); + m_particle_group->cell_move(); + m_sycl_target->profile_map.inc("NeutralParticleSystem", + "transfer_particles", 1, + profile_elapsed(t0, profile_timestamp())); + } +}; +} // namespace NESO::Solvers::H3LAPD +#endif // H3LAPD_NEUTRAL_PARTICLE_SYSTEM_H diff --git a/solvers/H3LAPD/main.cpp b/solvers/H3LAPD/main.cpp new file mode 100644 index 00000000..7d6004ae --- /dev/null +++ b/solvers/H3LAPD/main.cpp @@ -0,0 +1,31 @@ +/////////////////////////////////////////////////////////////////////////////// +// +// File: main.cpp +// +// +// Description: Entrypoint for the H3LAPD solver. +// +/////////////////////////////////////////////////////////////////////////////// +#include "H3LAPD.hpp" +#include +#include + +namespace LAPD = NESO::Solvers::H3LAPD; +int main(int argc, char *argv[]) { + + // MPI is initialised/finalised here to ensure that Nektar++ does not + // initialise/finalise MPI when we were not expecting it to. + int provided_thread_level; + if (MPI_Init_thread(&argc, &argv, MPI_THREAD_SERIALIZED, + &provided_thread_level) != MPI_SUCCESS) { + std::cout << "ERROR: MPI_Init != MPI_SUCCESS" << std::endl; + return -1; + } + int err = LAPD::run_H3LAPD(argc, argv); + if (MPI_Finalize() != MPI_SUCCESS) { + std::cout << "ERROR: MPI_Finalize != MPI_SUCCESS" << std::endl; + return -1; + } + + return err; +} diff --git a/solvers/SimpleSOL/CMakeLists.txt b/solvers/SimpleSOL/CMakeLists.txt index b478b8e2..b16875b7 100644 --- a/solvers/SimpleSOL/CMakeLists.txt +++ b/solvers/SimpleSOL/CMakeLists.txt @@ -7,7 +7,7 @@ set(SIMPLE_SOL_SRC_FILES # Put solver specific source in an object library so that tests can use it set(LIBRARY_NAME SimpleSOL_ObjLib) set(SOLVER_LIBS - "${SOLVER_LIBS} ${LIBRARY_NAME}" + ${SOLVER_LIBS} ${LIBRARY_NAME} CACHE INTERNAL "") add_library(${LIBRARY_NAME} OBJECT ${SIMPLE_SOL_SRC_FILES}) target_compile_options(${LIBRARY_NAME} PRIVATE ${BUILD_TYPE_COMPILE_FLAGS}) diff --git a/src/nektar_interface/geometry_transport/geometry_transport_2d.cpp b/src/nektar_interface/geometry_transport/geometry_transport_2d.cpp new file mode 100644 index 00000000..ada7a045 --- /dev/null +++ b/src/nektar_interface/geometry_transport/geometry_transport_2d.cpp @@ -0,0 +1,45 @@ +#include + +namespace NESO { + +/** + * Get all 2D geometry objects from a Nektar++ MeshGraph + * + * @param[in] graph MeshGraph instance. + * @param[in,out] std::map of Nektar++ Geometry2D pointers. + */ +void get_all_elements_2d( + Nektar::SpatialDomains::MeshGraphSharedPtr &graph, + std::map> &geoms) { + geoms.clear(); + + for (auto &e : graph->GetAllTriGeoms()) { + geoms[e.first] = + std::dynamic_pointer_cast(e.second); + } + for (auto &e : graph->GetAllQuadGeoms()) { + geoms[e.first] = + std::dynamic_pointer_cast(e.second); + } +} + +/** + * Get a local 2D geometry object from a Nektar++ MeshGraph + * + * @param graph Nektar++ MeshGraph to return geometry object from. + * @returns Local 2D geometry object. + */ +Geometry2DSharedPtr +get_element_2d(Nektar::SpatialDomains::MeshGraphSharedPtr &graph) { + { + auto geoms = graph->GetAllQuadGeoms(); + if (geoms.size() > 0) { + return std::dynamic_pointer_cast(geoms.begin()->second); + } + } + auto geoms = graph->GetAllTriGeoms(); + NESOASSERT(geoms.size() > 0, "No local 2D geometry objects found."); + return std::dynamic_pointer_cast(geoms.begin()->second); +} + +} // namespace NESO diff --git a/src/nektar_interface/geometry_transport/geometry_transport_3d.cpp b/src/nektar_interface/geometry_transport/geometry_transport_3d.cpp index 9a04f69c..8f4204f2 100644 --- a/src/nektar_interface/geometry_transport/geometry_transport_3d.cpp +++ b/src/nektar_interface/geometry_transport/geometry_transport_3d.cpp @@ -31,6 +31,37 @@ void get_all_elements_3d( } } +/** + * Get a local 3D geometry object from a Nektar++ MeshGraph + * + * @param graph Nektar++ MeshGraph to return geometry object from. + * @returns Local 3D geometry object. + */ +Geometry3DSharedPtr +get_element_3d(Nektar::SpatialDomains::MeshGraphSharedPtr &graph) { + { + auto geoms = graph->GetAllTetGeoms(); + if (geoms.size() > 0) { + return std::dynamic_pointer_cast(geoms.begin()->second); + } + } + { + auto geoms = graph->GetAllPyrGeoms(); + if (geoms.size() > 0) { + return std::dynamic_pointer_cast(geoms.begin()->second); + } + } + { + auto geoms = graph->GetAllPrismGeoms(); + if (geoms.size() > 0) { + return std::dynamic_pointer_cast(geoms.begin()->second); + } + } + auto geoms = graph->GetAllHexGeoms(); + NESOASSERT(geoms.size() > 0, "No local 3D geometry objects found."); + return std::dynamic_pointer_cast(geoms.begin()->second); +} + /** * Categorise geometry types by shape, local or remote and X-map type. * diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6d26a219..cb4b2b70 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -66,7 +66,7 @@ set(INTEGRATION_SRC_FILES ${INTEGRATION_SRC}/solvers/Electrostatic2D3V/TwoStream/test_two_stream.cpp ${INTEGRATION_SRC}/solvers/Electrostatic2D3V/ElectronBernsteinWaves/test_ebw.cpp ${INTEGRATION_SRC}/solvers/Electrostatic2D3V/Integrators/boris_uniform_b.cpp -) + ${INTEGRATION_SRC}/solvers/H3LAPD/test_H3LAPD.cpp) check_file_list(${INTEGRATION_SRC} cpp "${INTEGRATION_SRC_FILES}" "") diff --git a/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_config.xml b/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_config.xml new file mode 100644 index 00000000..d2a356b0 --- /dev/null +++ b/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_config.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 0.00125

+

NumSteps = 50

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps/10

+

IO_CheckSteps = NumSteps+1

+ +

Bxy = 1.0

+ +

d22 = 0.0

+ +

HW_alpha = 0.1

+

HW_kappa = 3.5

+ +

cn = 6.0

+

cw = 1.0

+

s = 0.5

+ +

num_particles_total = 0

+ +

growth_rates_recording_step = 1

+
+ + + ne + w + phi + + + + C[1] + C[2] + C[3] + C[4] + C[5] + C[6] + + + + + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + +

+

+

+ + + + + + + + + diff --git a/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_mesh.xml b/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_mesh.xml new file mode 100644 index 00000000..fe96b9a3 --- /dev/null +++ b/test/integration/solvers/H3LAPD/2Din3DHWGrowthRates/2Din3DHWGrowthRates_mesh.xml @@ -0,0 +1,35 @@ + + + + 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 + + + H[0-249] + F[4,9,14,19,24,29,34,39,44,49,54,58,62,66,70,74,78,82,86,90,95,99,103,107,111,115,119,123,127,131,136,140,144,148,152,156,160,164,168,172,177,181,185,189,193,197,201,205,209,213] + F[712,716,720,724,728,732,736,740,744,748,752,755,758,761,764,767,770,773,776,779,783,786,789,792,795,798,801,804,807,810,814,817,820,823,826,829,832,835,838,841,845,848,851,854,857,860,863,866,869,872] + F[1,6,11,16,21,26,31,36,41,46,216,220,224,228,232,236,240,244,248,252,381,385,389,393,397,401,405,409,413,417,546,550,554,558,562,566,570,574,578,582,711,715,719,723,727,731,735,739,743,747] + F[176,180,184,188,192,196,200,204,208,212,351,354,357,360,363,366,369,372,375,378,516,519,522,525,528,531,534,537,540,543,681,684,687,690,693,696,699,702,705,708,846,849,852,855,858,861,864,867,870,873] + F[0,51,92,133,174,215,256,287,318,349,380,421,452,483,514,545,586,617,648,679,710,751,782,813,844] + F[50,91,132,173,214,255,286,317,348,379,420,451,482,513,544,585,616,647,678,709,750,781,812,843,874] + + + C[0] + + + + + + + M0898 + 5.3.0 + 03-Oct-2023 12:46:33 + + -v -m peralign:surf1=1:surf2=2:dir=x:orient -m peralign:surf1=3:surf2=4:dir=y:orient -m peralign:surf1=5:surf2=6:dir=z:orient examples/H3LAPD/hw_fluid-only/cuboid_periodic_5x5x10.msh examples/H3LAPD/hw_fluid-only/cuboid_periodic_5x5x10.xml + + diff --git a/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_config.xml b/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_config.xml new file mode 100644 index 00000000..f58f6925 --- /dev/null +++ b/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_config.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeStep = 0.00125

+

NumSteps = 25

+

TFinal = NumSteps*TimeStep

+

IO_InfoSteps = NumSteps+1

+

IO_CheckSteps = NumSteps+1

+ +

Bxy = 1.0

+ +

d22 = 0.0

+ +

HW_alpha = 0.0

+

HW_kappa = 0.0

+ +

cn = 6.0

+

cw = 1.0

+

s = 0.5

+ +

num_particles_per_cell = -1

+

num_particle_steps_per_fluid_step = 1

+

num_particles_total = 100

+

particle_num_write_particle_steps = 0

+

particle_number_density = 1e15

+

particle_position_seed = 1

+

particle_thermal_velocity = 1.0

+

particle_drift_velocity = 2.0

+

particle_source_width = 0.2

+ +

Te_eV = 10.0

+ +

n_bg_SI = 1e18

+ +

t_to_SI = 2e-4

+

n_to_SI = 1e18

+ +

mass_recording_step = 1

+ + + + ne + w + phi + ne_src + + + + C[1] + C[2] + C[3] + C[4] + C[5] + C[6] + + + + + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + +

+

+

+

+ + + + + + + + + + diff --git a/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_mesh.xml b/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_mesh.xml new file mode 100644 index 00000000..fe96b9a3 --- /dev/null +++ b/test/integration/solvers/H3LAPD/Coupled2Din3DHWMassCons/Coupled2Din3DHWMassCons_mesh.xml @@ -0,0 +1,35 @@ + + + + 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 + + + H[0-249] + F[4,9,14,19,24,29,34,39,44,49,54,58,62,66,70,74,78,82,86,90,95,99,103,107,111,115,119,123,127,131,136,140,144,148,152,156,160,164,168,172,177,181,185,189,193,197,201,205,209,213] + F[712,716,720,724,728,732,736,740,744,748,752,755,758,761,764,767,770,773,776,779,783,786,789,792,795,798,801,804,807,810,814,817,820,823,826,829,832,835,838,841,845,848,851,854,857,860,863,866,869,872] + F[1,6,11,16,21,26,31,36,41,46,216,220,224,228,232,236,240,244,248,252,381,385,389,393,397,401,405,409,413,417,546,550,554,558,562,566,570,574,578,582,711,715,719,723,727,731,735,739,743,747] + F[176,180,184,188,192,196,200,204,208,212,351,354,357,360,363,366,369,372,375,378,516,519,522,525,528,531,534,537,540,543,681,684,687,690,693,696,699,702,705,708,846,849,852,855,858,861,864,867,870,873] + F[0,51,92,133,174,215,256,287,318,349,380,421,452,483,514,545,586,617,648,679,710,751,782,813,844] + F[50,91,132,173,214,255,286,317,348,379,420,451,482,513,544,585,616,647,678,709,750,781,812,843,874] + + + C[0] + + + + + + + M0898 + 5.3.0 + 03-Oct-2023 12:46:33 + + -v -m peralign:surf1=1:surf2=2:dir=x:orient -m peralign:surf1=3:surf2=4:dir=y:orient -m peralign:surf1=5:surf2=6:dir=z:orient examples/H3LAPD/hw_fluid-only/cuboid_periodic_5x5x10.msh examples/H3LAPD/hw_fluid-only/cuboid_periodic_5x5x10.xml + + diff --git a/test/integration/solvers/H3LAPD/common/.gitkeep b/test/integration/solvers/H3LAPD/common/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/solvers/H3LAPD/test_H3LAPD.cpp b/test/integration/solvers/H3LAPD/test_H3LAPD.cpp new file mode 100644 index 00000000..e054740d --- /dev/null +++ b/test/integration/solvers/H3LAPD/test_H3LAPD.cpp @@ -0,0 +1,15 @@ +#include + +#include "H3LAPD.hpp" +#include "test_H3LAPD.h" +/** + * Tests for the H3LAPD solver. + */ + +/** + * N.B. HWTests look for test-specific resources in + * ./HWTest::get_solver_name()/test_name + */ +TEST_F(HWTest, 2Din3DHWGrowthRates) { check_growth_rates(); } + +TEST_F(HWTest, Coupled2Din3DHWMassCons) { check_mass_cons(); } \ No newline at end of file diff --git a/test/integration/solvers/H3LAPD/test_H3LAPD.h b/test/integration/solvers/H3LAPD/test_H3LAPD.h new file mode 100644 index 00000000..9f495373 --- /dev/null +++ b/test/integration/solvers/H3LAPD/test_H3LAPD.h @@ -0,0 +1,150 @@ +#ifndef H3LAPD_TEST_H3LAPD_H +#define H3LAPD_TEST_H3LAPD_H + +#include + +#include "EquationSystems/DriftReducedSystem.hpp" +#include "EquationSystems/HW2Din3DSystem.hpp" +#include "H3LAPD.hpp" +#include "solver_test_utils.h" +#include "solvers/solver_callback_handler.hpp" +#include "solvers/solver_runner.hpp" + +// Growth rate tolerances +constexpr double E_growth_rate_tolerance = 5e-3; +constexpr double W_growth_rate_tolerance = 5e-3; +// Ignore first few steps to allow rate to stabilise +constexpr int first_check_step = 3; + +// Mass conservation tolerance +const double mass_cons_tolerance = 2e-12; + +namespace LAPD = NESO::Solvers::H3LAPD; +/** + * Struct to calculate and record energy and enstrophy growth rates and compare + * to expected values + * (see eqns 18-20 https://rnumata.org/research/materials/turb_ws_jan2006.pdf) + */ +struct CalcHWGrowthRates : public NESO::SolverCallback { + std::vector E; + std::vector W; + std::vector E_growth_rate_error; + std::vector W_growth_rate_error; + std::vector Gamma_a; + std::vector Gamma_n; + void call(LAPD::HW2Din3DSystem *state) { + auto md = state->m_diag_growth_rates_recorder; + + E.push_back(md->compute_energy()); + W.push_back(md->compute_enstrophy()); + Gamma_a.push_back(md->compute_Gamma_a()); + Gamma_n.push_back(md->compute_Gamma_n()); + + int nsteps = E.size(); + if (nsteps > first_check_step - 1 && nsteps > 1) { + int cur_idx = nsteps - 1; + double dt; + state->GetSession()->LoadParameter("TimeStep", dt); + const double avg_Gamma_n = + 0.5 * (Gamma_n[cur_idx - 1] + Gamma_n[cur_idx]); + const double avg_Gamma_a = + 0.5 * (Gamma_a[cur_idx - 1] + Gamma_a[cur_idx]); + const double dEdt_exp = avg_Gamma_n - avg_Gamma_a; + const double dWdt_exp = avg_Gamma_n; + const double dEdt_act = (E[cur_idx] - E[cur_idx - 1]) / dt; + const double dWdt_act = (W[cur_idx] - W[cur_idx - 1]) / dt; + this->E_growth_rate_error.push_back( + std::abs((dEdt_act - dEdt_exp) / dEdt_exp)); + this->W_growth_rate_error.push_back( + std::abs((dWdt_act - dWdt_exp) / dWdt_exp)); + } + } +}; + +/** + * Structs to check mass fluid-particle mass conservation + */ +struct CalcMassesPre : public NESO::SolverCallback { + void call(LAPD::HW2Din3DSystem *state) { + auto md = state->m_diag_mass_recorder; + md->compute_initial_fluid_mass(); + } +}; + +struct CalcMassesPost : public NESO::SolverCallback { + std::vector mass_error; + void call(LAPD::HW2Din3DSystem *state) { + auto md = state->m_diag_mass_recorder; + const double mass_particles = md->compute_particle_mass(); + const double mass_fluid = md->compute_fluid_mass(); + const double mass_total = mass_particles + mass_fluid; + const double mass_added = md->compute_total_added_mass(); + const double correct_total = mass_added + md->get_initial_mass(); + this->mass_error.push_back(std::fabs(correct_total - mass_total) / + std::fabs(correct_total)); + } +}; + +class HWTest : public NektarSolverTest { +protected: + void check_growth_rates() { + CalcHWGrowthRates calc_growth_rates_callback; + + MainFuncType runner = [&](int argc, char **argv) { + SolverRunner solver_runner(argc, argv); + auto equation_system = std::dynamic_pointer_cast( + solver_runner.driver->GetEqu()[0]); + + equation_system->m_solver_callback_handler.register_post_integrate( + calc_growth_rates_callback); + + solver_runner.execute(); + solver_runner.finalise(); + return 0; + }; + + int ret_code = run(runner); + ASSERT_EQ(ret_code, 0); + + ASSERT_THAT(calc_growth_rates_callback.E_growth_rate_error, + testing::Each(testing::Le(E_growth_rate_tolerance))); + ASSERT_THAT(calc_growth_rates_callback.W_growth_rate_error, + testing::Each(testing::Le(W_growth_rate_tolerance))); + } + + void check_mass_cons() { + CalcMassesPre calc_masses_callback_pre; + CalcMassesPost calc_masses_callback_post; + + MainFuncType runner = [&](int argc, char **argv) { + SolverRunner solver_runner(argc, argv); + if (solver_runner.session->DefinesParameter("mass_recording_step")) { + auto equation_system = std::dynamic_pointer_cast( + solver_runner.driver->GetEqu()[0]); + + equation_system->m_solver_callback_handler.register_pre_integrate( + calc_masses_callback_pre); + equation_system->m_solver_callback_handler.register_post_integrate( + calc_masses_callback_post); + + solver_runner.execute(); + solver_runner.finalise(); + return 0; + } else { + std::cerr << "check_mass_cons callback: session must define " + "'mass_recording_step'" + << std::endl; + return 1; + } + }; + + int ret_code = run(runner); + ASSERT_EQ(ret_code, 0); + ASSERT_THAT(calc_masses_callback_post.mass_error, + testing::Each(testing::Le(mass_cons_tolerance))); + } + + std::string get_solver_name() override { return "H3LAPD"; } +}; + +#endif // H3LAPD_TEST_H3LAPD_H diff --git a/test/integration/solvers/solver_test_utils.cpp b/test/integration/solvers/solver_test_utils.cpp index 5d8cd981..54690658 100644 --- a/test/integration/solvers/solver_test_utils.cpp +++ b/test/integration/solvers/solver_test_utils.cpp @@ -180,10 +180,15 @@ int NektarSolverTest::run(MainFuncType func, std::vector args, return solver_ret_code; } -void NektarSolverTest::SetUp() { - // Store solver name, test name for convenience - m_solver_name = solver_name_from_test_suite_name( +std::string NektarSolverTest::get_solver_name() { + return solver_name_from_test_suite_name( get_current_test_info()->test_suite_name()); +} + +void NektarSolverTest::SetUp() { + // Set solver name (allowing derived classes to override) + m_solver_name = get_solver_name(); + // Set test name m_test_name = get_current_test_info()->name(); // Determine test resource locations diff --git a/test/integration/solvers/solver_test_utils.h b/test/integration/solvers/solver_test_utils.h index 086d54fb..7a2666e6 100644 --- a/test/integration/solvers/solver_test_utils.h +++ b/test/integration/solvers/solver_test_utils.h @@ -64,6 +64,9 @@ class NektarSolverTest : public ::testing::Test { // on the test name std::vector get_default_args(); + // Allow derived classes to override solver_name + virtual std::string get_solver_name(); + // Create a temporary directory to run the test in and copy in required // resources void make_test_run_dir(); diff --git a/test/unit/nektar_interface/test_particle_geometry_interface.cpp b/test/unit/nektar_interface/test_particle_geometry_interface.cpp index a9136edf..0ebb6eb0 100644 --- a/test/unit/nektar_interface/test_particle_geometry_interface.cpp +++ b/test/unit/nektar_interface/test_particle_geometry_interface.cpp @@ -604,3 +604,53 @@ TEST(ParticleGeometryInterface, HaloExtend2D) { delete[] argv[0]; delete[] argv[1]; } + +TEST(ParticleGeometryInterface, PointInSubDomain2D) { + const int width = 1; + + LibUtilities::SessionReaderSharedPtr session; + SpatialDomains::MeshGraphSharedPtr graph; + + int argc = 2; + char *argv[2]; + copy_to_cstring(std::string("test_particle_geometry_interface"), &argv[0]); + + std::filesystem::path source_file = __FILE__; + std::filesystem::path source_dir = source_file.parent_path(); + std::filesystem::path test_resources_dir = + source_dir / "../../test_resources"; + std::filesystem::path mesh_file = + test_resources_dir / "square_triangles_quads.xml"; + copy_to_cstring(std::string(mesh_file), &argv[1]); + + // Create session reader. + session = LibUtilities::SessionReader::CreateInstance(argc, argv); + graph = SpatialDomains::MeshGraph::Read(session); + + auto mesh = std::make_shared(graph); + + double point[2]; + mesh->get_point_in_subdomain(point); + std::map> geoms; + get_all_elements_2d(graph, geoms); + + bool found = false; + Array to_test(3); + to_test[2] = 0.0; + for (int dimx = 0; dimx < 2; dimx++) { + to_test[dimx] = point[dimx]; + } + + for (auto geom : geoms) { + if (geom.second->ContainsPoint(to_test)) { + found = true; + break; + } + } + + ASSERT_TRUE(found); + + mesh->free(); + delete[] argv[0]; + delete[] argv[1]; +} diff --git a/test/unit/nektar_interface/test_particle_geometry_interface_3d.cpp b/test/unit/nektar_interface/test_particle_geometry_interface_3d.cpp index d173b206..706fc4a2 100644 --- a/test/unit/nektar_interface/test_particle_geometry_interface_3d.cpp +++ b/test/unit/nektar_interface/test_particle_geometry_interface_3d.cpp @@ -373,3 +373,55 @@ TEST(ParticleGeometryInterface, CoordinateMapping3D) { delete[] argv[0]; delete[] argv[1]; } + +TEST(ParticleGeometryInterface, PointInSubDomain3D) { + + LibUtilities::SessionReaderSharedPtr session; + SpatialDomains::MeshGraphSharedPtr graph; + + int argc = 3; + char *argv[3]; + copy_to_cstring(std::string("test_particle_geometry_interface"), &argv[0]); + + std::filesystem::path source_file = __FILE__; + std::filesystem::path source_dir = source_file.parent_path(); + std::filesystem::path test_resources_dir = + source_dir / "../../test_resources"; + std::filesystem::path conditions_file = + test_resources_dir / "reference_all_types_cube/conditions.xml"; + copy_to_cstring(std::string(conditions_file), &argv[1]); + std::filesystem::path mesh_file = + test_resources_dir / "reference_all_types_cube/mixed_ref_cube_0.2.xml"; + copy_to_cstring(std::string(mesh_file), &argv[2]); + + // Create session reader. + session = LibUtilities::SessionReader::CreateInstance(argc, argv); + + // Create MeshGraph. + graph = SpatialDomains::MeshGraph::Read(session); + auto mesh = std::make_shared(graph); + + double point[3]; + mesh->get_point_in_subdomain(point); + std::map> geoms; + get_all_elements_3d(graph, geoms); + + bool found = false; + Array to_test(3); + for (int dimx = 0; dimx < 3; dimx++) { + to_test[dimx] = point[dimx]; + } + + for (auto geom : geoms) { + if (geom.second->ContainsPoint(to_test)) { + found = true; + break; + } + } + + ASSERT_TRUE(found); + mesh->free(); + delete[] argv[0]; + delete[] argv[1]; + delete[] argv[2]; +}