diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 000000000..346c33d14 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,38 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: pytest + +on: + pull_request: + branches: ["starforge_dev"] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install . + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Setup MPI + uses: mpi4py/setup-mpi@v1 + with: + mpi: openmpi + - name: Install libraries + run: sudo apt install libhdf5-openmpi-dev libgsl-dev + - name: Test with pytest + run: | + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..bb1e0c316 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Files used for gizmo compilation. +*.o +GIZMO +GIZMO_config.h +compile_time_info.cc +Config.sh + +# Files used for gizmo test suite. +build/ +spcool_tables/ +TREECOOL +spcool_tables.tgz +test/*/*.txt +test/*/output +*egg-info +*.err +*.out +*.hdf5 +*.tmp +*.pyc +*values diff --git a/Makefile b/Makefile index a0d815d54..a7f3632c8 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ FINCL = #---------------------------------------------------------------------------------------------- ifeq ($(SYSTYPE),"Frontera") CC = mpicc -CXX = mpicxx -std=c++11 +CXX = mpicxx -std=c++17 FC = mpif90 -nofor_main OPTIMIZE = -ggdb -O2 -xCORE-AVX2 -Wno-unknown-pragmas -Wall -Wno-format-security -qopenmp ifeq (CHIMES,$(findstring CHIMES,$(CONFIGVARS))) @@ -146,6 +146,39 @@ OPT += -DHDF5_DISABLE_VERSION_CHECK # Compiles with following modules: 1) intel/20.1 2) hdf5/1.10.1 3) gsl/2.4 4) fftw/3.3.7 endif +#---------------------------------------------------------------------------------------------- +ifeq ($(SYSTYPE),"BigRed200") +CC = cc # For Cray use this instead of mpicc +CXX = CC # mpic++ +FC = $(CC) +OPTIMIZE = -O2 + +# Extra compile time warning flags +# OPTIMIZE += -Wall -Wextra -Wuninitialized -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion -Wno-unused-variable -Wno-unused-but-set-variable + +ifeq (OPENMP,$(findstring OPENMP,$(CONFIGVARS))) +OPTIMIZE += -qopenmp +endif +# All the library paths are already setup +MKL_INCL = +MKL_LIBS = -mkl=sequential +GSL_INCL = -I/N/soft/sles15sp6/gsl/gnu/2.8/include +GSL_LIBS = -L/N/soft/sles15sp6/gsl/gnu/2.8/lib +FFTW_INCL= +FFTW_LIBS= +HDF5INCL = +HDF5LIB = -lhdf5 -lz +MPICHLIB = + +# For mpi intel compilers and HDF5 +OPT += -DUSE_MPI_IN_PLACE -DH5_USE_16_API -DNO_ISEND_IRECV_IN_DOMAIN -DHDF5_DISABLE_VERSION_CHECK +## modules to load: (August 2025) +## module swap PrgEnv-gnu PrgEnv-intel +## module swap cray-mpich-ucx/9.0.0 cray-mpich-ucx/8.1.32 # currently defaults to a pre-release mpi version +## module load gsl cray-fftw cray-hdf5 +## srun --cpus-per-task=$SLURM_CPUS_PER_TASK --ntasks-per-node=$SLURM_NTASKS_PER_NODE --nodes=$SLURM_JOB_NUM_NODES ./GIZMO ./gizmo_parameters.txt +endif + #---------------------------------------------------------------------------------------------- ifeq ($(SYSTYPE),"Expanse") CC = mpicc @@ -170,9 +203,11 @@ OPT += # endif #---------------------------------------------------------------------------------------------- +# Environment for building GIZMO on a macbook with libraries installed via homebrew. But note +# that the specific GSL and HDF5 versions are hardcoded here... ifeq ($(SYSTYPE),"MacBookCellar") CC = mpicc -CXX = mpiccxx +CXX = mpicxx -std=c++11 FC = $(CC) #mpifort ## change this to "mpifort" for packages requiring linking secondary fortran code, currently -only- the helmholtz eos modules do this, so I leave it un-linked for now to save people the compiler headaches OPTIMIZE = -O1 -funroll-loops OPTIMIZE += -g -Wall # compiler warnings @@ -187,18 +222,18 @@ GSL_INCL = -I/opt/homebrew/Cellar/gsl/2.8/include #-I$(PORTINCLUDE) GSL_LIBS = -L/opt/homebrew/Cellar/gsl/2.8/lib #-L$(PORTLIB) FFTW_INCL= -I/usr/local/include FFTW_LIBS= -L/usr/local/lib -HDF5INCL = -I/opt/homebrew/Cellar/hdf5/1.14.3_1/include -DH5_USE_16_API #-I$(PORTINCLUDE) -DH5_USE_16_API -HDF5LIB = -L/opt/homebrew/Cellar/hdf5/1.14.3_1/lib -lhdf5 -lz #-L$(PORTLIB) +HDF5INCL = -I/opt/homebrew/Cellar/hdf5/1.14.6/include -DH5_USE_16_API #-I$(PORTINCLUDE) -DH5_USE_16_API +HDF5LIB = -L/opt/homebrew/Cellar/hdf5/1.14.6/lib -lhdf5 -lz #-L$(PORTLIB) MPICHLIB = # OPT += -DDISABLE_ALIGNED_ALLOC -DCHIMES_USE_DOUBLE_PRECISION # endif #---------------------------- -ifeq ($(SYSTYPE),"PopOS") +ifeq ($(SYSTYPE),"github-ubuntu") CC = mpicc -CXX = mpiccxx +CXX = mpicxx FC = $(CC) -OPTIMIZE = -g -fcommon -O1 -ffast-math -funroll-loops -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone ## optimizations for gcc compilers (1/2) +OPTIMIZE = -g -fcommon -O1 -funroll-loops -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -fipa-cp-clone ## optimizations for gcc compilers (1/2) OPTIMIZE += -ftree-loop-distribute-patterns -fvect-cost-model -ftree-partial-pre ## optimizations for gcc compilers (2/2) OPTIMIZE += -g -Wall # compiler warnings ifeq (CHIMES,$(findstring CHIMES,$(CONFIGVARS))) @@ -223,6 +258,24 @@ OPT += -DDISABLE_ALIGNED_ALLOC -DCHIMES_USE_DOUBLE_PRECISION ## to get required packages: sudo apt install libhdf5-openmpi-dev libgsl-dev libopenmpi-dev endif +#---------------------------- +# Should work on any Flatiron institute linux cluster environment: rusty, popeye and linux workstations +ifeq ($(SYSTYPE),"RUSTY") +CC = mpicc +ifeq (SOFTDOUBLEDOUBLE,$(findstring SOFTDOUBLEDOUBLE,$(OPT))) +CC = mpicxx +endif +FC = mpifort +OPTIMIZE = -O2 -g -Wall +GSL_INCL = -I$(GSL_BASE)/include +GSL_LIBS = -L$(GSL_BASE)/lib -Xlinker -R -Xlinker $(GSL_BASE) -lgsl -lgslcblas +FFTW_INCL= -I$(FFTW3_BASE)/include +FFTW_LIBS= -L$(FFTW3_BASE)/lib -Xlinker -R -Xlinker $(FFTW3_BASE)/lib +MPICHLIB = +HDF5INCL = -I$(HDF5_BASE)/include -DH5_USE_16_API +HDF5LIB = -L$(HDF5_BASE)/lib -Xlinker -R -Xlinker $(HDF5_BASE)/lib -lhdf5 -lz +endif + #---------------------------------------------------------------------------------------------- #---------------------------------------------------------------------------------------------- @@ -296,7 +349,8 @@ SINK_OBJS = sinks/sink.o \ RHD_OBJS = radiation/rt_utilities.o \ radiation/rt_CGmethod.o \ radiation/rt_source_injection.o \ - radiation/rt_chem.o + radiation/rt_chem.o \ + radiation/rt_dust_opacity.o FOF_OBJS = structure/fof.o \ structure/subfind/subfind.o \ diff --git a/Makefile.systype b/Makefile.systype index 20a821bfa..801e9e870 100644 --- a/Makefile.systype +++ b/Makefile.systype @@ -6,49 +6,10 @@ # ############# -SYSTYPE="Frontera" -#SYSTYPE="Stampede2" -#SYSTYPE="Bridges2" -#SYSTYPE="Anvil" -#SYSTYPE="Wheeler" -#SYSTYPE="CaltechHPC" -#SYSTYPE="MacBookPro" +SYSTYPE="github-ubuntu" +#SYSTYPE="Frontera" #SYSTYPE="MacBookCellar" -#SYSTYPE="Quest" -#SYSTYPE="Quest-intel" -#SYSTYPE="TSCC" #SYSTYPE="Expanse" -#SYSTYPE="Iron" +#SYSTYPE="CaltechHPC" #SYSTYPE="RUSTY" -#SYSTYPE="Darter" -#SYSTYPE="Comet" -#SYSTYPE="Gordon" -#SYSTYPE="odyssey" -#SYSTYPE="antares" -#SYSTYPE="Pleiades" -#SYSTYPE="Mira" -#SYSTYPE="Titan" -#SYSTYPE="Edison" -#SYSTYPE="SciNet" -#SYSTYPE="Ranger_intel" -#SYSTYPE="Ranger_pgi" -#SYSTYPE="Darwin" -#SYSTYPE="Magny" -#SYSTYPE="Magny-Intel" -#SYSTYPE="OpenSuse" -#SYSTYPE="OpenSuse64" -#SYSTYPE="HLRB2" -#SYSTYPE="MPA" -#SYSTYPE="VIP" -#SYSTYPE="Ubuntu" -#SYSTYPE="MBM" -#SYSTYPE="OpteronMPA-Gnu" -#SYSTYPE="OpteronMPA-Intel" -#SYSTYPE="Centos5-intel" -#SYSTYPE="Kolob" -#SYSTYPE="Centos5-Gnu" -#SYSTYPE="OPA-Cluster64-Intel" -#SYSTYPE="Stampede" -#SYSTYPE="Zwicky" -#SYSTYPE="BlueWaters" -#SYSTYPE="PopOS" \ No newline at end of file +#SYSTYPE="BigRed200" diff --git a/Template_Config.sh b/Template_Config.sh index fad431f6f..a92e5873d 100644 --- a/Template_Config.sh +++ b/Template_Config.sh @@ -503,8 +503,7 @@ #EOS_ENFORCE_ADIABAT=(1.0) # if set, this forces gas to lie -exactly- along the adiabat P=EOS_ENFORCE_ADIABAT*(rho^GAMMA) #HYDRO_REPLACE_RIEMANN_KT # replaces the hydro Riemann solver (HLLC) with a Kurganov-Tadmor flux derived in Panuelos, Wadsley, and Kevlahan, 2019. works with MFM/MFV/fixed-grid methods [-without- MHD active, but other modules are fine]. more diffusive, but smoother, and more stable convergence results #SLOPE_LIMITER_TOLERANCE=1 # sets the slope-limiters used. higher=more aggressive (less diffusive, but less stable). 1=default. 0=conservative. use on problems where sharp density contrasts in poor particle arrangement may cause errors. 2=use the original GIZMO paper (more aggressive) slope-limiters. more accurate for smooth problems, but these can introduce numerical instability in problems with poorly-resolved large noise or density contrasts (e.g. multi-phase, self-gravitating flows) -#ENERGY_ENTROPY_SWITCH_IS_ACTIVE # enable energy-entropy switch as described in GIZMO methods paper. This can greatly improve performance on some problems where the - # the flow is very cold and highly super-sonic. it can cause problems in multi-phase flows with strong cooling, though, and is not compatible with non-barytropic equations of state +#ENERGY_ENTROPY_SWITCH_IS_ACTIVE # enable energy-entropy switch as described in GIZMO methods paper. This can greatly improve performance on some problems where the the flow is very cold and highly super-sonic. it can cause problems in multi-phase flows with strong cooling, though, and is not compatible with non-barytropic equations of state #FORCE_ENTROPIC_EOS_BELOW=(0.01) # set (manually) the alternative energy-entropy switch which is enabled by default in MFM/MFV: if relative velocities are below this threshold, it uses the entropic EOS #HYDRO_KERNEL_SURFACE_VOLCORR # attempt to correct SPH/MFM/MFV cell volumes for free-surface effects, using the estimated boundary correction for the Wendland C2 kernel (works with others but most accurate for this) based on asymmetry of neighbors within kernel, as calibrated in Reinhardt & Stadel 2017 (arXiv:1701.08296), see e.g. their Fig 3 #DISABLE_SURFACE_VOLCORR # disables HYDRO_KERNEL_SURFACE_VOLCORR if it would be set by default (e.g. if EOS_ELASTIC is enabled) @@ -582,6 +581,7 @@ #GRAIN_RDI_TESTPROBLEM # top-level flag to enable a variety of test problem behaviors, customized for the idealized studies of dust dynamics in Moseley et al 2019MNRAS.489..325M, Seligman et al 2019MNRAS.485.3991S, Steinwandel et al arXiv:2111.09335, Ji et al arXiv:2112.00752, Hopkins et al 2020MNRAS.496.2123H and arXiv:2107.04608, Squire et al 2022MNRAS.510..110S. Cite these if used. #GRAIN_RDI_TESTPROBLEM_ACCEL_DEPENDS_ON_SIZE # Make the idealized external grain acceleration grain size-dependent; equivalent to assuming an absorption efficiency Q=1. Cite GRAIN_RDI_TESTPROBLEM papers. #GRAIN_RDI_TESTPROBLEM_LIVE_RADIATION_INJECTION # Enables idealized radiation injection by a source population designed to set up outflow test problems with live radiation-hydrodynamics as in Hopkins et al., arXiv:2107.04608. Cite that paper if this module is used. +#IO_DUST_NOT_IN_ICFILE # special flag needed if restarting from a snapshot (flag=2) with no dust. Will set dust species via params arguments and assume an MRN size distribution, if GALSF_ISMDUSTCHEM_MODEL is active # -------------------- # ----- MPI & Parallel-FFTW De-Bugging #DOUBLEPRECISION_FFTW # FFTW in double precision to match libraries @@ -591,6 +591,22 @@ #ALLOW_IMBALANCED_GASPARTICLELOAD # increases All.MaxPartGas to All.MaxPart: can allow better load-balancing in some cases, but uses more memory. But use me if you run into errors where it can't fit the domain (where you would increase PartAllocFac, but can't for some reason) #################################################################################################### +############################################################################################################################- +#------------------ ISM Dust Chemical Evolution Models (follow growth, destruction, and size evolution of different grain species) +#----------------- Users of any of these modules should cite Choban et al., 2022/25 for the methods/implementation in GIZMO and FIRE +############################################################################################################################- +#GALSF_ISMDUSTCHEM_MODEL=(1+2) #- enable live dust evolution model (value deteremines the dust species tracked). Use GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION to set the silicate composition. + #- model = 1: Track silicates and carbonaceous dust. + #- model = 2: Track metallic iron dust. + #- model = 4: Track oxygen bearing dust species which is a simple match to observations of MW oxygen depletion. + #- model = 8: Track metallic iron nanoparticles with set fraction assumed to be locked in silicate dust as inclusions based on Zhukovska+(2018). Requires GALSF_ISMDUSTCHEM_MODEL=2. +#GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION=(1+2+8) #- set the silicate dust chemical composition. This changes the production, growth, and destruction rates of silicate dust, and the max depletions of Mg, Fe, Si, and O in the gas phase. + #- model = 1 (default): olivine-pyroxene mix [(Fe_0.571 Mg_1.06) Si O_3.63] + #- model = 2: add 2 extra O atoms to better match O depletions. + #- model = 4: add 1 extra Fe atom to better match Fe depletions. + #- model = 8: remove all Fe. Use with additional metallic iron species to avoid Fe limiting silicate growth. +#GALSF_ISMDUSTCHEM_GRAINSIZEEVO=16 #- enable grain size evolution model w/ N number of logarithmically spaced bins (must also turn on GALSF_ISMDUSTCHEM_MODEL= 1 or (1 + 2) only and GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION) +############################################################################################################################- @@ -615,16 +631,4 @@ #USE_TIMESTEP_DILATION_FOR_ZOOMS #- enable time dilation modules, need to customize for applications, cannot be simply generically turned on without coding how they will work #DILATION_FOR_STELLAR_KINEMATICS_ONLY #- special version of time dilation designed for stellar kinematics in e.g. dense star clusters or galaxy centers #SINK_RIAF_SUBEDDINGTON_MODEL=(0.01) #- enable an arbitrary modular variation in the radiative efficiency of BHs as a function of eddington ratio or other particle properties, with the critical transition to the jet mode at this eddington ratio (defined in terms of mdot/mdot_crit) -####################################################################################################- - -############################################################################################################################- -#------------------ ISM Dust Chemical Evolution Models (follow growth and destruction of different grain types) -#----------------- Users of any of these modules should cite Choban et al., 2022 for the methods/implementation in GIZMO and FIRE -############################################################################################################################- -#GALSF_ISMDUSTCHEM_MODEL=(2+4+8) #- enable live dust evolution model (must select either elemental or species or other model codes as well) - #- model = 1: "dust by element" dust evolution model based off Bekki(2013)/McKinnon+(2016). Track generalized silicates and carbonaceous dust. - #- model = 2: "dust by species" dust evolution model based off Zhukovska+(2008/2016/2018). Tracks silicates (set composition), carbonaceous, SiC, and metallic iron dust along with optional iron nanoparticles and/or O reservoir dust species. - #- model = 4: additional metallic iron dust nano-particles with set fraction assumed to be locked in silicate dust as inclusions based on Zhukovska+(2018) - #- model = 8: additional oxygen bearing dust species which is a simple match to observations of MW oxygen depletion since they cannot be explained with purely silicate dust -#GALSF_ISMDUSTCHEM_PASSIVE #- decouples dust evolution from dust physics, chemisty, and feedback. Dust will evolve passively, with any physics or chemistry involving dust using constant, preset values (typically D/Z=0.4) -############################################################################################################################- +####################################################################################################- \ No newline at end of file diff --git a/cooling/cooling.cc b/cooling/cooling.cc index 31657a392..017630e78 100644 --- a/cooling/cooling.cc +++ b/cooling/cooling.cc @@ -197,16 +197,16 @@ void do_the_cooling_for_particle(int i) { int k_donor = rt_get_donation_target_bin(k); /* this is used to indicate whether in the rad drift-kick loop, absorption is immediately re-radiated or not, ie. whether or not we should account for it here */ double tau = fabs(rt_absorption_rate(i,k) * dtime), f_abs = 1.-exp(-tau); if(tau<0.01) {f_abs=tau*(1.-tau/2.);} /* fraction of energy absorbed in the timestep */ - double absorpted_rad_energy = DMIN(SphP[i].Rad_E_gamma[k],SphP[i].Rad_E_gamma_Pred[k]) * f_abs; /* estimate energy from the band that is absorbed in this timestep */ + double absorpted_rad_energy = DMIN(CellP[i].Rad_E_gamma[k],CellP[i].Rad_E_gamma_Pred[k]) * f_abs; /* estimate energy from the band that is absorbed in this timestep */ #ifdef RT_INFRARED if(k==RT_FREQ_BIN_INFRARED) { k_donor = -1; /* we use this below to indicate radiation which hasn't been re-radiated, which is handled in a special way for the adaptive bin here [which by default re-emits to itself], so set this here */ - double opacity_fraction_from_gas_absorption = rt_kappa_adaptive_IR_band(i,SphP[i].Dust_Temperature,SphP[i].Radiation_Temperature,-1,-1) / (rt_kappa_adaptive_IR_band(i,SphP[i].Dust_Temperature,SphP[i].Radiation_Temperature,0,0) + MIN_REAL_NUMBER); /* want the opacity from gas absorption as a fraction of total, because this is -not- assumed to re-radiate immediately in the drift/kick routine */ + double opacity_fraction_from_gas_absorption = rt_kappa_adaptive_IR_band(i,CellP[i].Dust_Temperature,CellP[i].Radiation_Temperature,-1,-1) / (rt_kappa_adaptive_IR_band(i,CellP[i].Dust_Temperature,CellP[i].Radiation_Temperature,0,0) + MIN_REAL_NUMBER); /* want the opacity from gas absorption as a fraction of total, because this is -not- assumed to re-radiate immediately in the drift/kick routine */ absorpted_rad_energy *= opacity_fraction_from_gas_absorption; } #endif if(k_donor >= 0) {continue;} /* re-emitted immediately, ignore */ - de_u_radabs += fabs(total_absorption_rate); /* sum up absorbed photon energy */ + de_u_radabs += fabs(absorpted_rad_energy); /* sum up absorbed photon energy */ } de_u_work += de_u_radabs; /* add this to the energy reservoir represented by the work function */ double de_u_touse = de_u - de_u_work; /* this is the actual difference between the implicit hydro work+absorption term and the total term, i.e. a corrected de_u_rad, which we use below */ @@ -259,7 +259,7 @@ void do_the_cooling_for_particle(int i) #endif #if defined(GALSF_ISMDUSTCHEM_MODEL) - update_dust_acc_and_sput(i, dtime*UNIT_TIME_IN_MYR*0.001); + update_dust_processes(i, dtime*UNIT_TIME_IN_MYR*0.001); #endif #ifdef COOL_MOLECFRAC_NONEQM @@ -993,16 +993,17 @@ double CoolingRate(double logT, double rho, double n_elec_guess, double *n_elec /* some blocks below to define useful variables before calculation of cooling rates: */ #ifdef COOL_METAL_LINES_BY_SPECIES - double *Z; + double Z[NUM_METAL_SPECIES] = {0.}; // gas-phase metallicity if(target>=0) { - Z = P[target].Metallicity; -#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_PASSIVE) - int k; for(k=0;k= 0) {CellP[target].DustCoolingRate = LambdaDust;} +#endif #endif #ifdef COOL_LOW_TEMPERATURES @@ -1077,7 +1082,7 @@ double CoolingRate(double logT, double rho, double n_elec_guess, double *n_elec #if defined(COOL_METAL_LINES_BY_SPECIES) && ((GALSF_FB_FIRE_STELLAREVOLUTION > 2) || !defined(GALSF_FB_FIRE_STELLAREVOLUTION)) double column = evaluate_NH_from_GradRho(CellP[target].Gradients.Density,P[target].KernelRadius,CellP[target].Density,P[target].NumNgb,1,target) * UNIT_SURFDEN_IN_CGS; // converts to cgs double Z_C = DMAX(1.e-6, Z[2]/All.SolarAbundances[2]), sqrt_T=sqrt(T), ncrit_CO=1.9e4*sqrt_T, Sigma_crit_CO=3.0e-5*T/Z_C, T3=T/1.e3, EXPmax=90.; // carbon abundance (relative to solar and 1/2 factor for original assumed 0.5 depletion), critical density and column -#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_PASSIVE) +#if defined(GALSF_ISMDUSTCHEM_MODEL) Z_C = DMAX(1.e-6, Z[2]/(0.5*All.SolarAbundances[2])); // gas-phase carbon abundance (relative to solar/2, usual assumption implicitly) #endif // TODO: can now get detailed C+, C, O, and CO abundances from SIMPLE_STEADYSTATE_CHEMISTRY routines, and compute the explicit cooling terms for slightly more accurate cooling here @@ -1118,7 +1123,7 @@ double CoolingRate(double logT, double rho, double n_elec_guess, double *n_elec #endif LambdaMol *= truncation_factor; // cutoff factor from above for where the tabulated rates take over at high temperatures LambdaDust = gas_dust_heating_coeff(target,T,Tdust) * (T-Tdust);// Note our sign convention is such that positive lambda = gas cooling -#if !defined(GALSF_ISMDUSTCHEM_MODEL) || defined(GALSF_ISMDUSTCHEM_PASSIVE) +#if !defined(GALSF_ISMDUSTCHEM_MODEL) if(T>3.e5) {double dx=(T-3.e5)/2.e5; LambdaDust *= exp(-DMIN(dx*dx,40.));} /* needs to truncate at high temperatures b/c of dust destruction (in some modules we solve for this explicitly - in that case can protect this more explicitly, but here, we will make a simple approximation, otherwise we run into problems. note this is not sublimation generally, but sputtering, that causes the destruction */ #endif LambdaDust *= truncation_factor; // cutoff factor from above for where the tabulated rates take over at high temperatures @@ -1205,6 +1210,9 @@ double CoolingRate(double logT, double rho, double n_elec_guess, double *n_elec LambdaPElec = -1.3e-24 * photoelec / nHcgs * (P[target].Metallicity[0]/All.SolarAbundances[0]) * return_dust_to_metals_ratio_vs_solar(target,0); // negative sign for lambda b/c heating double x_photoelec = photoelec * sqrt(T) / (0.5 * (1.0e-12+n_elec) * nHcgs); LambdaPElec *= 0.049/(1+pow(x_photoelec/1925.,0.73)) + 0.037*pow(T/1.0e4,0.7)/(1+x_photoelec/5000.); +#if defined(OUTPUT_COOLRATE_DETAIL) + if(target >= 0) {CellP[target].PElecHeatingRate = -LambdaPElec;} +#endif Heat -= LambdaPElec; } } @@ -1903,6 +1911,9 @@ void update_explicit_molecular_fraction(int i, double dtime_cgs) double dv_turb=gradv*dx_cell*UNIT_VEL_IN_KMS; // delta-velocity across cell double x00 = surface_density_local / surface_density_H2_0, x01 = x00 / (sqrt(1. + 3.*dv_turb*dv_turb/(v_thermal_rms*v_thermal_rms)) * sqrt(2.)*v_thermal_rms), y_ss, x_ss_1, x_ss_sqrt, fH2_tmp, fH2_max, fH2_min, Q_max, Q_min, Q_initial; // variable needed below. note the x01 term corrects following Gnedin+Draine 2014 for the velocity gradient at the sonic scale, assuming a Burgers-type spectrum [their Eq. 3] double b_time_Mach = 0.5 * dv_turb / (v_thermal_rms/sqrt(3.)); // cs_thermal for molecular [=rms v_thermal / sqrt(3)], dv_turb to full inside dx, assume "b" prefactor for compressive-to-solenoidal ratio corresponding to the 'natural mix' = 0.5. could further multiply by 1.58 if really needed to by extended dvturb to 2h = H, and vthermal from molecular to atomic for the generating field, but not as well-justified +#if defined(GALSF_ISMDUSTCHEM_MODEL) && defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + CellP[i].ISMDustChem_MachNumber = dv_turb / (v_thermal_rms/sqrt(3.)); // dust evolution model uses local mach number/clumping factors for numerous calculations +#endif double clumping_factor = 1. + b_time_Mach*b_time_Mach; // this is the exact clumping factor for a standard lognormal PDF with S=ln[1+b^2 Mach^2] // double clumping_factor_3 = clumping_factor*clumping_factor*clumping_factor; // clumping factor N for /^n = clumping factor^(N*(N-1)/2) // @@ -2114,6 +2125,9 @@ double get_equilibrium_dust_temperature_estimate(int i, double shielding_factor_ } else { // IR term is not vanishingly small. we will assume the IR radiation temperature is equal to the local Tdust. lacking any direct evolution of that field, this is a good proxy, and exact in the locally-IR-optically-thick limit. in the locally-IR-thin limit it slightly under-estimates Tdust, but usually in that limit the other terms dominate anyways, so this is pretty safe // double T0=2.92, q=pow(T0*e_IR,0.25), y=(T_cmb*e_CMB + T_hiegy*e_HiEgy)/(T0*e_IR*q); if(y<=1) {Tdust_eqm=T0*q*(0.8+sqrt(0.04+0.1*y));} else {double y5=pow(y,0.2), y5_3=y5*y5*y5, y5_4=y5_3*y5; Tdust_eqm=T0*q*(1.+15.*y5_4+sqrt(1.+30.*y5_4+25.*y5_4*y5_4))/(20.*y5_3);} // this gives an extremely accurate and exactly-joined solution to the full quintic equation assuming T_rad_IR=T_dust } +#if defined(OUTPUT_DUST_TEMPERATURE) && (GALSF_FB_FIRE_STELLAREVOLUTION > 2) + CellP[i].Dust_Temperature = DMAX(DMIN(Tdust_eqm , 2000.) , 1.); +#endif return DMAX(DMIN(Tdust_eqm , 2000.) , 1.); // limit at sublimation temperature or some very low temp // } diff --git a/core/begrun.cc b/core/begrun.cc index a188afc27..e4b9f7185 100644 --- a/core/begrun.cc +++ b/core/begrun.cc @@ -380,6 +380,26 @@ void begrun(void) All.CosmicRay_SNeFraction = all.CosmicRay_SNeFraction; #endif +#ifdef GALSF_ISMDUSTCHEM_MODEL + All.ISMDustChem_SNeIIDustScaling = all.ISMDustChem_SNeIIDustScaling; + All.ISMDustChem_SNeIaDustScaling = all.ISMDustChem_SNeIaDustScaling; + All.ISMDustChem_AGBDustScaling = all.ISMDustChem_AGBDustScaling; + All.ISMDustChem_DustAccretionScaling = all.ISMDustChem_DustAccretionScaling; + All.ISMDustChem_ThermalSputteringScaling = all.ISMDustChem_ThermalSputteringScaling; + All.ISMDustChem_AccretionTcutoffScaling = all.ISMDustChem_AccretionTcutoffScaling; + All.ISMDustChem_SNeGasClearedOfDustScaling = all.ISMDustChem_SNeGasClearedOfDustScaling; +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + All.ISMDustChem_SNeShatteringScaling = all.ISMDustChem_SNeShatteringScaling; + All.ISMDustChem_SNeSputteringScaling = all.ISMDustChem_SNeSputteringScaling; + All.ISMDustChem_ShatteringScaling = all.ISMDustChem_ShatteringScaling; + All.ISMDustChem_CoagDensityEnhancementScaling = all.ISMDustChem_CoagDensityEnhancementScaling; + All.ISMDustChem_VCoagScaling = all.ISMDustChem_VCoagScaling; + All.ISMDustChem_CoagulationScaling = all.ISMDustChem_CoagulationScaling; + All.ISMDustChem_GrainVelocityScaling = all.ISMDustChem_GrainVelocityScaling; + All.ISMDustChem_PhotodestructionScaling = all.ISMDustChem_PhotodestructionScaling; +#endif +#endif + #ifdef GR_TABULATED_COSMOLOGY All.DarkEnergyConstantW = all.DarkEnergyConstantW; #endif @@ -1195,6 +1215,85 @@ void read_parameter_file(char *fname) strcpy(alternate_tag[nt],"Initial_ISMDustChem_Silicate_to_Carbon_Dust_Ratio"); addr[nt] = &All.Initial_ISMDustChem_SiliconToCarbonRatio; id[nt++] = REAL; + + strcpy(tag[nt],"SNeIIDustScaling"); + addr[nt] = &All.ISMDustChem_SNeIIDustScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"SNeIaDustScaling"); + addr[nt] = &All.ISMDustChem_SNeIaDustScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"AGBDustScaling"); + addr[nt] = &All.ISMDustChem_AGBDustScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"DustAccretionScaling"); + addr[nt] = &All.ISMDustChem_DustAccretionScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"ThermalSputteringScaling"); + addr[nt] = &All.ISMDustChem_ThermalSputteringScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"SNeGasClearedOfDustScaling"); + addr[nt] = &All.ISMDustChem_SNeGasClearedOfDustScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"AccretionTcutoffScaling"); + addr[nt] = &All.ISMDustChem_AccretionTcutoffScaling; + id[nt++] = REAL; + +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + + strcpy(tag[nt], "UnitGrainNumber"); + addr[nt] = &All.UnitGrainNumber; + id[nt++] = REAL; + + strcpy(tag[nt], "UnitGrainLength_in_cm"); + addr[nt] = &All.UnitGrainLength_in_cm; + id[nt++] = REAL; + + strcpy(tag[nt],"ISMDustChem_Grain_Size_Min"); + addr[nt] = &All.ISMDustChem_Grain_Size_Min; + id[nt++] = REAL; + + strcpy(tag[nt],"ISMDustChem_Grain_Size_Max"); + addr[nt] = &All.ISMDustChem_Grain_Size_Max; + id[nt++] = REAL; + + strcpy(tag[nt],"SNeShatteringScaling"); + addr[nt] = &All.ISMDustChem_SNeShatteringScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"SNeSputteringScaling"); + addr[nt] = &All.ISMDustChem_SNeSputteringScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"GrainShatteringScaling"); + addr[nt] = &All.ISMDustChem_ShatteringScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"GrainCoagulationScaling"); + addr[nt] = &All.ISMDustChem_CoagulationScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"VCoagScaling"); + addr[nt] = &All.ISMDustChem_VCoagScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"CoagDensityEnhancementScaling"); + addr[nt] = &All.ISMDustChem_CoagDensityEnhancementScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"GrainVelocityScaling"); + addr[nt] = &All.ISMDustChem_GrainVelocityScaling; + id[nt++] = REAL; + + strcpy(tag[nt],"PhotodestructionScaling"); + addr[nt] = &All.ISMDustChem_PhotodestructionScaling; + id[nt++] = REAL; +#endif #endif #ifdef GALSF_FB_FIRE_STELLAREVOLUTION @@ -1878,6 +1977,12 @@ void read_parameter_file(char *fname) addr[nt] = &All.RadiationBackgroundRedshift; id[nt++] = REAL; #endif +#ifdef RT_INFRARED + strcpy(tag[nt], "InitRadiationTemp"); + strcpy(alternate_tag[nt], "InitRadTemp"); + addr[nt] = &All.InitRadiationTemp; + id[nt++] = REAL; +#endif #ifdef AGS_KERNELRADIUS_CALCULATION_IS_ACTIVE strcpy(tag[nt], "AGS_DesNumNgb"); @@ -2308,6 +2413,24 @@ void read_parameter_file(char *fname) #if defined(GALSF_ISMDUSTCHEM_MODEL) if(strcmp("Initial_ISMDustChem_Depletion",tag[i])==0) {*((double *)addr[i])=0; printf("Tag %s (%s) not set in parameter file: defaulting to zero (=%g) \n",tag[i],alternate_tag[i],All.Initial_ISMDustChem_Depletion); continue;} if(strcmp("Initial_ISMDustChem_SiltoCarbRatio",tag[i])==0) {*((double *)addr[i])=0.; printf("Tag %s (%s) not set in parameter file: defaulting to zero (=%g)\n",tag[i],alternate_tag[i],All.Initial_ISMDustChem_SiliconToCarbonRatio); continue;} + if(strcmp("SNeIIDustScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_SNeIIDustScaling); continue;} + if(strcmp("SNeIaDustScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_SNeIaDustScaling); continue;} + if(strcmp("AGBDustScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_AGBDustScaling); continue;} + if(strcmp("DustAccretionScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_DustAccretionScaling); continue;} + if(strcmp("ThermalSputteringScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_ThermalSputteringScaling); continue;} + if(strcmp("SNeGasClearedOfDustScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_SNeGasClearedOfDustScaling); continue;} +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + if(strcmp("ISMDustChem_Grain_Size_Min",tag[i])==0) {*((double *)addr[i])=1E-7; printf("Tag %s (%s) not set in parameter file: defaulting to 1E-7 (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_Grain_Size_Min); continue;} + if(strcmp("ISMDustChem_Grain_Size_Max",tag[i])==0) {*((double *)addr[i])=1E-4; printf("Tag %s (%s) not set in parameter file: defaulting to 1E-4 (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_Grain_Size_Max); continue;} + if(strcmp("SNeShatteringScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_SNeShatteringScaling); continue;} + if(strcmp("SNeSputteringScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_SNeSputteringScaling); continue;} + if(strcmp("GrainShatteringScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_ShatteringScaling); continue;} + if(strcmp("GrainCoagulationScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_CoagulationScaling); continue;} + if(strcmp("VCoagScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_VCoagScaling); continue;} + if(strcmp("CoagDensityEnhancementScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_CoagDensityEnhancementScaling); continue;} + if(strcmp("GrainVelocityScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_GrainVelocityScaling); continue;} + if(strcmp("PhotodestructionScaling",tag[i])==0) {*((double *)addr[i])=1.; printf("Tag %s (%s) not set in parameter file: defaulting to one (=%g)\n",tag[i],alternate_tag[i],All.ISMDustChem_PhotodestructionScaling); continue;} +#endif #endif #ifdef GALSF_FB_FIRE_STELLAREVOLUTION if(strcmp("SNeIIEnergyFrac",tag[i])==0) {*((double *)addr[i])=1; printf("Tag %s (%s) not set in parameter file: defaulting to standard stellar-evolution-defaults (=%g) \n",tag[i],alternate_tag[i],All.SNe_Energy_Renormalization); continue;} @@ -2366,6 +2489,9 @@ void read_parameter_file(char *fname) if(strcmp("Redshift_RT_Background",tag[i])==0) {*((double *)addr[i])=0.0; printf("Tag %s (%s) not set in parameter file: defaulting to assuming z=0 background radiation field \n",tag[i],alternate_tag[i]); continue;} #endif +#if defined(RT_INFRARED) + if(strcmp("InitRadiationTemp",tag[i])==0) {*((double *)addr[i])=20.; printf("Tag %s (%s) not set in parameter file: defaulting to assuming 20K radiation temperature \n",tag[i],alternate_tag[i]); continue;} +#endif #if defined(FIRE_PHYSICS_DEFAULTS) if(strcmp("SfEffPerFreeFall",tag[i])==0) {*((double *)addr[i])=1; printf("Tag %s (%s) not set in parameter file: defaulting to FIRE-default of unity (=%g) \n",tag[i],alternate_tag[i],All.MaxSfrTimescale); continue;} #if (FIRE_PHYSICS_DEFAULTS >= 3) @@ -2538,7 +2664,9 @@ void read_parameter_file(char *fname) #ifdef GALSF All.MaxNumNgbDeviation = All.DesNumNgb / 64.; #endif +#if NUMDIMS==3 if(All.MaxNumNgbDeviation < 0.05) All.MaxNumNgbDeviation = 0.05; +#endif #ifdef EOS_ELASTIC All.MaxNumNgbDeviation /= 20.0; #endif diff --git a/core/init.cc b/core/init.cc index d373b6bcc..d86ef25a1 100644 --- a/core/init.cc +++ b/core/init.cc @@ -209,6 +209,9 @@ void init(void) All.SolarAbundances[6]=7.57e-4; All.SolarAbundances[7]=7.12e-4; All.SolarAbundances[8]=3.31e-4; All.SolarAbundances[9]=6.87e-5; All.SolarAbundances[10]=1.38e-3;} #endif #endif +#if defined(GALSF_ISMDUSTCHEM_MODEL) + Initialize_ISMDustChem_Global_Variables(); +#endif #endif @@ -391,7 +394,7 @@ void init(void) } // if(RestartFlag == 0) #if defined(GALSF_ISMDUSTCHEM_MODEL) - Initialize_ISMDustChem_Variables(i); + if (P[i].Type == 0) {Initialize_ISMDustChem_Particle_Variables(i);} #endif #ifdef CHIMES @@ -441,6 +444,10 @@ void init(void) #if (SINGLE_STAR_SINK_FORMATION & 8) P[i].Sink_Ngb_Flag = 0; #endif +#ifdef SINGLE_STAR_FB_TIMESTEP_LIMIT + // start with a large value (> plausible values v_ejecta or v_wind) as a conservative choice when starting up a simulation with an active feedback-emmiting star - this will get updated to a more reasonable value once the particle walks the gravity tree, but need this to ensure the first timestep is stable. + P[i].MaxFeedbackVel = 1e4 / UNIT_VEL_IN_KMS; +#endif #ifdef SINGLE_STAR_TIMESTEPPING P[i].Min_Sink_Approach_Time = P[i].Min_Sink_Freefall_time = MAX_REAL_NUMBER; #if (SINGLE_STAR_TIMESTEPPING > 0) diff --git a/core/proto.h b/core/proto.h index 69a4a93af..741a203fd 100644 --- a/core/proto.h +++ b/core/proto.h @@ -570,17 +570,46 @@ double get_age_tracer_bin_start_time(int k); #if defined(GALSF_ISMDUSTCHEM_MODEL) -void update_dust_acc_and_sput(int i, double dtime_gyr); -void get_SNe_dust(double *yields, double *dust_yields, double *species_yields, double t_gyr); +void Initialize_ISMDustChem_Global_Variables(); +void Initialize_ISMDustChem_Particle_Variables(int i); +void update_dust_processes(int i, double dtime_gyr); void ISMDustChem_get_SNe_dust_yields(double *yields, int i, double t_gyr, int SNeIaFlag, double Msne); void ISMDustChem_get_wind_dust_yields(double *yields, int i); -double specific_Z_AGB_dust(int dust_type, double star_age, int z_bound); +double specific_Z_AGB_dust(int spec_indx, double star_age, int z_bound); double cumulative_AGB_dust_returns(int dust_type, double star_age, double z); +void update_dense_molecular_fields(int i, double temp, double rho, double nh0, double ne); +void update_dust_accretion(int i, double dtime_gyr, double temp, double rho); +void update_dust_sputtering(int i, double dtime_gyr, double temp, double rho); double Lambda_Dust_HighTemperature_Gas_ISM(int target, double T, double n_elec); -void Initialize_ISMDustChem_Variables(int i); -double return_ismdustchem_species_of_interest_for_diffusion_and_yields(int i, int k); -double ISMDustChem_Return_Mass_Fraction_Where_Dust_Destroyed(double rho_cell_in_code_units, double Esne51_into_cell, double mass_preshock_in_code_units); -void update_ISMDustChem_after_mechanical_injection(int j, double massfrac_destroyed, double m0, double mf, double *Z_injected); +double return_ismdustchem_species_of_interest_for_diffusion_and_yields(int i, int k, double mass); +double ISMDustChem_Return_Mass_Where_Dust_Shocked(double rho_cell_in_code_units, double Esne51_into_cell, double mass_preshock_in_code_units, double Z_cell); +void update_ISMDustChem_after_mechanical_injection(int j, double mass_shocked, double m0, double mf, double *Z_injected); +void ISMDustChem_update_iron_inclusions(int i); +void ISMDustChem_get_elem_yields_from_species_yields(double *dust_yields, double *species_yields); +void ISMDustChem_get_species_key_elem(int spec_indx, double *dust_metallicity, int *key_elem, double *key_num_atoms, double *key_mass); +void ISMDustChem_get_species_properties(int spec_indx, double *dust_atomic_weight, double *bulk_dens); +void ISMDustChemEvo_renormalize_dust_fields(int i); +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) +void Initialize_ISMDustChemEvo_Particle_Variables(int i); +double get_ISMDustChemEvo_bin_mass(int i, int j, int k); +void update_ISMDustChemEvo_bin_number_and_slope(int i, int j, int k, double number_in_bin, double mass_in_bin); +void check_for_slope_limiting(int k, double bulk_dens, double *number_in_bin, double *slope_in_bin, double mass_in_bin); +void ISMDustChemEvo_get_SNe_dust_grain_size_yields(double *yields, int i, int SNeIaFlag, double Msne); +void ISMDustChemEvo_get_wind_dust_grain_size_yields(double *yields, double Msne); +void ISMDustChemEvo_update_bins_given_grain_size_change(int i, int j, double *bin_da, double mass_limit); +void update_dust_shattering_and_coagulation(int i, double dtime_gyr, double temp, double rho); +void update_dust_photodestruction(int i, double dtime_gyr); +void ISMDustChemEvo_precompute_poly_coeffs(void); +double ISMDustChemEvo_fast_shat_coag_poly(int i, int spec_indx, int bin_i, int bin_j); +double ISMDustChemEvo_explicit_shat_coag_poly(double ail, double aiu, double aic, double ajl, double aju, double ajc, double Ni, double si, double Nj, double sj); +void ISMDustChemEvo_update_bins_given_mass_change(int i, int j, double *bin_dM, double bulk_dens); +void ISMDustChemEvo_get_new_bin_N_and_slope_given_mass_change(double *bin_dM, double *bin_M, double *bin_N, double *bin_slope, double *new_bin_N, double *new_bin_slope, double bulk_dens); +void ISMDustChem_SNe_sputtering_step(int spec_indx, double *init_bin_N, double *init_bin_slope, double *init_bin_M, double *final_bin_N, double *final_bin_slope, double *final_bin_M, double bulk_dens); +void ISMDustChem_SNe_shattering_step(int spec_indx, double *init_bin_N, double *init_bin_slope, double *init_bin_M, double *final_bin_N, double *final_bin_slope, double *final_bin_M, double bulk_dens); +// Below functions only for debugging +void ISMDustChemEvo_check_bins_after_update(int i, int update_process, double mass); +void ISMDustChemEvo_check_yields_before_update(double *bin_nums, double *bin_slopes, double *bin_masses, int yields_process, int species_num, double total_mass); +#endif #endif @@ -880,6 +909,7 @@ void rt_update_driftkick(int i, double dt_entr, int mode); #ifdef RT_SOURCE_INJECTION void rt_source_injection(void); #endif +MyFloat dust_planck_mean_opacity(MyFloat Trad, MyFloat Tdust); #ifdef RADTRANSFER void rt_set_simple_inits(int RestartFlag); diff --git a/core/timestep.cc b/core/timestep.cc index 1d15d04d9..5ccf614c5 100644 --- a/core/timestep.cc +++ b/core/timestep.cc @@ -1281,6 +1281,9 @@ void process_wake_ups(void) MPI_Allreduce(&NeedToWakeupParticles_local, &NeedToWakeupParticles, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD); // if one process processes wakeups then they all should, just in case a woke particle gets swapped to another process before we get here + int wakeup_bin_offset = 0; + while(((integertime)1 << wakeup_bin_offset) < (integertime)WAKEUP) wakeup_bin_offset++; + if(NeedToWakeupParticles){ for(i = 0; i < NumPart; i++) { @@ -1292,7 +1295,16 @@ void process_wake_ups(void) binold = P[i].TimeBin; if(TimeBinActive[binold]) {continue;} - bin = max_time_bin_active < binold ? max_time_bin_active : binold; + if(P[i].wakeup > 0) { + /* hydro wakeup: target timestep = dt_waker / WAKEUP */ + int waker_bin = P[i].wakeup - 1; + bin = IMAX(0, waker_bin - wakeup_bin_offset); + } else { + /* generic wakeup (sinks, merge, etc.): use highest active bin */ + bin = max_time_bin_active; + } + if(bin > max_time_bin_active) {bin = max_time_bin_active;} /* must be active at next kick */ + if(bin >= binold) {bin = binold;} /* don't increase timestep */ if(bin != binold) { diff --git a/declarations/allvars.h b/declarations/allvars.h index 680982cf7..4ee224006 100644 --- a/declarations/allvars.h +++ b/declarations/allvars.h @@ -623,6 +623,9 @@ extern struct global_data_all_processes double InterstellarRadiationFieldStrength; double RadiationBackgroundRedshift; #endif +#ifdef RT_INFRARED + double InitRadiationTemp; +#endif #ifdef RT_LEBRON double PhotonMomentum_Coupled_Fraction; @@ -782,9 +785,46 @@ extern struct global_data_all_processes double Initial_ISMDustChem_SiliconToCarbonRatio; /* sets rough mass ratio between silicates are carbonaceous dust for given initial depletion */ double ISMDustChem_AtomicMassTable[NUM_ISMDUSTCHEM_ELEMENTS]; /* atomic mass for each element in metallicity field */ double ISMDustChem_SNeSputteringShutOffTime; /* amount of time to turn off thermal sputtering after SNe event to avoid double counting dust destruction */ - int ISMDustChem_SilicateMetallicityFieldIndexTable[GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES]; /* index in metallicity field for elements which make up silicate dust (O, Mg, Si, and possibly Fe) */ - double ISMDustChem_SilicateNumberOfAtomsTable[GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES]; /* number of O, Mg, Si, and possibly Fe in one formula unit of silicate dust */ + int ISMDustChem_SilicateMetallicityFieldIndexTable[GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES]; /* index in metallicity field for elements which make up silicate dust (O, Mg, Si, and Fe) */ + double ISMDustChem_SilicateNumberOfAtomsTable[GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES]; /* number of O, Mg, Si, and Fe in one formula unit of silicate dust */ double ISMDustChem_EffectiveSilicateDustAtomicWeight; /* atomic weight of one formula unit of silicate dust, depends on which optional module you use */ + // Scaling arguements from parameter file used to adjust each dust process + double ISMDustChem_SNeIIDustScaling; + double ISMDustChem_SNeIaDustScaling; + double ISMDustChem_AGBDustScaling; + double ISMDustChem_DustAccretionScaling; + double ISMDustChem_ThermalSputteringScaling; + double ISMDustChem_AccretionTcutoffScaling; + double ISMDustChem_SNeGasClearedOfDustScaling; + double ISMDustChem_SpeciesBulkDens[3]; /* condensed bulk density for silicates, carbonaceous, and metallic iron */ + int ISMDustChem_TrackedSpeciesIDTable[NUM_ISMDUSTCHEM_SPECIES]; /* contains unique ID numbers for each tracked dust species which correspond to their location in ISMDustChem_SpeciesFieldIndexTable. Returns -1 for untracked species */ + int ISMDustChem_SpeciesFieldIndexTable[NUM_ISMDUSTCHEM_SPECIES]; /* index in dust species field for given dust species. Length should be equal to the number of unique indices listed below. */ + int ISMDustChem_Sil_Index; + int ISMDustChem_Carb_Index; + int ISMDustChem_FreeIron_Index ; + int ISMDustChem_ORes_Index; + int ISMDustChem_InclIron_Index; +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + double UnitGrainNumber; /* factor to convert internal grain number unit to number of grains */ + double UnitGrainLength_in_cm; /* factor to convert internal grain length unit to cm */ + double ISMDustChem_SNeShatteringScaling; + double ISMDustChem_SNeSputteringScaling; + double ISMDustChem_ShatteringScaling; + double ISMDustChem_CoagulationScaling; + double ISMDustChem_VCoagScaling; + double ISMDustChem_CoagDensityEnhancementScaling; + double ISMDustChem_GrainVelocityScaling; + double ISMDustChem_PhotodestructionScaling; /* dispersion of grain velocities in the ISM */ + double ISMDustChem_Grain_Size_Min; + double ISMDustChem_Grain_Size_Max; + double ISMDustChem_GrainBinSize; /* bin width of logarithmically spaced grain sizes */ + double ISMDustChem_GrainBinEdges[NUM_ISMDUSTCHEM_SIZE_BINS+1]; /* edges of each grain size bin */ + double ISMDustChem_GrainBinCenters[NUM_ISMDUSTCHEM_SIZE_BINS]; /* centers of each grain size bin in log space */ + double ISMDustChem_C_NiNj[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS]; /* pre-computed coefficients for coagulation/shattering polynomial */ + double ISMDustChem_C_Njsi[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS]; + double ISMDustChem_C_Nisj[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS]; + double ISMDustChem_C_sisj[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS]; +#endif #endif @@ -1205,6 +1245,9 @@ enum iofields IO_DUSTCHEMZMET, IO_DUSTCHEMSPECIESMET, IO_ISMDUSTCHEMMOL, + IO_MACHNUM, + IO_DUSTCHEMGRAINBINNUMBERS, + IO_DUSTCHEMGRAINBINMASS, IO_SINKMASS, IO_SINKMASSALPHA, IO_SINK_ANGMOM, @@ -1233,6 +1276,8 @@ enum iofields IO_NHRATE, IO_HHRATE, IO_MCRATE, + IO_DCRATE, + IO_PHRATE, IO_DTENTR, IO_TSTP, IO_BFLD, @@ -1309,6 +1354,7 @@ enum iofields IO_DENS_AROUND_STAR, IO_DELAY_TIME_HII, IO_MOLECULARFRACTION, + IO_SHOCKMACHNUM, IO_LASTENTRY /* This should be kept - it signals the end of the list */ }; diff --git a/declarations/cell_data.h b/declarations/cell_data.h index 44942961d..c60fe429f 100644 --- a/declarations/cell_data.h +++ b/declarations/cell_data.h @@ -28,14 +28,28 @@ extern struct gas_cell_data MyDouble Volume_1; /*!< 1st-order cell volume for mesh-free (MFM/MFV-type) reconstruction at 1st-order volume quadrature */ #endif -#if defined(GALSF_ISMDUSTCHEM_MODEL) +#ifdef OUTPUT_MACH_NUMBER + MyDouble ISMDustChem_MachNumber; /*!< mach number used for sub-resolution density enhancements from turbulence */ +#endif +#ifdef OUTPUT_SHOCK_MACH_NUMBER + MyFloat ShockMachNumber; /*!< estimated local shock Mach number from pairwise Riemann problem */ +#endif +#ifdef GALSF_ISMDUSTCHEM_MODEL MyDouble ISMDustChem_Dust_Source[NUM_ISMDUSTCHEM_SOURCES]; /*!< amount of dust from each source of dust creation. 0=gas-dust accretion, 1=Sne Ia, 2=SNe II, 3=AGB */ MyDouble ISMDustChem_Dust_Metal[NUM_ISMDUSTCHEM_ELEMENTS]; /*!< metallicity (species-by-species) of dust */ - MyDouble ISMDustChem_Dust_Species[NUM_ISMDUSTCHEM_SPECIES]; /*!< metallicity of dust species types. 0=silicates, 1=carbon, 2=SiC, 3=free-flying iron, (optional) 4=oxygen reservoir, (optional) 5=iron inclusions in silicates */ + MyDouble ISMDustChem_Dust_Species[NUM_ISMDUSTCHEM_SPECIES]; /*!< metallicity of dust species types */ MyDouble ISMDustChem_DelayTimeSNeSputtering; /*!< delay time for thermal sputtering due to recent SNe, used to not double count dust destruction with thermal sputtering */ +#if (!defined(RADTRANSFER) && !defined(RT_INFRARED)) && (defined(OUTPUT_DUST_TEMPERATURE) && (GALSF_FB_FIRE_STELLAREVOLUTION > 2)) + MyFloat Dust_Temperature; +#endif +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + MyDouble ISMDustChem_Dust_NumberInBin[NUM_ISMDUSTCHEM_SPECIES][NUM_ISMDUSTCHEM_SIZE_BINS]; + MyDouble ISMDustChem_Dust_SlopeInBin[NUM_ISMDUSTCHEM_SPECIES][NUM_ISMDUSTCHEM_SIZE_BINS]; +#else MyDouble ISMDustChem_C_in_CO; /*!< C metallicity locked in CO */ MyDouble ISMDustChem_MassFractionInDenseMolecular; /*!< mass fraction of gas in dense MC phase */ #endif +#endif #ifdef MAGNETIC MyDouble Face_Area[3]; /*!< vector sum of effective areas of 'faces'; this is used to check closure for meshless methods */ @@ -123,6 +137,10 @@ extern struct gas_cell_data #endif #ifdef RT_COMPGRAD_EDDINGTON_TENSOR MyDouble Rad_E_gamma_ET[N_RT_FREQ_BINS][3]; +#endif +#if defined(RT_M1_SECONDORDER) && defined(RT_EVOLVE_FLUX) + MyFloat Rad_E_gamma_Grad[N_RT_FREQ_BINS][3]; + MyFloat Rad_Flux_Grad[N_RT_FREQ_BINS][3][3]; #endif } Gradients; MyDouble NV_T[3][3]; /*!< holds the tensor used for gradient estimation */ @@ -373,6 +391,10 @@ extern struct gas_cell_data MyFloat NetHeatingRateQ; MyFloat HydroHeatingRate; MyFloat MetalCoolingRate; + MyFloat PElecHeatingRate; +#if defined(GALSF_ISMDUSTCHEM_MODEL) && defined(GALSF_ISMDUSTCHEM_HIGHTEMPDUSTCOOLING) + MyFloat DustCoolingRate; +#endif #endif #if defined(COOLING) && defined(COOL_GRACKLE) diff --git a/declarations/precompiler_logic.h b/declarations/precompiler_logic.h index 8f00109a7..ef65b192c 100644 --- a/declarations/precompiler_logic.h +++ b/declarations/precompiler_logic.h @@ -1077,27 +1077,27 @@ #if defined(COOLING) #define GALSF_ISMDUSTCHEM_HIGHTEMPDUSTCOOLING // optional, can turn off #endif +#ifndef OUTPUT_MOLECULAR_FRACTION +#define OUTPUT_MOLECULAR_FRACTION // useful since we track dense molecular fraction +#endif + #define NUM_ISMDUSTCHEM_ELEMENTS (1+NUM_LIVE_SPECIES_FOR_COOLTABLES) // number of metal species evolved for dust #define NUM_ISMDUSTCHEM_SOURCES (4) // Sources of dust creation/growth 0=gas-dust accretion, 1=SNe Ia, 2=SNe II, 3=AGB outflows -#if (GALSF_ISMDUSTCHEM_MODEL & 2) -#if (GALSF_ISMDUSTCHEM_MODEL & 4) && (GALSF_ISMDUSTCHEM_MODEL & 8) -#define NUM_ISMDUSTCHEM_SPECIES 6 /* 0=silicates, 1=carbonaceous, 2=SiC, 3=free-flying iron, 4=O reservoir, 5=iron inclusions in silicates */ -#elif (GALSF_ISMDUSTCHEM_MODEL & 4) || (GALSF_ISMDUSTCHEM_MODEL & 8) -#define NUM_ISMDUSTCHEM_SPECIES 5 /* 0=silicates, 1=carbonaceous, 2=SiC, 3=free-flying iron, 4=O reservoir or iron inclusions in silicates */ -#else -#define NUM_ISMDUSTCHEM_SPECIES 4 /* 0=silicates, 1=carbonaceous, 2=SiC, 3=free-flying iron */ -#endif -#else -#define NUM_ISMDUSTCHEM_SPECIES 0 /* no explicit dust species evolved */ -#endif -#if (GALSF_ISMDUSTCHEM_MODEL & 4) // explicit iron nanoparticle model active -#define GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES 3 /* Assume only O, Mg, and Si in silicate structure while Fe is already present via iron inclusions */ -#else +#define NUM_ISMDUSTCHEM_SPECIES (2*(GALSF_ISMDUSTCHEM_MODEL & 1) + (GALSF_ISMDUSTCHEM_MODEL & 2)/2 + (GALSF_ISMDUSTCHEM_MODEL & 4)/4 + (GALSF_ISMDUSTCHEM_MODEL & 8)/8) /* number of dust species tracked, depends on the model */ #define GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES 4 /* O, Mg, Si, and Fe needed to make silicates */ -#endif #undef NUM_ADDITIONAL_PASSIVESCALAR_SPECIES_FOR_YIELDS_AND_DIFFUSION +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO +#ifndef OUTPUT_MACH_NUMBER // mach number is used for subresolved clumping in grain size evo model +#define OUTPUT_MACH_NUMBER +#endif +#define NUM_ISMDUSTCHEM_SIZE_BINS (GALSF_ISMDUSTCHEM_GRAINSIZEEVO) +#define UNIT_GRAIN_NUMBER (All.UnitGrainNumber) +#define UNIT_GRAIN_LENGTH (All.UnitGrainLength_in_cm) +#define NUM_ADDITIONAL_PASSIVESCALAR_SPECIES_FOR_YIELDS_AND_DIFFUSION (NUM_ISMDUSTCHEM_ELEMENTS+NUM_ISMDUSTCHEM_SOURCES+NUM_ISMDUSTCHEM_SPECIES+(2*NUM_ISMDUSTCHEM_SPECIES*NUM_ISMDUSTCHEM_SIZE_BINS)) +#else #define NUM_ADDITIONAL_PASSIVESCALAR_SPECIES_FOR_YIELDS_AND_DIFFUSION (NUM_ISMDUSTCHEM_ELEMENTS+NUM_ISMDUSTCHEM_SOURCES+NUM_ISMDUSTCHEM_SPECIES) #endif +#endif // end of GALSF_ISMDUSTCHEM_MODEL block /* end of metals block */ diff --git a/eos/eos.cc b/eos/eos.cc index 90bd98f21..12a5682d1 100644 --- a/eos/eos.cc +++ b/eos/eos.cc @@ -342,7 +342,7 @@ double return_dust_to_metals_ratio_vs_solar(int i, double T_dust_manual_override double Z_scaled = P[i].Metallicity[0]/All.SolarAbundances[0]; // metallicity of the particle in solar return (kappa_interp_geo_cgs / kappa_solar_geo_cgs) / (Z_scaled); // will be multiplied by metallicity to convert later #endif -#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_PASSIVE) +#if defined(GALSF_ISMDUSTCHEM_MODEL) if(P[i].Metallicity[0]>0) {return (CellP[i].ISMDustChem_Dust_Metal[0]/P[i].Metallicity[0])/0.5;} else {return 0;} // use total amount of dust from 'live' dust evolution models #endif #if defined(RT_INFRARED) diff --git a/file_io/io.cc b/file_io/io.cc index 796460d40..5809316bc 100644 --- a/file_io/io.cc +++ b/file_io/io.cc @@ -417,6 +417,28 @@ void fill_write_buffer(enum iofields blocknr, int *startindex, int pc, int type) #endif break; + case IO_DCRATE: +#if defined(OUTPUT_COOLRATE_DETAIL) && defined(COOLING) && defined(GALSF_ISMDUSTCHEM_MODEL) && defined(GALSF_ISMDUSTCHEM_HIGHTEMPDUSTCOOLING) + for(n = 0; n < pc; pindex++) + if(P[pindex].Type == type) + { + *fp++ = (MyOutputFloat) CellP[pindex].DustCoolingRate; + n++; + } +#endif + break; + + case IO_PHRATE: +#if defined(OUTPUT_COOLRATE_DETAIL) && defined(COOLING) + for(n = 0; n < pc; pindex++) + if(P[pindex].Type == type) + { + *fp++ = (MyOutputFloat) CellP[pindex].PElecHeatingRate; + n++; + } +#endif + break; + case IO_KERNELRADIUS: /* gas kernel length */ for(n = 0; n < pc; pindex++) if(P[pindex].Type == type) @@ -481,7 +503,7 @@ void fill_write_buffer(enum iofields blocknr, int *startindex, int pc, int type) #endif break; - case IO_DUST_TO_GAS: /* grain size */ + case IO_DUST_TO_GAS: /* dust to gas mass ratio */ #ifdef OUTPUT_DUST_TO_GAS_RATIO for(n = 0; n < pc; pindex++) if(P[pindex].Type == type) @@ -568,7 +590,7 @@ void fill_write_buffer(enum iofields blocknr, int *startindex, int pc, int type) break; case IO_DUSTCHEMSPECIESMET: /* gas dust species following Species routines */ -#if (GALSF_ISMDUSTCHEM_MODEL & 2) +#if defined(GALSF_ISMDUSTCHEM_MODEL) for(n = 0; n < pc; pindex++) if(P[pindex].Type == type) { @@ -579,8 +601,8 @@ void fill_write_buffer(enum iofields blocknr, int *startindex, int pc, int type) #endif break; - case IO_ISMDUSTCHEMMOL: /* sub-resolved molecular gas properties (fraction of C in CO and fraction of gas that is in dense molecular phase) used in dust routines */ -#if defined(GALSF_ISMDUSTCHEM_MODEL) + case IO_ISMDUSTCHEMMOL: /* sub-resolved molecular gas properties (fraction of C in CO and fraction of gas that is in dense molecular phase) used in dust routines wiuthout grain size evolution */ +#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) for(n = 0; n < pc; pindex++) if(P[pindex].Type == type) { @@ -592,6 +614,62 @@ void fill_write_buffer(enum iofields blocknr, int *startindex, int pc, int type) #endif break; + case IO_MACHNUM: /* sub-resolved molecular gas properties (fraction of C in CO and fraction of gas that is in dense molecular phase) used in dust routines */ +#if defined(OUTPUT_MACH_NUMBER) + for(n = 0; n < pc; pindex++) + if(P[pindex].Type == type) + { + *fp++ = (MyOutputFloat) CellP[pindex].ISMDustChem_MachNumber; + n++; + } +#endif + break; + + case IO_SHOCKMACHNUM: /* local shock Mach number from pairwise Riemann problem */ +#if defined(OUTPUT_SHOCK_MACH_NUMBER) + for(n = 0; n < pc; pindex++) + if(P[pindex].Type == type) + { + *fp++ = (MyOutputFloat) CellP[pindex].ShockMachNumber; + n++; + } +#endif + break; + + case IO_DUSTCHEMGRAINBINNUMBERS: /* number of grains for each grain size bin for each dust species */ +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + for(n = 0; n < pc; pindex++) + if(P[pindex].Type == type) + { + int k1, k2; + for(k1=0;k1 2) for(n = 0; n < pc; pindex++) if(P[pindex].Type == type) { @@ -1776,6 +1854,8 @@ int get_bytes_per_blockelement(enum iofields blocknr, int mode) case IO_NHRATE: case IO_HHRATE: case IO_MCRATE: + case IO_DCRATE: + case IO_PHRATE: case IO_KERNELRADIUS: case IO_SFR: case IO_AGE: @@ -1922,7 +2002,7 @@ int get_bytes_per_blockelement(enum iofields blocknr, int mode) #endif break; - case IO_DUSTCHEMZMET: + case IO_DUSTCHEMZMET: #if defined(GALSF_ISMDUSTCHEM_MODEL) if(mode) bytes_per_blockelement = (NUM_ISMDUSTCHEM_ELEMENTS + NUM_ISMDUSTCHEM_SOURCES) * sizeof(MyInputFloat); @@ -1931,8 +2011,8 @@ int get_bytes_per_blockelement(enum iofields blocknr, int mode) #endif break; - case IO_DUSTCHEMSPECIESMET: -#if (GALSF_ISMDUSTCHEM_MODEL & 2) + case IO_DUSTCHEMSPECIESMET: +#if defined(GALSF_ISMDUSTCHEM_MODEL) if(mode) bytes_per_blockelement = (NUM_ISMDUSTCHEM_SPECIES) * sizeof(MyInputFloat); else @@ -1940,11 +2020,41 @@ int get_bytes_per_blockelement(enum iofields blocknr, int mode) #endif break; - case IO_ISMDUSTCHEMMOL: + case IO_ISMDUSTCHEMMOL: +#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) if(mode) bytes_per_blockelement = 2 * sizeof(MyInputFloat); else bytes_per_blockelement = 2 * sizeof(MyOutputFloat); +#endif + break; + + case IO_MACHNUM: +#if defined(OUTPUT_MACH_NUMBER) + if(mode) + bytes_per_blockelement = sizeof(MyInputFloat); + else + bytes_per_blockelement = sizeof(MyOutputFloat); +#endif + break; + + case IO_SHOCKMACHNUM: +#if defined(OUTPUT_SHOCK_MACH_NUMBER) + if(mode) + bytes_per_blockelement = sizeof(MyInputFloat); + else + bytes_per_blockelement = sizeof(MyOutputFloat); +#endif + break; + + case IO_DUSTCHEMGRAINBINNUMBERS: + case IO_DUSTCHEMGRAINBINMASS: +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + if(mode) + bytes_per_blockelement = (NUM_ISMDUSTCHEM_SPECIES * NUM_ISMDUSTCHEM_SIZE_BINS) * sizeof(MyInputFloat); + else + bytes_per_blockelement = (NUM_ISMDUSTCHEM_SPECIES * NUM_ISMDUSTCHEM_SIZE_BINS) * sizeof(MyOutputFloat); +#endif break; case IO_CHIMES_ABUNDANCES: @@ -2073,6 +2183,8 @@ int get_values_per_blockelement(enum iofields blocknr) case IO_NHRATE: case IO_HHRATE: case IO_MCRATE: + case IO_DCRATE: + case IO_PHRATE: case IO_KERNELRADIUS: case IO_SFR: case IO_AGE: @@ -2200,24 +2312,41 @@ int get_values_per_blockelement(enum iofields blocknr) case IO_DUSTCHEMZMET: #if defined(GALSF_ISMDUSTCHEM_MODEL) - values = NUM_ISMDUSTCHEM_ELEMENTS + NUM_ISMDUSTCHEM_SOURCES; -#else - values = 0; + values = (NUM_ISMDUSTCHEM_ELEMENTS + NUM_ISMDUSTCHEM_SOURCES); #endif break; case IO_DUSTCHEMSPECIESMET: -#if (GALSF_ISMDUSTCHEM_MODEL & 2) +#if defined(GALSF_ISMDUSTCHEM_MODEL) values = NUM_ISMDUSTCHEM_SPECIES; -#else - values = 0; #endif break; case IO_ISMDUSTCHEMMOL: +#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) values = 2; +#endif + break; + + case IO_MACHNUM: +#if defined(OUTPUT_MACH_NUMBER) + values = 1; +#endif + break; + + case IO_SHOCKMACHNUM: +#if defined(OUTPUT_SHOCK_MACH_NUMBER) + values = 1; +#endif break; + case IO_DUSTCHEMGRAINBINNUMBERS: + case IO_DUSTCHEMGRAINBINMASS: +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + values = (NUM_ISMDUSTCHEM_SPECIES*NUM_ISMDUSTCHEM_SIZE_BINS); +#endif + break; + case IO_CHIMES_ABUNDANCES: #ifdef CHIMES values = ChimesGlobalVars.totalNumberOfSpecies; @@ -2340,6 +2469,8 @@ long get_particles_in_block(enum iofields blocknr, int *typelist) case IO_NHRATE: case IO_HHRATE: case IO_MCRATE: + case IO_DCRATE: + case IO_PHRATE: case IO_DELAYTIME: case IO_SFR: case IO_DTENTR: @@ -2397,6 +2528,10 @@ long get_particles_in_block(enum iofields blocknr, int *typelist) case IO_DUSTCHEMZMET: case IO_DUSTCHEMSPECIESMET: case IO_ISMDUSTCHEMMOL: + case IO_MACHNUM: + case IO_SHOCKMACHNUM: + case IO_DUSTCHEMGRAINBINNUMBERS: + case IO_DUSTCHEMGRAINBINMASS: for(i = 1; i < 6; i++) {typelist[i] = 0;} return ngas; break; @@ -2526,7 +2661,7 @@ int blockpresent(enum iofields blocknr) break; case IO_DUST_TEMP: -#if defined(RADTRANSFER) && defined(RT_INFRARED) +#if (defined(RADTRANSFER) && defined(RT_INFRARED)) || (defined(OUTPUT_DUST_TEMPERATURE) && (GALSF_FB_FIRE_STELLAREVOLUTION > 2)) return 1; #endif break; @@ -2587,17 +2722,36 @@ int blockpresent(enum iofields blocknr) break; case IO_DUSTCHEMSPECIESMET: -#if (GALSF_ISMDUSTCHEM_MODEL & 2) +#if defined(GALSF_ISMDUSTCHEM_MODEL) return 1; #endif break; case IO_ISMDUSTCHEMMOL: -#if defined(GALSF_ISMDUSTCHEM_MODEL) +#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) return 1; #endif break; + case IO_DUSTCHEMGRAINBINNUMBERS: + case IO_DUSTCHEMGRAINBINMASS: +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + return 1; +#endif + break; + + case IO_MACHNUM: +#if defined(OUTPUT_MACH_NUMBER) + return 1; +#endif + break; + + case IO_SHOCKMACHNUM: +#if defined(OUTPUT_SHOCK_MACH_NUMBER) + return 1; +#endif + break; + case IO_CHIMES_ABUNDANCES: #if defined(CHIMES_REDUCED_OUTPUT) if(Chimes_incl_full_output == 1) {return 1;} else {return 0;} @@ -2690,11 +2844,18 @@ int blockpresent(enum iofields blocknr) case IO_NHRATE: case IO_HHRATE: case IO_MCRATE: + case IO_PHRATE: #if defined(OUTPUT_COOLRATE_DETAIL) && defined(COOLING) return 1; #endif break; + case IO_DCRATE: +#if defined(OUTPUT_COOLRATE_DETAIL) && defined(COOLING) && defined(GALSF_ISMDUSTCHEM_MODEL) && defined(GALSF_ISMDUSTCHEM_HIGHTEMPDUSTCOOLING) + return 1; +#endif + break; + case IO_POT: #if defined(OUTPUT_POTENTIAL) return 1; @@ -3155,6 +3316,12 @@ void get_Tab_IO_Label(enum iofields blocknr, char *label) case IO_MCRATE: strncpy(label, "MCRATE", 4); break; + case IO_DCRATE: + strncpy(label, "DCRATE", 4); + break; + case IO_PHRATE: + strncpy(label, "PHRATE", 4); + break; case IO_KERNELRADIUS: strncpy(label, "HSML", 4); break; @@ -3191,6 +3358,18 @@ void get_Tab_IO_Label(enum iofields blocknr, char *label) case IO_ISMDUSTCHEMMOL: strncpy(label, "DMOL", 4); break; + case IO_MACHNUM: + strncpy(label, "MACH", 4); + break; + case IO_SHOCKMACHNUM: + strncpy(label, "SMAC", 4); + break; + case IO_DUSTCHEMGRAINBINNUMBERS: + strncpy(label, "DBNU", 4); + break; + case IO_DUSTCHEMGRAINBINMASS: + strncpy(label, "DBMA", 4); + break; case IO_CHIMES_ABUNDANCES: strncpy(label, "CHIM", 4); break; @@ -3562,6 +3741,12 @@ void get_dataset_name(enum iofields blocknr, char *buf) case IO_MCRATE: strcpy(buf, "MetalCoolingRate"); break; + case IO_DCRATE: + strcpy(buf, "DustCoolingRate"); + break; + case IO_PHRATE: + strcpy(buf, "PElecHeatingRate"); + break; case IO_DELAYTIME: strcpy(buf, "DelayTime"); break; @@ -3598,6 +3783,18 @@ void get_dataset_name(enum iofields blocknr, char *buf) case IO_ISMDUSTCHEMMOL: strcpy(buf, "DustMolecularSpeciesFractions"); break; + case IO_MACHNUM: + strcpy(buf, "MachNumber"); + break; + case IO_SHOCKMACHNUM: + strcpy(buf, "ShockMachNumber"); + break; + case IO_DUSTCHEMGRAINBINNUMBERS: + strcpy(buf, "DustBinNumbers"); + break; + case IO_DUSTCHEMGRAINBINMASS: + strcpy(buf, "DustBinMasses"); + break; case IO_CHIMES_ABUNDANCES: strcpy(buf, "ChimesAbundances"); break; @@ -4666,9 +4863,30 @@ void write_header_attributes_in_hdf5(hid_t handle) #ifdef GALSF_ISMDUSTCHEM_MODEL {int holder=NUM_ISMDUSTCHEM_SPECIES; hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "ISMDustChem_NumberOfSpecies", H5T_NATIVE_INT, hdf5_dataspace, H5P_DEFAULT); H5Awrite(hdf5_attribute, H5T_NATIVE_INT, &holder); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} -#ifdef GALSF_ISMDUSTCHEM_PASSIVE - {int holder=1; hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "ISMDustChem_PassiveDustEvolution", H5T_NATIVE_INT, hdf5_dataspace, H5P_DEFAULT); - H5Awrite(hdf5_attribute, H5T_NATIVE_INT, &holder); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} + /* list out composition (element key and number of atoms per element) of silicates since this changes between versions */ + // key for each element + {hdf5_dataspace = H5Screate(H5S_SIMPLE); hsize_t tmp_dim[1]={GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES}; + H5Sset_extent_simple(hdf5_dataspace, 1, tmp_dim, NULL); + hdf5_attribute = H5Acreate(handle, "Silicates_Element_Key", H5T_NATIVE_INT, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_INT, All.ISMDustChem_SilicateMetallicityFieldIndexTable); + H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} + // number of atoms per element + {hdf5_dataspace = H5Screate(H5S_SIMPLE); hsize_t tmp_dim[1]={GALSF_ISMDUSTCHEM_VAR_ELEM_IN_SILICATES}; + H5Sset_extent_simple(hdf5_dataspace, 1, tmp_dim, NULL); + hdf5_attribute = H5Acreate(handle, "Silicates_Element_Number", H5T_NATIVE_DOUBLE, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_DOUBLE, All.ISMDustChem_SilicateNumberOfAtomsTable); + H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + {int holder=NUM_ISMDUSTCHEM_SIZE_BINS; hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "ISMDustChem_Num_Grain_Size_Bins", H5T_NATIVE_INT, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_INT, &holder); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} + {double tmp=UNIT_GRAIN_NUMBER; hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "UnitGrainNumber", H5T_NATIVE_DOUBLE, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_DOUBLE, &tmp); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} + {double tmp=UNIT_GRAIN_LENGTH; hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "UnitGrainLength_in_cm", H5T_NATIVE_DOUBLE, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_DOUBLE, &tmp); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace);} + hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "ISMDustChem_Grain_Size_Min", H5T_NATIVE_DOUBLE, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_DOUBLE, &All.ISMDustChem_Grain_Size_Min); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace); + hdf5_dataspace = H5Screate(H5S_SCALAR); hdf5_attribute = H5Acreate(handle, "ISMDustChem_Grain_Size_Max", H5T_NATIVE_DOUBLE, hdf5_dataspace, H5P_DEFAULT); + H5Awrite(hdf5_attribute, H5T_NATIVE_DOUBLE, &All.ISMDustChem_Grain_Size_Max); H5Aclose(hdf5_attribute); H5Sclose(hdf5_dataspace); #endif #endif #endif // METALS diff --git a/file_io/read_ic.cc b/file_io/read_ic.cc index c971e0520..5debde844 100644 --- a/file_io/read_ic.cc +++ b/file_io/read_ic.cc @@ -274,23 +274,62 @@ void empty_read_buffer(enum iofields blocknr, int offset, int pc, int type) #if defined(GALSF_ISMDUSTCHEM_MODEL) for(n = 0; n < pc; n++) { for(k = 0; k < NUM_ISMDUSTCHEM_ELEMENTS; k++) {CellP[offset + n].ISMDustChem_Dust_Metal[k] = *fp++;} // Get dust fractions - for(k = 0; k < NUM_ISMDUSTCHEM_SOURCES; k++) {CellP[offset + n].ISMDustChem_Dust_Source[k] = *fp++;} // Then get the sources of dust + for(k = 0; k < NUM_ISMDUSTCHEM_SOURCES; k++) {CellP[offset + n].ISMDustChem_Dust_Source[k] = (*fp++) * CellP[offset + n].ISMDustChem_Dust_Metal[0];} // Then get the sources of dust, converting from dust mass fraction to total gas mass fraction } #endif break; case IO_DUSTCHEMSPECIESMET: -#if (GALSF_ISMDUSTCHEM_MODEL & 2) - for(n = 0; n < pc; n++) {for(k = 0; k < NUM_ISMDUSTCHEM_SPECIES; k++) {CellP[offset + n].ISMDustChem_Dust_Species[k] = *fp++;}} +#if defined(GALSF_ISMDUSTCHEM_MODEL) + for(n = 0; n < pc; n++) {for(k = 0; k < NUM_ISMDUSTCHEM_SPECIES; k++) {CellP[offset + n].ISMDustChem_Dust_Species[k] = *fp++;}} // Get dust species fractions #endif break; case IO_ISMDUSTCHEMMOL: /* gas dust species following Species routines */ -#if defined(GALSF_ISMDUSTCHEM_MODEL) +#if defined(GALSF_ISMDUSTCHEM_MODEL) && !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) for(n = 0; n < pc; n++) {CellP[offset + n].ISMDustChem_MassFractionInDenseMolecular = *fp++; CellP[offset + n].ISMDustChem_C_in_CO = *fp++;} #endif break; + case IO_DUSTCHEMGRAINBINNUMBERS: +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + // Need to check whether the number of grain size bins is the same as the snapshot. If not zero extra bins for now and recalcuate them later in the code + for(n = 0; n < pc; n++) { + int nmax=NUM_ISMDUSTCHEM_SIZE_BINS; + for(k=0;k 2)) for(n = 0; n < pc; n++) {CellP[offset + n].Dust_Temperature = *fp++;} #endif break; @@ -597,6 +636,8 @@ void empty_read_buffer(enum iofields blocknr, int offset, int pc, int type) case IO_NHRATE: case IO_HHRATE: case IO_MCRATE: + case IO_PHRATE: + case IO_DCRATE: case IO_TSTP: case IO_IMF: case IO_DIVB: @@ -638,6 +679,7 @@ void empty_read_buffer(enum iofields blocknr, int offset, int pc, int type) case IO_DELAY_TIME_HII: case IO_CHIMES_FLUX_G0: case IO_CHIMES_FLUX_ION: + case IO_MACHNUM: break; case IO_LASTENTRY: @@ -980,6 +1022,20 @@ void read_file(char *fname, int readTask, int lastTask) if(RestartFlag == 2 && blocknr == IO_MOLECULARFRACTION) {continue;} #endif +#if defined(IO_DUST_NOT_IN_ICFILE) +#if defined(GALSF_ISMDUSTCHEM_MODEL) + if(RestartFlag == 2 && blocknr == IO_DUST_TO_GAS) {continue;} + if(RestartFlag == 2 && blocknr == IO_DUSTCHEMZMET) {continue;} + if(RestartFlag == 2 && blocknr == IO_DUSTCHEMSPECIESMET) {continue;} + if(RestartFlag == 2 && blocknr == IO_ISMDUSTCHEMMOL) {continue;} + if(RestartFlag == 2 && blocknr == IO_DCRATE) {continue;} +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + if(RestartFlag == 2 && blocknr == IO_MACHNUM) {continue;} + if(RestartFlag == 2 && blocknr == IO_DUSTCHEMGRAINBINNUMBERS) {continue;} + if(RestartFlag == 2 && blocknr == IO_DUSTCHEMGRAINBINMASS) {continue;} +#endif +#endif +#endif if(blocknr == IO_HSMS) {continue;} @@ -1001,7 +1057,6 @@ void read_file(char *fname, int readTask, int lastTask) #ifdef METALS /* some trickery here to enable snapshot-restarts from runs with different numbers of metal species */ if(blocknr==IO_Z && RestartFlag==2 && All.ICFormat==3 && header.flag_metals0) {bytes_per_blockelement = (header.flag_metals) * sizeof(MyInputFloat);} #endif - size_t MyBufferSize = All.BufferSize; blockmaxlen = (size_t) ((MyBufferSize * 1024 * 1024) / bytes_per_blockelement); npart = get_particles_in_block(blocknr, &typelist[0]); diff --git a/galaxy_sf/mechanical_fb.cc b/galaxy_sf/mechanical_fb.cc index 23b3e2c94..a5d00da01 100644 --- a/galaxy_sf/mechanical_fb.cc +++ b/galaxy_sf/mechanical_fb.cc @@ -126,7 +126,7 @@ static struct temporary_mech_fb_data_tohold { int N_injected; double m_injected, p_injected[3], KE_injected, TE_injected, Z_injected[NUM_METAL_SPECIES+NUM_ADDITIONAL_PASSIVESCALAR_SPECIES_FOR_YIELDS_AND_DIFFUSION]; #if defined(GALSF_ISMDUSTCHEM_MODEL) - double Mass_Fraction_Where_Dust_Destroyed; + double Mass_Where_Dust_Shocked; #endif } *LocalGasMechFBInfoTemp; @@ -298,8 +298,11 @@ int addFB_evaluate(int target, int mode, int *exportflag, int *exportnodecount, Metallicity_j[k] = P[j].Metallicity[k]; // this can get modified below, so we need to read it thread-safe now } #if defined(GALSF_ISMDUSTCHEM_MODEL) - double Mass_Fraction_Where_Dust_Destroyed = 0; - for(k=NUM_METAL_SPECIES;k0) {for(k=NUM_METAL_SPECIES;k0 && loop_iteration < 2) { +#if defined(GALSF_ISMDUSTCHEM_MODEL) + // treat like any other yield when doing stellar mass exchange + for(k=NUM_METAL_SPECIES;k0) {for(k=NUM_METAL_SPECIES;k0 && loop_iteration < 2) { +#if defined(GALSF_ISMDUSTCHEM_MODEL) + // treat like any other yield when doing stellar mass exchange + for(k=NUM_METAL_SPECIES;k= 0) @@ -815,7 +832,7 @@ void verify_and_assign_local_mechfb_integrals(void) double mf=m0+dm; /* save for below */ for(k=0;kGQuant.Rad_E_gamma[k] = CellP[i].Rad_E_gamma_Pred[k]; int k_et; for(k_et = 0; k_et < 6; k_et++) {in->GQuant.Rad_E_gamma_ET[k][k_et] = CellP[i].ET[k][k_et];} +#if defined(RT_M1_SECONDORDER) && defined(RT_EVOLVE_FLUX) + int k_d; for(k_d = 0; k_d < 3; k_d++) {in->GQuant.Rad_Flux[k][k_d] = CellP[i].Rad_Flux_Pred[k][k_d];} +#endif } #endif #ifdef DOGRAD_INTERNAL_ENERGY @@ -473,6 +479,13 @@ static inline void out2particle_GasGrad(struct GasGraddata_out *out, int i, int MAX_ADD(GasGradDataPasser[i].Maxima.Rad_E_gamma[j],out->Maxima.Rad_E_gamma[j],mode); MIN_ADD(GasGradDataPasser[i].Minima.Rad_E_gamma[j],out->Minima.Rad_E_gamma[j],mode); for(k=0;k<3;k++) {ASSIGN_ADD_PRESET(GasGradDataPasser[i].Gradients_Rad_E_gamma[j][k],out->Gradients[k].Rad_E_gamma[j],mode);} +#if defined(RT_M1_SECONDORDER) && defined(RT_EVOLVE_FLUX) + {int k_d; for(k_d=0;k_d<3;k_d++) { + MAX_ADD(GasGradDataPasser[i].Maxima.Rad_Flux[j][k_d], out->Maxima.Rad_Flux[j][k_d], mode); + MIN_ADD(GasGradDataPasser[i].Minima.Rad_Flux[j][k_d], out->Minima.Rad_Flux[j][k_d], mode); + for(k=0;k<3;k++) {ASSIGN_ADD_PRESET(CellP[i].Gradients.Rad_Flux_Grad[j][k_d][k], out->Gradients[k].Rad_Flux[j][k_d], mode);} + }} +#endif } /* the gradient dotted into the Eddington tensor is more complicated: let's handle this below */ { @@ -629,6 +642,12 @@ void hydro_gradient_calc(void) #endif #ifdef RT_COMPGRAD_EDDINGTON_TENSOR for(k2=0;k2 0) { + double cf_a3inv_fac = (All.ComovingIntegrationOn) ? All.cf_a3inv : 1.0; + double P_L_phys = Riemann_vec.L.p * cf_a3inv_fac; /* face-reconstructed pressure from j */ + double P_R_phys = Riemann_vec.R.p * cf_a3inv_fac; /* face-reconstructed pressure from i */ + if(P_L_phys > 0 && P_R_phys > 0) { + /* upstream = lower-pressure side (pre-shock gas) */ + double P_up = (P_L_phys <= P_R_phys) ? P_L_phys : P_R_phys; + double cs_up = (P_L_phys <= P_R_phys) ? kernel.sound_j : kernel.sound_i; + if(cs_up > 0) { + double pjump = Riemann_out.P_M / P_up; + if(pjump > 1.0) { + /* Rankine-Hugoniot normal Mach number */ + double gamma_eos = GAMMA_DEFAULT; + double mach2_RH = ((gamma_eos + 1.0) * pjump + (gamma_eos - 1.0)) / (2.0 * gamma_eos); + double mach_RH = (mach2_RH > 1.0) ? sqrt(mach2_RH) : 1.0; + if(mach_RH > 1.0) { + if(mach_RH > out.MaxShockMachNumber) {out.MaxShockMachNumber = (MyFloat)mach_RH;} + if(j_is_active_for_fluxes && mach_RH > CellP[j].ShockMachNumber) {CellP[j].ShockMachNumber = (MyFloat)mach_RH;} + } + } + } + } + } + } +#endif /* OUTPUT_SHOCK_MACH_NUMBER && !EOS_GENERAL */ + } else { /* nothing but bad riemann solutions found! */ memset(&Fluxes, 0, sizeof(struct Conserved_var_Riemann)); diff --git a/hydro/hydro_evaluate.h b/hydro/hydro_evaluate.h index 19b496233..a4ca0c117 100644 --- a/hydro/hydro_evaluate.h +++ b/hydro/hydro_evaluate.h @@ -473,7 +473,7 @@ int hydro_force_evaluate(int target, int mode, int *exportflag, int *exportnodec { if(kernel.vsig > WAKEUP*CellP[j].MaxSignalVel) { #pragma omp atomic write - P[j].wakeup = 1; + P[j].wakeup = (short int)(local.TimeBin + 1); #pragma omp atomic write NeedToWakeupParticles_local = 1; } diff --git a/hydro/hydro_toplevel.cc b/hydro/hydro_toplevel.cc index 36af3cb03..04b2278b9 100644 --- a/hydro/hydro_toplevel.cc +++ b/hydro/hydro_toplevel.cc @@ -203,6 +203,10 @@ struct INPUT_STRUCT_NAME #endif #if defined(RT_SOLVER_EXPLICIT) && defined(RT_COMPGRAD_EDDINGTON_TENSOR) MyDouble Rad_E_gamma_ET[N_RT_FREQ_BINS][3]; +#endif +#if defined(RT_M1_SECONDORDER) && defined(RT_EVOLVE_FLUX) + MyFloat Rad_E_gamma_Grad[N_RT_FREQ_BINS][3]; + MyFloat Rad_Flux_Grad[N_RT_FREQ_BINS][3][3]; #endif } Gradients; MyDouble NV_T[3][3]; @@ -290,6 +294,9 @@ struct INPUT_STRUCT_NAME MyFloat Elastic_Stress_Tensor[3][3]; #endif +#ifdef WAKEUP + int TimeBin; +#endif int NodeList[NODELISTLENGTH]; } *DATAIN_NAME, *DATAGET_NAME; @@ -306,6 +313,9 @@ struct OUTPUT_STRUCT_NAME MyDouble DtInternalEnergy; //MyDouble dInternalEnergy; //manifest-indiv-timestep-debug// MyFloat MaxSignalVel; +#ifdef OUTPUT_SHOCK_MACH_NUMBER + MyFloat MaxShockMachNumber; +#endif #ifdef ENERGY_ENTROPY_SWITCH_IS_ACTIVE MyFloat MaxKineticEnergyNgb; #endif @@ -443,6 +453,12 @@ static inline void particle2in_hydra(struct INPUT_STRUCT_NAME *in, int i, int lo #endif #if defined(RT_SOLVER_EXPLICIT) && defined(RT_COMPGRAD_EDDINGTON_TENSOR) for(j=0;jGradients.Rad_E_gamma_ET[j][k] = CellP[i].Gradients.Rad_E_gamma_ET[j][k];} +#endif +#if defined(RT_M1_SECONDORDER) && defined(RT_EVOLVE_FLUX) + for(j=0;jGradients.Rad_E_gamma_Grad[j][k] = CellP[i].Gradients.Rad_E_gamma_Grad[j][k]; + int j_d; for(j_d=0;j_d<3;j_d++) {in->Gradients.Rad_Flux_Grad[j][j_d][k] = CellP[i].Gradients.Rad_Flux_Grad[j][j_d][k];} + } #endif } @@ -470,7 +486,7 @@ static inline void particle2in_hydra(struct INPUT_STRUCT_NAME *in, int i, int lo #if defined(TURB_DIFF_METALS) || (defined(METALS) && defined(HYDRO_MESHLESS_FINITE_VOLUME)) for(k=0;kMetallicity[k] = P[i].Metallicity[k];} #if defined(GALSF_ISMDUSTCHEM_MODEL) - for(k=NUM_METAL_SPECIES;kMetallicity[k] = return_ismdustchem_species_of_interest_for_diffusion_and_yields(i,k);} + for(k=NUM_METAL_SPECIES;kMetallicity[k] = return_ismdustchem_species_of_interest_for_diffusion_and_yields(i,k,0);} #endif #endif @@ -533,6 +549,10 @@ static inline void particle2in_hydra(struct INPUT_STRUCT_NAME *in, int i, int lo in->DelayTime = CellP[i].DelayTime; #endif +#ifdef WAKEUP + in->TimeBin = P[i].TimeBin; +#endif + } @@ -559,6 +579,9 @@ static inline void out2particle_hydra(struct OUTPUT_STRUCT_NAME *out, int i, int for(k=0;k<3;k++) {CellP[i].GravWorkTerm[k] += out->GravWorkTerm[k];} #endif if(CellP[i].MaxSignalVel < out->MaxSignalVel) {CellP[i].MaxSignalVel = out->MaxSignalVel;} +#ifdef OUTPUT_SHOCK_MACH_NUMBER + if(CellP[i].ShockMachNumber < out->MaxShockMachNumber) {CellP[i].ShockMachNumber = out->MaxShockMachNumber;} +#endif #ifdef ENERGY_ENTROPY_SWITCH_IS_ACTIVE if(CellP[i].MaxKineticEnergyNgb < out->MaxKineticEnergyNgb) {CellP[i].MaxKineticEnergyNgb = out->MaxKineticEnergyNgb;} #endif @@ -799,6 +822,19 @@ void hydro_final_operations_and_cleanup(void) for(k=0;k 1) double norm=0, dp[3]; int m; dp[0]=dp[1]=dp[2]=0; @@ -639,13 +654,29 @@ int split_particle_i(int i, int n_particles_split, int i_nearest) double qq = get_random_number(63432*k + 84*i + 99*j + 358453 + 84537*ThisTask); if(qq < 0.5) {norm *= -1.;} // randomly decide which direction along principle axis to orient split (since this is arbitrary, this helps prevent accidental collisions) for(k=0;k 0 && CellP[i].Density > 0) { + double relative_gradient = grad_rho_norm * Get_Particle_Size(i) / CellP[i].Density; // dimensionless: |grad_rho|*dx/rho + if(relative_gradient > 0.1) { // non-trivial gradient: project dp onto isodensity plane + double dot=0; for(k=0;k 0.1) {for(k=0;k 1.e6) {return 0;} // stop merging beyond a certain point, only want to downgrade the resolution so much - if(evaluate_stellar_age_Gyr(i) < 0.05) {return 0;} // sufficiently old (don't want to do this for extremely young stars as messes up feedback and early dynamics) + double dt_gyr = evaluate_stellar_age_Gyr(i), dt_threshold_gyr = 0.05; +#ifdef GALSF_SFR_IMF_SAMPLING_DISTRIBUTE_SF + dt_gyr -= P[i].TimeDistribOfStarFormation*UNIT_TIME_IN_GYR; // want to be sure we're this far past the last possible star that formed +#endif + if(dt_gyr < dt_threshold_gyr) {return 0;} // sufficiently old (don't want to do this for extremely young stars as messes up feedback and early dynamics) #ifdef ADAPTIVE_GRAVSOFT_FROM_TIDAL_CRITERION // need to figure out if the new version of this makes sense double r_NGB = 1.25 * pow((All.DesNumNgb*All.G*P[i].Mass)/P[i].tidal_tensor_mag_prev , 1./3.); // kernel size enclosing some target neighbor number in a constant-density medium if(r_NGB > 0.5*ForceSoftening_KernelRadius(i)) {return 0;} // sufficiently dense region (need to have effective nearest-neighbor spacing approaching the minimum softening, with some arbitrary threshold we set) -#else -#ifdef COMPUTE_TIDAL_TENSOR_IN_GRAVTREE +#elif defined(COMPUTE_TIDAL_TENSOR_IN_GRAVTREE) double h_i=ForceSoftening_KernelRadius(i), tidal_mag=0., fac_self=-P[i].Mass*kernel_gravity(0.,1.,1.,1)/(h_i*h_i*h_i); int k,j; // get what's needed for tidal tensor computation - //for(k=0;k<3;k++) {for(j=0;j<3;j++) {double ttkj=P[i].tidal_tensorps[k][j]; if(j==k) {ttkj+=fac_self;} // compute tidal tensor including self-contribution - // tidal_mag+=ttkj*ttkj;}} // want the frobenius norm for(k=0;k<3;k++) {tidal_mag -= P[i].tidal_tensorps[k][k];} // want the trace, actually, and in general this -shouldn't- include the self-contribution if(tidal_mag > 0) { - //tidal_mag = sqrt(tidal_mag); // squared norm. note this is in code units double ngb_dist = 1.25 * pow( (All.DesNumNgb * All.G * P[i].Mass / tidal_mag) , 1./3. ); // distance to the N'th nearest-neighbor if(ngb_dist > h_i) {return 0;} // sufficiently dense region (need to have effective nearest-neighbor spacing approaching the minimum softening, with some arbitrary threshold we set) } -#endif +#else + if(GET_PARTICLE_TIMESTEP_IN_PHYSICAL(i)*UNIT_TIME_IN_GYR*1000. > 0.01) {return 0;} // if the particle is taking a very long timestep, it's probably in a very low-density region, so don't allow it to merge (this is a crude proxy for the local density, but it's very fast to check and works well in practice) #endif return 1; // allow this particle to -consider- the possibility of a merger } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..fc17600d3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools >= 77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "gizmo" +version = "0.1" +description = "Tests and helper scripts for the gizmo mesh-free multi-physics code" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] +dependencies = ["pytest","h5py","matplotlib","astropy","numpy", "scipy", "meshoid"] + +license = "MIT" + +[project.urls] +Homepage = "https://github.com/phopkins/gizmo" +Issues = "https://github.com/phopkins/gizmo/issues" + +[tool.setuptools.packages.find] +where = ["python_src"] \ No newline at end of file diff --git a/python_src/gizmo/compress_gizmosnap.py b/python_src/gizmo/compress_gizmosnap.py new file mode 100644 index 000000000..04666e343 --- /dev/null +++ b/python_src/gizmo/compress_gizmosnap.py @@ -0,0 +1,59 @@ +import os +import sys +import h5py +import argparse +import functools +import tempfile + +## this is the core compression subroutine +def copy_level0(fs, fd, name, node): + if isinstance(node, h5py.Dataset): + # create the new dataset, defaulting to gzip level 4 compression, + # including chunking/shuffle options, which work well. + dnew = fd.create_dataset(name, data=node, dtype=node.dtype, chunks=True, + shuffle=True, compression="gzip", compression_opts=4, fletcher32=True); + print(' ..dataset {} ({}) compressed'.format(name,node.dtype)); + elif isinstance(node, h5py.Group) and name == 'Header': + # header entries don't get compressed, but are negligible for storage + fs.copy(name, fd, name=name); + print(' Header copied'); + else: + print(' Group {}'.format(name)); + +## this is the main loop, called by parser, which does all the os-level operations +def main(filename): + print('Losslessly compressing snapshot {}'.format(filename)); # start + fs = h5py.File(filename, 'r'); # open file + # check that the file has not already been compressed, in which case, exit + if('CompactLevel' in fs['Header'].attrs): + print(' .. this snapshot has already been compressed, done'); + fs.close(); + return; + # create a temporary working file for intermediate steps + tmpfilename = filename+'__tmp__'; + fd = h5py.File(tmpfilename, 'w'); + # recursively do the work here on the entries (call and compress) + copy_datasets = functools.partial(copy_level0, fs, fd); + fs.visititems(copy_datasets); + # encode that the file is compressed, so it can be skipped in future + fd['Header'].attrs['CompactLevel'] = 0; + # close and rename temp file to replace original file + fd.close(); fs.close(); os.rename(tmpfilename,filename); + print(' Completed'); + return; + +## this is the parser, meant to be called from the command-line +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Losslessly compress GIZMO hdf5 snapshots.' + '(c) R. Feldmann 2017, modified by PFH 2020') + parser.add_argument('filename', help='hdf5 file to be compactified ' + '(file will be over-written)') + + if len(sys.argv[1:]) == 0: + print('Error: {} requires more parameters'.format(__file__)); + parser.print_help(); + parser.exit(); + + args = parser.parse_args(); + main(args.filename) diff --git a/python_src/gizmo/load_from_snapshot.py b/python_src/gizmo/load_from_snapshot.py new file mode 100644 index 000000000..194cb314f --- /dev/null +++ b/python_src/gizmo/load_from_snapshot.py @@ -0,0 +1,314 @@ +import h5py +import numpy +import os +## This file was written by Phil Hopkins (phopkins@caltech.edu) for GIZMO ## + + +def load_from_snapshot(value,ptype,sdir,snum,particle_mask=numpy.zeros(0),axis_mask=numpy.zeros(0), + units_to_physical=True,four_char=False,snapshot_name='snapshot',snapdir_name='snapdir',extension='.hdf5'): + ''' + + The routine 'load_from_snapshot' is designed to load quantities directly from GIZMO + snapshots in a robust manner, independent of the detailed information actually saved + in the snapshot. It is able to do this because of how HDF5 works, so it --only-- + works for HDF5-format snapshots [For binary-format, you need to know -exactly- the + datasets saved and their order in the file, which means you cannot do this, and should + use the 'readsnap.py' routine instead.] + + The routine automatically handles multi-part snapshot files for you (concatenating). + This should work with both python2.x and python3.x + + Syntax: + loaded_value = load_from_snapshot(value,ptype,sdir,snum,....) + + For example, to load the coordinates of gas (type=0) elements in the file + snapshot_001.hdf5 (snum=1) located in the active directory ('.'), just call + xyz_coordinates = load_from_snapshot('Coordinates',0,'.',1) + + More details and examples are given in the GIZMO user guide. + + Arguments: + value: the value to extract from the HDF5 file. this is a string with the same name + as in the HDF5 file. if you arent sure what those values might be, setting + value to 'keys' will return a list of all the HDF5 keys for the chosen + particle type, or 'header_keys' will return all the keys in the header. + (example: 'Time' returns the simulation time in code units (single scalar). + 'Coordinates' will return the [x,y,z] coordinates in an [N,3] + matrix for the N resolution elements of the chosen type) + + ptype: element type (int) = 0[gas],1,2,3,4,5[meaning depends on simulation, see + user guide for details]. if your chosen 'value' is in the file header, + this will be ignored + + sdir: parent directory (string) of the snapshot file or immediate snapshot + sub-directory if it is a multi-part file + + snum: number (int) of the snapshot. e.g. snapshot_001.hdf5 is '1' + Note for multi-part files, this is just the number of the 'set', i.e. + if you have snapshot_001.N.hdf5, set this to '1', not 'N' or '1.N' + + Optional: + particle_mask: if set to a mask (boolean array), of length N where N is the number + of elements of the desired ptype, will return only those elements + + axis_mask: if set to a mask (boolean array), return only the chosen -axis-. this + is useful for some quantities like metallicity fields, with [N,X] dimensions + where X is large (lets you choose to read just one of the "X") + + units_to_physical: default 'True': code will auto-magically try to detect if the + simulation is cosmological by comparing time and redshift information in the + snapshot, and if so, convert units to physical. if you want default snapshot units + set this to 'False' + + four_char: default numbering is that snapshots with numbers below 1000 have + three-digit numbers. if they were numbered with four digits (e.g. snapshot_0001), + set this to 'True' (default False) + + snapshot_name: default 'snapshot': the code will automatically try a number of + common snapshot and snapshot-directory prefixes. but it can't guess all of them, + especially if you use an unusual naming convention, e.g. naming your snapshots + 'xyzBearsBeetsBattleStarGalactica_001.hdf5'. In that case set this to the + snapshot name prefix (e.g. 'xyzBearsBeetsBattleStarGalactica') + + snapdir_name: default 'snapdir': like 'snapshot_name', set this if you use a + non-standard prefix for snapshot subdirectories (directories holding multi-part + snapshots pieces) + + extension: default 'hdf5': again like 'snapshot' set if you use a non-standard + extension (it checks multiply options like 'h5' and 'hdf5' and 'bin'). but + remember the file must actually be hdf5 format! + + ''' + + + + # attempt to verify if a file with this name and directory path actually exists + fname,fname_base,fname_ext = check_if_filename_exists(sdir,snum,\ + snapshot_name=snapshot_name,snapdir_name=snapdir_name,extension=extension,four_char=four_char) + # if no valid file found, give up + if(fname=='NULL'): + print('Could not find a valid file with this path/name/extension - please check these settings') + return 0 + # check if file has the correct extension + if(fname_ext!=extension): + print('File has the wrong extension, you specified ',extension,' but found ',fname_ext,' - please specify this if it is what you actually want') + return 0 + # try to open the file + try: + file = h5py.File(fname,'r') # Open hdf5 snapshot file + except: + print('Unexpected error: could not read hdf5 file ',fname,' . Please check the format, name, and path information is correct') + return 0 + + # try to parse the header + try: + header_toparse = file["Header"].attrs # Load header dictionary (to parse below) + except: + print('Was able to open the file but not the header, please check this is a valid GIZMO hdf5 file') + file.close() + return 0 + # check if desired value is contained in header -- if so just return it and exit + if(value=='header_keys')|(value=='Header_Keys')|(value=='HEADER_KEYS')|(value=='headerkeys')|(value=='HeaderKeys')|(value=='HEADERKEYS')|((value=='keys' and not (ptype == 0 or ptype == 1 or ptype == 2 or ptype == 3 or ptype == 4 or ptype == 5))): + q = header_toparse.keys() + print('Returning list of keys from header, includes: ',q) + file.close() + return q + if(value in header_toparse): + q = header_toparse[value] # value contained in header, no need to go further + file.close() + return q + + # ok desired quantity is not in the header, so we need to go into the particle data + + # check that a valid particle type is specified + if not (ptype == 0 or ptype == 1 or ptype == 2 or ptype == 3 or ptype == 4 or ptype == 5): + print('Particle type needs to be an integer = 0,1,2,3,4,5. Returning 0') + file.close() + return 0 + # check that the header contains the expected data needed to parse the file + if not ('NumFilesPerSnapshot' in header_toparse and 'NumPart_Total' in header_toparse + and 'Time' in header_toparse and 'Redshift' in header_toparse + and 'HubbleParam' in header_toparse and 'NumPart_ThisFile' in header_toparse): + print('Header appears to be missing critical information. Please check that this is a valid GIZMO hdf5 file') + file.close() + return 0 + # parse data needed for checking sub-files + numfiles = header_toparse["NumFilesPerSnapshot"] + npartTotal = header_toparse["NumPart_Total"] + if(npartTotal[ptype]<1): + print('No particles of designated type exist in this snapshot, returning 0') + file.close() + return 0 + # parse data needed for converting units [if necessary] + if(units_to_physical): + time = header_toparse["Time"] + z = header_toparse["Redshift"] + hubble = header_toparse["HubbleParam"] + cosmological = False + ascale = 1.0; + # attempt to guess if this is a cosmological simulation from the agreement or lack thereof between time and redshift. note at t=1,z=0, even if non-cosmological, this won't do any harm + if(numpy.abs(time*(1.+z)-1.) < 1.e-6): + cosmological=True; ascale=time; + # close the initial header we are parsing + file.close() + + # now loop over all snapshot segments to identify and extract the relevant particle data + check_counter = 0 + for i_file in range(numfiles): + # augment snapshot sub-set number + if (numfiles>1): fname = fname_base+'.'+str(i_file)+fname_ext + # check for existence of file + if(os.stat(fname).st_size>0): + # exists, now try to read it + try: + file = h5py.File(fname,'r') # Open hdf5 snapshot file + except: + print('Unexpected error: could not read hdf5 file ',fname,' . Please check the format, name, and path information is correct, and that this file is not corrupted') + return 0 + # read in, now attempt to parse. first check for needed information on particle number + npart = file["Header"].attrs["NumPart_ThisFile"] + if(npart[ptype] > 1): + # return particle key data, if requested + if((value=='keys')|(value=='Keys')|(value=='KEYS')): + q = file['PartType'+str(ptype)].keys() + print('Returning list of valid keys for this particle type: ',q) + file.close() + return q + # check if requested data actually exists as a valid keyword in the file + if not (value in file['PartType'+str(ptype)].keys()): + print('The value ',value,' given does not appear to exist in the file ',fname," . Please check that you have specified a valid keyword. You can run this routine with the value 'keys' to return a list of valid value keys. Returning 0") + file.close() + return 0 + # now actually read the data + axis_mask = numpy.array(axis_mask) + if(axis_mask.size > 0): + q_t = numpy.array(file['PartType'+str(ptype)+'/'+value+'/']).take(axis_mask,axis=1) + else: + q_t = numpy.array(file['PartType'+str(ptype)+'/'+value+'/']) + # check data has non-zero size + if(q_t.size > 0): + # if this is the first time we are actually reading it, parse it and determine the shape of the vector, to build the data container + if(check_counter == 0): + qshape=numpy.array(q_t.shape); qshape[0]=0; q=numpy.zeros(qshape); check_counter+=1; + # add the data to our appropriately-shaped container, now + try: + q = numpy.concatenate([q,q_t],axis=0) + except: + print('Could not concatenate data for ',value,' in file ',fname,' . The format appears to be inconsistent across your snapshots or with the usual GIZMO conventions. Please check this is a valid GIZMO snapshot file.') + file.close() + return 0 + file.close() + else: + print('Expected file ',fname,' appears to be missing. Check if your snapshot has the complete data set here') + + # convert units if requested by the user. note this only does a few obvious units: there are many possible values here which cannot be anticipated! + if(units_to_physical): + hinv=1./hubble; rconv=ascale*hinv; + if((value=='Coordinates')|(value=='SmoothingLength')): q*=rconv; # comoving length + if(value=='Velocities'): q *= numpy.sqrt(ascale); # special comoving velocity units + if((value=='Density')|(value=='Pressure')): q *= hinv/(rconv*rconv*rconv); # density = mass/comoving length^3 + if((value=='StellarFormationTime')&(cosmological==False)): q*=hinv; # time has h^-1 in non-cosmological runs + if((value=='Masses')|('BH_Mass' in value)|(value=='CosmicRayEnergy')|(value=='PhotonEnergy')): q*=hinv; # mass x [no-h] units + + # return final value, if we have not already + particle_mask=numpy.array(particle_mask) + if(particle_mask.size > 0): q=q.take(particle_mask,axis=0) + return q + + + + + +def check_if_filename_exists(sdir,snum,snapshot_name='snapshot',snapdir_name='snapdir',extension='.hdf5',four_char=False): + ''' + This subroutine attempts to check if a snapshot or snapshot directory with + valid GIZMO outputs exists. It will check several common conventions for + file and directory names, and extensions. + + Input: + sdir: parent directory of the snapshot file or immediate snapshot sub-directory + if it is a multi-part file. string. + + snum: number (int) of the snapshot. e.g. snapshot_001.hdf5 is '1' + + Optional: + snapshot_name: default 'snapshot': the code will automatically try a number of + common snapshot and snapshot-directory prefixes. but it can't guess all of them, + especially if you use an unusual naming convention, e.g. naming your snapshots + 'xyzBearsBeetsBattleStarGalactica_001.hdf5'. In that case set this to the + snapshot name prefix (e.g. 'xyzBearsBeetsBattleStarGalactica') + + snapdir_name: default 'snapdir': like 'snapshot_name', set this if you use a + non-standard prefix for snapshot subdirectories (directories holding multi-part + snapshots pieces) + + extension: default 'hdf5': again like 'snapshot' set if you use a non-standard + extension (it checks multiply options like 'h5' and 'hdf5' and 'bin'). but + remember the file must actually be hdf5 format! + + four_char: default numbering is that snapshots with numbers below 1000 have + three-digit numbers. if they were numbered with four digits (e.g. snapshot_0001), + set this to 'True' (default False) + ''' + + # loop over possible extension names to try and check for valid files + for extension_touse in [extension,'.h5','.bin','']: + fname=sdir+'/'+snapshot_name+'_' + + # begin by identifying the snapshot extension with the file number + ext='00'+str(snum); + if (snum>=10): ext='0'+str(snum) + if (snum>=100): ext=str(snum) + if (four_char==True): ext='0'+ext + if (snum>=1000): ext=str(snum) + fname+=ext + fname_base=fname + + # isolate the specific path up to the snapshot name, because we will try to append several different choices below + s0=sdir.split("/"); snapdir_specific=s0[len(s0)-1]; + if(len(snapdir_specific)<=1): snapdir_specific=s0[len(s0)-2]; + + ## try several common notations for the directory/filename structure + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is it a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot'? + fname_base=sdir+'/snap_'+ext; + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot', AND its a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap(snapdir)' instead of 'snapshot'? + fname_base=sdir+'/snap_'+snapdir_specific+'_'+ext; + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot', AND its a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is it in a snapshot sub-directory? (we assume this means multi-part files) + fname_base=sdir+'/'+snapdir_name+'_'+ext+'/'+snapshot_name+'_'+ext; + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is it in a snapshot sub-directory AND named 'snap' instead of 'snapshot'? + fname_base=sdir+'/'+snapdir_name+'_'+ext+'/'+'snap_'+ext; + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## wow, still couldn't find it... ok, i'm going to give up! + fname_found = 'NULL' + fname_base_found = 'NULL' + fname_ext = 'NULL' + continue; + if(os.stat(fname).st_size <= 0): + ## file exists but is null size, do not use + fname_found = 'NULL' + fname_base_found = 'NULL' + fname_ext = 'NULL' + continue; + fname_found = fname; + fname_base_found = fname_base; + fname_ext = extension_touse + break; # filename does exist! + return fname_found, fname_base_found, fname_ext; diff --git a/python_src/gizmo/make_IC.py b/python_src/gizmo/make_IC.py new file mode 100644 index 000000000..0d9f0a378 --- /dev/null +++ b/python_src/gizmo/make_IC.py @@ -0,0 +1,166 @@ +################################################################################ +###### This is an example script to generate HDF5-format ICs for GIZMO +###### The specific example below is obviously arbitrary, but could be generalized +###### to whatever IC you need. +################################################################################ +################################################################################ + +## load libraries we will use +import numpy as np +import h5py as h5py + +# the main routine. this specific example builds an N-dimensional box of gas plus +# a collisionless particle species, with a specified mass ratio. the initial +# gas particles are distributed in a uniform lattice; the initial collisionless +# particles laid down randomly according to a uniform probability distribution +# with a specified random velocity dispersion +# +def make_IC(): + ''' + This is an example subroutine provided to demonstrate how to make HDF5-format + ICs for GIZMO. The specific example here is arbitrary, but can be generalized + to whatever IC you need + ''' + + DIMS=2; # number of dimensions + N_1D=32; # 1D particle number (so total particle number is N_1D^DIMS) + fname='box_3d_r32.hdf5'; # output filename + + Lbox = 1.0 # box side length + rho_desired = 1.0 # box average initial gas density + P_desired = 1.0 # initial gas pressure + vgrainrms = 1.0 # rms velocity of collisionless particles + dust_to_gas_ratio = 0.01 # mass ratio of collisionless particles to gas + gamma_eos = 5./3. # polytropic index of ideal equation of state the run will assume + + # first we set up the gas properties (particle type 0) + + # make a regular 1D grid for particle locations (with N_1D elements and unit length) + x0=np.arange(-0.5,0.5,1./N_1D); x0+=0.5*(0.5-x0[-1]); + # now extend that to a full lattice in DIMS dimensions + if(DIMS==3): + xv_g, yv_g, zv_g = np.meshgrid(x0,x0,x0, sparse=False, indexing='xy') + if(DIMS==2): + xv_g, yv_g = np.meshgrid(x0,x0, sparse=False, indexing='xy'); zv_g = 0.0*xv_g + if(DIMS==1): + xv_g=x0; yv_g = 0.0*xv_g; zv_g = 0.0*xv_g; + # the gas particle number is the lattice size: this should be the gas particle number + Ngas = xv_g.size + # flatten the vectors (since our ICs should be in vector, not matrix format): just want a + # simple list of the x,y,z positions here. Here we multiply the desired box size in + xv_g=xv_g.flatten()*Lbox; yv_g=yv_g.flatten()*Lbox; zv_g=zv_g.flatten()*Lbox; + # set the initial velocity in x/y/z directions (here zero) + vx_g=0.*xv_g; vy_g=0.*xv_g; vz_g=0.*xv_g; + # set the initial magnetic field in x/y/z directions (here zero). + # these can be overridden (if constant field values are desired) by BiniX/Y/Z in the parameterfile + bx_g=0.*xv_g; by_g=0.*xv_g; bz_g=0.*xv_g; + # set the particle masses. Here we set it to be a list the same length, with all the same mass + # since their space-density is uniform this gives a uniform density, at the desired value + mv_g=rho_desired/((1.*Ngas)/(Lbox*Lbox*Lbox)) + 0.*xv_g + # set the initial internal energy per unit mass. recall gizmo uses this as the initial 'temperature' variable + # this can be overridden with the InitGasTemp variable (which takes an actual temperature) + uv_g=P_desired/((gamma_eos-1.)*rho_desired) + 0.*xv_g + # set the gas IDs: here a simple integer list + id_g=np.arange(1,Ngas+1) + + # now we set the properties of the collisionless particles: we will assign these to particle type '3', + # but (barring special compile-time flags being set) GIZMO will treat all collisionless particle types + # the same. so the setup would be identical for any of the particle types 1,2,3,4,5 + + # set the desired number of particles (here to about twice as many as the gas particles, because we feel like it) + Ngrains = int(np.round(2. * (1.*N_1D)**DIMS)) + # set the x/y/z positions: again a simple list for each: here to random numbers from a uniform distribution + xv_d = (np.random.rand(Ngrains)-0.5)*Lbox + yv_d = (np.random.rand(Ngrains)-0.5)*Lbox + zv_d = (np.random.rand(Ngrains)-0.5)*Lbox + # set the IDs: these must be unique, so we start from the maximum gas ID and go up + id_d = np.arange(Ngas+1,Ngrains+Ngas+1) + # set the velocities. again we will set to a random value, here a Gaussian-distributed one + vx_d = np.random.randn(Ngrains) * vgrainrms/np.sqrt(3.) + vy_d = np.random.randn(Ngrains) * vgrainrms/np.sqrt(3.) + vz_d = np.random.randn(Ngrains) * vgrainrms/np.sqrt(3.) + # set the masses, again a list with all the same mass + mv_d = dust_to_gas_ratio * (1.*Ngas)/(1.*Ngrains) * mv_g[0] + 0.*xv_d + # set the types for grains. GrainType = 1: Epstein/Stokes; 2: Charged Epstein/Stokes; 3: Cosmic Rays + type_d = (np.ones(Ngrains) * 3).astype("int") + + + + + # now we get ready to actually write this out + # first - open the hdf5 ics file, with the desired filename + file = h5py.File(fname,'w') + + # set particle number of each type into the 'npart' vector + # NOTE: this MUST MATCH the actual particle numbers assigned to each type, i.e. + # npart = np.array([number_of_PartType0_particles,number_of_PartType1_particles,number_of_PartType2_particles, + # number_of_PartType3_particles,number_of_PartType4_particles,number_of_PartType5_particles]) + # or else the code simply cannot read the IC file correctly! + # + npart = np.array([Ngas,0,0,Ngrains,0,0]) # we have gas and particles we will set for type 3 here, zero for all others + + # now we make the Header - the formatting here is peculiar, for historical (GADGET-compatibility) reasons + h = file.create_group("Header"); + # here we set all the basic numbers that go into the header + # (most of these will be written over anyways if it's an IC file; the only thing we actually *need* to be 'correct' is "npart") + h.attrs['NumPart_ThisFile'] = npart; # npart set as above - this in general should be the same as NumPart_Total, it only differs + # if we make a multi-part IC file. with this simple script, we aren't equipped to do that. + h.attrs['NumPart_Total'] = npart; # npart set as above + h.attrs['NumPart_Total_HighWord'] = 0*npart; # this will be set automatically in-code (for GIZMO, at least) + h.attrs['MassTable'] = np.zeros(6); # these can be set if all particles will have constant masses for the entire run. however since + # we set masses explicitly by-particle this should be zero. that is more flexible anyways, as it + # allows for physics which can change particle masses + h.attrs['Time'] = 0.0; # initial time + h.attrs['NumFilesPerSnapshot'] = 1; # number of files for multi-part snapshots + h.attrs['Flag_DoublePrecision'] = 0; # flag indicating whether ICs are in single/double precision + ## no other parameters will be read from the header if we are parsing an HDF5 file for reading in + + # Now, the actual data! + # These blocks should all be written in the order of their particle type (0,1,2,3,4,5) + # If there are no particles of a given type, nothing is needed (no block at all) + # PartType0 is 'special' as gas. All other PartTypes take the same, more limited set of information in their ICs + + # start with particle type zero. first (assuming we have any gas particles) create the group + p = file.create_group("PartType0") + # now combine the xyz positions into a matrix with the correct format + q=np.zeros((Ngas,3)); q[:,0]=xv_g; q[:,1]=yv_g; q[:,2]=zv_g; + # write it to the 'Coordinates' block + p.create_dataset("Coordinates",data=q) + # similarly, combine the xyz velocities into a matrix with the correct format + q=np.zeros((Ngas,3)); q[:,0]=vx_g; q[:,1]=vy_g; q[:,2]=vz_g; + # write it to the 'Velocities' block + p.create_dataset("Velocities",data=q) + # write particle ids to the ParticleIDs block + p.create_dataset("ParticleIDs",data=id_g) + # write particle masses to the Masses block + p.create_dataset("Masses",data=mv_g) + # write internal energies to the InternalEnergy block + p.create_dataset("InternalEnergy",data=uv_g) + # combine the xyz magnetic fields into a matrix with the correct format + q=np.zeros((Ngas,3)); q[:,0]=bx_g; q[:,1]=by_g; q[:,2]=bz_g; + # write magnetic fields to the MagneticField block. note that this is unnecessary if the code is compiled with + # MAGNETIC off. however, it is not a problem to have the field there, even if MAGNETIC is off, so you can + # always include it with some dummy values and then use the IC for either case + p.create_dataset("MagneticField",data=q) + + # no PartType1 for this IC + # no PartType2 for this IC + + # now assign the collisionless particles to PartType3. note that this block looks exactly like + # what we had above for the gas. EXCEPT there are no "InternalEnergy" or "MagneticField" fields (for + # obvious reasons). + p = file.create_group("PartType3") + q=np.zeros((Ngrains,3)); q[:,0]=xv_d; q[:,1]=yv_d; q[:,2]=zv_d; + p.create_dataset("Coordinates",data=q) + q=np.zeros((Ngrains,3)); q[:,0]=vx_d; q[:,1]=vy_d; q[:,2]=vz_d; + p.create_dataset("Velocities",data=q) + p.create_dataset("ParticleIDs",data=id_d) + p.create_dataset("Masses",data=mv_d) + p.create_dataset("PICParticleType",data=type_d) + + # no PartType4 for this IC + # no PartType5 for this IC + + # close the HDF5 file, which saves these outputs + file.close() + # all done! diff --git a/python_src/gizmo/readsnap.py b/python_src/gizmo/readsnap.py new file mode 100644 index 000000000..a8ef5a327 --- /dev/null +++ b/python_src/gizmo/readsnap.py @@ -0,0 +1,501 @@ +import numpy as np +import h5py as h5py +import os.path +## This file was written by Phil Hopkins (phopkins@caltech.edu) for GIZMO ## + + + +def readsnap(sdir,snum,ptype, + snapshot_name='snapshot', + extension='.hdf5', + h0=0,cosmological=0,skip_bh=0,four_char=0, + header_only=0,loud=0): + ''' + This is a sub-routine designed to copy a GIZMO snapshot portion - specifically + all the data corresponding to particles of a given type - into active memory in + a parent structure for use in python. The routine automatically handles multi-part + snapshot files for you (concatenating), and works with python2.x and python3.x, + and both GIZMO hdf5 and un-formatted binary outputs. + + Syntax: + P = readsnap(sdir,snum,ptype,....) + + Here "P" is a structure which contains the data. The snapshot file[s] are opened, + the data fully copied out, and the file closed. This attempts to copy + all the common data types, all together, into "P". Three things to note: + (1) the fields in P (visible by typing P.keys()) are not given the same names + as those in the raw snapshot, but 'shorthand' names, for convenience. you should + look at the keys and be sure you know which associate with which files. + (2) because of the full-copy approach into new-named fields, this will not + handle arbitrary new data types (it is impossible to handle these in full + generality with un-formatted binary, you need to code the file-order and byte + numbers for each new data structure). if you add new fields (with e.g. + additional physics modules) beyond what this code looks for, you need to + add code here, or use the more general 'load_from_snapshot.py' routine. + (3) also because of the full copy strategy, this routine is much more expensive + in time and memory compared to 'load_from_snapshot.py'. use that if you want + a light-weight, more flexible reading option, and have HDF5 outputs. + + For example, after calling, the 'Coordinates' field from the snapshot is + accessible from the new structure P by calling P['p']. 'Velocities' as P['v'], + 'Masses' as P['m']. Fields specific to gas include 'Density' as P['rho'], + 'InternalEnergy' as P['u'], and more. + + More details and examples are given in the GIZMO user guide. + + Arguments: + sdir: parent directory (string) of the snapshot file or immediate snapshot sub-directory + if it is a multi-part file. + + snum: number (int) of the snapshot. e.g. snapshot_001.hdf5 is '1' + Note for multi-part files, this is just the number of the 'set', i.e. + if you have snapshot_001.N.hdf5, set this to '1', not 'N' or '1.N' + + ptype: element type (int) = 0[gas],1,2,3,4,5[meaning depends on simulation, see + user guide for details]. if your chosen 'value' is in the file header, + this will be ignored + + Optional: + header_only: the structure "P" will return the file header, instead of the + particle data. you can see the data in the header then by simply typing + P.keys() -- this contains data like the time of the snapshot, as + P['Time']. With this specific routine the header information is only + saved if you choose this option. Default 0/False, turn on by setting to + 1 or True. + + cosmological: default 0/False: turn on (set to 1/True) to convert cosmological + co-moving units to physical units. will specifically convert Coordinates, + Masses, Velocities, Densities, Smoothing Lengths, and Times/Ages. If this + is on, you do not need to set 'h0' (this will force it to be set -also-), + but it does no harm to set it as well. + + h0: default 0/False: turn on (set to 1/True) for the code to use the value + of the hubble constant (h = H0/100 km/s/Mpc) saved in the snapshot to convert + from code units. Recall, units of time, length, and mass in the code are in + h^-1. So this on means your units are physical, with no "h" in them. + Will specifically convert Coordinates, Masses, Densities, Smoothing Lengths, + and Times/Ages. + + skip_bh: default 0/False: turn on (set to 1/True) to skip black hole-specific + fields for particles of type 5 (use if your snapshot contains elements of type 5, + but the black hole physics modules were not actually used; otherwise you will + get an error). + + four_char: default numbering is that snapshots with numbers below 1000 have + three-digit numbers. if they were numbered with four digits (e.g. snapshot_0001), + set this to 1 or True (default 0/False) + + snapshot_name: default 'snapshot': the code will automatically try a number of + common snapshot and snapshot-directory prefixes. but it can't guess all of them, + especially if you use an unusual naming convention, e.g. naming your snapshots + 'xyzBearsBeetsBattleStarGalactica_001.hdf5'. In that case set this to the + snapshot name prefix (e.g. 'xyzBearsBeetsBattleStarGalactica') + + extension: default 'hdf5': again like 'snapshot' set if you use a non-standard + extension (it checks multiply options like 'h5' and 'hdf5' and 'bin'). but + remember the file must actually be hdf5 format! + + loud: print additional checks as it reads, useful for debugging, + set to 1 or True if desired (default 0/False) + + + + ''' + + + if (ptype<0): return {'k':-1}; + if (ptype>5): return {'k':-1}; + + fname,fname_base,fname_ext = check_if_filename_exists(sdir,snum,\ + snapshot_name=snapshot_name,extension=extension,four_char=four_char) + if(fname=='NULL'): return {'k':-1} + if(loud==1): print('loading file : '+fname) + + ## open file and parse its header information + nL = 0 # initial particle point to start at + if(fname_ext=='.hdf5'): + file = h5py.File(fname,'r') # Open hdf5 snapshot file + header_topdict = file["Header"] # Load header dictionary (to parse below) + header_toparse = header_topdict.attrs + else: + file = open(fname) # Open binary snapshot file + header_toparse = load_gadget_format_binary_header(file) + + npart = header_toparse["NumPart_ThisFile"] + massarr = header_toparse["MassTable"] + time = header_toparse["Time"] + redshift = header_toparse["Redshift"] + flag_sfr = header_toparse["Flag_Sfr"] + flag_feedbacktp = header_toparse["Flag_Feedback"] + npartTotal = header_toparse["NumPart_Total"] + flag_cooling = header_toparse["Flag_Cooling"] + numfiles = header_toparse["NumFilesPerSnapshot"] + boxsize = header_toparse["BoxSize"] + hubble = header_toparse["HubbleParam"] + flag_stellarage = header_toparse["Flag_StellarAge"] + flag_metals = header_toparse["Flag_Metals"] + print("npart_file: ",npart) + print("npart_total:",npartTotal) + + hinv=1. + if (h0==1): + hinv=1./hubble + ascale=1. + if (cosmological==1): + ascale=time + hinv=1./hubble + if (cosmological==0): + time*=hinv + + boxsize*=hinv*ascale + if (npartTotal[ptype]<=0): file.close(); return {'k':-1}; + if (header_only==1): file.close(); return {'k':0,'time':time, + 'boxsize':boxsize,'hubble':hubble,'npart':npart,'npartTotal':npartTotal}; + + # initialize variables to be read + pos=np.zeros([npartTotal[ptype],3],dtype=np.float64) + vel=np.copy(pos) + ids=np.zeros([npartTotal[ptype]],dtype=long) + mass=np.zeros([npartTotal[ptype]],dtype=np.float64) + if (ptype==0): + ugas=np.copy(mass) + rho=np.copy(mass) + hsml=np.copy(mass) + #if (flag_cooling>0): + nume=np.copy(mass) + numh=np.copy(mass) + #if (flag_sfr>0): + sfr=np.copy(mass) + metal=np.copy(mass) + if (ptype == 0 or ptype == 4) and (flag_metals > 0): + metal=np.zeros([npartTotal[ptype],flag_metals],dtype=np.float64) + if (ptype == 4) and (flag_sfr > 0) and (flag_stellarage > 0): + stellage=np.copy(mass) + if (ptype == 5) and (skip_bh == 0): + bhmass=np.copy(mass) + bhmdot=np.copy(mass) + + # loop over the snapshot parts to get the different data pieces + for i_file in range(numfiles): + if (numfiles>1): + file.close() + fname = fname_base+'.'+str(i_file)+fname_ext + if(fname_ext=='.hdf5'): + file = h5py.File(fname,'r') # Open hdf5 snapshot file + else: + file = open(fname) # Open binary snapshot file + header_toparse = load_gadget_format_binary_header(file) + + if (fname_ext=='.hdf5'): + input_struct = file + npart = file["Header"].attrs["NumPart_ThisFile"] + bname = "PartType"+str(ptype)+"/" + else: + npart = header_toparse['NumPart_ThisFile'] + input_struct = load_gadget_format_binary_particledat(file, header_toparse, ptype, skip_bh=skip_bh) + bname = '' + + + # now do the actual reading + if(npart[ptype]>0): + nR=nL + npart[ptype] + pos[nL:nR,:]=input_struct[bname+"Coordinates"] + vel[nL:nR,:]=input_struct[bname+"Velocities"] + ids[nL:nR]=input_struct[bname+"ParticleIDs"] + mass[nL:nR]=massarr[ptype] + if (massarr[ptype] <= 0.): + mass[nL:nR]=input_struct[bname+"Masses"] + if (ptype==0): + ugas[nL:nR]=input_struct[bname+"InternalEnergy"] + rho[nL:nR]=input_struct[bname+"Density"] + hsml[nL:nR]=input_struct[bname+"SmoothingLength"] + if (flag_cooling > 0): + nume[nL:nR]=input_struct[bname+"ElectronAbundance"] + numh[nL:nR]=input_struct[bname+"NeutralHydrogenAbundance"] + if (flag_sfr > 0): + sfr[nL:nR]=input_struct[bname+"StarFormationRate"] + if (ptype == 0 or ptype == 4) and (flag_metals > 0): + metal_t=input_struct[bname+"Metallicity"] + if (flag_metals > 1): + if (metal_t.shape[0] != npart[ptype]): + metal_t=np.transpose(metal_t) + else: + metal_t=np.reshape(np.array(metal_t),(np.array(metal_t).size,1)) + metal[nL:nR,:]=metal_t + if (ptype == 4) and (flag_sfr > 0) and (flag_stellarage > 0): + stellage[nL:nR]=input_struct[bname+"StellarFormationTime"] + if (ptype == 5) and (skip_bh == 0): + bhmass[nL:nR]=input_struct[bname+"BH_Mass"] + bhmdot[nL:nR]=input_struct[bname+"BH_Mdot"] + nL = nR # sets it for the next iteration + + ## correct to same ID as original gas particle for new stars, if bit-flip applied + if ((np.min(ids)<0) | (np.max(ids)>1.e9)): + bad = (ids < 0) | (ids > 1.e9) + ids[bad] += (long(1) << 31) + + # do the cosmological conversions on final vectors as needed + pos *= hinv*ascale # snapshot units are comoving + mass *= hinv + vel *= np.sqrt(ascale) # remember gizmo's (and gadget's) weird velocity units! + if (ptype == 0): + rho *= (hinv/((ascale*hinv)**3)) + hsml *= hinv*ascale + if (ptype == 4) and (flag_sfr > 0) and (flag_stellarage > 0) and (cosmological == 0): + stellage *= hinv + if (ptype == 5) and (skip_bh == 0): + bhmass *= hinv + + file.close(); + if (ptype == 0): + return {'k':1,'p':pos,'v':vel,'m':mass,'id':ids,'u':ugas,'rho':rho,'h':hsml,'ne':nume,'nh':numh,'sfr':sfr,'z':metal}; + if (ptype == 4): + return {'k':1,'p':pos,'v':vel,'m':mass,'id':ids,'z':metal,'age':stellage} + if (ptype == 5) and (skip_bh == 0): + return {'k':1,'p':pos,'v':vel,'m':mass,'id':ids,'mbh':bhmass,'mdot':bhmdot} + return {'k':1,'p':pos,'v':vel,'m':mass,'id':ids} + + + +def check_if_filename_exists(sdir,snum,snapshot_name='snapshot',extension='.hdf5',four_char=0): + for extension_touse in [extension,'.bin','']: + fname=sdir+'/'+snapshot_name+'_' + ext='00'+str(snum); + if (snum>=10): ext='0'+str(snum) + if (snum>=100): ext=str(snum) + if (four_char==1): ext='0'+ext + if (snum>=1000): ext=str(snum) + fname+=ext + fname_base=fname + + s0=sdir.split("/"); snapdir_specific=s0[len(s0)-1]; + if(len(snapdir_specific)<=1): snapdir_specific=s0[len(s0)-2]; + + ## try several common notations for the directory/filename structure + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is it a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot'? + fname_base=sdir+'/snap_'+ext; + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot', AND its a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap(snapdir)' instead of 'snapshot'? + fname_base=sdir+'/snap_'+snapdir_specific+'_'+ext; + fname=fname_base+extension_touse; + if not os.path.exists(fname): + ## is the filename 'snap' instead of 'snapshot', AND its a multi-part file? + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is it in a snapshot sub-directory? (we assume this means multi-part files) + fname_base=sdir+'/snapdir_'+ext+'/'+snapshot_name+'_'+ext; + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## is it in a snapshot sub-directory AND named 'snap' instead of 'snapshot'? + fname_base=sdir+'/snapdir_'+ext+'/'+'snap_'+ext; + fname=fname_base+'.0'+extension_touse; + if not os.path.exists(fname): + ## wow, still couldn't find it... ok, i'm going to give up! + fname_found = 'NULL' + fname_base_found = 'NULL' + fname_ext = 'NULL' + continue; + fname_found = fname; + fname_base_found = fname_base; + fname_ext = extension_touse + break; # filename does exist! + return fname_found, fname_base_found, fname_ext; + + + +def load_gadget_format_binary_header(f): + ### Read header. + import array + # Skip 4-byte integer at beginning of header block. + f.read(4) + # Number of particles of each type. 6*unsigned integer. + Npart = array.array('I') + Npart.fromfile(f, 6) + # Mass of each particle type. If set to 0 for a type which is present, + # individual particle masses from the 'mass' block are used instead. + # 6*double. + Massarr = array.array('d') + Massarr.fromfile(f, 6) + # Expansion factor (or time, if non-cosmological sims) of output. 1*double. + a = array.array('d') + a.fromfile(f, 1) + a = a[0] + # Redshift of output. Should satisfy z=1/a-1. 1*double. + z = array.array('d') + z.fromfile(f, 1) + z = float(z[0]) + # Flag for star formation. 1*int. + FlagSfr = array.array('i') + FlagSfr.fromfile(f, 1) + # Flag for feedback. 1*int. + FlagFeedback = array.array('i') + FlagFeedback.fromfile(f, 1) + # Total number of particles of each type in the simulation. 6*int. + Nall = array.array('i') + Nall.fromfile(f, 6) + # Flag for cooling. 1*int. + FlagCooling = array.array('i') + FlagCooling.fromfile(f, 1) + # Number of files in each snapshot. 1*int. + NumFiles = array.array('i') + NumFiles.fromfile(f, 1) + # Box size (comoving kpc/h). 1*double. + BoxSize = array.array('d') + BoxSize.fromfile(f, 1) + # Matter density at z=0 in units of the critical density. 1*double. + Omega_Matter = array.array('d') + Omega_Matter.fromfile(f, 1) + # Vacuum energy density at z=0 in units of the critical density. 1*double. + Omega_Lambda = array.array('d') + Omega_Lambda.fromfile(f, 1) + # Hubble parameter h in units of 100 km s^-1 Mpc^-1. 1*double. + h = array.array('d') + h.fromfile(f, 1) + h = float(h[0]) + # Creation times of stars. 1*int. + FlagAge = array.array('i') + FlagAge.fromfile(f, 1) + # Flag for metallicity values. 1*int. + FlagMetals = array.array('i') + FlagMetals.fromfile(f, 1) + + # For simulations that use more than 2^32 particles, most significant word + # of 64-bit total particle numbers. Otherwise 0. 6*int. + NallHW = array.array('i') + NallHW.fromfile(f, 6) + + # Flag that initial conditions contain entropy instead of thermal energy + # in the u block. 1*int. + flag_entr_ics = array.array('i') + flag_entr_ics.fromfile(f, 1) + + # Unused header space. Skip to particle positions. + f.seek(4+256+4+4) + + return {'NumPart_ThisFile':Npart, 'MassTable':Massarr, 'Time':a, 'Redshift':z, \ + 'Flag_Sfr':FlagSfr[0], 'Flag_Feedback':FlagFeedback[0], 'NumPart_Total':Nall, \ + 'Flag_Cooling':FlagCooling[0], 'NumFilesPerSnapshot':NumFiles[0], 'BoxSize':BoxSize[0], \ + 'Omega_Matter':OmegaMatter[0], 'Omega_Lambda':OmegaLambda[0], 'HubbleParam':h, \ + 'Flag_StellarAge':FlagAge[0], 'Flag_Metals':FlagMetals[0], 'Nall_HW':NallHW, \ + 'Flag_EntrICs':flag_entr_ics[0]} + + +def load_gadget_format_binary_particledat(f, header, ptype, skip_bh=0): + ## load old format=1 style gadget-format binary snapshot files (unformatted fortran binary) + import array + gas_u=0.; gas_rho=0.; gas_ne=0.; gas_nhi=0.; gas_hsml=0.; gas_SFR=0.; star_age=0.; + zmet=0.; bh_mass=0.; bh_mdot=0.; mm=0.; + Npart = header['NumPart_ThisFile'] + Massarr = header['MassTable'] + NpartTot = np.sum(Npart) + NpartCum = np.cumsum(Npart) + n0 = NpartCum[ptype] - Npart[ptype] + n1 = NpartCum[ptype] + + ### particles positions. 3*Npart*float. + pos = array.array('f') + pos.fromfile(f, 3*NpartTot) + pos = np.reshape(pos, (NpartTot,3)) + f.read(4+4) # Read block size fields. + + ### particles velocities. 3*Npart*float. + vel = array.array('f') + vel.fromfile(f, 3*NpartTot) + vel = np.reshape(vel, (NpartTot,3)) + f.read(4+4) # Read block size fields. + + ### Particle IDs. # (Npart[0]+...+Npart[5])*int + id = array.array('i') + id.fromfile(f, NpartTot) + id = np.array(id) + f.read(4+4) # Read block size fields. + + ### Variable particle masses. + Npart_MassCode = np.copy(np.array(Npart)) + Npart=np.array(Npart) + Npart_MassCode[(Npart <= 0) | (np.array(Massarr,dtype='d') > 0.0)] = long(0) + NwithMass = np.sum(Npart_MassCode) + mass = array.array('f') + mass.fromfile(f, NwithMass) + f.read(4+4) # Read block size fields. + if (Massarr[ptype]==0.0): + Npart_MassCode_Tot = np.cumsum(Npart_MassCode) + mm = mass[Npart_MassCode_Tot[ptype]-Npart_MassCode[ptype]:Npart_MassCode_Tot[ptype]] + + if ((ptype == 0) | (ptype == 4) | (ptype == 5)): + if (Npart[0]>0): + ### Internal energy of gas particles ((km/s)^2). + gas_u = array.array('f') + gas_u.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + ### Density for the gas paraticles (units?). + gas_rho = array.array('f') + gas_rho.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + + if (header['Flag_Cooling'] > 0): + ### Electron number density for gas particles (fraction of n_H; can be >1). + gas_ne = array.array('f') + gas_ne.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + ### Neutral hydrogen number density for gas particles (fraction of n_H). + gas_nhi = array.array('f') + gas_nhi.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + + ### Smoothing length (kpc/h). ### + gas_hsml = array.array('f') + gas_hsml.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + + if (header['Flag_Sfr'] > 0): + ### Star formation rate (Msun/yr). ### + gas_SFR = array.array('f') + gas_SFR.fromfile(f, Npart[0]) + f.read(4+4) # Read block size fields. + + if (Npart[4]>0): + if (header['Flag_Sfr'] > 0): + if (header['Flag_StellarAge'] > 0): + ### Star formation time (in code units) or scale factor ### + star_age = array.array('f') + star_age.fromfile(f, Npart[4]) + f.read(4+4) # Read block size fields. + + if (Npart[0]+Npart[4]>0): + if (header['Flag_Metals'] > 0): + ## Metallicity block (species tracked = Flag_Metals) + if (Npart[0]>0): + gas_z = array.array('f') + gas_z.fromfile(f, header['Flag_Metals']*Npart[0]) + if (Npart[4]>0): + star_z = array.array('f') + star_z.fromfile(f, header['Flag_Metals']*Npart[4]) + f.read(4+4) # Read block size fields. + if (ptype==0): zmet=np.reshape(gas_z,(-1,header['Flag_Metals'])) + if (ptype==4): zmet=np.reshape(star_z,(-1,header['Flag_Metals'])) + + if (Npart[5]>0): + if (skip_bh > 0): + ## BH mass (same as code units, but this is the separately-tracked BH mass from particle mass) + bh_mass = array.array('f') + bh_mass.fromfile(f, Npart[5]) + f.read(4+4) # Read block size fields. + ## BH accretion rate in snapshot + bh_mdot = array.array('f') + bh_mdot.fromfile(f, Npart[5]) + f.read(4+4) # Read block size fields. + + return {'Coordinates':pos[n0:n1,:], 'Velocities':vel[n0:n1,:], 'ParticleIDs':id[n0:n1], \ + 'Masses':mm, 'Metallicity':zmet, 'StellarFormationTime':star_age, 'BH_Mass':bh_mass, \ + 'BH_Mdot':bh_mdot, 'InternalEnergy':gas_u, 'Density':gas_rho, 'SmoothingLength':gas_hsml, \ + 'ElectronAbundance':gas_ne, 'NeutralHydrogenAbundance':gas_nhi, 'StarFormationRate':gas_SFR} diff --git a/python_src/gizmo/test.py b/python_src/gizmo/test.py new file mode 100644 index 000000000..439c393e4 --- /dev/null +++ b/python_src/gizmo/test.py @@ -0,0 +1,201 @@ +"""General routines to build gizmo for a test and obtain ICs and params files""" + +from os import system, environ, path, chdir, cpu_count, remove +from urllib.request import urlretrieve, HTTPError +from shutil import move, rmtree +from glob import glob +import pytest +from matplotlib import pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable +import h5py + + +def flush_colorbar(mappable, ax=None, label=None, **kwargs): + """Add a colorbar that is flush with the axes and lines up with its edges.""" + if ax is None: + ax = mappable.axes + fig = ax.get_figure() + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.05) + return fig.colorbar(mappable, cax=cax, label=label, **kwargs) + + +def clean_test_outputs(test_name: str): + """Remove output directory, plot PNGs, and log files from a previous test run.""" + test_dir = f"test/{test_name}" + output_dir = path.join(test_dir, "output") + if path.isdir(output_dir): + rmtree(output_dir) + for f in glob(path.join(test_dir, "*.png")): + remove(f) + for f in glob(path.join(test_dir, f"test_{test_name}.out")): + remove(f) + for f in glob(path.join(test_dir, f"test_{test_name}.err")): + remove(f) + + +def default_omp_threads(): + """Return the default number of OpenMP threads for tests.""" + return 2 + + +def default_mpi_ranks(max_ranks=None): + """Return the number of MPI ranks to use, defaulting to half the available CPU count + (to leave room for OpenMP threads). Optionally cap at max_ranks.""" + n = cpu_count() // 2 + if max_ranks is not None: + n = min(n, max_ranks) + return max(n, 1) + + +def build_gizmo_for_test(test_name: str, num_openmp_threads: int = 0): + """Sets environment variables and runs a script for building gizmo for a given test. + If num_openmp_threads > 0, appends OPENMP= to Config.sh before building.""" + system("rm -f GIZMO test/*/GIZMO") + system(f"cp test/{test_name}/Config.sh .") + if num_openmp_threads > 0: + with open("Config.sh", "a") as f: + f.write(f"\nOPENMP={num_openmp_threads}\n") + system("make clean && make -j8") + if not path.isfile("GIZMO"): + raise FileNotFoundError("Did not successfully build GIZMO") + move("GIZMO", f"test/{test_name}/GIZMO") + system(f"chmod +x test/{test_name}/GIZMO") + + +def download_test_files(test_name: str): + """Downloads the ICs and parameter files for a test of a given name""" + + website_path = "http://www.tapir.caltech.edu/~phopkins/sims/" + website_path2 = f"https://users.flatironinstitute.org/~mgrudic/gizmo_tests/{test_name}/" + + # Note: we are assuming a convention for the test ICs, params, and exact values + icfile = f"{test_name}_ics.hdf5" + exactfile = f"{test_name}_exact.txt" # exact solution (might not exist!) + exactfile2 = f"{test_name}_exact.hdf5" # exact solution (might not exist!) + + for f in icfile, exactfile, exactfile2: + try: + urlretrieve(website_path + f, f) + except HTTPError as err: + try: + urlretrieve(website_path2 + f, f) + except HTTPError as err: + print(f"Could not find {f} at {website_path} or {website_path2}") + + if not path.isfile(icfile): + raise (FileNotFoundError(f"Could not find ICs and params for test {test_name}")) + + +def run_test(test_name: str, num_mpi_ranks: int = 1, num_openmp_threads: int = 0): + """Runs the test. If num_openmp_threads > 0, sets OMP_NUM_THREADS for the run.""" + if num_openmp_threads > 0: + environ["OMP_NUM_THREADS"] = str(num_openmp_threads) + paramsfile = f"{test_name}.params" + system(f"mpirun -np {num_mpi_ranks} --use-hwthread-cpus ./GIZMO {paramsfile} 0 1>test_{test_name}.out 2>test_{test_name}.err") + + +def get_cooling_tables(test_directory="."): + """Downloads spcool_tables.tar.gz and copies TREECOOL to test directory""" + + url = "https://users.flatironinstitute.org/~mgrudic/gizmo_tests/spcool_tables.tgz" + urlretrieve(url, f"{test_directory}/spcool_tables.tgz") + system(f"tar -xvf {test_directory}/spcool_tables.tgz -C {test_directory}/; rm spcool_tables.tgz") + system(f"cp cooling/TREECOOL {test_directory}") + + +def build_and_run_test(test_name: str, num_mpi_ranks: int = 1, num_openmp_threads: int = 0): + """Top-level routine that does all necessary building, downloading, and running of the test""" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_openmp_threads) + chdir(f"test/{test_name}/") + download_test_files(test_name) + run_test(test_name, num_mpi_ranks, num_openmp_threads) + chdir("../../") + + +def parse_params(params_file: str) -> dict: + """Parse a GIZMO parameter file and return a dict of key-value pairs.""" + params = {} + with open(params_file) as f: + for line in f: + line = line.split("%")[0].strip() + if not line: + continue + parts = line.split() + if len(parts) >= 2: + params[parts[0]] = parts[1] + return params + + +def get_final_snapshot(test_name: str) -> str: + """Return the path to the last snapshot produced by a test.""" + snaps = sorted(glob(f"test/{test_name}/output/snapshot_*.hdf5")) + if not snaps: + raise RuntimeError(f"No snapshots found for test {test_name}") + return snaps[-1] + + +def assert_final_time(snapshot_file: str, test_name: str, rtol: float = 1e-6): + """Assert that the snapshot time matches TimeMax from the test's parameter file.""" + params_file = f"test/{test_name}/{test_name}.params" + params = parse_params(params_file) + time_max = float(params["TimeMax"]) + with h5py.File(snapshot_file, "r") as F: + time = float(F["Header"].attrs["Time"]) + assert abs(time - time_max) < rtol * abs(time_max), ( + f"Snapshot time {time} does not match TimeMax {time_max} (rtol={rtol})" + ) + + +def assert_snapshots_are_close( + snapshot1: str, + snapshot2: str, + fields_to_compare: tuple = ("Density", "Velocities", "InternalEnergy"), + rtol: float = 1e-2, + atol: float = 0, +): + """Test-assert that the specified gas data fields in two snapshots are within specified tolerance""" + fields_to_read = ("ParticleIDs",) + fields_to_compare + + datafields = {snapshot1: {}, snapshot2: {}} + for s in snapshot1, snapshot2: + with h5py.File(s, "r") as F: # read + for f in fields_to_read: + datafields[s][f] = F["PartType0/" + f][:] + + id_order = datafields[s]["ParticleIDs"].argsort() + for f in fields_to_read: # sort by ID + datafields[s][f] = datafields[s][f][id_order] + + for f in fields_to_compare: + pytest.approx((datafields[snapshot1][f], datafields[snapshot2][f]), rel=rtol, abs=atol) + + +def plot_1D_snapshot_comparison( + snapshot1: str, + snapshot2: str, + fields_to_plot: tuple = ("Density", "Velocities", "InternalEnergy"), + output_dir: str = ".", +): + """Plot 1D comparison of gas data fields between two snapshots, sorted by particle ID.""" + fields_to_read = ("ParticleIDs", "Coordinates") + fields_to_plot + + datafields = {snapshot1: {}, snapshot2: {}} + for s in snapshot1, snapshot2: + with h5py.File(s, "r") as F: + for f in fields_to_read: + datafields[s][f] = F["PartType0/" + f][:] + + id_order = datafields[s]["ParticleIDs"].argsort() + for f in fields_to_read: + datafields[s][f] = datafields[s][f][id_order] + + for f in fields_to_plot: + plt.plot(datafields[snapshot1]["Coordinates"][:, 0], datafields[snapshot1][f], ".", label="Initial") + plt.plot(datafields[snapshot2]["Coordinates"][:, 0], datafields[snapshot2][f], ".", label="Final") + plt.legend() + plt.ylabel(f) + plt.xlabel("x") + plt.savefig(path.join(output_dir, f"{f}.png")) + plt.close() diff --git a/radiation/rt_diffusion_explicit.h b/radiation/rt_diffusion_explicit.h index 8212736b3..ec31cdcba 100644 --- a/radiation/rt_diffusion_explicit.h +++ b/radiation/rt_diffusion_explicit.h @@ -134,12 +134,30 @@ int k_freq; double Fluxes_Rad_Flux[N_RT_FREQ_BINS][3], V_i_phys = V_i / All.cf_a3inv, V_j_phys = V_j / All.cf_a3inv; +#ifdef RT_M1_SECONDORDER + /* face distances for second-order reconstruction: face lies at midpoint, s_star_ij=0 */ + double dist_rt_i[3], dist_rt_j[3]; + for(k=0;k<3;k++) {dist_rt_i[k] = -0.5*kernel.dp[k]; dist_rt_j[k] = 0.5*kernel.dp[k];} +#endif for(k_freq=0;k_freq0)&&(local.Mass>0)&&(P[j].Mass>0)&&(dt_hydrostep>0)&&(Face_Area_Norm>0)) { double d_scalar = scalar_i - scalar_j; @@ -149,8 +167,23 @@ /* calculate the eigenvalues for the HLLE flux-weighting */ for(k=0;k<3;k++) { - flux_i[k] = local.Rad_Flux[k_freq][k]/V_i_phys - rsol_corr*v_frame[k]*scalar_i; - flux_j[k] = CellP[j].Rad_Flux_Pred[k_freq][k]/V_j_phys - rsol_corr*v_frame[k]*scalar_j; // units (E_phys/[t_phys*L_phys^2]) [physical]. include advective flux terms here + double flux_i_raw = local.Rad_Flux[k_freq][k]/V_i_phys; + double flux_j_raw = CellP[j].Rad_Flux_Pred[k_freq][k]/V_j_phys; +#ifdef RT_M1_SECONDORDER + /* reconstruct each flux component at the face; grad is d(F_k/V_code)/dx_code, scale by cf_a3inv */ + { + MyFloat grad_F_i[3], grad_F_j[3]; + int k3; for(k3=0;k3<3;k3++) { + grad_F_i[k3] = (MyFloat)(local.Gradients.Rad_Flux_Grad[k_freq][k][k3] * All.cf_a3inv); + grad_F_j[k3] = (MyFloat)(CellP[j].Gradients.Rad_Flux_Grad[k_freq][k][k3] * All.cf_a3inv); + } + double flux_i_face, flux_j_face; + reconstruct_face_states(flux_i_raw, grad_F_i, flux_j_raw, grad_F_j, dist_rt_i, dist_rt_j, &flux_j_face, &flux_i_face, 1); + flux_i_raw = flux_i_face; flux_j_raw = flux_j_face; + } +#endif + flux_i[k] = flux_i_raw - rsol_corr*v_frame[k]*scalar_i; + flux_j[k] = flux_j_raw - rsol_corr*v_frame[k]*scalar_j; // units (E_phys/[t_phys*L_phys^2]) [physical]. include advective flux terms here double grad = 0.5*(flux_i[k] + flux_j[k]); grad_norm += grad*grad; face_dot_flux += Face_Area_Vec[k] * grad; /* remember, our 'flux' variable is a volume-integral */ @@ -193,10 +226,12 @@ q = 0.5 * c_hll * (kernel.r * All.cf_atime) / fabs(MIN_REAL_NUMBER + kappa_ij); q = (0.2 + q) / (0.2 + q + q*q); renormerFAC = DMIN(1.,fabs(cos_theta_face_flux*cos_theta_face_flux * q * hll_corr)); +#ifndef RT_M1_SECONDORDER /* with RT_M1_SECONDORDER, scalar_i/j are already face-reconstructed so this correction is redundant */ double scalar_jr=scalar_j, scalar_ir=scalar_i, d_scalar_hll=d_scalar, d_scalar_ij=0; for(k=0;k<3;k++) {scalar_jr+=0.5*kernel.dp[k]*local.Gradients.Rad_E_gamma_ET[k_freq][k]*All.cf_a3inv; scalar_ir-=0.5*kernel.dp[k]*CellP[j].Gradients.Rad_E_gamma_ET[k_freq][k]*All.cf_a3inv;} d_scalar_ij=scalar_ir-scalar_jr; if((d_scalar_ij*d_scalar>0)&&(fabs(d_scalar_ij)= N_TDUST) { // Tdust exceeds table max +#if defined(RT_OPACITY_FROM_EXPLICIT_GRAINS) || defined(GALSF_ISMDUSTCHEM_MODEL) || defined(RT_INFRARED) || (defined(COOL_LOW_TEMPERATURES) && !defined(SINGLE_STAR_SINK_DYNAMICS)) + Tdust_idx = N_TDUST-1; // Tdust is hot but we have explicit grain model and need IR opacity to calculate dust-to-gas ratios, so return max table value +#else + return MIN_REAL_NUMBER; // Tdust is hot so dust is sublimated; return smol value +#endif + } + + if (logT >= logTmax) { + return pow(10., log_kappadust_table[Tdust_idx][N_TRAD - 1]); + } + if (logT <= logTmin) { + return pow(10., log_kappadust_table[Tdust_idx][0]); + } + + MyFloat dlogT = logTrad_table[1] - logTrad_table[0]; + int Trad_idx = (int)(N_TRAD - 1) * logT / (logTmax - logTmin); + MyFloat wt1 = 1 - (logT - logTrad_table[Trad_idx]) / dlogT, wt2 = 1 - wt1; + MyFloat log_kappa = wt1 * log_kappadust_table[Tdust_idx][Trad_idx] + + wt2 * log_kappadust_table[Tdust_idx][Trad_idx + 1]; + return pow(10., log_kappa); +} diff --git a/radiation/rt_utilities.cc b/radiation/rt_utilities.cc index 6fb23bad6..efc3b5266 100644 --- a/radiation/rt_utilities.cc +++ b/radiation/rt_utilities.cc @@ -182,6 +182,20 @@ double rt_kappa(int i, int k_freq) #ifdef GALSF_FB_FIRE_RT_LONGRANGE /* three-band (UV, OPTICAL, IR) approximate spectra for stars as used in the FIRE (Hopkins et al.) models. mean opacities here come from integrating over the Hopkins 2004 (Pei 1992) opacities versus wavelength for the large bands here, using a luminosity-weighted mean stellar spectrum from the same starburst99 models used to compute the stellar feedback */ #if (GALSF_FB_FIRE_STELLAREVOLUTION > 2) +#if defined(GALSF_ISMDUSTCHEM_MODEL) + // Use either MW (FIRE-3 default) or SMC (FIRE-2 default) opacities depending on the evolved local dust population composition + // If silicate mass / carbonaceous mass >= 5 use SMC opacities, else MW opacities. + double sil_to_C = 0; + if (CellP[i].ISMDustChem_Dust_Metal[0]>0 && CellP[i].ISMDustChem_Dust_Species[1]>0) + {sil_to_C = (CellP[i].ISMDustChem_Dust_Metal[0] - CellP[i].ISMDustChem_Dust_Species[1])/CellP[i].ISMDustChem_Dust_Species[1];} // Everything that isn't carbonaceous dust is silicates for our purpose + else {sil_to_C = 100;} + if (sil_to_C >= 5) + { + if(k_freq==RT_FREQ_BIN_FIRE_UV) {return DMAX(kappa_HHe, 1800.*(1.e-2 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/neutral H opacities. effective wavelength here is at something like ~0.2 micron, but spans a broad range from ~0.09-0.36 microns. + if(k_freq==RT_FREQ_BIN_FIRE_OPT) {return DMAX(kappa_HHe, 180.*(1.e-3 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/bound-free/bound-bound H opacities [Kramer's-type law gives the 1e-3 'floor' effective here]. see O-NIR band notes below, effective wavelength here is ~R-band (0.36-3.4 micron is the rough range of the effective band) + if(k_freq==RT_FREQ_BIN_FIRE_IR) {return DMAX(kappa_HHe, 10*(1.e-3 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/bound-free/bound-bound H opacities [Kramer's-type law gives the 1e-3 'floor' effective here]. this is updated to integrate through the 2007+ Draine+Li MW-like dust models, for a typical M101-like disk SED. slightly higher for e.g. M82-like with warmer dust. note this is a broad band, from ~3-300 micron, so contributions both from old-star direct IR emission and dust re-emission (warm and cold), which is why this is a bit higher than you would get for pure cold-dust re-emission. + } +#endif if(k_freq==RT_FREQ_BIN_FIRE_UV) {return DMAX(kappa_HHe, 800.*(1.e-2 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/neutral H opacities. effective wavelength here is at something like ~0.2 micron, but spans a broad range from ~0.09-0.36 microns. if(k_freq==RT_FREQ_BIN_FIRE_OPT) {return DMAX(kappa_HHe, 180.*(1.e-3 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/bound-free/bound-bound H opacities [Kramer's-type law gives the 1e-3 'floor' effective here]. see O-NIR band notes below, effective wavelength here is ~R-band (0.36-3.4 micron is the rough range of the effective band) if(k_freq==RT_FREQ_BIN_FIRE_IR) {return DMAX(kappa_HHe, 6.5*(1.e-3 + Zfac*dust_to_metals_vs_standard)) * fac;} // floored at Thomson/bound-free/bound-bound H opacities [Kramer's-type law gives the 1e-3 'floor' effective here]. this is updated to integrate through the 2007+ Draine+Li MW-like dust models, for a typical M101-like disk SED. slightly higher for e.g. M82-like with warmer dust. note this is a broad band, from ~3-300 micron, so contributions both from old-star direct IR emission and dust re-emission (warm and cold), which is why this is a bit higher than you would get for pure cold-dust re-emission. @@ -1007,7 +1021,7 @@ void rt_apply_boundary_conditions(int i) #ifdef RT_INFRARED if(k==RT_FREQ_BIN_INFRARED) { CellP[i].Radiation_Temperature = background_isrf_cmb_Teff(); - CellP[i].Dust_Temperature = DMIN(All.InitGasTemp,100.); + CellP[i].Dust_Temperature = DMIN(All.InitRadiationTemp,100.); } #endif } @@ -1042,7 +1056,7 @@ void get_background_isrf_urad(int i, double *urad){ double background_isrf_cmb_Teff(){ // Returns the energy-weighted effective temperature of the background ISRF that has equivalent average photon energy to the sum of the ISRF and CMB // Necessary because current IR band treatment lumps both radiation fields together - double urad_ISRF_CGS_eV = All.InterstellarRadiationFieldStrength * 0.39, Trad_ISRF = DMIN(All.InitGasTemp,100.); + double urad_ISRF_CGS_eV = All.InterstellarRadiationFieldStrength * 0.39, Trad_ISRF = DMIN(All.InitRadiationTemp,100.); double fac_TCMB= 1.+All.RadiationBackgroundRedshift, fac_uCMB = pow(fac_TCMB,4); double urad_CMB_CGS_eV = fac_uCMB * 0.262, Trad_CMB = 2.73 * fac_TCMB; return (urad_ISRF_CGS_eV * Trad_ISRF + urad_CMB_CGS_eV * Trad_CMB) / (urad_ISRF_CGS_eV + urad_CMB_CGS_eV); // weighting by SED energy @@ -1071,7 +1085,7 @@ void rt_set_simple_inits(int RestartFlag) { int k; #ifdef RT_INFRARED - if(flag_to_reset_values_on_startup) {CellP[i].Radiation_Temperature = CellP[i].Dust_Temperature = DMIN(All.InitGasTemp,100.);} //get_min_allowed_dustIRrad_temperature(); // in K, floor = CMB temperature or 10K + if(flag_to_reset_values_on_startup) {CellP[i].Radiation_Temperature = CellP[i].Dust_Temperature = DMIN(All.InitRadiationTemp,100.);} //get_min_allowed_dustIRrad_temperature(); // in K, floor = CMB temperature or 10K #ifdef RT_ISRF_BACKGROUND if(flag_to_reset_values_on_startup) {CellP[i].Radiation_Temperature = background_isrf_cmb_Teff();} //CellP[i].Dust_Temperature; #endif @@ -1115,7 +1129,7 @@ void rt_set_simple_inits(int RestartFlag) #ifdef RT_INFRARED if(flag_to_reset_values_on_startup && k==RT_FREQ_BIN_INFRARED) { // only initialize the IR energy if starting a new run, otherwise use what's in the snapshot - CellP[i].Rad_E_gamma[RT_FREQ_BIN_INFRARED] = (4.*5.67e-5 / C_LIGHT_CGS) * pow(DMIN(All.InitGasTemp,100.),4.) / UNIT_PRESSURE_IN_CGS * P[i].Mass / (CellP[i].Density*All.cf_a3inv); + CellP[i].Rad_E_gamma[RT_FREQ_BIN_INFRARED] = (4.*5.67e-5 / C_LIGHT_CGS) * pow(DMIN(All.InitRadiationTemp,100.),4.) / UNIT_PRESSURE_IN_CGS * P[i].Mass / (CellP[i].Density*All.cf_a3inv); } #endif #ifdef RT_ISRF_BACKGROUND @@ -1720,7 +1734,7 @@ double rt_kappa_adaptive_IR_band(int i, double T_dust, double Trad, int do_emiss if(dust_or_gas_opacity_only_flag >= 0) // dust opacities { -#ifdef RT_INFRARED // use fancy detailed fit with composition varying by dust temperature + // use fancy detailed fit with composition varying by dust temperature /* opacities are from tables of Semenov et al 2003; we use their 'standard' model, for each -dust- temperature range (which gives a different dust composition, hence different wavelength-dependent specific opacity). We then integrate to @@ -1732,29 +1746,7 @@ double rt_kappa_adaptive_IR_band(int i, double T_dust, double Trad, int do_emiss the deviations from the fit functions are much smaller than the deviations owing to different grain composition choices (porous/non, composite/non, 5-layer/aggregated/etc) in Semenov et al's paper */ - -#if defined(RT_INFRARED) || defined(COOL_LOW_TEMPERATURES) - T_dust_opacitytable = DMIN(T_dust , 1499.9); // limit to <1500 so always use opacities for 'capped' value at 1500 below, but don't ignore, because we're assuming the dust destruction above 1500K is accounted for in the self-consistent calculation of the dust-to-metals ratio, NOT in the opacities here // -#endif - if(T_dust_opacitytable < 160.) // Tdust < 160 K (all dust constituents present) - { - kappa = exp(0.72819004 + 0.75142468*x - 0.07225763*x*x - 0.01159257*x*x*x + 0.00249064*x*x*x*x); - } else if(T_dust_opacitytable < 275.) { // 160 < Tdust < 275 (no ice present) - kappa = exp(0.16658241 + 0.70072926*x - 0.04230367*x*x - 0.01133852*x*x*x + 0.0021335*x*x*x*x); - } else if(T_dust_opacitytable < 425.) { // 275 < Tdust < 425 (no ice or volatile organics present) - kappa = exp(0.03583845 + 0.68374146*x - 0.03791989*x*x - 0.01135789*x*x*x + 0.00212918*x*x*x*x); - } else if(T_dust_opacitytable < 680.) { // 425 < Tdust < 680 (silicates, iron, & troilite present) - kappa = exp(-0.76576135 + 0.57053532*x - 0.0122809*x*x - 0.01037311*x*x*x + 0.00197672*x*x*x*x); - } else if(T_dust_opacitytable <= MAX_DUST_TEMP) { // 680 < Tdust < 1500 (silicates & iron present) - kappa = exp(-2.23863222 + 0.81223269*x + 0.08010633*x*x + 0.00862152*x*x*x - 0.00271909*x*x*x*x); - } else { - kappa = MIN_REAL_NUMBER; // here this following the hottest composition; we assume dust is absent above MAX_DUST_TEMP, but that's handled in the dust-to-gas mass ratio subroutine; this needs an opacity to calculate what the dust temperature -would- be in this limit; but actually shouldn't be able to hit this given the catches above for that limit - } - if(dx_excess > 0) {kappa *= exp(0.57*dx_excess);} // assumes kappa scales linearly with temperature (1/lambda) above maximum in fit; pretty good approximation // - kappa = DMIN(1.e-3 * Trad * Trad, kappa); // ensure that we extrapolate to low temperatures with a beta=2 law, like in the S03 paper fiducial model -#else - kappa = DMIN(1.e-3 * Trad * Trad, 5.); // beta=2 law capped at 5 cm^2/g, rough approximation of Semenov model neglecting jumps in composition -#endif + kappa = dust_planck_mean_opacity(Trad, T_dust_opacitytable); #ifdef RADTRANSFER if((do_emission_absorption_scattering_opacity==1) || (do_emission_absorption_scattering_opacity==-1)) { kappa *= (1.-0.5/(1.+((725.*725.)/(1.+Trad*Trad)))); /* rough interpolation for dust depending on the radiation temperature: high Trad, this is 1/2, low Trad, gets closer to unity */ diff --git a/scripts/gizmo_documentation.md b/scripts/gizmo_documentation.md index 8bc21c768..e094cc2cc 100644 --- a/scripts/gizmo_documentation.md +++ b/scripts/gizmo_documentation.md @@ -3781,7 +3781,7 @@ I've also made an effort to "pre-package" these as much as possible, so you can This is a simple linear one-dimensional traveling soundwave, following the example [here](http://www.astro.princeton.edu/~jstone/Athena/tests/linear-waves/linear-waves.html) -This is problem is analytically trivial; however, since virtually all schemes are first-order for discontinuities such as shocks, smooth linear problems with known analytic solutions are the only way to measure and quantitatively test the accuracy and formal convergence rate of numerical algorithms. We initialize a periodic domain of unit length, with a polytropic $\gamma=5/3$ gas with unit mean density and sound speed (so pressure $P=3/5$). We then add to this a traveling soundwave with small amplitude $\delta \rho/\rho = 10^{-6}$ (to avoid any non-linear effects) with unit wavelength. After the wave has propagated one wavelength ($t=1.5$), it should have returned exactly to its initial condition. +This is problem is analytically trivial; however, since virtually all schemes are first-order for discontinuities such as shocks, smooth linear problems with known analytic solutions are the only way to measure and quantitatively test the accuracy and formal convergence rate of numerical algorithms. We initialize a periodic domain of unit length, with a polytropic $\gamma=5/3$ gas with unit mean density and internal energy set such that the sound speed $c_s=2/3$. We then add to this a traveling soundwave with small amplitude $\delta \rho/\rho = 10^{-6}$ (to avoid any non-linear effects) with unit wavelength. After the wave has propagated one wavelength ($t=1.5$), it should have returned exactly to its initial condition. Initial conditions are `soundwave_ics.hdf5` diff --git a/scripts/params.txt b/scripts/params.txt index b50f5341e..635c8d1d4 100644 --- a/scripts/params.txt +++ b/scripts/params.txt @@ -148,6 +148,37 @@ Grain_InteractionVelocityScale 0 % velocity [code units]: if>0, cross-sect Grain_DissipationFactor 0 % 0=elastic, 1=pure dissipative (fractional dissipation of kinetic energy in event) Grain_KickPerCollision 0 % velocity 'kick' [code units] per collision (this^2=specific energy/mass released) +%------------------------------------------------------------ +%------------------ Dust Evolution -------------------------- +%------------------------------------------------------------ + +%---- initialize dust depletion of gas in simulation (for GALSF_ISMDUSTCHEM_MODEL) +Initial_ISMDustChem_Depletion 0. % initial silicate dust depletion (fraction of Si locked in dust and Fe if using the iron nanoparticles option) +Initial_ISMDustChem_SiltoCarbRatio 0. % initial silicate to carbonaceous dust mass ratio (MW value ~2) + +%---- Dust Grain Size Distribution Evolution (for GALSF_ISMDUSTCHEM_MODEL+GALSF_ISMDUSTCHEM_GRAINSIZEEVO) +UnitGrainNumber 1E50 % 1E50. Code unit is 1E50 for number of grains in size bins. Set this to a high value to fit in float datatype. +UnitGrainLength_in_cm 1E-4 % 1 micron. Code unit is 1 micron for grain sizes. Only used for grain bin slopes. +ISMDustChem_Grain_Size_Min 1e-7 % minimum grain size in cm (default 1 nm) +ISMDustChem_Grain_Size_Max 1E-4 % maximum grain size in cm (default 1 micron) + +%---- set dust evolution scaling parameters +SNeIIDustScaling 1.0 % scaling factor for efficiency of SNe II dust production (1 = default model parameter, note this will be limited to a total efficiency of 100%) +SNeIaDustScaling 1.0 % scaling factor for efficiency of SNe Ia dust production (1 = default model parameter, note this will be limited to a total efficiency of 100%) +AGBDustScaling 1.0 % scaling factor for efficiency of AGB dust production (1 = default model parameter, note this will be limited to a total efficiency of 100%) +DustAccretionScaling 1.0 % scaling factor for gas-dust accretion rate (1 = default model parameter) +ThermalSputteringScaling 1.0 % scaling factor for thermal sputtering rate (1 = default model parameter) +SNeGasClearedOfDustScaling 1.0 % scaling factor the total mass of gas cleared of dust per SNe (1 = default model parameter) +AccretionTcutoffScaling 1.0 % scaling factor for gas-dust accretion temperature cutoff (1 = default model parameter) +%---- grain size evolution scaling parameters +SNeShatteringScaling 1.0 % scaling factor for efficiency of dust shattering in SNRs +SNeSputteringScaling 1.0 % scaling factor for efficiency of dust sputtering in SNRs +GrainShatteringScaling 1.0 % scaling factor for grain-grain shattering rate +GrainCoagulationScaling 1.0 % scaling factor for grain-grain coagulation rate +VCoagScaling 1.0 % scaling factor for coagulation velocity +CoagDensityEnhancementScaling 1.0 % scaling factor for the density enhancement used in dense gas to account for coagulation +GrainVelocityScaling 1.0 % scaling factor for grain velocities. Note this affects relative grain velocities which determine if shattering/coagulation occurs and their relative rates +PhotodestructionScaling 1.0 % scaling factor for photodestruction rate in HII regions %------------------------------------------------------------------------- %------------------ Star, Black Hole, and Galaxy Formation --------------- @@ -159,9 +190,6 @@ SfEffPerFreeFall 1.0 % SFR/(Mgas/tfreefall) for gas which meets %---- initialize metallicity+ages of gas+stars in simulation (for GALSF or metal-dependent COOLING) InitMetallicity 1e-5 % initial gas+stellar metallicity [for all pre-existing elements] (in solar) [Default=0] InitStellarAge 1 % initial mean age (in Gyr; for pre-existing stars in sim ICs) [Default=10] -%---- initialize dust depletion of gas in simulation (for GALSF_ISMDUSTCHEM_MODEL) -Initial_ISMDustChem_Depletion 0. % initial silicate dust depletion (fraction of Si locked in dust and Fe if using the iron nanoparticles option) -Initial_ISMDustChem_SiltoCarbRatio 0. % initial silicate to carbonaceous dust mass ratio (MW value ~2) %---- sub-grid (Springel+Hernquist/GADGET/AREPO) "effective equation of state" diff --git a/sidm/cbe_integrator_flux_computation.h b/sidm/cbe_integrator_flux_computation.h index bdba24abf..0966aaecc 100644 --- a/sidm/cbe_integrator_flux_computation.h +++ b/sidm/cbe_integrator_flux_computation.h @@ -147,7 +147,7 @@ #ifdef WAKEUP if(!(TimeBinActive[P[j].TimeBin]) && (All.Time > All.TimeBegin)) {if(vsig > WAKEUP*P[j].AGS_vsig) { #pragma omp atomic write - P[j].wakeup = 1; + P[j].wakeup = -1; #pragma omp atomic write NeedToWakeupParticles_local = 1; }} diff --git a/sidm/sidm_core_flux_computation.h b/sidm/sidm_core_flux_computation.h index 74c654b03..539b5839a 100644 --- a/sidm/sidm_core_flux_computation.h +++ b/sidm/sidm_core_flux_computation.h @@ -18,7 +18,7 @@ #ifdef WAKEUP if(!(TimeBinActive[P[j].TimeBin])) {if(WAKEUP*local.dtime < Pj_dtime) { #pragma omp atomic write - P[j].wakeup=1; + P[j].wakeup=-1; #pragma omp atomic write NeedToWakeupParticles_local = 1; }} diff --git a/sinks/sink_swallow_and_kick.cc b/sinks/sink_swallow_and_kick.cc index 86743c3d4..cd2e34644 100644 --- a/sinks/sink_swallow_and_kick.cc +++ b/sinks/sink_swallow_and_kick.cc @@ -1021,7 +1021,7 @@ int sink_spawn_particle_wind_shell( int i, int dummy_cell_i_to_clone, int num_al P[j].Ti_begstep = All.Ti_Current; P[j].Ti_current = All.Ti_Current; #ifdef WAKEUP /* note - you basically MUST have this flag on for this routine to work at all -- */ P[j].dt_step = GET_INTEGERTIME_FROM_TIMEBIN(bin); - P[j].wakeup = 1; + P[j].wakeup = -1; NeedToWakeupParticles_local = 1; #endif /* this is a giant pile of variables to zero out. dont need everything here because we cloned a valid particle, but handy anyways */ diff --git a/solids/ism_dust_chemistry.cc b/solids/ism_dust_chemistry.cc index 468a8c5da..bd17312a4 100644 --- a/solids/ism_dust_chemistry.cc +++ b/solids/ism_dust_chemistry.cc @@ -4,127 +4,447 @@ #include #include #include +#include #include "../declarations/allvars.h" #include "../core/proto.h" #include "../mesh/kernel.h" -/* This module collects the live ism dust chemistry modules developed by Caleb Choban in Choban et al., 2022. +/* This module collects the live ism dust chemistry modules developed by Caleb Choban in Choban et al., 2022/25. Written by C. Choban, reorganized and collected by PFH. */ #if defined(GALSF_ISMDUSTCHEM_MODEL) +#define ACCRETION_T_CUTOFF 300 /* The cutoff temperature for gas-dust accretion. Also used as a cutoff for density enhancements of dust-dust coagulation. */ +#if (GALSF_ISMDUSTCHEM_MODEL & 8) +#define GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC 0.7 /* assumed fraction of iron dust mass locked as inclusions in silicates, this scales with the total fraction of silicate formed vs maximum amount of possible silicate dust */ +#else +#define GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC 0 /* no iron inclusions tracked */ +#endif +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO +#define MAXIMUM_SUBCYCLE_STEPS 200 /* maximum number of subcycle steps for grain size evolution. The current choice is somewhat arbitrary but works for default FIRE resolution. Beware using too small of values or else coagulation and shattering wont be properly time-resolved. */ +#define ACC_SPUT_SUBCYCLE_PARAMETER 0.3 /* subcycling parameter for accretion and sputtering. This sets the maximum fraction of the smallest grain size bin that can be traversed in one timestep before subcycling must be used. Default is 30% */ +#define SHAT_COAG_SUBCYCLE_PARAMETER 0.1 /* subcycling parameter for shattering and coagulation. This sets the maximum fraction of either mass or number of grains that can be removed across all bins in one timestep for shattering and coagulation respectively. Default is 10% */ +#define COAGULATION_DENSITY_ENHANCEMENT 2000 /* Enhancement factor for gas density to account for subresolved dust-dust coagulation which is efficient at nH>=10^4 cm^-3. Tuned for 7100 Msol mass simulations so median extinction curve matches mean MW curve. */ +#endif -#define GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC 0.7 /* assumed fraction of iron dust mass locked as inclusions in silicates, this scales with the total fraction of silicate formed vs maximum amount of possible silicate dust */ +/* Intializes global dust variables at startup of runs */ +void Initialize_ISMDustChem_Global_Variables() +{ + int j; + /* atomic mass for each element in metallicity field, and some other variables. these always need to be initialized */ + All.ISMDustChem_AtomicMassTable[0] = 1.01; // H + All.ISMDustChem_AtomicMassTable[1] = 4.0; // He + All.ISMDustChem_AtomicMassTable[2] = 12.01; // C + All.ISMDustChem_AtomicMassTable[3] = 14; // N + All.ISMDustChem_AtomicMassTable[4] = 15.99; // O + All.ISMDustChem_AtomicMassTable[5] = 20.2; // Ne + All.ISMDustChem_AtomicMassTable[6] = 24.305; // Mg + All.ISMDustChem_AtomicMassTable[7] = 28.086; // Si + All.ISMDustChem_AtomicMassTable[8] = 32.065; // S + All.ISMDustChem_AtomicMassTable[9] = 40.078; // Ca + All.ISMDustChem_AtomicMassTable[10] = 55.845; // Fe + All.ISMDustChem_SNeSputteringShutOffTime = 0.3E-3; // Destruction of dust due to SNe thermal sputtering ends around 0.3 Myr after SNe (from idealized SNe in Hu+2019) + // Fiducial olivine-pyroxene silicate dust composition with olivine fraction = 0.63 and Mg frac = 0.65. If using iron nanoparticles assume iron is always present for silicate structure in the form of iron inclusions. index in metallicity field for elements which make up silicate dust (O,Mg,Si) + All.ISMDustChem_SilicateMetallicityFieldIndexTable[0] = 4; + All.ISMDustChem_SilicateMetallicityFieldIndexTable[1] = 6; + All.ISMDustChem_SilicateMetallicityFieldIndexTable[2] = 7; + All.ISMDustChem_SilicateMetallicityFieldIndexTable[3] = 10; + // number of O, Mg, and Si in one formula unit of silicate dust + All.ISMDustChem_SilicateNumberOfAtomsTable[0] = 3.63; + All.ISMDustChem_SilicateNumberOfAtomsTable[1] = 1.06; + All.ISMDustChem_SilicateNumberOfAtomsTable[2] = 1.; + All.ISMDustChem_SilicateNumberOfAtomsTable[3] = 0.571; + if (GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION & 2) {All.ISMDustChem_SilicateNumberOfAtomsTable[0] += 2;} + if (GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION & 4) {All.ISMDustChem_SilicateNumberOfAtomsTable[3] += 1;} + if (GALSF_ISMDUSTCHEM_SILICATE_COMPOSITION & 8) {All.ISMDustChem_SilicateNumberOfAtomsTable[3] = 0;} + All.ISMDustChem_EffectiveSilicateDustAtomicWeight = 0.; for(j=0;j 0 && temp < temp_cutoff) + { + for(j=0;j 0) { // if this element is in silicate composition and not Si itself then set depletion based on Si depletion and silicate stoichiometry + CellP[i].ISMDustChem_Dust_Metal[All.ISMDustChem_SilicateMetallicityFieldIndexTable[k]] += CellP[i].ISMDustChem_Dust_Metal[7] / (All.ISMDustChem_SilicateNumberOfAtomsTable[2] * All.ISMDustChem_AtomicMassTable[7]) * (All.ISMDustChem_SilicateNumberOfAtomsTable[k] * All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[k]]); + sil_mass_frac += CellP[i].ISMDustChem_Dust_Metal[All.ISMDustChem_SilicateMetallicityFieldIndexTable[k]]; + } + } + CellP[i].ISMDustChem_Dust_Species[j] = sil_mass_frac; + } + else if (spec_indx==All.ISMDustChem_Carb_Index) { + // Carbonaceous dust + CellP[i].ISMDustChem_Dust_Metal[2] = DMIN(P[i].Metallicity[2],sil_mass_frac/All.Initial_ISMDustChem_SiliconToCarbonRatio); + CellP[i].ISMDustChem_Dust_Species[j] = CellP[i].ISMDustChem_Dust_Metal[2]; + } + else if (spec_indx==All.ISMDustChem_FreeIron_Index) { + // Free-flying iron + CellP[i].ISMDustChem_Dust_Metal[10] = All.Initial_ISMDustChem_Depletion*P[i].Metallicity[10]; + CellP[i].ISMDustChem_Dust_Species[j] = (1.-GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC)*CellP[i].ISMDustChem_Dust_Metal[10]; + } + else if (spec_indx==All.ISMDustChem_InclIron_Index) { + CellP[i].ISMDustChem_Dust_Species[j] = GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC*CellP[i].ISMDustChem_Dust_Metal[10]; + } + } + for (j=1;j 2)) + CellP[i].Dust_Temperature = DMIN(All.InitGasTemp,100.); +#endif + } +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + Initialize_ISMDustChemEvo_Particle_Variables(i); +#endif +} + +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) +void Initialize_ISMDustChemEvo_Particle_Variables(int i) +{ + int j,k,l; +#if defined(IO_DUST_NOT_IN_ICFILE) + if(RestartFlag == 0 || RestartFlag == 2) { +#else + if(RestartFlag == 0) { +#endif + CellP[i].ISMDustChem_MachNumber = 0; + if(All.Initial_ISMDustChem_Depletion > 0 && CellP[i].ISMDustChem_Dust_Metal[0] > 0) + { + // Assume MRN powerlaw size distribution + double powerlaw = -3.5; + for (j=0;j 0) {*key_elem = k; break;}} // start with first element in silicates + for(k=0;k sil_elem_abunds[k] / All.ISMDustChem_SilicateNumberOfAtomsTable[k]) *key_elem = k; + } + *key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[*key_elem]; + *key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[*key_elem]; + *key_mass = All.ISMDustChem_AtomicMassTable[*key_elem]; + } + /******** CARBONACEOUS ********/ + else if (spec_indx==All.ISMDustChem_Carb_Index) {if (dust_metallicity[2]>0) {*key_elem=2; *key_mass=All.ISMDustChem_AtomicMassTable[*key_elem];}} + /******** METALLIC IRON ********/ + else if (spec_indx==All.ISMDustChem_FreeIron_Index || spec_indx==All.ISMDustChem_InclIron_Index) {if (dust_metallicity[10]>0) {*key_elem=10; *key_mass=All.ISMDustChem_AtomicMassTable[*key_elem];}} + /******** O RESERVOIR ********/ + else if (spec_indx==All.ISMDustChem_ORes_Index) {if (dust_metallicity[4]>0) {*key_elem=4; *key_mass=All.ISMDustChem_AtomicMassTable[*key_elem];}} +} + + +/* Approximate dust cooling via electron-dust collisions for MRN sized dust in plasmas from Dwek(1987)+Dwek&Werner(1981). Should surpass metal-line cooling for >10^6 K (even without considering dust depletion), but this will also overpredict dust cooling for <10^7 K since cooling is dominated by small grains which should be destroyed via sputtering */ +double Lambda_Dust_HighTemperature_Gas_ISM(int target, double T, double n_elec) +{ + if(target<0 || T<1.e5) {return 0;} // dust cooling << metal-line cooling below 10^5 K + if(CellP[target].ISMDustChem_Dust_Metal[0] <= 0) {return 0;} + // rho_c (gm cm^-3) grain solid density (intermediate between silicate and carbonaceous), a3 (cm^3) average grain volume for MRN grain size distribution with a=4-250nm (i.e. integrate a^3 dn/da with dn/da normalize to unity), Havg (erg s^−1 cm^3) average heating rate for a dust grain assuming MRN size distribution by incident electrons + double rho_c=3., a3=2.21e-18, h_frac = 1-(P[target].Metallicity[0]+P[target].Metallicity[1]); + double Havg, coolrate; + if (T>=7.17E7) {Havg=1.43E-11;} + else if (T>=2.39E7) {Havg=-2.07E-12+1.23E-16*pow(T,0.745)+2.10E-17*pow(T,0.75)-1.07E-17*pow(T,0.88);} + else if (T>=4.55E6) {Havg=-2.07E-12+1.70E-17*pow(T,0.745)+3.96E-17*pow(T,0.75)-5.44E-23*pow(T,1.5);} + else if (T>=1.52E6) {Havg=-1.06E-16*pow(T,0.745)+1.86E-17*pow(T,0.75)+1.56E-17*pow(T,0.88)-5.44E-23*pow(T,1.5);} + else {Havg=3.76E-22*pow(T,1.5);} + // Lambda/nH^2 cooling rate (ergs s^-1 cm^3) same as rest of cooling routine (note n_elec is the ratio of electron to H densities) + coolrate = (3.*CellP[target].ISMDustChem_Dust_Metal[0]*PROTONMASS_CGS)/(4.*M_PI*rho_c*h_frac)*n_elec*(Havg/a3); + if(!isfinite(coolrate)) {coolrate=0;} + return coolrate; +} + /* routine to give yields for dust for different types of SNe (Ia & II) followed in-code */ void ISMDustChem_get_SNe_dust_yields(double *yields, int i, double t_gyr, int SNeIaFlag, double Msne) { - double dust_yields[NUM_ISMDUSTCHEM_ELEMENTS]={0}, sources_yields[NUM_ISMDUSTCHEM_SOURCES]={0}, species_yields[NUM_ISMDUSTCHEM_SPECIES]={0}; double SNeIa_age = 0.03753; int k,source_key=1; + double dust_yields[NUM_ISMDUSTCHEM_ELEMENTS]={0}, species_yields[NUM_ISMDUSTCHEM_SPECIES]={0}; double SNeIa_age = 0.03753; int j,k,spec_indx,source_key=1; #if (GALSF_FB_FIRE_STELLAREVOLUTION > 2) SNeIa_age = 0.044; #endif if(t_gyr < SNeIa_age) {source_key=2;} // 1=1a, 2=II for(k=0;kyields[4]) {dust_yields[4]=yields[4];} // Just in case there's not enough O - for(k=2;ksil_elem_abund[k]) key_elem = k;} - species_yields[0] = SNeII_sil_cond * yields[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]] * All.ISMDustChem_EffectiveSilicateDustAtomicWeight / (All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem] * All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]); - for (k=0;k0 && yields[7]>0) - { - if (yields[7]/All.ISMDustChem_AtomicMassTable[7] < yields[2]/All.ISMDustChem_AtomicMassTable[2]) key_elem = 7; - else key_elem = 2; - species_yields[2] = SNeII_SiC_cond * yields[key_elem] * ((All.ISMDustChem_AtomicMassTable[2] + All.ISMDustChem_AtomicMassTable[7]) / All.ISMDustChem_AtomicMassTable[key_elem]); - dust_yields[2] += species_yields[2] * All.ISMDustChem_AtomicMassTable[2] / (All.ISMDustChem_AtomicMassTable[2] + All.ISMDustChem_AtomicMassTable[7]); - dust_yields[7] += species_yields[2] * All.ISMDustChem_AtomicMassTable[7] / (All.ISMDustChem_AtomicMassTable[2] + All.ISMDustChem_AtomicMassTable[7]); + for (k=0;k 2) transition_age = 0.044; #endif if(star_age <= transition_age) {return;} // no yield here if too young, otherwise continue - if(GALSF_ISMDUSTCHEM_MODEL & 1) { - double condens_eff = 0.8; - if((yields[2]/All.ISMDustChem_AtomicMassTable[2])/(yields[4]/All.ISMDustChem_AtomicMassTable[4]) > 1.0) // AGB stars with abundace ratio C/O > 1 only produce carbonacous dust - { - dust_yields[2] = yields[2] - 0.75*yields[4]; dust_yields[0] = dust_yields[2]; // C - } else { // AGB stars with abundance C/O < 1 produce general silicate dust - dust_yields[6] = condens_eff * yields[6]; // Mg - dust_yields[7] = condens_eff * yields[7]; // Si - dust_yields[10] = condens_eff * yields[10]; // Fe - dust_yields[4] = 16 * (dust_yields[6]/All.ISMDustChem_AtomicMassTable[6] + dust_yields[7]/All.ISMDustChem_AtomicMassTable[7] + dust_yields[10]/All.ISMDustChem_AtomicMassTable[10]); // O - // Check to make sure we dont produce too much O dust given the leftover O dust not in CO - if (dust_yields[4] > yields[4]-(4./3.*yields[2])) {dust_yields[4] = yields[4]-(4./3.*yields[2]);} - for(k=2;k 1.0) // AGB stars with abundace ratio C/O > 1 only produce carbonacous dust + // { + // dust_yields[2] = yields[2] - 0.75*yields[4]; dust_yields[0] = dust_yields[2]; // C + // } else { // AGB stars with abundance C/O < 1 produce general silicate dust + // dust_yields[6] = condens_eff * yields[6]; // Mg + // dust_yields[7] = condens_eff * yields[7]; // Si + // dust_yields[10] = condens_eff * yields[10]; // Fe + // dust_yields[4] = 16 * (dust_yields[6]/All.ISMDustChem_AtomicMassTable[6] + dust_yields[7]/All.ISMDustChem_AtomicMassTable[7] + dust_yields[10]/All.ISMDustChem_AtomicMassTable[10]); // O + // // Check to make sure we dont produce too much O dust given the leftover O dust not in CO + // if (dust_yields[4] > yields[4]-(4./3.*yields[2])) {dust_yields[4] = yields[4]-(4./3.*yields[2]);} + // for(k=2;k0.) + if (total_dust>0.) { // Now convert from instantaneous dust injection rates to dust yields using instantaneous wind rate wind_rate=0.41987*pow(star_age,-1.1)/(12.9-log(star_age)); @@ -133,79 +453,57 @@ void ISMDustChem_get_wind_dust_yields(double *yields, int i) #endif if(star_age < 0.033) {wind_rate *= 0.01 + calculate_relative_light_to_mass_ratio_from_imf(star_age,i,1);} // late-time independent of massive stars wind_rate *= All.StellarMassLoss_Rate_Renormalization; - - for (k=0;k yields[2]) - { - species_yields[1] *= yields[2]/elem_yield; - species_yields[2] *= yields[2]/elem_yield; - } - // Check O - elem_yield = species_yields[0] * All.ISMDustChem_AtomicMassTable[4] * All.ISMDustChem_SilicateNumberOfAtomsTable[0] / All.ISMDustChem_EffectiveSilicateDustAtomicWeight; - if (elem_yield > yields[4]) - { - species_yields[0] *= yields[4]/elem_yield; - } - // Check Mg - elem_yield = species_yields[0] * All.ISMDustChem_AtomicMassTable[6] * All.ISMDustChem_SilicateNumberOfAtomsTable[1] / All.ISMDustChem_EffectiveSilicateDustAtomicWeight; - if (elem_yield > yields[6]) - { - species_yields[0] *= yields[6]/elem_yield; + // First need to add up the total dust yields for each element from the tracked dust species + ISMDustChem_get_elem_yields_from_species_yields(dust_yields,species_yields); + for (k=2;kyields[k]) { + // Check each dust species so that we decrease the yields for all dust species which are composed of the given element + // Since we do this all in one go, if multiple elements all from one dust species are over the limit then we will decrease that + // dust species yield multiple times instead of once, but this shouldn't have much of an effect. + for (j=0;j0 && (spec_indx==All.ISMDustChem_Sil_Index)))) { + species_yields[j] *= yields[k]/dust_yields[k]; + } + } + dust_yields[k] = yields[k]; + } } - // Check Si - elem_yield = species_yields[0] * All.ISMDustChem_AtomicMassTable[7] * All.ISMDustChem_SilicateNumberOfAtomsTable[2] / All.ISMDustChem_EffectiveSilicateDustAtomicWeight + species_yields[2] * All.ISMDustChem_AtomicMassTable[7] / (All.ISMDustChem_AtomicMassTable[2] + All.ISMDustChem_AtomicMassTable[7]); - if (elem_yield > yields[7]) - { - species_yields[0] *= yields[7]/elem_yield; - species_yields[2] *= yields[7]/elem_yield; - } - // Check Fe - if(GALSF_ISMDUSTCHEM_MODEL & 4) { - if (species_yields[3] > yields[10]) {species_yields[3] = yields[10];} // Fe is only in free-flying iron, assume no iron inclusions in stellar dust - } else { // Fe is in free-flying iron and silicates - elem_yield = species_yields[0] * All.ISMDustChem_AtomicMassTable[10] * All.ISMDustChem_SilicateNumberOfAtomsTable[3] / All.ISMDustChem_EffectiveSilicateDustAtomicWeight + species_yields[3]; - if(elem_yield > yields[10]) {species_yields[0] *= yields[10]/elem_yield; species_yields[3] *= yields[10]/elem_yield;} - } - // Now convert from dust species to dust elemental mass - // silicates - for (k=0;k 0) - { - for(j=0;ja_cut && high_edge>a_cut) { + bin_number = C1_norm*sqrt(M_PI/2)*sigma*(erf(log(high_edge/a0)/(sqrt(2)*sigma))-erf(log(low_edge/a0)/(sqrt(2)*sigma))); + bin_mass = C1_norm*pow(a0,3)*pow(2*M_PI,3./2.)/3*bulk_dens*sigma*exp(9*sigma*sigma/2)*(erf((3*sigma*sigma+log(a0/low_edge))/(sqrt(2)*sigma))-erf((3*sigma*sigma+log(a0/high_edge))/(sqrt(2)*sigma))); } - for (j=1;ja_cut) { + bin_number = C2_norm*pow(a_cut*low_edge,-gamma)*(pow(a_cut,gamma)*low_edge-a_cut*pow(low_edge,gamma))/(gamma-1) + + C1_norm*sqrt(M_PI/2)*sigma*(erf(log(high_edge/a0)/(sqrt(2)*sigma))-erf(log(a_cut/a0)/(sqrt(2)*sigma))); + bin_mass = C2_norm*4*M_PI*bulk_dens/(3*(gamma-4))*(pow(low_edge,4-gamma)-pow(a_cut,4-gamma)) + + C1_norm*pow(a0,3)*pow(2*M_PI,3./2.)/3*bulk_dens*sigma*exp(9*sigma*sigma/2) * (erf((3*sigma*sigma-log(a_cut/a0))/(sqrt(2)*sigma))-erf((3*sigma*sigma-log(high_edge/a0))/(sqrt(2)*sigma))); + } + else { + bin_number = C2_norm/(gamma-1)*pow(high_edge*low_edge,-gamma)*(pow(high_edge,gamma)*low_edge-high_edge*pow(low_edge,gamma)); + bin_mass = (C2_norm*4.*M_PI*bulk_dens)/(3*(gamma-4))*(pow(low_edge,4-gamma)-pow(high_edge,4-gamma)); + } + // Deal with rounding errors for near empty bins + if (bin_number<=0 || bin_mass<=0) {bin_number=0;bin_mass=0;} + number_yields[k][l] = bin_number; + mass_yields[k][l] = bin_mass; } - else + } + + for(k=0;k10^6 K, but this will also overpredict dust cooling for <10^7 K since cooling is dominated by small grains which should be destroyed via sputtering */ -double Lambda_Dust_HighTemperature_Gas_ISM(int target, double T, double n_elec) +/* return the mass of gas shocked by an SNe in which dust can be destroyed */ +double ISMDustChem_Return_Mass_Where_Dust_Shocked(double rho_cell_in_code_units, double Esne51_into_cell, double mass_preshock_in_code_units, double Z_cell) { - if(target<0 || T<1.e5) {return 0;} - if(CellP[target].ISMDustChem_Dust_Metal[0] <= 0) {return 0;} - double rho_c=3., a3=2.21e-18, DG, Havg; // rho_c=gm cm^-3 grain solid density (intermediate between silicate and carbonaceous), a3=cm^3 average grain volume for MRN grain size distribution - if (T>=7.17E7) {Havg=1.43E-11;} else if (T>=2.4E7) {Havg=-2.07E-12+1.23E-16*pow(T,0.745)+2.10E-17*pow(T,0.75)-1.07E-17*pow(T,0.88);} - else if (T>=4.55E6) {Havg=-2.07E-12+1.70E-17*pow(T,0.745)+3.96E-17*pow(T,0.75)-5.44E-23*pow(T,1.5);} - else if (T>=1.52E6) {Havg=-1.06E-16*pow(T,0.745)+1.86E-17*pow(T,0.75)+1.56E-17*pow(T,0.88)-5.44E-23*pow(T,1.5);} else {Havg=3.76E-22*pow(T,1.5);} - double coolrate = (3./(4.*M_PI)*(PROTONMASS_CGS/HYDROGEN_MASSFRAC))*(CellP[target].ISMDustChem_Dust_Metal[0]/rho_c*n_elec*(Havg/a3)); - if(!isfinite(coolrate)) {coolrate=0;} - return coolrate; -} - - + double vs7=1., local_n0=rho_cell_in_code_units*All.cf_a3inv*UNIT_DENSITY_IN_NHCGS; // dust destruction efficiency, minimum gas shock velocity in ~10^7 cm/s which destroys dust, and number density around SNe + double mass_shocked_in_code_units; // mass shocked to 100 km/s which destroys dust. use the weights to distribute shocked mass across the neighboring gas particles +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + /* From detailed SNR simulations in Schaffler+ 2025 */ + // TBD + /* From fits in Yamasawa+ 2011 */ + mass_shocked_in_code_units = 1535 * Esne51_into_cell / (pow(local_n0, 0.202) * pow(Z_cell/All.SolarAbundances[0]+0.039,0.298) * UNIT_MASS_IN_SOLAR); +#else + /* Simple radiative SNR case from McKee 1989 and Cioffi 1988 */ + mass_shocked_in_code_units = 2460 * Esne51_into_cell / (pow(local_n0, 0.1) * pow(vs7, 9./7.) * UNIT_MASS_IN_SOLAR); +#endif -/* return the mass fraction we will assume of dust destroyed in surrounding gas due to SNe shock, taken from McKee 1989 and Cioffi 1988 */ -double ISMDustChem_Return_Mass_Fraction_Where_Dust_Destroyed(double rho_cell_in_code_units, double Esne51_into_cell, double mass_preshock_in_code_units) -{ - double dest_eff=0.4, vs7=1., local_n0=rho_cell_in_code_units*All.cf_a3inv*UNIT_DENSITY_IN_NHCGS; // dust destruction efficiency, minimum gas shock velocity in 10^7 cm/s which destroys dust, and number density around SNe - double mass_shocked = dest_eff * 2460 * Esne51_into_cell / (pow(local_n0, 0.1) * pow(vs7, 9./7.) * UNIT_MASS_IN_SOLAR); // mass shocked to 100 km/s which destroys dust. use the weights to distribute shocked mass across the neighboring gas particles - return DMIN(1., mass_shocked / mass_preshock_in_code_units); // mass fraction destroyed + return DMIN(mass_shocked_in_code_units * All.ISMDustChem_SNeGasClearedOfDustScaling, mass_preshock_in_code_units); // mass shocked limited to the entire mass of the gas particle } -/* Subroutine to update the dust abundances after enrichment in the mechanical feedback subroutine */ -void update_ISMDustChem_after_mechanical_injection(int j, double massfrac_destroyed, double m0, double mf, double *Z_injected) +/* Subroutine to update the dust abundances after enrichment in the mechanical feedback subroutine and destroy dust from SNe shocks */ +void update_ISMDustChem_after_mechanical_injection(int j, double mass_shocked, double m0, double mf, double *Z_injected) { // If SNe events happened need to first destroy the appropriate amount of dust if there is any dust - int k; - if((massfrac_destroyed > 0) && (CellP[j].ISMDustChem_Dust_Metal[0] > 0)) - { + int k,l,spec_indx; +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + // Mass is injected before this function in the feedback routine so this check will fail if we don't make a temporary mass change + // For evolving grain sizes the fraction of dust destroyed depends on the initial grain size distribution + // This uses a novel routine presented in Choban+25 (in prep) to approximate shattering and subsequent sputtering of dust grains + double mass_frac_shocked = DMIN(1,mass_shocked/m0), bulk_dens, dust_atomic_weight; + if ((mass_frac_shocked > 0) && (CellP[j].ISMDustChem_Dust_Metal[0] > 0)) { // if no gas shocked or no dust in gas then no dust destroyed CellP[j].ISMDustChem_DelayTimeSNeSputtering = All.ISMDustChem_SNeSputteringShutOffTime; // update thermal sputtering delay time due to SNe - if (massfrac_destroyed >= 1.) // destroy all dust - { - for(k=0;k m0) {mass_shocked = m0;} // limit total mass shocked to mass of gas particle + double massfrac_destroyed = mass_shocked*dest_eff/m0; + if((massfrac_destroyed > 0) && (CellP[j].ISMDustChem_Dust_Metal[0] > 0)) + { + CellP[j].ISMDustChem_DelayTimeSNeSputtering = All.ISMDustChem_SNeSputteringShutOffTime; // update thermal sputtering delay time due to SNe + if (massfrac_destroyed >= 1.) // destroy all dust + { + for(k=0;k sil_elem_abunds[k] / All.ISMDustChem_SilicateNumberOfAtomsTable[k]) key_elem = k; + ISMDustChem_update_iron_inclusions(j); + } + } + } +#endif // GALSF_ISMDUSTCHEM_GRAINSIZEEVO + // Inject newly created dust from star + int skip_injection = 1; +#ifndef GALSF_USE_SNE_ONELOOP_SCHEME + // AGB dust routines can give neglible amounts of dust and the feedback routine can cause yields with initially zero dust to have floating point precision errors. + // To avoid this, only inject dust when the species mass fractional change is greater than a very small number or dust is being injected where none exists. + for(k=0;k0) || fabs(((1./mf) * DMAX(0.,Z_injected[0+NUM_METAL_SPECIES])) / ((m0/mf) * CellP[j].ISMDustChem_Dust_Metal[0])) > 1E-10) { + skip_injection = 0; + break; + } + } +#else + skip_injection=0; // Cant check particle info for FIRE-2 since its not thread-safe +#endif + if (~skip_injection) { + // Z_injection has the total mass injected so need to be careful updating scalars + for(k=0;k0 && inject_M_in_bin>0) { + new_N_in_bin = CellP[j].ISMDustChem_Dust_NumberInBin[k][l] + inject_N_in_bin; + new_M_in_bin = get_ISMDustChemEvo_bin_mass(j,k,l) + inject_M_in_bin; + update_ISMDustChemEvo_bin_number_and_slope(j,k,l,new_N_in_bin,new_M_in_bin); } - key_mass = All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]; - key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem]; - key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]; - frac_of_max_sil = CellP[j].ISMDustChem_Dust_Species[0] / (P[j].Metallicity[key_elem] * All.ISMDustChem_EffectiveSilicateDustAtomicWeight/(key_num_atoms * key_mass)); - incl_frac = DMAX(DMIN(GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC*frac_of_max_sil,GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC),0.); - CellP[j].ISMDustChem_Dust_Species[3] = (1.-incl_frac) * CellP[j].ISMDustChem_Dust_Metal[10]; - CellP[j].ISMDustChem_Dust_Species[NUM_ISMDUSTCHEM_SPECIES-1] = incl_frac * CellP[j].ISMDustChem_Dust_Metal[10]; } } +#endif + } // ~skip_injection + else { + for(k=0;k 0) { + // Get total mass of grains that are shattered and the change in mass for each bin + for(l=0;la_frag_max) {alupper=a_frag_max;} // Catch bins that have include the maximum fragment size + bin_dM[l] += total_M_shat * (pow(alupper,0.7) - pow(allower,0.7)) / (pow(a_frag_max,0.7) - pow(a_frag_min,0.7)); + } + final_bin_M[l] = init_bin_M[l] + bin_dM[l]; + } + ISMDustChemEvo_get_new_bin_N_and_slope_given_mass_change(bin_dM, init_bin_M, init_bin_N, init_bin_slope, final_bin_N, final_bin_slope, bulk_dens); } - for(k=0;k 0) {CellP[i].ISMDustChem_DelayTimeSNeSputtering = DMAX(0,CellP[i].ISMDustChem_DelayTimeSNeSputtering-dtime_gyr);} + else { + update_dust_sputtering(i,dtime_gyr,temp,rho); + +#if defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) + update_dust_shattering_and_coagulation(i,dtime_gyr,temp,rho); + + update_dust_photodestruction(i,dtime_gyr); +#endif + } +} + +#if !defined(GALSF_ISMDUSTCHEM_GRAINSIZEEVO) +void update_dense_molecular_fields(int i, double temp, double rho, double nh0, double ne) +{ + /* Choban+22 version for FIRE-2. + * Calculate H2 fraction to determine whether gas is in the CNM/diffuse MC or dense MC phase. + * Gas-dust accretion is assumed to have Coloumb enhancing in CNM/diffuse MC due to dust grain charge and ionized metal species. + * In dense MC we assume no enhancing due to neutral metal species. + * Also use dense MC fraction to determine when C is locked up in CO and thuse unavailable for gas-dust accretion. + * We assume C rapidly converts to CO once the gas is sufficently molecular. */ double fH2=0., new_ISMDustChem_MassFractionInDenseMolecular=0.; // mass fraction of gas that is H2 and gas in dense MC phase - double NH2 = 1.5E21; // cm^-2 Column density of H2 needed to be in dense MC phase + double NH2 = 1.5E21; // cm^-2 Column density of H2 needed to be in dense MC phase (this is a tuned value but falls within observed range for rapid C->CO conversion) double l_depth, x_dens; // depth into cloud to reach NH2 and radial fraction of cloud in dense MC phase double surface_density = evaluate_NH_from_GradRho(P[i].GradRho,P[i].KernelRadius,CellP[i].Density,P[i].NumNgb,1,i) * UNIT_SURFDEN_IN_CGS; // converts to cgs // shielding length giving effective radius of gas particle @@ -555,211 +1045,214 @@ void update_dust_acc_and_sput(int i, double dtime_gyr) } } } - else - { - CellP[i].ISMDustChem_C_in_CO = 0.; - } + else {CellP[i].ISMDustChem_C_in_CO = 0.;} + CellP[i].ISMDustChem_MassFractionInDenseMolecular = new_ISMDustChem_MassFractionInDenseMolecular; - - if (CellP[i].ISMDustChem_Dust_Metal[0] <= 0) {return;} // No dust so nothing more to do - +} +#endif + + +void update_dust_accretion(int i, double dtime_gyr, double temp, double rho) +{ + int j,k,spec_indx; double dF; // change in fraction of element condensed into dust - double growth_timescale, sputter_timescale, t_ref, T_ref, avg_grain_radius; + double growth_timescale, t_ref, T_ref, avg_grain_radius; double dust_yields[NUM_ISMDUSTCHEM_ELEMENTS] = {0.0}; int source = 0; - // now accrete and sputter dust // -#if (GALSF_ISMDUSTCHEM_MODEL & 1) - CellP[i].ISMDustChem_Dust_Metal[0] = 0.; // First renorm dust due to building numerical error that can arise from stellar feedback. This may no longer be necessary. - for (k=2;k 0) {CellP[i].ISMDustChem_DelayTimeSNeSputtering = DMAX(0,CellP[i].ISMDustChem_DelayTimeSNeSputtering-dtime_gyr);} // count off clock since last SNe - else // Now determine amount of dust destroyed by thermal sputtering - { - T_ref = 2E6; avg_grain_radius = 0.032; /* um */ t_ref = 0.17; /* Gyr */ - sputter_timescale = t_ref * (avg_grain_radius / 0.1) / (rho*1E27) * (pow((T_ref/ temp), 2.5) + 1.); - for (k=0;k 0.) {no_dust = 0; break;}} - if (no_dust) - { - CellP[i].ISMDustChem_Dust_Metal[0] = 0.; - // if all dust is destroyed need to zero creation sources - for(k=0;k 300 K + // Use clumping factor here to account for gas with high mach numbers which will have effective temperatures below the cutoff + if (temp * temp_clump_factor <= T_cutoff) + { + for (k=0;kCO conversion for carbonaceous grains or ice freeze out for all other grains. The densities for these two processes are slightly different. + nH_max = 1E4; // This is default except for carbonaceous grains + if (spec_indx==All.ISMDustChem_Sil_Index) {D_small=10; D_large=0.5;} + else if (spec_indx==All.ISMDustChem_Carb_Index) {D_small=3; D_large=0;nH_max = 1E3;} + else if (spec_indx==All.ISMDustChem_FreeIron_Index) {D_small=20; D_large=1;} + else{D_small=1; D_large=1;} + // Determine clumping factors for density and temperature + // Note this an effective clumping factor which accounts for the turn off of accretion past a maximum density. + // nH_max is set either by the typical C to CO critical density for carbonaceous dust or density at which + // molecules freeze-out onto dust for all other species. + eff_clump_factor = exp(sigma_squared)/2*erfc((3*sigma_squared/2 - log(nH_max/nHcgs))/(sqrt(2*sigma_squared))); + // need all the constituent elements for the given dust species to grow + if (key_elem != -1) { + key_depl = CellP[i].ISMDustChem_Dust_Metal[key_elem]/P[i].Metallicity[key_elem]; + key_num_dens = rho * P[i].Metallicity[key_elem] * (1-key_depl)/ (key_mass * PROTONMASS_CGS); + ISMDustChem_get_species_properties(spec_indx, &dust_atomic_weight, &bulk_dens); + + // calculate Coulomb enhancement for each grain size bin + for (j=0;j=1) {break;} // Catch case where we run out of key element to grow dust + key_num_dens = rho * P[i].Metallicity[key_elem] * (1-key_depl) / (key_mass * PROTONMASS_CGS); + init_species_mass = final_species_mass; + } + for (j=0;j 300 K - if (temp <= 300) + if (temp <= T_cutoff) { - // Determine number density of each element in the gas phase, use this to determine the key element for each dust species - num_dens[0] = rho * (1. - P[i].Metallicity[0] - P[i].Metallicity[1]) / (All.ISMDustChem_AtomicMassTable[0] * PROTONMASS_CGS); - for (k=1;k num_dens[All.ISMDustChem_SilicateMetallicityFieldIndexTable[k]]/All.ISMDustChem_SilicateNumberOfAtomsTable[k]) {key_elem = k;} - } - key_mass = All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]; - key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem]; - key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]; - max_num_dens = rho * P[i].Metallicity[key_elem] / (key_mass * PROTONMASS_CGS); - - growth_timescale = t_ref * (key_num_atoms * sqrt(key_mass) / dust_formula_mass) * bulk_dens / max_num_dens / sqrt(temp); - // change in dust condensation for key element - dF = dtime_gyr * (1. - CellP[i].ISMDustChem_Dust_Metal[key_elem] / P[i].Metallicity[key_elem]) * CellP[i].ISMDustChem_Dust_Metal[key_elem] / growth_timescale; - // Check in case we use up the rest of the remaining metal in the gas phase and deal with unphysical values - dF = DMAX(0.,DMIN(P[i].Metallicity[key_elem]-CellP[i].ISMDustChem_Dust_Metal[key_elem],dF)); - - // change in dust condensation for all elements in silicates - for (k=0;k C -> CO is quick, assume C+ -> CO so carbon dust only grows in CNM environments - // Also need to take into account C in CO reduces the maximum amount of carbon dust which can be formed - if (CellP[i].ISMDustChem_MassFractionInDenseMolecular < 1.) - { - t_ref_CNM = 1.54E-3; // Gyr - t_ref = t_ref_CNM / (1.-CellP[i].ISMDustChem_MassFractionInDenseMolecular); - key_elem = 2; key_mass = All.ISMDustChem_AtomicMassTable[key_elem]; key_num_atoms = 1.; bulk_dens = 2.25; dust_formula_mass = key_mass; - if (num_dens[key_elem] > 0) - { - max_num_dens = rho * P[i].Metallicity[key_elem]/ (key_mass * PROTONMASS_CGS); - growth_timescale = t_ref * sqrt(key_mass) / dust_formula_mass * bulk_dens / max_num_dens / sqrt(temp); - dF = dtime_gyr * (1. - CellP[i].ISMDustChem_Dust_Metal[key_elem] / (P[i].Metallicity[key_elem] - CellP[i].ISMDustChem_C_in_CO)) * CellP[i].ISMDustChem_Dust_Metal[key_elem] / growth_timescale; + else if (spec_indx==All.ISMDustChem_Carb_Index) { + if (CellP[i].ISMDustChem_MassFractionInDenseMolecular < 1.) { + // Since the transition between C+ -> C -> CO is quick, assume C+ -> CO so carbon dust only grows in CNM environments + // Also need to take into account C in CO reduces the maximum amount of carbon dust which can be formed + t_ref_CNM = 1.54E-3; // Gyr + t_ref = t_ref_CNM / (1.-CellP[i].ISMDustChem_MassFractionInDenseMolecular) / All.ISMDustChem_DustAccretionScaling; + // Need to account for C locked in CO + key_elem_DZ = CellP[i].ISMDustChem_Dust_Metal[key_elem] / (P[i].Metallicity[key_elem] - CellP[i].ISMDustChem_C_in_CO); + key_gas_Z = gas_Z[key_elem] - CellP[i].ISMDustChem_C_in_CO; + } + } + else if (spec_indx==All.ISMDustChem_FreeIron_Index) { + // nano-particle sized or MRN-sized iron + if(GALSF_ISMDUSTCHEM_MODEL & 4) {t_ref_CNM = 1.66E-6; t_ref_MC = 0.139E-3;} + else {t_ref_CNM = 0.252E-3; t_ref_MC = 1.38E-3;} // Gyr + t_ref = (t_ref_CNM * t_ref_MC) / (CellP[i].ISMDustChem_MassFractionInDenseMolecular * t_ref_CNM + (1.-CellP[i].ISMDustChem_MassFractionInDenseMolecular) * t_ref_MC) / All.ISMDustChem_DustAccretionScaling; + key_elem_DZ = CellP[i].ISMDustChem_Dust_Metal[key_elem] / P[i].Metallicity[key_elem]; + key_gas_Z = gas_Z[key_elem]; + } + // O reservior is a special case + else if (spec_indx==All.ISMDustChem_ORes_Index) { + /* Observed O depletions (Jenkins 2009) cannot be explained by silicate dust alone. So + * throw extra oxygen into a reservoir to better match observations given O depletions vs + * number density from Whittet (2010). We assume that this reservoir only holds as much O + * as would be needed to match this trend scaled with the amount of silicate dust vs + * the maximum allowable amount of silicate dust in the gas. So if the maximum amount + * of silicate dust has formed than the O depletions should exactly match with + * observations. This scaling allows for some variability in bursty environments. + */ + double nHcgs = HYDROGEN_MASSFRAC * rho / PROTONMASS_CGS; /* hydrogen number dens in cgs units */ + double D_O = 1. - 0.65441 / pow(nHcgs,0.103725); /* expected fractional O depletion (upper limit) */ + double max_O_in_sil; /* max O depletion due to silicates */ + double extra_O=0.; /* extra O that needs to be depleted to match observations */ + double frac_of_sil; /* fraction of maximum amount of silicate present in gas */ + double O_in_CO; /* mass fraction of O in CO, sets max for D_O */ + O_in_CO = CellP[i].ISMDustChem_C_in_CO * All.ISMDustChem_AtomicMassTable[4] / All.ISMDustChem_AtomicMassTable[2] / P[i].Metallicity[4]; + D_O = DMAX(0.,DMIN(D_O, 1.-O_in_CO)); // set depletion upper limit to O in CO + int sil_indx = All.ISMDustChem_Sil_Index; + // Now determine maximum possible silicate dust based on the least abundant element + // This roughly scales with the fraction of the key element (usually Si) depleted into dust + ISMDustChem_get_species_key_elem(sil_indx, P[i].Metallicity, &key_elem, &key_num_atoms, &key_mass); + ISMDustChem_get_species_properties(sil_indx, &dust_atomic_weight, &bulk_dens); + // No extra O if there is no silicate dust + if (key_elem != -1) { + frac_of_sil = CellP[i].ISMDustChem_Dust_Species[All.ISMDustChem_SpeciesFieldIndexTable[sil_indx]] / (P[i].Metallicity[key_elem] * dust_atomic_weight/(key_num_atoms * key_mass)); + max_O_in_sil = P[i].Metallicity[key_elem] * ((All.ISMDustChem_SilicateNumberOfAtomsTable[0] * All.ISMDustChem_AtomicMassTable[4])/(key_num_atoms * key_mass)); + extra_O = frac_of_sil * D_O * P[i].Metallicity[4] - max_O_in_sil - CellP[i].ISMDustChem_Dust_Species[k]; + if (extra_O>0) {species_yields[k] = extra_O;} + } + } + + // check to make sure we have all the constituent elements for the given dust species and it is allowed to grow + if (t_ref > 0 && key_gas_Z > 0) { + ISMDustChem_get_species_properties(spec_indx, &dust_atomic_weight, &bulk_dens); + max_num_dens = rho * P[i].Metallicity[key_elem] / (key_mass * PROTONMASS_CGS); + growth_timescale = t_ref * (key_num_atoms * sqrt(key_mass) / dust_atomic_weight) * bulk_dens / max_num_dens / sqrt(temp); + // change in dust condensation for key element + dF = dtime_gyr * (1. - key_elem_DZ) * CellP[i].ISMDustChem_Dust_Metal[key_elem] / growth_timescale; // Check in case we use up the rest of the remaining metal in the gas phase and deal with unphysical values - dF = DMAX(0,DMIN(P[i].Metallicity[key_elem]-CellP[i].ISMDustChem_C_in_CO-CellP[i].ISMDustChem_Dust_Metal[key_elem],dF)); - dust_yields[key_elem] += dF; - species_yields[1] = dF; + dF = DMAX(0.,DMIN(key_gas_Z,dF)); + species_yields[k] = dF * (dust_atomic_weight / (key_num_atoms * key_mass)); } } - - /******** METALLIC IRON ********/ - if(GALSF_ISMDUSTCHEM_MODEL & 4) {t_ref_CNM = 1.66E-6; t_ref_MC = 0.139E-3;} else {t_ref_CNM = 0.252E-3; t_ref_MC = 1.38E-3;} // Gyr - // Calculate effective timescale assuming produced dust is redistributed throughout the gas - t_ref = (t_ref_CNM * t_ref_MC) / (CellP[i].ISMDustChem_MassFractionInDenseMolecular * t_ref_CNM + (1.-CellP[i].ISMDustChem_MassFractionInDenseMolecular) * t_ref_MC); - key_elem = 10; key_mass = All.ISMDustChem_AtomicMassTable[key_elem]; key_num_atoms = 1.; bulk_dens = 7.86; dust_formula_mass = All.ISMDustChem_AtomicMassTable[key_elem]; - if (num_dens[key_elem] > 0) - { - max_num_dens = rho * P[i].Metallicity[key_elem]/(key_mass * PROTONMASS_CGS); - growth_timescale = t_ref * sqrt(key_mass) / dust_formula_mass * bulk_dens / max_num_dens / sqrt(temp); - dF = dtime_gyr * (1. - CellP[i].ISMDustChem_Dust_Metal[key_elem] / P[i].Metallicity[key_elem]) * CellP[i].ISMDustChem_Dust_Species[3] / growth_timescale; - // Check in case we use up the rest of the remaining metal in the gas phase and deal with unphysical values - dF = DMAX(0.,DMIN(P[i].Metallicity[key_elem]-CellP[i].ISMDustChem_Dust_Metal[key_elem],dF)); - dust_yields[key_elem] += dF; - species_yields[3] = dF; - } - - // Add up all the dust elements - for (k=2;k sil_elem_abunds[k] / All.ISMDustChem_SilicateNumberOfAtomsTable[k]) {key_elem = k;} - } - key_mass = All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]; - key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem]; - key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]; - frac_of_max_sil = CellP[i].ISMDustChem_Dust_Species[0] / (P[i].Metallicity[key_elem] * All.ISMDustChem_EffectiveSilicateDustAtomicWeight/(key_num_atoms * key_mass)); - incl_frac = DMAX(DMIN(GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC*frac_of_max_sil,GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC),0.); - CellP[i].ISMDustChem_Dust_Species[3] = (1.-incl_frac) * CellP[i].ISMDustChem_Dust_Metal[10]; - CellP[i].ISMDustChem_Dust_Species[NUM_ISMDUSTCHEM_SPECIES-1] = incl_frac * CellP[i].ISMDustChem_Dust_Metal[10]; + if(GALSF_ISMDUSTCHEM_MODEL & 8) { // Update amount of free-flying iron and iron inclusions since some of the free-flying particles become inclusions in silicates. Scales with local amount of silicates + ISMDustChem_update_iron_inclusions(i); } } - } // if (temp <= 300) - - /* Observed O depletions (Jenkins 2009) cannot be explained by silicate dust alone. So - * throw extra oxygen into a reservoir to better match observations given O depletions vs - * number density from Whittet (2010). We assume that this reservoir only holds as much O - * as would be needed to match this trend scaled with the amount of silicate dust vs - * the maximum allowable amount of silicate dust in the gas. So if the maximum amount - * of silicate dust has formed than the O depletions should exactly match with - * observations. This scaling allows for some variability in bursty environments. - */ - if((GALSF_ISMDUSTCHEM_MODEL & 8) && (temp <= 300)) // We only add to the O reservoir when in an environment where dust can grow - { +#endif + } // if (temp <= 300) +} + + +// Update dust grains due to thermal sputtering. This primarily depends on the local gas temperature and density. +void update_dust_sputtering(int i, double dtime_gyr, double temp, double rho) +{ + // Sputtering timescales are negligable for cool gas + if (temp>1E4) { + int k,j,spec_indx; + double dust_yields[NUM_ISMDUSTCHEM_ELEMENTS] = {0.0}; + double species_yields[NUM_ISMDUSTCHEM_SPECIES] = {0.0}; +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + int k_cycle, n_subcycle; + double carbSput, silSput, ironSput, Y_sput, dadt, dust_formula_mass, clumping_factor; + double logt = log10(temp); double nHcgs = HYDROGEN_MASSFRAC * rho / PROTONMASS_CGS; /* hydrogen number dens in cgs units */ - double D_O = 1. - 0.65441 / pow(nHcgs,0.103725); /* expected fractional O depletion (upper limit) */ - double max_O_in_sil; /* max O depletion due to silicates */ - double extra_O; /* extra O that needs to be depleted to match observations */ - double frac_of_sil; /* fraction of maximum amount of silicate present in gas */ - double O_in_CO; /* mass fraction of O in CO, sets max for D_O */ - O_in_CO = CellP[i].ISMDustChem_C_in_CO * All.ISMDustChem_AtomicMassTable[4] / All.ISMDustChem_AtomicMassTable[2] / P[i].Metallicity[4]; - D_O = DMAX(0.,DMIN(D_O, 1.-O_in_CO)); // set depletion upper limit to O in CO - - // Now determine maximum possible silicate dust based on the least abundant element - // This roughly scales with the fraction of the key element (usually Si) depleted into dust - key_elem = 0; - for(k=0;k num_dens[index] / All.ISMDustChem_SilicateNumberOfAtomsTable[k]) key_elem = k; - } - key_mass = All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]; - key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem]; - key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]; - frac_of_sil = CellP[i].ISMDustChem_Dust_Species[0] / (P[i].Metallicity[key_elem] * All.ISMDustChem_EffectiveSilicateDustAtomicWeight/(key_num_atoms * key_mass)); - max_O_in_sil = P[i].Metallicity[key_elem] * ((All.ISMDustChem_SilicateNumberOfAtomsTable[0] * All.ISMDustChem_AtomicMassTable[4])/(key_num_atoms * key_mass)); - extra_O = frac_of_sil * D_O * P[i].Metallicity[4] - max_O_in_sil - CellP[i].ISMDustChem_Dust_Species[4]; - // If needed O depletion can't be attributed to silicate dust and what's already in the oxygen reservoir throw more oxygen into the reservoir - if (extra_O > 0) - { - // Update creation source - CellP[i].ISMDustChem_Dust_Source[source] += extra_O; - // Now add the dust - CellP[i].ISMDustChem_Dust_Metal[0] += extra_O; - CellP[i].ISMDustChem_Dust_Metal[4] += extra_O; - CellP[i].ISMDustChem_Dust_Species[4] += extra_O; - } - } // if using o reservoir and (temp <= 300) - - // Check if sputtering is delayed due to recent SNe - if(CellP[i].ISMDustChem_DelayTimeSNeSputtering > 0) {CellP[i].ISMDustChem_DelayTimeSNeSputtering = DMAX(0,CellP[i].ISMDustChem_DelayTimeSNeSputtering-dtime_gyr);} // count off clock since last SNe - else // Now determine amount of dust destroyed by thermal sputtering - { - T_ref = 2E6; avg_grain_radius = 0.032; /* um */ t_ref = 0.17; /* Gyr */ - sputter_timescale = t_ref * (avg_grain_radius / 0.1) / (rho*1E27) * (pow((T_ref/ temp), 2.5) + 1.); - - for (k=0;k 0) { + dadt = DMIN(0, -Y_sput * 1E-4 * nHcgs * 1E9 * clumping_factor * All.ISMDustChem_ThermalSputteringScaling); // cm/Gyr + // Find smallest bin with grains + a1_width = All.ISMDustChem_GrainBinEdges[1]-All.ISMDustChem_GrainBinEdges[0]; + for (j=0;j 0) { + a1_width=All.ISMDustChem_GrainBinEdges[j+1]-All.ISMDustChem_GrainBinEdges[j]; + break; + } + } + // Check if we need to subcycle the timesteps given the change in grain size + dt_sput = fabs(ACC_SPUT_SUBCYCLE_PARAMETER*a1_width/dadt); + if (dt_sput < dtime_gyr) {n_subcycle = IMIN(MAXIMUM_SUBCYCLE_STEPS,ceil(dtime_gyr/dt_sput)); dt_subcycle = dtime_gyr/n_subcycle;} + else {{n_subcycle = 1; dt_subcycle = dtime_gyr;}} + + for (j=0;j0) {all_dest = 0; break;}} + for (k=0;k0 && All.ISMDustChem_TrackedSpeciesIDTable[k]!=All.ISMDustChem_InclIron_Index) {all_dest = 0; break;}} if (all_dest) { - for(k=0;k sil_elem_abunds[k] / All.ISMDustChem_SilicateNumberOfAtomsTable[k]) {key_elem = k;} + if(GALSF_ISMDUSTCHEM_MODEL & 8) { // Update amount of free-flying iron and iron inclusions since some of the inclusions are released as silicates are sputtered. This scales with local amount of silicates. Note if all free-flying dust is destroyed then we assume all iron inclusions are also destroyed + ISMDustChem_update_iron_inclusions(i); + } + } + } +#endif // size evo + } // temperature cutoff +} + + +void update_dust_shattering_and_coagulation(int i, double dtime_gyr, double temp, double rho) +{ +#ifdef GALSF_ISMDUSTCHEM_GRAINSIZEEVO + + // Gas cell volume (cm^-3), relative velocity between colliding grains (cm/s), mass of shattered grains (g), i, j, k grain velocities (cm/s), mach factor for grain velocities, cos theta for angle of impact between two grains + double Vcell, mshat, vgri, vgrk, vgrj, vikrel, vkjrel, vikcoag, vkjcoag, Mach=CellP[i].ISMDustChem_MachNumber, cos_imp_angle, b_time_Mach, clumping_factor; + double vgr[NUM_ISMDUSTCHEM_SIZE_BINS], m_bin[NUM_ISMDUSTCHEM_SIZE_BINS], vrel[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS], vcoag[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS], poly[NUM_ISMDUSTCHEM_SIZE_BINS][NUM_ISMDUSTCHEM_SIZE_BINS]; + double nH_cgs = HYDROGEN_MASSFRAC * rho / PROTONMASS_CGS; // hydrogen number dens in cgs units + // Dust physical properties + // shattering and coagulation thresholds (cm/s), critical pressure (dyn cm^-2), surface energy per area (dyn cm^-2), Poisson's ratio (dyn cm^-2), Young's modulus + double vshat, P1, gamma, nu_poisson, E_young; + double ailower, aiupper, aicenter, ajlower, ajupper, ajcenter, aklower, akupper, akcenter; + double mlost_shat, mgained_shat, mlost_coag, mgained_coag, total_mgained, total_mlost, miavg, mkj_shat, mkj_coag; + double mk, mj, mej, phi, Eimp, QDstar, afmax, afmin, m_inj, mremnant, aremnant, aaggregate; + int k, bin_i, bin_j, bin_k, spec_indx; + int k_cycle, n_subcycle; + double tau_coll, dMdt_moved, dNdt_moved, dt_subcycle=dtime_gyr, total_N; + // additional clumping factor for coagulation following a power-law, used only for coagulation and not shattering + double enh_factor=1, nH_min=0.1, nH_max=100; + double enh_power=log10(COAGULATION_DENSITY_ENHANCEMENT * All.ISMDustChem_CoagDensityEnhancementScaling)/log10(nH_max/nH_min); + b_time_Mach = 0.5*Mach; + clumping_factor = 1+b_time_Mach*b_time_Mach; + Vcell = (P[i].Mass*UNIT_MASS_IN_CGS)/rho; // cm^3 + + // Coagulation is efficient in dense MC gas (nH~10^4) which is beyond typical FIRE resolutions. + // To overcome this we artificially enhance the density of cool gas, using the same temperature + // cutoff as accretion, a power law density enhancement factor depending on the density, and an + // assumed Mach number of 1 for dense gas to lower grain velocities. + double T_cutoff = ACCRETION_T_CUTOFF*All.ISMDustChem_AccretionTcutoffScaling; + if (temp / sqrt(clumping_factor) <= T_cutoff) { + if (nH_cgs <= nH_min) {enh_factor=1;} + else if (nH_cgs <= nH_max) {enh_factor = pow(nH_cgs/nH_min, enh_power);} + else {enh_factor = COAGULATION_DENSITY_ENHANCEMENT * All.ISMDustChem_CoagDensityEnhancementScaling;} + nH_cgs *= enh_factor; + Vcell /= enh_factor; + // Need to curtail Mach number for grain velocities to be below coagulation threshold + Mach=1; + } + gsl_rng *random_generator_fordust; /* generate uniform random number for grain impact angle */ + random_generator_fordust = gsl_rng_alloc(gsl_rng_ranlxd1); + gsl_rng_set(random_generator_fordust, P[i].ID + 11 + All.NumCurrentTiStep); + + for(k=0;k vshat) {mlost_shat += All.ISMDustChem_ShatteringScaling * vikrel * poly[bin_i][bin_k];} + // Mass lost from bin i due to coagulating collisions with grains in bin k + else if (vikrel <= vikcoag) {mlost_coag += All.ISMDustChem_CoagulationScaling * (vikrel) * poly[bin_i][bin_k];} + for (bin_j=0;bin_j vshat) { + Eimp = 0.5*(mk*mj/(mk+mj))*vkjrel*vkjrel; // impact energy betwen grains + QDstar = P1/(2*bulk_dens); // specific impact energy that causes more than 1/2 of mk to be disrupted + phi = Eimp/(mk*QDstar); + mej = phi/(1+phi)*mk; // total mass of material ejected and shattered from mk + // Assume dn_frag/da = C_frag * a^-3.3 with max and min size fragments, where C_frag is a normalization factor to recover the total ejecta mass mej + afmax = pow(0.02*mej/mk,1./3.)*akcenter; afmin = 0.01*afmax; + // Determine if there are any fragments moving into this bin based on integration bounds + double aintlower = ailower, aintupper = aiupper; + // No injected fragments of grains in this bin + if (afmin > aiupper || afmax < ailower) {mkj_shat=0;} + // Injected mass from fragments of grain into bin k + else { + // Deal with shattered grains smaller than the minimum bin by injection them into the minimum bin + if (bin_i==0 && afmin ailower) {aintlower = afmin;} + if (afmax < aiupper) {aintupper = afmax;} + mkj_shat = (pow(aintupper,0.7) - pow(aintlower,0.7))/(pow(afmax,0.7) - pow(afmin,0.7))*mej; + } + // Check to injected mass from remnant (if there is any) of grain in bin k after shattering + if (mej < mk) { + aremnant = DMAX(0,pow(akcenter*akcenter*akcenter - mej/(4./3.*M_PI*bulk_dens),1./3.)); + mremnant = DMAX(0,mk-mej); + if (aremnant > ailower && aremnant <= aiupper) {mkj_shat += mremnant;} + } + mgained_shat += All.ISMDustChem_ShatteringScaling * vkjrel * mkj_shat * poly[bin_k][bin_j]; + } + // Mass gained in bin i due to coagulating collisions between grains in bin k and bin j producing aggregate grains + else if (vkjrel <= vkjcoag) { + aaggregate = pow((mk + mj)/(4.*M_PI/3.*bulk_dens),1./3.); + if (aaggregate < aiupper && aaggregate >= ailower) {mkj_coag = (mk + mj)/2;} // Counted twice so divide by 2 + else {mkj_coag = 0;} + mgained_coag += All.ISMDustChem_CoagulationScaling * (vkjrel) * mkj_coag * poly[bin_k][bin_j]; + } + } + } + // Note change in Vcell due to coagulation density enhancement only applied to coagulation mass change + total_mlost = (mlost_coag+mlost_shat)*M_PI*miavg; // units of g/s cm^3 + total_mgained = (mgained_coag+mgained_shat)*M_PI; // units of g/s cm^3 + dM[bin_i] = (total_mgained-total_mlost)*clumping_factor/Vcell*dt_subcycle*1E9*SECONDS_PER_YEAR; // grams + // Keep track of the net mass and number grains moved out of bins for time step subcycling check + if (k_cycle==0) { + total_N += CellP[i].ISMDustChem_Dust_NumberInBin[k][bin_i]; + if (dM[bin_i] < 0) { + dMdt_moved-=dM[bin_i]/dt_subcycle; + dNdt_moved-=dM[bin_i]/miavg/dt_subcycle; + } + } + } + // Determine if we need to subcycle timesteps if either the number or mass of grains moved out of bins is greater than epsilon_cycle fraction of the total in the particle + if (k_cycle == 0) { + if (dMdt_moved == 0) {break;} // If no dust moved nothing to do here + tau_coll = SHAT_COAG_SUBCYCLE_PARAMETER * DMIN(fabs(CellP[i].ISMDustChem_Dust_Species[k]*P[i].Mass*UNIT_MASS_IN_CGS / dMdt_moved),fabs(total_N / dNdt_moved)); // Gyr + // No sub cycling needed so we can finish + if (tau_coll > dtime_gyr) { + ISMDustChemEvo_update_bins_given_mass_change(i, k, dM, bulk_dens); + } + // Sub cycling is needed so we need to determine the number of subcycles and the timestep for each subcycle + else { + n_subcycle = DMIN(MAXIMUM_SUBCYCLE_STEPS,ceil(dtime_gyr/tau_coll)); + dt_subcycle = dtime_gyr/n_subcycle; + } + } + // We are subcyling so only need to update the bins + else {ISMDustChemEvo_update_bins_given_mass_change(i, k, dM, bulk_dens);} + k_cycle++; + } + } + gsl_rng_free(random_generator_fordust); +#endif +} + + +void update_dust_photodestruction(int i, double dtime_gyr) +{ + // still in development so off by default + // current implementation is too effective at destroying dust + // going to rework this to inject destruction similar to SNe model. + // Such as assume stromgren sphere and clear dust mass within said sphere. +#if defined(DUSTPHOTODESTRUCTION_TURNON) + // If gas has been recently photoionized then dust should also be destroyed via photodestruction + // The delay time is zeroed after recombination so it serves as a useful tracker of HII regions + if (CellP[i].DelayTimeHII != 0) { + double dt_limited = DMIN(dtime_gyr, 0.01);// PDRs dont last longer than ~10 Myr so limit the timestep in case we dont time resolve the recombination + // Largest grain size photodestroyed (5 nm) and typical photodestruction timescale (5 Myr) + double a_pd = 5E-7, tau_pd = 5E-3; + int k, j, k_cycle, n_subcycle; + double dadt, a1_width, dt_pd, dt_subcycle; + double dust_yields[NUM_ISMDUSTCHEM_ELEMENTS] = {0.0}; + double species_yields[NUM_ISMDUSTCHEM_SPECIES] = {0.0}; + double bin_da[NUM_ISMDUSTCHEM_SIZE_BINS] = {0.0}; + for(k=0;k 0) { + a1_width=All.ISMDustChem_GrainBinEdges[j+1]-All.ISMDustChem_GrainBinEdges[j]; + break; } - key_mass = All.ISMDustChem_AtomicMassTable[All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]]; - key_num_atoms = All.ISMDustChem_SilicateNumberOfAtomsTable[key_elem]; - key_elem = All.ISMDustChem_SilicateMetallicityFieldIndexTable[key_elem]; - frac_of_max_sil = CellP[i].ISMDustChem_Dust_Species[0] / (P[i].Metallicity[key_elem] * All.ISMDustChem_EffectiveSilicateDustAtomicWeight/(key_num_atoms * key_mass)); - incl_frac = DMAX(DMIN(GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC*frac_of_max_sil,GALSF_ISMDUSTCHEM_VAR_IRON_INCL_FRAC),0.); - CellP[i].ISMDustChem_Dust_Species[3] = (1.-incl_frac) * CellP[i].ISMDustChem_Dust_Metal[10]; - CellP[i].ISMDustChem_Dust_Species[NUM_ISMDUSTCHEM_SPECIES-1] = incl_frac * CellP[i].ISMDustChem_Dust_Metal[10]; } + // Check if we need to subcycle the timesteps given the change in grain size + dt_pd = fabs(ACC_SPUT_SUBCYCLE_PARAMETER*a1_width/dadt); + if (dt_pd < dt_limited) {n_subcycle = IMIN(MAXIMUM_SUBCYCLE_STEPS,ceil(dt_limited/dt_pd)); dt_subcycle = dt_limited/n_subcycle;} + else {{n_subcycle = 1; dt_subcycle = dt_limited;}} + + for (j=0;jP[i].Metallicity[k]) { + // Check each dust species so that we decrease the yields for all dust species which are composed of the given element + for (j=0;j0 && (spec_indx==All.ISMDustChem_Sil_Index)))) { + CellP[i].ISMDustChem_Dust_Species[j] *= P[i].Metallicity[k]/dust_yields[k]; + } + } + dust_yields[k] = P[i].Metallicity[k]; + } + } + // Recalculate total dust yields after renorm + dust_yields[0]=0; + for (k=2;k0 && mass_in_bin>0) { + + double alower = All.ISMDustChem_GrainBinEdges[k], aupper = All.ISMDustChem_GrainBinEdges[k+1], acenter=All.ISMDustChem_GrainBinCenters[k]; + double bulk_dens, dust_atomic_weight; + ISMDustChem_get_species_properties(All.ISMDustChem_TrackedSpeciesIDTable[j], &dust_atomic_weight, &bulk_dens); + slope_in_bin = (3*mass_in_bin/(4*M_PI*bulk_dens)-number_in_bin/(4*(aupper-alower))*(pow(aupper,4)-pow(alower,4))) / ((pow(aupper,5)-pow(alower,5))/5-acenter/4*(pow(aupper,4)-pow(alower,4))); + check_for_slope_limiting(k, bulk_dens, &number_in_bin, &slope_in_bin, mass_in_bin); + } + else { + number_in_bin = 0; + slope_in_bin = 0; + } + // Assign new number and slope + CellP[i].ISMDustChem_Dust_NumberInBin[j][k] = number_in_bin; + CellP[i].ISMDustChem_Dust_SlopeInBin[j][k] = slope_in_bin; +} + +/* routine to check bin slopes for unphysical values within bin k (i.e dn/da < 0 at bin edge) for a dust species with a given bulk density. + If found then adjust slope and number such that slope is zero at bin edge and mass in conserved */ +void check_for_slope_limiting(int k, double bulk_dens, double *number_in_bin, double *slope_in_bin, double mass_in_bin) +{ + double alower = All.ISMDustChem_GrainBinEdges[k], aupper = All.ISMDustChem_GrainBinEdges[k+1], acenter=All.ISMDustChem_GrainBinCenters[k]; + double lower_edge, upper_edge; + + lower_edge = *number_in_bin / (aupper-alower) + *slope_in_bin * (alower-acenter); + upper_edge = *number_in_bin / (aupper-alower) + *slope_in_bin * (aupper-acenter); + // Large slopes can cause negative values at bin edges which are unphysical, so correct if needed. + if (lower_edge < 0 || upper_edge < 0) { + // To fix, conserve the mass of the bin but change the slope and number of grains so the + // grain size distribution is zero (to machine accuracy) at the edge + if (lower_edge < 0) { + *number_in_bin = 15*(alower-acenter)*mass_in_bin/(M_PI*bulk_dens*(alower-aupper)*(pow(alower,3)+2*pow(alower,2)*aupper+3*alower*pow(aupper,2)+4*pow(aupper,3))); + *slope_in_bin = 15*mass_in_bin/(M_PI*bulk_dens*(pow(alower,5)-5*alower*pow(aupper,4)+4*pow(aupper,5))); + } + else if (upper_edge < 0) { + *number_in_bin = 15*(acenter-aupper)*mass_in_bin/(M_PI*bulk_dens*(alower-aupper)*(4*pow(alower,3)+3*pow(alower,2)*aupper+2*alower*pow(aupper,2)+pow(aupper,3))); + *slope_in_bin = -15*mass_in_bin/(M_PI*bulk_dens*(4*pow(alower,5)-5*aupper*pow(alower,4)+pow(aupper,5))); + } + } +} + + +/* routine to update grain size bins for dust species j of particle i given change (only all positive or all negative) in grain size for each bin. For increasing grain sizes, give an expected limit to the mass (in grams) of the dust species so that you don't grow more dust than is availabe from metallicity */ +void ISMDustChemEvo_update_bins_given_grain_size_change(int i, int j, double *bin_da, double mass_limit) +{ + int l, m, m_start, m_stop; + double x1, x2, l_low_edge, l_high_edge, m_low_edge, m_high_edge,m_center, bulk_dens, dust_atomic_weight, da, sign=0; + double new_bin_masses[NUM_ISMDUSTCHEM_SIZE_BINS+2]={0.}, new_bin_numbers[NUM_ISMDUSTCHEM_SIZE_BINS+2]={0.}; + ISMDustChem_get_species_properties(All.ISMDustChem_TrackedSpeciesIDTable[j], &dust_atomic_weight, &bulk_dens); + // Determine sign of change + for(l=0;lx1) { + new_bin_numbers[l+1] += (CellP[i].ISMDustChem_Dust_NumberInBin[j][m] * (x2-x1))/(m_high_edge-m_low_edge) + CellP[i].ISMDustChem_Dust_SlopeInBin[j][m] * (1/2.*(x2*x2-x1*x1)-m_center*(x2-x1)); + // define these short hands since the mass equation is quite long + double fm_x1 = pow(x1,5)/5 + (3*da - m_center)/4*pow(x1,4) + da*(da-m_center)*pow(x1,3) + da*da*(da-3*m_center)/2*x1*x1 - da*da*da*m_center*x1; + double fm_x2 = pow(x2,5)/5 + (3*da - m_center)/4*pow(x2,4) + da*(da-m_center)*pow(x2,3) + da*da*(da-3*m_center)/2*x2*x2 - da*da*da*m_center*x2; + new_bin_masses[l+1] += 4*M_PI*bulk_dens/3*(CellP[i].ISMDustChem_Dust_NumberInBin[j][m] / (4*(m_high_edge-m_low_edge)) * (pow(x2+da,4)-pow(x1+da,4)) + CellP[i].ISMDustChem_Dust_SlopeInBin[j][m]*(fm_x2-fm_x1)); + } + + } + } + + // Check to make sure growing grains don't use up more metals than there are available. + // If this happens shift all the grain bin masses down (i.e. shrink the grains a little) + if (sign>0) { + double total_mass = 0; + for(l=0;lmass_limit) { + for(l=0;l0 && new_bin_numbers[NUM_ISMDUSTCHEM_SIZE_BINS+1]>0) { + double avg_size, new_avg_size, new_total_mass, rebinned_number, last_bin_num, last_bin_mass, last_bin_slope; + double new_slope_in_bin, new_number_in_bin; + m = NUM_ISMDUSTCHEM_SIZE_BINS-1; + m_low_edge = All.ISMDustChem_GrainBinEdges[m]; m_high_edge = All.ISMDustChem_GrainBinEdges[m+1]; + m_center = All.ISMDustChem_GrainBinCenters[m]; + last_bin_num = CellP[i].ISMDustChem_Dust_NumberInBin[j][m]; + last_bin_slope = CellP[i].ISMDustChem_Dust_SlopeInBin[j][m]; + last_bin_mass = get_ISMDustChemEvo_bin_mass(i,j,m); + // 1: average grain size in last bin before any rebinning + if (CellP[i].ISMDustChem_Dust_NumberInBin[j][m]>0) { + avg_size = (m_high_edge*m_high_edge-m_low_edge*m_low_edge)/(2*(m_high_edge-m_low_edge))+last_bin_slope/last_bin_num * ((pow(m_high_edge,3)-pow(m_low_edge,3))/3 - (m_high_edge*m_high_edge-m_low_edge*m_low_edge)*m_center/2); + } + else {avg_size = 0;} + // 2: number of grains to be rebinned by shrinking grains to the max grain size but conserving mass + rebinned_number = new_bin_masses[l+1]/(4./3.*M_PI*bulk_dens*pow(m_high_edge,3)); + // 3: new average grain size in last bin after shrinking rebinned grains + new_avg_size = (last_bin_num*avg_size + rebinned_number*m_high_edge) / (last_bin_num + rebinned_number); + // 4: new total mass after we shift all excess mass back into last bin + new_total_mass = last_bin_mass + new_bin_masses[l+1]; + // 5 : new number and slope for last bin. Solved by assuming total mass before and after rebinning is conserved and determining the new average grain size + // in the last bin when you assume the rebinned mass are all grains of the maximum grain size. + // This results in a mess of an equation, but substituting m_center = (m_low_edge+m_high_edge)/2 makes it simpler. + new_slope_in_bin = (-45*new_total_mass*(m_low_edge + m_high_edge - 2*new_avg_size)) / + (pow(m_low_edge - m_high_edge,3)*(2*(m_low_edge + m_high_edge)*(pow(m_low_edge,2) + 3*m_low_edge*m_high_edge + pow(m_high_edge,2)) - + 3*(3*pow(m_low_edge,2) + 4*m_low_edge*m_high_edge + 3*pow(m_high_edge,2))*new_avg_size)*M_PI*bulk_dens); + new_number_in_bin = (-15*new_total_mass)/(2.*(2*(m_low_edge + m_high_edge)*(pow(m_low_edge,2) + 3*m_low_edge*m_high_edge + + pow(m_high_edge,2)) - 3*(3*pow(m_low_edge,2) + 4*m_low_edge*m_high_edge + 3*pow(m_high_edge,2))*new_avg_size)*M_PI*bulk_dens); + // make sure to limit the new slope if necessary + check_for_slope_limiting(m, bulk_dens, &new_number_in_bin, &new_slope_in_bin, new_total_mass); + // Assign new number and slope + CellP[i].ISMDustChem_Dust_NumberInBin[j][m] = new_number_in_bin; + CellP[i].ISMDustChem_Dust_SlopeInBin[j][m] = new_slope_in_bin; + } + // (case l = -1) for grains which shrink below the min grain size we assume they are fully destroyed so nothing to do here + } +} + + +// Update bin numbers and slopes for particle i and dust species j given mass change from mass-conserving processes +void ISMDustChemEvo_update_bins_given_mass_change(int i, int j, double *bin_dM, double bulk_dens) +{ + int bin_i; + double a_avg_new, a_avg_old, a_avg_inj, m_avg_inj, N_add, ailower, aiupper, aicenter, new_mass_in_bin, new_slope_in_bin, new_number_in_bin; + double total_dM=0, total_pos_dM=0, total_neg_dM=0; + double bin_M[NUM_ISMDUSTCHEM_SIZE_BINS], new_bin_slope[NUM_ISMDUSTCHEM_SIZE_BINS], new_bin_N[NUM_ISMDUSTCHEM_SIZE_BINS]; + // First ensure total mass change is zero by limiting dM + for (bin_i=0;bin_i0) {total_pos_dM+=bin_dM[bin_i];} + else if (bin_dM[bin_i]<0) {total_neg_dM+=bin_dM[bin_i];} + } + total_dM = total_pos_dM+total_neg_dM; + + for (bin_i=0;bin_i 0 && bin_dM[bin_i]>0) {bin_dM[bin_i] *= (1 - total_dM/total_pos_dM);} + else if (total_dM < 0 && bin_dM[bin_i]<0) {bin_dM[bin_i] *= (1 - total_dM/total_neg_dM);} + } + + // Now update bin number and slope + ISMDustChemEvo_get_new_bin_N_and_slope_given_mass_change(bin_dM, bin_M, CellP[i].ISMDustChem_Dust_NumberInBin[j], CellP[i].ISMDustChem_Dust_SlopeInBin[j], new_bin_N, new_bin_slope, bulk_dens); + for (bin_i=0;bin_i0) { + // Assume injected grains have an average size and mass + a_avg_inj = 1.77*(pow(aiupper,-1.3) - pow(ailower,-1.3))/(pow(aiupper,-2.3) - pow(ailower,-2.3)); + m_avg_inj = (4*M_PI*bulk_dens/3)*(-3.286*(pow(aiupper,0.7) - pow(ailower,0.7)))/(pow(aiupper,-2.3) - pow(ailower,-2.3)); + N_add = bin_dM[bin_i]/m_avg_inj; + // Assume preexisting grains in bin have the same average size + if (bin_N[bin_i]>0) { + a_avg_old = (aiupper*aiupper-ailower*ailower)/(2*(aiupper-ailower)) + bin_slope[bin_i]/bin_N[bin_i] * ((pow(aiupper,3)-pow(ailower,3))/3 - (aiupper*aiupper-ailower*ailower)*aicenter/2); + a_avg_new = (bin_N[bin_i] * a_avg_old + N_add * a_avg_inj) / (bin_N[bin_i] + N_add); + } + else {a_avg_new = a_avg_inj;} + } + // If mass is lost, assume average grain size stays the same + else {a_avg_new = (aiupper*aiupper-ailower*ailower)/(2*(aiupper-ailower)) + bin_slope[bin_i]/bin_N[bin_i] * ((pow(aiupper,3)-pow(ailower,3))/3 - (aiupper*aiupper-ailower*ailower)*aicenter/2); + } + // 2: Determine new mass + new_mass_in_bin = bin_M[bin_i] + bin_dM[bin_i]; + // 3 : Determine new bin number and slope from new mass and average grain size. Same as what's done for edge case rebinning. + new_bin_slope[bin_i] = (-45*new_mass_in_bin*(ailower + aiupper - 2*a_avg_new)) / + (pow(ailower - aiupper,3)*(2*(ailower + aiupper)*(pow(ailower,2) + 3*ailower*aiupper + pow(aiupper,2)) - + 3*(3*pow(ailower,2) + 4*ailower*aiupper + 3*pow(aiupper,2))*a_avg_new)*M_PI*bulk_dens); + new_bin_N[bin_i] = (-15*new_mass_in_bin)/(2.*(2*(ailower + aiupper)*(pow(ailower,2) + 3*ailower*aiupper + + pow(aiupper,2)) - 3*(3*pow(ailower,2) + 4*ailower*aiupper + 3*pow(aiupper,2))*a_avg_new)*M_PI*bulk_dens); + + // make sure to limit the new slope if necessary + check_for_slope_limiting(bin_i, bulk_dens, &new_bin_N[bin_i], &new_bin_slope[bin_i], new_mass_in_bin); + } + } +} + + +// Debugging function to check the total mass of each dust species against the +// total mass calculated from their grain size bins. +// Halts run if this doesn't matchup and tells you what process caused the issue. +// Note the mass argument is only needed when checking update_ISMDustChem_after_mechanical_injection() for FIRE-2 +// since the particle mass is not thread-safe. +void ISMDustChemEvo_check_bins_after_update(int i, int update_process, double mass) +{ + int k,l; + double total_bin_mass, species_mass, species_frac; + double bin_masses[NUM_ISMDUSTCHEM_SIZE_BINS]; + int failed=0; + double percent_error = 0.001; + double min_mass_frac = 1E-20; // Minimum mass fraction to consider for debugging. Very small values will always be prone to rounding errors + double has_nan=0; + if (mass==0) {mass = P[i].Mass;} // only needed for certain routines like SNe feedback/injection since particle mass changes are not thread-safe + + for(k=0;kP[i].Metallicity[0] || (species_mass<=0 && total_bin_mass>0) || (species_mass>0 && species_frac>min_mass_frac && (fabs((total_bin_mass-species_mass)/species_mass))>percent_error)) { + printf("Debugging checks not passed for particle %i type %i species %i mass %e \n",i,P[i].Type,k, mass*UNIT_MASS_IN_SOLAR); + printf("update_process: %i \n",update_process); + printf("total_bin_mass: %e total_species_mass: %e species_met: %e total_met: %e \n", total_bin_mass,species_mass,species_frac,P[i].Metallicity[0]); + for(l=0;l0.001) {check+=1;} + } + if (total_mass>0 && fabs(total_bin_mass-total_mass)/total_mass>0.01) {check+=1;} + if (check) { + printf("Debugging checks not passed\n"); + printf("yields_process: %i species_num: %i \n",yields_process,species_num); + for(k=0;k 5x mean) + rho_mean0 = np.mean(rho0) + blob_mass_initial = mass0[rho0 > 5 * rho_mean0].sum() + total_mass_initial = mass0.sum() + total_mass_final = mass_f.sum() + + plot_blob_density(pos_f, rho_f, output_dir="test/blob") + + # Mass conservation + mass_err = abs(total_mass_final - total_mass_initial) / total_mass_initial + assert mass_err < 1e-3, f"Total mass not conserved: relative error {mass_err:.6f}" + + # Blob should still have significant mass (not fully disrupted at t=2) + rho_mean_f = np.mean(rho_f) + blob_mass_final = mass_f[rho_f > 5 * rho_mean_f].sum() + assert ( + blob_mass_final > 0.1 * blob_mass_initial + ), f"Blob lost too much mass: {blob_mass_final/blob_mass_initial:.2%} remaining" diff --git a/test/briowu/Config.sh b/test/briowu/Config.sh new file mode 100644 index 000000000..2aa166375 --- /dev/null +++ b/test/briowu/Config.sh @@ -0,0 +1,11 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_LONG_X=16 +BOX_LONG_Y=1 +BOX_LONG_Z=1 +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(2.0) +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/briowu/briowu.params b/test/briowu/briowu.params new file mode 100644 index 000000000..df77e3185 --- /dev/null +++ b/test/briowu/briowu.params @@ -0,0 +1,31 @@ +% Brio & Wu MHD shock tube test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_LONG_X=16 +% BOX_LONG_Y=1 +% BOX_LONG_Z=1 +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(2.0) +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile briowu_ics +OutputDir output +TimeMax 0.2 +BoxSize 0.25 +TimeBetSnapshot 0.1 +MaxSizeTimestep 0.04 +DesNumNgb 20 +MaxNumNgbDeviation 0.1 +MaxMemSize 2000 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +DivBcleaningParabolicSigma 1.0 +DivBcleaningHyperbolicSigma 1.0 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 diff --git a/test/briowu/test_briowu.py b/test/briowu/test_briowu.py new file mode 100644 index 000000000..6f51dd65f --- /dev/null +++ b/test/briowu/test_briowu.py @@ -0,0 +1,52 @@ +"""Brio & Wu MHD shock tube test (Hopkins & Raives 2016) + +Classic MHD shock tube problem testing the MHD Riemann solver. Since there is no +provided exact solution file, this test verifies that the simulation runs to +completion and produces output with physically reasonable values. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py + +from gizmo.test import build_and_run_test, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_briowu(num_mpi_ranks, num_omp_threads): + test_name = "briowu" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + x_sim = F["PartType0/Coordinates"][:, 0] + rho_sim = F["PartType0/Density"][:] + vel_sim = F["PartType0/Velocities"][:, 0] + B_sim = F["PartType0/MagneticField"][:] + + # Plot profiles + order = x_sim.argsort() + for label, data in [ + ("Density", rho_sim), + ("Velocity", vel_sim), + ("By", B_sim[:, 1]), + ]: + plt.figure() + plt.plot(x_sim[order], data[order], ".", markersize=1) + plt.xlabel("x") + plt.ylabel(label) + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + # Basic sanity checks + assert np.all(np.isfinite(rho_sim)), "Non-finite density values found" + assert np.all(rho_sim > 0), "Negative density values found" + assert np.all(np.isfinite(B_sim)), "Non-finite magnetic field values found" + # Check density is in expected range for this problem + assert rho_sim.max() < 2.0, f"Maximum density {rho_sim.max()} unreasonably high" + assert rho_sim.min() > 0.05, f"Minimum density {rho_sim.min()} unreasonably low" diff --git a/test/build_gizmo_for_test.sh b/test/build_gizmo_for_test.sh new file mode 100644 index 000000000..a91f8535a --- /dev/null +++ b/test/build_gizmo_for_test.sh @@ -0,0 +1,6 @@ +# Generic commands to build GIZMO for a given test + +rm GIZMO +rm test/*/GIZMO +cp test/$TEST_NAME/Config.sh . # retrieve the config file +make clean && make -j8 diff --git a/test/core/Config.sh b/test/core/Config.sh new file mode 100644 index 000000000..b17b0adf5 --- /dev/null +++ b/test/core/Config.sh @@ -0,0 +1,10 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +EOS_GAMMA=(5.0/3.0) +EOS_MHD_CORE_BAROTROPIC +GRAVITY_NOT_PERIODIC +MAGNETIC +MHD_B_SET_IN_PARAMS +ADAPTIVE_GRAVSOFT_FORGAS +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/core/core.params b/test/core/core.params new file mode 100644 index 000000000..7a69fd81a --- /dev/null +++ b/test/core/core.params @@ -0,0 +1,51 @@ +% Magnetized core collapse test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% EOS_GAMMA=(5.0/3.0) +% EOS_ENFORCE_ADIABAT=(1.0) +% EOS_ENFORCE_ADIABAT_CORE_BAROTROPIC +% GRAVITY_NOT_PERIODIC +% MAGNETIC +% MHD_B_SET_IN_PARAMS +% ADAPTIVE_GRAVSOFT_FORGAS +% +InitCondFile core_ics +OutputDir output +TimeMax 0.25 +BoxSize 0.15 +TimeBetSnapshot 0.05 +DesNumNgb 32 +BiniX 0 +BiniY 0 +BiniZ 6.1019e-05 +UnitLength_in_cm 3.085678e+18 +UnitMass_in_g 1.9891e+33 +UnitVelocity_in_cm_per_s 100000 +UnitMagneticField_in_gauss 1 +SofteningGas 5e-6 +Softening_Type1 5e-6 +Softening_Type2 5e-6 +Softening_Type3 5e-6 +Softening_Type4 5e-6 +Softening_Type5 5e-6 +SofteningGasMaxPhys 5e-6 +Softening_Type1_MaxPhysLimit 5e-6 +Softening_Type2_MaxPhysLimit 5e-6 +Softening_Type3_MaxPhysLimit 5e-6 +Softening_Type4_MaxPhysLimit 5e-6 +Softening_Type5_MaxPhysLimit 5e-6 +MaxSizeTimestep 5e-05 +MaxHsml 100 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +ErrTolTheta 0.7 +TimeBetStatistics 0.5 +MaxMemSize 3000 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/core/test_core.py b/test/core/test_core.py new file mode 100644 index 000000000..c2d5e0c84 --- /dev/null +++ b/test/core/test_core.py @@ -0,0 +1,54 @@ +"""Magnetized core collapse test (Hopkins 2015) + +Tests the collapse of a magnetized molecular cloud core with a barotropic +EOS. The core should contract and form a dense central region. Checks that +the central density increases significantly and that the magnetic field +is amplified by compression. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_core(num_mpi_ranks, num_omp_threads): + test_name = "core" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + rho0 = F["PartType0/Density"][:] + B0 = F["PartType0/MagneticField"][:] + mass0 = F["PartType0/Masses"][:] + with h5py.File(snaps[-1], "r") as F: + rho_f = F["PartType0/Density"][:] + Bf = F["PartType0/MagneticField"][:] + mass_f = F["PartType0/Masses"][:] + pos_f = F["PartType0/Coordinates"][:] + + # The core should have collapsed: max density should increase significantly + rho_max_ratio = rho_f.max() / rho0.max() + assert rho_max_ratio > 10, ( + f"Core did not collapse enough: max density ratio = {rho_max_ratio:.1f}" + ) + + # Magnetic field should be amplified by compression + Bmag0_max = np.max(np.sqrt(np.sum(B0**2, axis=1))) + Bmagf_max = np.max(np.sqrt(np.sum(Bf**2, axis=1))) + assert Bmagf_max > Bmag0_max, "Magnetic field should be amplified during collapse" + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" diff --git a/test/currentsheet/Config.sh b/test/currentsheet/Config.sh new file mode 100644 index 000000000..961a22b6c --- /dev/null +++ b/test/currentsheet/Config.sh @@ -0,0 +1,7 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/currentsheet/currentsheet.params b/test/currentsheet/currentsheet.params new file mode 100644 index 000000000..53b08d523 --- /dev/null +++ b/test/currentsheet/currentsheet.params @@ -0,0 +1,27 @@ +% Current sheet reconnection test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile currentsheet_A0pt1_b0pt1_ics +OutputDir output +TimeMax 0.1 +BoxSize 1 +TimeBetSnapshot 0.01 +MaxSizeTimestep 1e-05 +DesNumNgb 16 +CourantFac 0.1 +ErrTolIntAccuracy 0.01 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/currentsheet/test_currentsheet.py b/test/currentsheet/test_currentsheet.py new file mode 100644 index 000000000..7a41c8fae --- /dev/null +++ b/test/currentsheet/test_currentsheet.py @@ -0,0 +1,53 @@ +"""Current sheet reconnection test (Hopkins & Raives 2016) + +Tests magnetic reconnection in a current sheet. Checks that the magnetic +energy dissipates (reconnection occurs) and that the code runs stably. +""" + +import pytest +import numpy as np +import h5py +import glob +from os import path, chdir +from urllib.request import urlretrieve +from gizmo.test import build_gizmo_for_test, download_test_files, run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +WEBSITE = "http://www.tapir.caltech.edu/~phopkins/sims/" + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(max_ranks=8),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_currentsheet(num_mpi_ranks, num_omp_threads): + test_name = "currentsheet" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_omp_threads) + chdir(f"test/{test_name}/") + + # Download ICs (non-standard name) + ic_file = "currentsheet_A0pt1_b0pt1_ics.hdf5" + if not path.isfile(ic_file): + urlretrieve(WEBSITE + ic_file, ic_file) + + run_test(test_name, num_mpi_ranks, num_omp_threads) + chdir("../../") + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + B0 = F["PartType0/MagneticField"][:] + with h5py.File(snaps[-1], "r") as F: + Bf = F["PartType0/MagneticField"][:] + + # Magnetic energy should decrease due to reconnection + Emag0 = np.mean(np.sum(B0**2, axis=1)) + Emagf = np.mean(np.sum(Bf**2, axis=1)) + + assert Emagf < Emag0, "Magnetic energy should decrease due to reconnection" + # But shouldn't go to zero - reconnection is a gradual process + assert Emagf > 0.01 * Emag0, "Magnetic energy dropped too much" diff --git a/test/dustywave/Config.sh b/test/dustywave/Config.sh new file mode 100644 index 000000000..41326d5e5 --- /dev/null +++ b/test/dustywave/Config.sh @@ -0,0 +1,10 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=1 +EOS_GAMMA=(5./3.) +EOS_ENFORCE_ADIABAT=(3./5.) +SELFGRAVITY_OFF +GRAIN_FLUID +GRAIN_BACKREACTION +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/dustywave/dustywave.params b/test/dustywave/dustywave.params new file mode 100644 index 000000000..4331dadf0 --- /dev/null +++ b/test/dustywave/dustywave.params @@ -0,0 +1,32 @@ +% Dusty wave test (Hopkins & Lee 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=1 +% EOS_GAMMA=(5./3.) +% EOS_ENFORCE_ADIABAT=(3./5.) +% SELFGRAVITY_OFF +% GRAIN_FLUID +% GRAIN_BACKREACTION +% +InitCondFile dustywave_ics +OutputDir output +TimeMax 2.5 +BoxSize 1 +TimeBetSnapshot 0.1 +DesNumNgb 4 +MaxNumNgbDeviation 1e-6 +MaxMemSize 1000 +Grain_Internal_Density 1 +Grain_Size_Min 1.23608 +Grain_Size_Max 1.23608 +Grain_Size_Spectrum_Powerlaw 0.5 +MaxSizeTimestep 0.01 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 diff --git a/test/dustywave/test_dustywave.py b/test/dustywave/test_dustywave.py new file mode 100644 index 000000000..ece69e93a --- /dev/null +++ b/test/dustywave/test_dustywave.py @@ -0,0 +1,82 @@ +"""Dusty wave test (Hopkins & Lee 2016) + +Tests the coupled dust-gas wave propagation. Compares dust and gas velocities +against the reference solution at t=1.2. +The exact solution file has columns: x-position, x-velocity of dust, x-velocity of gas +(velocities in units of 1e-4). +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from matplotlib import pyplot as plt +import h5py +from os import path, chdir +from urllib.request import urlretrieve +from gizmo.test import build_gizmo_for_test, download_test_files, run_test, clean_test_outputs, assert_final_time, get_final_snapshot, default_mpi_ranks, default_omp_threads + + +WEBSITE = "http://www.tapir.caltech.edu/~phopkins/sims/" + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(max_ranks=2),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_dustywave(num_mpi_ranks, num_omp_threads): + test_name = "dustywave" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_omp_threads) + chdir(f"test/{test_name}/") + + # Download ICs (standard name) and exact solution (non-standard name) + download_test_files(test_name) + if not path.isfile("dustwave_exact.txt"): + urlretrieve(WEBSITE + "dustwave_exact.txt", "dustwave_exact.txt") + + run_test(test_name, num_mpi_ranks, num_omp_threads) + chdir("../../") + + outputdir = f"test/{test_name}/output" + # t=1.2 corresponds to snapshot 12 (TimeBetSnapshot=0.1) + snap_file = outputdir + "/snapshot_012.hdf5" + if not path.isfile(snap_file): + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(get_final_snapshot(test_name), test_name) + + # Load simulation data - gas is PartType0, dust is PartType3 + with h5py.File(snap_file, "r") as F: + x_gas = F["PartType0/Coordinates"][:, 0] + vx_gas = F["PartType0/Velocities"][:, 0] + x_dust = F["PartType3/Coordinates"][:, 0] + vx_dust = F["PartType3/Velocities"][:, 0] + + # Load exact solution: x, v_dust, v_gas (velocities in units of 1e-4) + exact = np.loadtxt(f"test/{test_name}/dustwave_exact.txt") + x_exact = exact[:, 0] + vdust_exact = exact[:, 1] * 1e-4 # convert to code units + vgas_exact = exact[:, 2] * 1e-4 + + # Interpolate exact solution to particle positions + vgas_interp = interp1d(x_exact, vgas_exact, bounds_error=False, fill_value="extrapolate")(x_gas) + vdust_interp = interp1d(x_exact, vdust_exact, bounds_error=False, fill_value="extrapolate")(x_dust) + + # Plot + plt.figure() + gas_order = x_gas.argsort() + dust_order = x_dust.argsort() + plt.plot(x_gas[gas_order], vx_gas[gas_order], "b.", markersize=3, label="Gas (GIZMO)") + plt.plot(x_dust[dust_order], vx_dust[dust_order], "r.", markersize=3, label="Dust (GIZMO)") + plt.plot(x_exact, vgas_exact, "b-", linewidth=0.5, label="Gas (exact)") + plt.plot(x_exact, vdust_exact, "r-", linewidth=0.5, label="Dust (exact)") + plt.xlabel("x") + plt.ylabel("v_x") + plt.legend() + plt.savefig(f"test/{test_name}/velocities.png") + plt.close() + + # Compute L1 errors + amp = np.max(np.abs(vgas_exact)) + L1_gas = np.mean(np.abs(vx_gas - vgas_interp)) / amp + L1_dust = np.mean(np.abs(vx_dust - vdust_interp)) / amp + + assert L1_gas < 0.15, f"Gas velocity L1 error {L1_gas:.4f} exceeds tolerance" + assert L1_dust < 0.15, f"Dust velocity L1 error {L1_dust:.4f} exceeds tolerance" diff --git a/test/evrard/Config.sh b/test/evrard/Config.sh new file mode 100644 index 000000000..1968a3ff9 --- /dev/null +++ b/test/evrard/Config.sh @@ -0,0 +1,5 @@ +HYDRO_MESHLESS_FINITE_MASS +EOS_GAMMA=(5.0/3.0) +ADAPTIVE_GRAVSOFT_FORGAS +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/evrard/evrard.params b/test/evrard/evrard.params new file mode 100644 index 000000000..b35970975 --- /dev/null +++ b/test/evrard/evrard.params @@ -0,0 +1,31 @@ +% Evrard adiabatic collapse test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% EOS_GAMMA=(5.0/3.0) +% ADAPTIVE_GRAVSOFT_FORGAS +% +InitCondFile evrard_ics +OutputDir output +PartAllocFactor 5 +TimeMax 0.8 +TimeBetSnapshot 0.1 +MaxSizeTimestep 0.001 +DesNumNgb 40 +MaxNumNgbDeviation 0.1 +MaxMemSize 3000 +GravityConstantInternal 1 +MaxHsml 2000 +SofteningGas 0.0001 +SofteningHalo 0.07 +Softening_Type2 0.003 +Softening_Type3 0.003 +Softening_Type4 0.003 +Softening_Type5 0.003 +CourantFac 0.1 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +ErrTolIntAccuracy 0.01 +MaxRMSDisplacementFac 0.1 diff --git a/test/evrard/test_evrard.py b/test/evrard/test_evrard.py new file mode 100644 index 000000000..226f25af0 --- /dev/null +++ b/test/evrard/test_evrard.py @@ -0,0 +1,100 @@ +"""Evrard adiabatic collapse test (Hopkins 2015) + +Compares the radial density, entropy, and velocity profiles at t=0.8 against +a high-resolution reference solution. +The exact solution file has columns: radius, density, entropy, velocity. +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from scipy.stats import binned_statistic +from matplotlib import pyplot as plt +import h5py + +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, flush_colorbar, assert_final_time, get_final_snapshot + + +def plot_evrard_density_slice(coords, rho, output_dir="."): + """Plot a density slice through the Evrard collapse center.""" + M = Meshoid(coords) + center = np.average(coords, axis=0) + rho_slice = M.Slice(np.log10(rho), res=1024, plane="z", center=center, size=1., order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="inferno", extent=[-0.5, 0.5, -0.5, 0.5]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("Evrard Collapse - Density Slice") + fig.savefig(output_dir + "/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + +@pytest.mark.parametrize("num_mpi_ranks,num_omp_threads", [(16, 0), (1, 16), (4, 4)]) +def test_evrard(num_mpi_ranks, num_omp_threads): + test_name = "evrard" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + rho_sim = F["PartType0/Density"][:] + vel = F["PartType0/Velocities"][:] + u_sim = F["PartType0/InternalEnergy"][:] + + # Compute radius from origin and radial velocity + r_sim = np.sqrt(np.sum(coords**2, axis=1)) + vr_sim = np.sum(vel * coords, axis=1) / (r_sim + 1e-30) + + # Entropy: S = P / rho^gamma = (gamma-1) * u / rho^(gamma-1) + gamma = 5.0 / 3.0 + entropy_sim = (gamma - 1) * u_sim / rho_sim ** (gamma - 1) + + # Load exact solution: radius, density, entropy, velocity + exact = np.loadtxt(f"test/{test_name}/evrard_exact.txt") + r_exact = exact[:, 0] + rho_exact = exact[:, 1] + entropy_exact = exact[:, 2] + vr_exact = exact[:, 3] + + # Bin simulation data by radius + r_bins = np.logspace(np.log10(r_exact.min()), np.log10(r_exact.max()), 30) + rho_binned = binned_statistic(r_sim, rho_sim, "median", r_bins)[0] + vr_binned = binned_statistic(r_sim, vr_sim, "median", r_bins)[0] + entropy_binned = binned_statistic(r_sim, entropy_sim, "median", r_bins)[0] + r_centers = 0.5 * (r_bins[:-1] + r_bins[1:]) + + # Interpolate exact solution to bin centers + rho_exact_interp = interp1d(r_exact, rho_exact, bounds_error=False, fill_value="extrapolate")(r_centers) + vr_exact_interp = interp1d(r_exact, vr_exact, bounds_error=False, fill_value="extrapolate")(r_centers) + + plot_evrard_density_slice(coords, rho_sim, output_dir=f"test/{test_name}") + + # Plot comparison + for label, binned, exact_vals, log in [ + ("Density", rho_binned, rho_exact_interp, True), + ("RadialVelocity", vr_binned, vr_exact_interp, False), + ]: + plt.figure() + if log: + plt.loglog(r_centers, binned, "o", markersize=3, label="GIZMO") + plt.loglog(r_centers, exact_vals, "-", color="red", label="Exact") + else: + plt.semilogx(r_centers, binned, "o", markersize=3, label="GIZMO") + plt.semilogx(r_centers, exact_vals, "-", color="red", label="Exact") + plt.xlabel("r") + plt.ylabel(label) + plt.legend() + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + # Check density profile + good = np.isfinite(rho_binned) & np.isfinite(rho_exact_interp) & (rho_exact_interp > 0) + if np.any(good): + L1_rho = np.nanmean(np.abs(np.log10(rho_binned[good]) - np.log10(rho_exact_interp[good]))) + assert L1_rho < 0.3, f"Log density profile L1 error {L1_rho:.4f} exceeds tolerance" diff --git a/test/field_loop/Config.sh b/test/field_loop/Config.sh new file mode 100644 index 000000000..961a22b6c --- /dev/null +++ b/test/field_loop/Config.sh @@ -0,0 +1,7 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/field_loop/field_loop.params b/test/field_loop/field_loop.params new file mode 100644 index 000000000..cc7adb9ad --- /dev/null +++ b/test/field_loop/field_loop.params @@ -0,0 +1,28 @@ +% Field loop advection test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile field_loop_ics +OutputDir output +TimeMax 2 +BoxSize 1 +TimeOfFirstSnapshot 0.01 +TimeBetSnapshot 1 +MaxSizeTimestep 0.1 +DesNumNgb 20 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/field_loop/test_field_loop.py b/test/field_loop/test_field_loop.py new file mode 100644 index 000000000..5ddec9c0d --- /dev/null +++ b/test/field_loop/test_field_loop.py @@ -0,0 +1,41 @@ +"""Field loop advection test (Hopkins & Raives 2016) + +Tests advection of a magnetic field loop. The loop should be preserved +after one full advection period. Checks magnetic flux conservation. +""" + +import pytest +import numpy as np +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_field_loop(num_mpi_ranks, num_omp_threads): + test_name = "field_loop" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + B0 = F["PartType0/MagneticField"][:] + pos0 = F["PartType0/Coordinates"][:] + mass0 = F["PartType0/Masses"][:] + with h5py.File(snaps[-1], "r") as F: + Bf = F["PartType0/MagneticField"][:] + mass_f = F["PartType0/Masses"][:] + + # Total magnetic energy should be approximately conserved + Emag0 = np.sum(np.sum(B0**2, axis=1) * mass0) + Emagf = np.sum(np.sum(Bf**2, axis=1) * mass_f) + + rel_err = abs(Emagf - Emag0) / Emag0 + assert rel_err < 0.5, f"Magnetic energy not conserved: relative error {rel_err:.4f}" diff --git a/test/gmc_cooling/Config.sh b/test/gmc_cooling/Config.sh new file mode 100644 index 000000000..0d980af37 --- /dev/null +++ b/test/gmc_cooling/Config.sh @@ -0,0 +1,8 @@ +SINGLE_STAR_STARFORGE_DEFAULTS +MAGNETIC +COOLING +METALS +COOL_METAL_LINES_BY_SPECIES +BOX_PERIODIC +GRAVITY_NOT_PERIODIC +ADAPTIVE_TREEFORCE_UPDATE=0.0625 diff --git a/test/gmc_cooling/README.md b/test/gmc_cooling/README.md new file mode 100644 index 000000000..e6481c29b --- /dev/null +++ b/test/gmc_cooling/README.md @@ -0,0 +1,15 @@ +# GMC cooling test + +This test initializes a $$2\times10^4 M_\odot$$ giant molecular cloud at $1M_\odot$ resolution, runs it for about a crossing time, and verifies that the temperature-density statistics are in agreement with a pre-run setup, within a reasonable tolerance. + +This is a benchmark test. Failing this test does not necessarily imply that there is a problem, but rather indicates that something has changed that should be noted. + +Compile-time flags used for this setup: +``` + SINGLE_STAR_STARFORGE_DEFAULTS + COOLING + MAGNETIC + BOX_PERIODIC + GRAVITY_NOT_PERIODIC + ADAPTIVE_TREEFORCE_UPDATE=0.0625 +``` diff --git a/test/gmc_cooling/gmc_cooling.params b/test/gmc_cooling/gmc_cooling.params new file mode 100644 index 000000000..9a0765e10 --- /dev/null +++ b/test/gmc_cooling/gmc_cooling.params @@ -0,0 +1,249 @@ +%------------------------------------------------------------------------- +%---- This file contains the input parameters needed at run-time for +% simulations. It is based on and closely resembles the GADGET-3 +% parameterfile (format of which and parsing routines written by +% Volker Springel [volker.springel@h-its.org]). It has been updated +% with new naming conventions and additional variables as needed by +% Phil Hopkins [phopkins@caltech.edu] for GIZMO. +%------------------------------------------------------------------------- + +%---- Relevant files (filenames and directories) +InitCondFile gmc_cooling_ics +OutputDir output + +%---- File formats (input and output) +ICFormat 3 % 1=unformatted (gadget) binary, 3=hdf5, 4=cluster +SnapFormat 3 % 1=unformatted (gadget) binary, 3=hdf5 + + +%---- Output parameters +RestartFile restart +SnapshotFileBase snapshot +OutputListOn 0 % =1 to use list in "OutputListFilename" +OutputListFilename output_times.txt % list of times (in code units) for snaps +NumFilesPerSnapshot 1 +NumFilesWrittenInParallel 1 % must be < N_processors & power of 2 + +%---- Output frequency +TimeOfFirstSnapshot 0. % time (code units) of first snapshot +TimeBetSnapshot 0.1 % time between (if OutputListOn=0), code units +TimeBetStatistics 2.52e-02 % time between additional statistics (e.g. energy) + +%---- CPU run-time and checkpointing time-limits +TimeLimitCPU 172800 % in seconds +CpuTimeBetRestartFile 7200 % in seconds +ResubmitOn 0 +ResubmitCommand my-scriptfile + +%---- Desired simulation beginning and end times (in code units) for run +TimeBegin 0.0 % Beginning of the simulation +TimeMax 1.0 % End of the simulation + +%---- Maximum and minimum timesteps allowed +MaxSizeTimestep 2.52e-02 % in code units, set for your problem +MinSizeTimestep 1.0e-15 % set this very low, or get the wrong answer + + +%---- System of units +UnitLength_in_cm 3.09e+18 +UnitMass_in_g 1.99e+33 +UnitVelocity_in_cm_per_s 1.00e+05 +UnitMagneticField_in_gauss 1.00e+00 +GravityConstantInternal 1e-100 % calculated by code if =0 + +%---- Cosmological parameters +ComovingIntegrationOn 0 % is it cosmological? (yes=1, no=0) +BoxSize 1.00e+02 % in code units +Omega0 0. % =0 for non-cosmological +OmegaLambda 0. % =0 for non-cosmological +OmegaBaryon 0. % =0 for non-cosmological +HubbleParam 1. % little 'h'; =1 for non-cosmological runs + + +%----- Memory allocation +MaxMemSize 5000 % sets maximum MPI process memory use in MByte +PartAllocFactor 5.0 % memory load allowed for better cpu balance +BufferSize 500 % in MByte + +%---- Rebuild domains when >this fraction of particles active +TreeDomainUpdateFrequency 0.005 % 0.0005-0.05, dept on core+particle number + + +%---- (Optional) Initial hydro temperature & temperature floor (in Kelvin) +InitGasTemp 1e4 % set by IC file if =0 +MinGasTemp 2.73 % don't trust cooling function below ~5K, whether dust, molecular lines, or photoelectric heating + +%---- Hydro reconstruction (kernel) parameters +DesNumNgb 32 % domain-reconstruction kernel number: 32 standard, 60-114 for quintic +MaxHsml 1.0e10 % minimum gas kernel length (some very large value to prevent errors) +MinGasHsmlFractional 0 % minimum kernel length relative to gas force softening (<= 1) + + +%---- Gravitational softening lengths +%----- Softening lengths per particle type. If ADAPTIVE_GRAVSOFT is set, these +%-------- are the minimum softening allowed for each type ------- +%-------- (units are co-moving for cosmological integrations) +SofteningGas 1e-10 % gas (particle type=0) (in co-moving code units) +SofteningHalo 0.020 % dark matter/collisionless particles (type=1) +SofteningDisk 0.150 % collisionless particles (type=2) +SofteningBulge 0.500 % collisionless particles (type=3) +SofteningStars 1.00e-01 % stars spawned from gas (type=4) +SofteningBndry 1.00e-01 % black holes (if active), or collisionless (type=5) + +%---- if these are set in cosmo runs, SofteningX switches from comoving to physical +%------- units when the comoving value exceeds the choice here +%------- (these are ignored, and *only* the above are used, for non-cosmo runs) +SofteningGasMaxPhys 0.0005 % e.g. switch to 0.5pc physical below z=1 +SofteningHaloMaxPhys 0.010 +SofteningDiskMaxPhys 0.075 +SofteningBulgeMaxPhys 0.250 +SofteningStarsMaxPhys 0.0005 +SofteningBndryMaxPhys 0.0005 +%----- parameters for adaptive gravitational softening +AGS_DesNumNgb 32 % neighbor number for calculating adaptive gravsoft + + + + +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- +%---------- Physics Modules ---------------------------------------------- +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- + + +%------------------------------------------------------------ +%------------------ Additional Fluid Physics ---------------- +%------------------------------------------------------------ + +%---- Magneto-Hydrodynamics Parameters (MAGNETIC on) +%----- Initial B-Field Strengths (if MHD_B_SET_IN_PARAMS on, otherwise read from IC file) +BiniX 1.0e-8 % initial B_x, in code units +BiniY 1.0e-8 % initial B_y, in code units +BiniZ 1.0e-8 % initial B_z, in code units + +%---- Thermal Conduction (CONDUCTION on) +%----- set coefficient kappa [code units] or, if CONDUCTION_SPITZER on, multiplies value +ConductionCoeff 1.0 % set/multiply conduction coefficient + +%---- Navier-Stokes Viscosity (VISCOSITY on) +%--- set coefficients eta,zeta [code units] or, if VISCOSITY_BRAGINSKII on, multiplies value +ShearViscosityCoeff 1.0 % set/multiply shear viscosity coefficient +BulkViscosityCoeff 1.0 % set/multiply bulk viscosity coefficient + +%---- Turbulent Diffusion Master Switch (TURB_DIFFUSION on) +TurbDiffusionCoefficient 1.0 % Normalizes diffusion rates relative to Smagorinsky-Lilly theory [best calibration] (~0.5-2) + +%---- Cosmic Ray + Gas Fluids (COSMIC_RAYS on) +CosmicRayDiffusionCoeff 1.0 % multiplies anisotropic diffusion/streaming coefficients + +%---- Dust-Gas Mixtures (GRAIN_FLUID on) +Grain_Internal_Density 1.0 % internal/material density of grains in g/cm^3 +Grain_Size_Min 1.e-6 % minimum grain size in cm +Grain_Size_Max 1.e-4 % maximum grain size in cm +Grain_Size_Spectrum_Powerlaw 0.5 % power-law distribution of grain sizes (dm/dlnr~r^x) + + +%------------------------------------------------------------------------- +%------------------ Star, Black Hole, and Galaxy Formation --------------- +%------------------------------------------------------------------------- + + +%---- Star Formation parameters (GALSF on) +CritPhysDensity 1.00e+02 % critical physical density for star formation (cm^(-3)) +SfEffPerFreeFall 1.0 % SFR/(Mgas/tfreefall) for gas which meets SF criteria + + + +%-------------- FIRE (PFH) explicit star formation & feedback model (FIRE on) +%--- initial metallicity of gas & stars in simulation +InitMetallicity 1.00e+00 % initial gas+stellar metallicity (in solar) +InitStellarAge 0.0 % initial mean age (in Gyr; for stars in sim ICs) +%--- local radiation-pressure driven winds (GALSF_FB_FIRE_RT_LOCALRP) +WindMomentumLoading 1.0 % fraction of photon momentum to couple +%--- SneII Heating Model (GALSF_FB_MECHANICAL) +SNeIIEnergyFrac 1.0 % fraction of mechanical energy to couple +%--- HII region photo-heating model (GALSF_FB_FIRE_RT_HIIHEATING) +HIIRegion_fLum_Coupled 1.0 % fraction of ionizing photons allowed to see gas +%--- long-range radiation pressure acceleration (GALSF_FB_FIRE_RT_LONGRANGE) +PhotonMomentum_Coupled_Fraction 1.0 % fraction of L to allow incident +PhotonMomentum_fUV 0.0 % incident SED f(L) in UV (minimum scattering) +PhotonMomentum_fOPT 0.0 % incident SED f(L) in optical/near-IR +%--- gas return/recycling +GasReturnFraction 1.0 % fraction of gas mass returned (relative to ssp) +GasReturnEnergy 1.0 % fraction of returned gas energy+momentum (relative to ssp) +%--- cosmic rays (COSMIC_RAYS) +CosmicRay_SNeFraction 0.1 % fraction of SNe ejecta kinetic energy into cosmic rays (~10%) + +InterstellarRadiationFieldStrength 1.00e+00 % interstellar radiation field intensity relative to values measured in Solar neighborhood + +%-------------- Black Hole accretion & formation (BLACK_HOLES on) +%--- formation/seeding +SeedBlackHoleMass 5.00e-01 % initial mass (on-the-fly or single galaxy) +SeedAlphaDiskMass 0.0 % initial mass in the alpha disk (BH_ALPHADISK_ACCRETION) +SeedBlackHoleMinRedshift 2.0 % minimum redshift where new BH particles are seeded (lower-z ceases seeding) +SeedBlackHoleMassSigma 0.5 % lognormal standard deviation (in dex) in initial BH seed masses +%----- (specific options for on-the-fly friends-of-friends based BH seeding: FOF on) +MinFoFMassForNewSeed 10. % minimum mass of FOF group (stars or DM) to get seed, in code units +TimeBetOnTheFlyFoF 1.01 % time (in code units, e.g. scale-factor) between on-the-fly FOF searches +%--- accretion +BlackHoleAccretionFactor 1.0 % multiplier for mdot (relative to model) +BlackHoleEddingtonFactor 1e100 % fraction of eddington to cap (can be >1) +BlackHoleNgbFactor 1.0 % multiplier for kernel neighbors for BH +BlackHoleMaxAccretionRadius 1.00e+02 % max radius for BH neighbor search/accretion (code units) +BlackHoleRadiativeEfficiency 5e-7 % radiative efficiency (for accretion and feedback) +%--- feedback +BlackHoleFeedbackFactor 1.0 % generic feedback strength multiplier +BH_FluxMomentumFactor 0.0 % multiply radiation pressure (BH_PHOTONMOMENTUM), set it to zero to avoid launching gas from rad. pressure +BAL_f_accretion 0.7 % fraction of gas swallowed by BH (BH_WIND options) +BAL_v_outflow 100. % velocity (km/s) of BAL outflow (BH_WIND options) +BAL_internal_temperature 1.0e3 % internal temperature (K) of BAL outflow (BH_WIND_SPAWN) +BAL_wind_particle_mass 1.00e-01 % mass of 'virtual wind particles' in code units (BH_WIND_SPAWN) +BAL_wind_particle_mass_MS 1.00e-02 % mass of 'virtual wind particles' in MS stellar winds, if 0 then BAL_wind_particle_mass is used, in code units (SINGLE_STAR_FB_WINDS) + + +%------------------------------------------------------------------------- +%------------------ Grackle cooling module ----------------- +%------------------------------------------------------------------------- + +%-------------- Grackle UVB file (COOL_GRACKLE on) +GrackleDataFile CloudyData_UVB=HM2012.h5 + + + +%------------------------------------------------------------------------- +%------------------ Driven Turbulence (Large-Eddy boxes) ----------------- +%------------------------------------------------------------------------- + +%-------------- Turbulent stirring parameters (TURB_DRIVING on) +TurbDrive_ApproxRMSVturb 3.21e+00 +TurbDrive_MinWavelength 5.00e+00 +TurbDrive_MaxWavelength 2.00e+01 +TurbDrive_SolenoidalFraction 1 +TurbDrive_CoherenceTime 2.51e+00 +TurbDrive_DrivingSpectrum 1 +TurbDrive_RandomNumberSeed 42 +TurbDrive_TimeBetweenTurbUpdates 2.52e-02 +TurbDrive_TimeBetTurbSpectrum 2.52e-02 + + +%------------------------------------------------------------------------------------------------- +%------------------ Non-Standard Dark Matter, Dark Energy, Gravity, or Expansion ----------------- +%------------------------------------------------------------------------------------------------- + +%-------------- Parameters for non-standard or time-dependent Gravity/Dark Energy/Expansion (GR_TABULATED_COSMOLOGY on) +DarkEnergyConstantW -1 % time-independent DE parameter w, used only if no table +TabulatedCosmologyFile CosmoTbl % table with cosmological parameters + +%--- Developer-Mode Parameters (usually hard-coded, but set manually if DEVELOPER_MODE is on) -------- +ErrTolTheta 0.5 % 0.7=standard +ErrTolForceAcc 0.0025 % 0.0025=standard +ErrTolIntAccuracy 0.01 % <0.02 +CourantFac 0.2 % <0.20 +MaxRMSDisplacementFac 0.125 % <0.25 +MaxNumNgbDeviation 0.05 % <this fraction of particles active +TreeDomainUpdateFrequency 0.005 % 0.0005-0.05, dept on core+particle number + + +%---- (Optional) Initial hydro temperature & temperature floor (in Kelvin) +InitGasTemp 1e4 % set by IC file if =0 +MinGasTemp 2.73 % don't trust cooling function below ~5K, whether dust, molecular lines, or photoelectric heating + +%---- Hydro reconstruction (kernel) parameters +DesNumNgb 32 % domain-reconstruction kernel number: 32 standard, 60-114 for quintic +MaxHsml 1.0e10 % minimum gas kernel length (some very large value to prevent errors) +MinGasHsmlFractional 0 % minimum kernel length relative to gas force softening (<= 1) + + +%---- Gravitational softening lengths +%----- Softening lengths per particle type. If ADAPTIVE_GRAVSOFT is set, these +%-------- are the minimum softening allowed for each type ------- +%-------- (units are co-moving for cosmological integrations) +SofteningGas 1e-10 % gas (particle type=0) (in co-moving code units) +SofteningHalo 0.020 % dark matter/collisionless particles (type=1) +SofteningDisk 0.150 % collisionless particles (type=2) +SofteningBulge 0.500 % collisionless particles (type=3) +SofteningStars 1.00e-01 % stars spawned from gas (type=4) +SofteningBndry 1.00e-01 % black holes (if active), or collisionless (type=5) + +%---- if these are set in cosmo runs, SofteningX switches from comoving to physical +%------- units when the comoving value exceeds the choice here +%------- (these are ignored, and *only* the above are used, for non-cosmo runs) +SofteningGasMaxPhys 0.0005 % e.g. switch to 0.5pc physical below z=1 +SofteningHaloMaxPhys 0.010 +SofteningDiskMaxPhys 0.075 +SofteningBulgeMaxPhys 0.250 +SofteningStarsMaxPhys 0.0005 +SofteningBndryMaxPhys 0.0005 +%----- parameters for adaptive gravitational softening +AGS_DesNumNgb 32 % neighbor number for calculating adaptive gravsoft + + + + +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- +%---------- Physics Modules ---------------------------------------------- +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- + + +%------------------------------------------------------------ +%------------------ Additional Fluid Physics ---------------- +%------------------------------------------------------------ + +%---- Magneto-Hydrodynamics Parameters (MAGNETIC on) +%----- Initial B-Field Strengths (if MHD_B_SET_IN_PARAMS on, otherwise read from IC file) +BiniX 1.0e-8 % initial B_x, in code units +BiniY 1.0e-8 % initial B_y, in code units +BiniZ 1.0e-8 % initial B_z, in code units + +%---- Thermal Conduction (CONDUCTION on) +%----- set coefficient kappa [code units] or, if CONDUCTION_SPITZER on, multiplies value +ConductionCoeff 1.0 % set/multiply conduction coefficient + +%---- Navier-Stokes Viscosity (VISCOSITY on) +%--- set coefficients eta,zeta [code units] or, if VISCOSITY_BRAGINSKII on, multiplies value +ShearViscosityCoeff 1.0 % set/multiply shear viscosity coefficient +BulkViscosityCoeff 1.0 % set/multiply bulk viscosity coefficient + +%---- Turbulent Diffusion Master Switch (TURB_DIFFUSION on) +TurbDiffusionCoefficient 1.0 % Normalizes diffusion rates relative to Smagorinsky-Lilly theory [best calibration] (~0.5-2) + +%---- Cosmic Ray + Gas Fluids (COSMIC_RAYS on) +CosmicRayDiffusionCoeff 1.0 % multiplies anisotropic diffusion/streaming coefficients + +%---- Dust-Gas Mixtures (GRAIN_FLUID on) +Grain_Internal_Density 1.0 % internal/material density of grains in g/cm^3 +Grain_Size_Min 1.e-6 % minimum grain size in cm +Grain_Size_Max 1.e-4 % maximum grain size in cm +Grain_Size_Spectrum_Powerlaw 0.5 % power-law distribution of grain sizes (dm/dlnr~r^x) + + +%------------------------------------------------------------------------- +%------------------ Star, Black Hole, and Galaxy Formation --------------- +%------------------------------------------------------------------------- + + +%---- Star Formation parameters (GALSF on) +CritPhysDensity 1.00e+02 % critical physical density for star formation (cm^(-3)) +SfEffPerFreeFall 1.0 % SFR/(Mgas/tfreefall) for gas which meets SF criteria + + + +%-------------- FIRE (PFH) explicit star formation & feedback model (FIRE on) +%--- initial metallicity of gas & stars in simulation +InitMetallicity 1.00e+00 % initial gas+stellar metallicity (in solar) +InitStellarAge 0.0 % initial mean age (in Gyr; for stars in sim ICs) +%--- local radiation-pressure driven winds (GALSF_FB_FIRE_RT_LOCALRP) +WindMomentumLoading 1.0 % fraction of photon momentum to couple +%--- SneII Heating Model (GALSF_FB_MECHANICAL) +SNeIIEnergyFrac 1.0 % fraction of mechanical energy to couple +%--- HII region photo-heating model (GALSF_FB_FIRE_RT_HIIHEATING) +HIIRegion_fLum_Coupled 1.0 % fraction of ionizing photons allowed to see gas +%--- long-range radiation pressure acceleration (GALSF_FB_FIRE_RT_LONGRANGE) +PhotonMomentum_Coupled_Fraction 1.0 % fraction of L to allow incident +PhotonMomentum_fUV 0.0 % incident SED f(L) in UV (minimum scattering) +PhotonMomentum_fOPT 0.0 % incident SED f(L) in optical/near-IR +%--- gas return/recycling +GasReturnFraction 1.0 % fraction of gas mass returned (relative to ssp) +GasReturnEnergy 1.0 % fraction of returned gas energy+momentum (relative to ssp) +%--- cosmic rays (COSMIC_RAYS) +CosmicRay_SNeFraction 0.1 % fraction of SNe ejecta kinetic energy into cosmic rays (~10%) + +InterstellarRadiationFieldStrength 1.00e+00 % interstellar radiation field intensity relative to values measured in Solar neighborhood + +%-------------- Black Hole accretion & formation (BLACK_HOLES on) +%--- formation/seeding +SeedBlackHoleMass 5.00e-01 % initial mass (on-the-fly or single galaxy) +SeedAlphaDiskMass 0.0 % initial mass in the alpha disk (BH_ALPHADISK_ACCRETION) +SeedBlackHoleMinRedshift 2.0 % minimum redshift where new BH particles are seeded (lower-z ceases seeding) +SeedBlackHoleMassSigma 0.5 % lognormal standard deviation (in dex) in initial BH seed masses +%----- (specific options for on-the-fly friends-of-friends based BH seeding: FOF on) +MinFoFMassForNewSeed 10. % minimum mass of FOF group (stars or DM) to get seed, in code units +TimeBetOnTheFlyFoF 1.01 % time (in code units, e.g. scale-factor) between on-the-fly FOF searches +%--- accretion +BlackHoleAccretionFactor 1.0 % multiplier for mdot (relative to model) +BlackHoleEddingtonFactor 1e100 % fraction of eddington to cap (can be >1) +BlackHoleNgbFactor 1.0 % multiplier for kernel neighbors for BH +BlackHoleMaxAccretionRadius 1.00e+02 % max radius for BH neighbor search/accretion (code units) +BlackHoleRadiativeEfficiency 5e-7 % radiative efficiency (for accretion and feedback) +%--- feedback +BlackHoleFeedbackFactor 1.0 % generic feedback strength multiplier +BH_FluxMomentumFactor 0.0 % multiply radiation pressure (BH_PHOTONMOMENTUM), set it to zero to avoid launching gas from rad. pressure +BAL_f_accretion 0.7 % fraction of gas swallowed by BH (BH_WIND options) +BAL_v_outflow 100. % velocity (km/s) of BAL outflow (BH_WIND options) +BAL_internal_temperature 1.0e3 % internal temperature (K) of BAL outflow (BH_WIND_SPAWN) +BAL_wind_particle_mass 1.00e-01 % mass of 'virtual wind particles' in code units (BH_WIND_SPAWN) +BAL_wind_particle_mass_MS 1.00e-02 % mass of 'virtual wind particles' in MS stellar winds, if 0 then BAL_wind_particle_mass is used, in code units (SINGLE_STAR_FB_WINDS) + + +%------------------------------------------------------------------------- +%------------------ Grackle cooling module ----------------- +%------------------------------------------------------------------------- + +%-------------- Grackle UVB file (COOL_GRACKLE on) +GrackleDataFile CloudyData_UVB=HM2012.h5 + + + +%------------------------------------------------------------------------- +%------------------ Driven Turbulence (Large-Eddy boxes) ----------------- +%------------------------------------------------------------------------- + +%-------------- Turbulent stirring parameters (TURB_DRIVING on) +TurbDrive_ApproxRMSVturb 3.21e+00 +TurbDrive_MinWavelength 5.00e+00 +TurbDrive_MaxWavelength 2.00e+01 +TurbDrive_SolenoidalFraction 1 +TurbDrive_CoherenceTime 2.51e+00 +TurbDrive_DrivingSpectrum 1 +TurbDrive_RandomNumberSeed 42 +TurbDrive_TimeBetweenTurbUpdates 2.52e-02 +TurbDrive_TimeBetTurbSpectrum 2.52e-02 + + +%------------------------------------------------------------------------------------------------- +%------------------ Non-Standard Dark Matter, Dark Energy, Gravity, or Expansion ----------------- +%------------------------------------------------------------------------------------------------- + +%-------------- Parameters for non-standard or time-dependent Gravity/Dark Energy/Expansion (GR_TABULATED_COSMOLOGY on) +DarkEnergyConstantW -1 % time-independent DE parameter w, used only if no table +TabulatedCosmologyFile CosmoTbl % table with cosmological parameters + +%--- Developer-Mode Parameters (usually hard-coded, but set manually if DEVELOPER_MODE is on) -------- +ErrTolTheta 0.5 % 0.7=standard +ErrTolForceAcc 0.0025 % 0.0025=standard +ErrTolIntAccuracy 0.01 % <0.02 +CourantFac 0.2 % <0.20 +MaxRMSDisplacementFac 0.125 % <0.25 +MaxNumNgbDeviation 0.05 % <= 0.4 + +Tests that the vortex is preserved over time. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py + +from gizmo.test import build_and_run_test, assert_final_time, default_omp_threads, default_mpi_ranks, get_final_snapshot + + +def gresho_vphi_analytic(r): + """Analytic azimuthal velocity profile for the Gresho vortex.""" + vphi = np.zeros_like(r) + vphi[r < 0.2] = 5 * r[r < 0.2] + mask = (r >= 0.2) & (r < 0.4) + vphi[mask] = 2 - 5 * r[mask] + return vphi + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_gresho(num_mpi_ranks, num_omp_threads): + test_name = "gresho" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + vel = F["PartType0/Velocities"][:] + + # Compute radius from center and azimuthal velocity + x = coords[:, 0] - 0.5 + y = coords[:, 1] - 0.5 + r = np.sqrt(x**2 + y**2) + # v_phi = (-sin(theta) * vx + cos(theta) * vy) = (-y*vx + x*vy)/r + vphi_sim = (-y * vel[:, 0] + x * vel[:, 1]) / (r + 1e-30) + + # Analytic solution + vphi_analytic = gresho_vphi_analytic(r) + + # Plot comparison + order = r.argsort() + plt.figure() + plt.plot(r[order], vphi_sim[order], ".", markersize=0.5, alpha=0.3, label="GIZMO") + r_plot = np.linspace(0, 0.5, 200) + plt.plot(r_plot, gresho_vphi_analytic(r_plot), "-", color="red", linewidth=2, label="Analytic") + plt.xlabel("r") + plt.ylabel(r"$v_\phi$") + plt.legend() + plt.savefig(f"test/{test_name}/vphi.png") + plt.close() + + # Compute L1 error on v_phi for r < 0.4 (where vortex is active) + inside = r < 0.4 + L1 = np.mean(np.abs(vphi_sim[inside] - vphi_analytic[inside])) / np.mean(np.abs(vphi_analytic[inside])) + assert L1 < 0.15, f"Gresho v_phi L1 error {L1:.4f} exceeds tolerance" diff --git a/test/how_to_add_new_tests.md b/test/how_to_add_new_tests.md new file mode 100644 index 000000000..9bac05fc4 --- /dev/null +++ b/test/how_to_add_new_tests.md @@ -0,0 +1,97 @@ +# How to add new tests + +Whenever you add a new feature to `gizmo` or fix a bug, you are probably running some kind of test to make sure that it works already. With a little extra work, we can package that test into something that can be run automatically by anyone, which can save a lot of work in the long run. + +## Anatomy of a test + +Every test has: +1. A name, e.g. `my_test_name`. This is must be a unique identifier. +2. A subdirectory of `gizmo/test` to store the files associated uniquely with the test, e.g. `gizmo/test/my_test_name`. +3. An initial conditions file. This follows the filename convention `my_test_name_ics.hdf5`. These can be too big for the git repo, so are stored remotely. gizmo will currently try to find it in one of two places: `http://www.tapir.caltech.edu/~phopkins/sims`, or `https://users.flatironinstitute.org/~mgrudic/gizmo_tests/my_test_name/`. +4. The `Config.sh` of option flags with which to build GIZMO for that test. This should be tracked by git and go in `gizmo/test/my_test_name/Config.sh`. +5. The parameters file of runtime parameters passed to GIZMO: `gizmo/test/my_test_name/my_test_name.params`. +6. The python source file containing the actual implementation of the pytest test: `gizmo/test/my_test_name/my_test_name.py`. See [Test implementation](#test-implementation) for what this should look like. +7. The test readme `gizmo/test/my_test_name/README.md` that contains a description of the test. +8. **OPTIONAL**: A reference plaintext and/or HDF5 data file containing data describing the exact or benchmark solution, named `my_test_name_exact.txt` and/or `my_test_name_exact.hdf5`. These should live in the same place as the IC file, and will be downloaded on-the-fly when the test is run. + +## Test implementation + +The actual pass/fail logic of your test should be implemented in a function `test_my_test_name()` in `gizmo/test/my_test_name/my_test_name.py`. This will generally vary from test to test, but the basic idea is to run the simulation, and have some test statistic that you compute from its output to compare with a reference value. This comparison is done via `assert` statement(s) in the `test_my_test_name()` routine. The function may also be responsible for downloading any other special files needed to run GIZMO or perform the test. + +### Example: `gmc_cooling` + +This is the implementation of the [gmc_cooling](https://github.com/mikegrudic/gizmo_imf/tree/master/test/gmc_cooling) test, which runs an idealized giant molecular cloud with the microphysics enabled by `COOLING` for a set time, and compares the resulting density vs. temperature relation determined by the cooling physics to a reference solution. + +```python +"""GMC cooling and chemistry test""" + +from gizmo.test import build_and_run_test, get_cooling_tables +from os import path +from matplotlib import pyplot as plt +import h5py +from astropy import units as u, constants as c +from scipy.stats import binned_statistic +import numpy as np + + +def compute_test_statistic(f, save_reference_solution=False, plot=False): + """Returns the test statistic to be compared with the reference solution. Optionally, saves the input snapshot data as the reference solution.""" + + # get data required to plot n_H vs. T + with h5py.File(f, "r") as F: + Z = F["PartType0/Metallicity"][:] + XH = 1 - F["PartType0/Metallicity"][:, 0] - F["PartType0/Metallicity"][:, 1] + rho_to_nH = XH * (u.Msun / u.pc**3).to(c.m_p / u.cm**3) + rho = F["PartType0/Density"][:] + nH = F["PartType0/Density"][:] * rho_to_nH + T = F["PartType0/Temperature"][:] + + if save_reference_solution: + # this option will save the current test solution as the reference solution + with h5py.File("gmc_cooling_exact.hdf5", "w") as F: + F.create_dataset("PartType0/Metallicity", data=Z) + F.create_dataset("PartType0/Density", data=rho) + F.create_dataset("PartType0/Temperature", data=T) + + if plot: # generate a plot of n_H vs T for the test and benchmark solutions + plt.loglog(nH, T, ".", markersize=1, color="black", label="Test") + with h5py.File("gmc_cooling_exact.hdf5", "r") as F: + nH_ref = F["PartType0/Density"][:] * rho_to_nH + T_ref = F["PartType0/Temperature"][:] + plt.loglog(nH_ref, T_ref, ".", markersize=1, color="red", label="Benchmark") + plt.xlabel(r"$n_{\rm H}\,\rm\left(\rm cm^{-3}\right)$") + plt.ylabel(r"$T (\rm K)$") + plt.legend(loc=3) + plt.savefig("test/gmc_cooling/nH_vs_T.png", bbox_inches="tight") + plt.close() + + # set logarithmic bins for n_H in which to measure the median temperature + nH_bins = np.logspace(1, 3, 10) + + # return nH-binned median temperature + return binned_statistic(nH, T, "median", nH_bins)[0] + + +def test_gmc_cooling(): + # specify the test name + test_name = "gmc_cooling" + + # download necessary cooling tables (needed for tests with COOLING flag) + get_cooling_tables() + + # build GIZMO and run the test + build_and_run_test(test_name) + + # Check that the specific required output file exists: + if not path.isfile("test/gmc_cooling/output/snapshot_010.hdf5"): + raise (RuntimeError("GIZMO did not run successfully.")) + + # Compute a test statistic from the output + test_stats = compute_test_statistic("test/gmc_cooling/output/snapshot_010.hdf5", plot=True) + + # compute that same test statistic from a benchmark snapshot + benchmark_stats = compute_test_statistic("gmc_cooling_exact.hdf5") + + # check that the test and benchmark agree within 10% + assert np.all(np.isclose(test_stats, benchmark_stats, rtol=0.1)) +``` \ No newline at end of file diff --git a/test/how_to_run_tests.md b/test/how_to_run_tests.md new file mode 100644 index 000000000..603082848 --- /dev/null +++ b/test/how_to_run_tests.md @@ -0,0 +1,20 @@ +# How to run and interpret tests + +## Step 1: install gizmo as a python package + +Run `pip install .` from the gizmo code directory. This will install a python package `gizmo` in your python environment (optional step 0: set up a python virtual environment first if you don't want to mess with your regular environment). + +The `gizmo` package includes the python code in `gizmo/python_src`. In addition to conveniently packaging some of the scripts included in `gizmo/scripts` for convenient use (e.g. `load_from_snapshot`), this package has a submodule `test` that implements various routines for downloading ICs, building gizmo, and running tests. + +## Step 2: specify your build environment in `Makefile.systype` + +If you are not working in an environment that works with the default build environment, uncomment the build environment you normally use in `Makefile.systype`, e.g. `SYSTYPE=MacBookCellar` should work for a typical macbook environment with openmpi, hdf5, and gsl installed with homebrew. + +## Step 3: run pytest + +Run `pytest` or `python -m pytest` from the gizmo source code directory. This will run all tests found in the subdirectories of `gizmo/test` and let you know which tests have passed or failed. Optionally, you can pass a pattern to pytest to the test subdirectories you want to run, e.g. `pytest test/my_test_name*`. + +## Step 4: Interpret the results +A test can fail for several reasons: failure to build `GIZMO` for the provided `Config.sh` flags, failure to run `GIZMO` due to a runtime error, or a failure of the code output to pass the actual test. A failure to build GIZMO will raise an explicit error and may be accompanied with compiler messages hinting at the problem. A failure to pass the test on the output will result in a pytest failure. Runtime failures may be diagnosed by examining `GIZMO`'s standard output files: `test_my_test_name.out` and `test_my_test_name.err`. + +The actual simulation output directory for the test may be found in `test/my_test_name/output` for direct inspection. Optionally, the test may generate informative diagnostic plots that should be written to `test/my_test_name`. \ No newline at end of file diff --git a/test/interactblast/Config.sh b/test/interactblast/Config.sh new file mode 100644 index 000000000..bffbfd669 --- /dev/null +++ b/test/interactblast/Config.sh @@ -0,0 +1,9 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_BND_PARTICLES +BOX_SPATIAL_DIMENSION=1 +BOX_REFLECT_X +SELFGRAVITY_OFF +EOS_GAMMA=(1.4) +INPUT_IN_DOUBLEPRECISION +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/interactblast/interactblast.params b/test/interactblast/interactblast.params new file mode 100644 index 000000000..c845b8263 --- /dev/null +++ b/test/interactblast/interactblast.params @@ -0,0 +1,28 @@ +% Interacting blast waves test (Woodward & Colella 1984; Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_BND_PARTICLES +% BOX_SPATIAL_DIMENSION=1 +% BOX_REFLECT_X +% SELFGRAVITY_OFF +% EOS_GAMMA=(1.4) +% +InitCondFile interactblast_ics +OutputDir output +TimeMax 0.038 +BoxSize 1 +TimeBetSnapshot 0.0038 +MaxSizeTimestep 2e-07 +MinSizeTimestep 2e-07 +DesNumNgb 4 +MaxNumNgbDeviation 1e-6 +MaxMemSize 1000 +ErrTolIntAccuracy 0.002 +CourantFac 0.01 +MaxRMSDisplacementFac 0.05 +ErrTolForceAcc 0.001 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +TimeBetStatistics 0.5 +TreeAllocFactor 5.0 diff --git a/test/interactblast/test_interactblast.py b/test/interactblast/test_interactblast.py new file mode 100644 index 000000000..62de78263 --- /dev/null +++ b/test/interactblast/test_interactblast.py @@ -0,0 +1,61 @@ +"""Interacting blast waves test (Woodward & Colella 1984; Hopkins 2015) + +Compares the final snapshot against a high-resolution reference solution. +The exact solution file has columns: i-zone, x, density, V1, V2, V3, pressure. +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from matplotlib import pyplot as plt +import h5py + +from gizmo.test import build_and_run_test, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_interactblast(num_mpi_ranks, num_omp_threads): + test_name = "interactblast" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + x_sim = F["PartType0/Coordinates"][:, 0] + rho_sim = F["PartType0/Density"][:] + vel_sim = F["PartType0/Velocities"][:, 0] + + # Load exact solution: i-zone, x, density, V1, V2, V3, pressure + exact = np.loadtxt(f"test/{test_name}/interactblast_exact.txt") + x_exact = exact[:, 1] + rho_exact = exact[:, 2] + vel_exact = exact[:, 3] + + # Interpolate exact solution to particle positions + rho_interp = interp1d(x_exact, rho_exact, bounds_error=False, fill_value="extrapolate")(x_sim) + vel_interp = interp1d(x_exact, vel_exact, bounds_error=False, fill_value="extrapolate")(x_sim) + + # Compute L1 errors + L1_rho = np.mean(np.abs(rho_sim - rho_interp)) / np.mean(np.abs(rho_interp)) + L1_vel = np.mean(np.abs(vel_sim - vel_interp)) / (np.mean(np.abs(vel_interp)) + 1e-10) + + # Plot comparison + order = x_sim.argsort() + for label, sim, exact_vals in [ + ("Density", rho_sim, rho_interp), + ("Velocity", vel_sim, vel_interp), + ]: + plt.figure() + plt.plot(x_sim[order], sim[order], ".", markersize=1, label="GIZMO") + plt.plot(x_sim[order], exact_vals[order], "-", color="red", linewidth=0.5, label="Exact") + plt.xlabel("x") + plt.ylabel(label) + plt.legend() + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + assert L1_rho < 0.1, f"Density L1 error {L1_rho:.4f} exceeds tolerance" + assert L1_vel < 0.15, f"Velocity L1 error {L1_vel:.4f} exceeds tolerance" diff --git a/test/isodisk/Config.sh b/test/isodisk/Config.sh new file mode 100644 index 000000000..9d7f8b623 --- /dev/null +++ b/test/isodisk/Config.sh @@ -0,0 +1,7 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +PMGRID=64 +ADAPTIVE_GRAVSOFT_FORGAS +COOLING +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/isodisk/isodisk.params b/test/isodisk/isodisk.params new file mode 100644 index 000000000..912e69bff --- /dev/null +++ b/test/isodisk/isodisk.params @@ -0,0 +1,42 @@ +% Isolated disk galaxy test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% PMGRID=64 +% ADAPTIVE_GRAVSOFT_FORGAS +% COOLING +% +InitCondFile isodisk_ics +OutputDir output +TimeMax 0.5 +BoxSize 200 +TimeBetSnapshot 0.1 +MaxSizeTimestep 0.005 +DesNumNgb 32 +MaxNumNgbDeviation 0.1 +UnitLength_in_cm 3.085678e+21 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +SofteningGas 0.001 +Softening_Type1 0.05 +Softening_Type2 0.1 +Softening_Type3 0.1 +Softening_Type4 0.001 +Softening_Type5 0 +SofteningGasMaxPhys 0.001 +Softening_Type1_MaxPhysLimit 0.05 +Softening_Type2_MaxPhysLimit 0.1 +Softening_Type3_MaxPhysLimit 0.1 +Softening_Type4_MaxPhysLimit 0.001 +Softening_Type5_MaxPhysLimit 0 +MinGasTemp 10 +InitGasTemp 1e4 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +ErrTolTheta 0.7 +TimeBetStatistics 0.5 +MaxMemSize 3000 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/isodisk/test_isodisk.py b/test/isodisk/test_isodisk.py new file mode 100644 index 000000000..97a79ef8a --- /dev/null +++ b/test/isodisk/test_isodisk.py @@ -0,0 +1,74 @@ +"""Isolated disk galaxy test (Hopkins 2015) + +Tests the evolution of an isolated disk galaxy with cooling. The disk +should remain stable and develop spiral structure. Checks that the +disk doesn't blow apart or lose too much mass from the disk plane. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from os import path, chdir +from meshoid import Meshoid +from gizmo.test import build_gizmo_for_test, download_test_files, run_test, default_mpi_ranks, clean_test_outputs, get_cooling_tables, flush_colorbar, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_isodisk(num_mpi_ranks, num_omp_threads): + test_name = "isodisk" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_omp_threads) + chdir(f"test/{test_name}/") + + download_test_files(test_name) + get_cooling_tables() + + run_test(test_name, num_mpi_ranks, num_omp_threads) + chdir("../../") + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + pos0 = F["PartType0/Coordinates"][:] + mass0 = F["PartType0/Masses"][:] + boxsize = F["Header"].attrs["BoxSize"] + with h5py.File(snaps[-1], "r") as F: + pos_f = F["PartType0/Coordinates"][:] + mass_f = F["PartType0/Masses"][:] + rho_f = F["PartType0/Density"][:] + + center = boxsize / 2.0 + + # Plot face-on view of the disk using Meshoid slice interpolation + M = Meshoid(pos_f, boxsize=boxsize) + disk_center = np.array([center, center, center]) + rho_slice = M.Slice(np.log10(rho_f), res=1024, plane="z", center=disk_center, size=60., order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="inferno", extent=[-30, 30, -30, 30]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x (kpc)") + ax.set_ylabel("y (kpc)") + ax.set_title("Isolated Disk - Face-on") + fig.savefig(f"test/{test_name}/Density_faceon.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" + + # Disk should still exist: most gas mass should be within the disk region + r0 = np.sqrt(np.sum((pos0 - center) ** 2, axis=1)) + rf = np.sqrt(np.sum((pos_f - center) ** 2, axis=1)) + mass_in_disk0 = mass0[r0 < 50].sum() + mass_in_disk_f = mass_f[rf < 50].sum() + assert mass_in_disk_f > 0.8 * mass_in_disk0, ( + f"Disk lost too much mass: {mass_in_disk_f/mass_in_disk0:.2%} remaining" + ) diff --git a/test/keplerian/Config.sh b/test/keplerian/Config.sh new file mode 100644 index 000000000..1dd1c5e4e --- /dev/null +++ b/test/keplerian/Config.sh @@ -0,0 +1,9 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_SPATIAL_DIMENSION=2 +SELFGRAVITY_OFF +GRAVITY_ANALYTIC +GRAVITY_TESTPROBLEM_KEPLERIAN +EOS_GAMMA=(7.0/5.0) +ENERGY_ENTROPY_SWITCH_IS_ACTIVE +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/keplerian/keplerian.params b/test/keplerian/keplerian.params new file mode 100644 index 000000000..80d2b74c5 --- /dev/null +++ b/test/keplerian/keplerian.params @@ -0,0 +1,27 @@ +% Keplerian disk test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_SPATIAL_DIMENSION=2 +% SELFGRAVITY_OFF +% GRAVITY_ANALYTIC +% GRAVITY_TESTPROBLEM_KEPLERIAN +% EOS_GAMMA=(7.0/5.0) +% ENERGY_ENTROPY_SWITCH_IS_ACTIVE +% +InitCondFile keplerian_ics +OutputDir output +TimeMax 10 +BoxSize 8 +TimeBetSnapshot 2.5 +MaxSizeTimestep 0.1 +DesNumNgb 20 +ErrTolIntAccuracy 0.002 +CourantFac 0.025 +MaxRMSDisplacementFac 0.125 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/keplerian/test_keplerian.py b/test/keplerian/test_keplerian.py new file mode 100644 index 000000000..ff1ce7547 --- /dev/null +++ b/test/keplerian/test_keplerian.py @@ -0,0 +1,73 @@ +"""Keplerian disk test (Hopkins 2015) + +Tests a Keplerian disk with analytic gravity. The disk should maintain +its structure and orbital profile over several orbits. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_keplerian(num_mpi_ranks, num_omp_threads): + test_name = "keplerian" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + pos0 = F["PartType0/Coordinates"][:] + vel0 = F["PartType0/Velocities"][:] + rho0 = F["PartType0/Density"][:] + mass0 = F["PartType0/Masses"][:] + boxsize = F["Header"].attrs["BoxSize"] + with h5py.File(snaps[-1], "r") as F: + pos_f = F["PartType0/Coordinates"][:] + vel_f = F["PartType0/Velocities"][:] + rho_f = F["PartType0/Density"][:] + mass_f = F["PartType0/Masses"][:] + + center = boxsize / 2.0 + r0 = np.sqrt((pos0[:, 0] - center) ** 2 + (pos0[:, 1] - center) ** 2) + rf = np.sqrt((pos_f[:, 0] - center) ** 2 + (pos_f[:, 1] - center) ** 2) + + # Compute azimuthal velocity v_phi = (x*vy - y*vx) / r + vphi0 = ((pos0[:, 0] - center) * vel0[:, 1] - (pos0[:, 1] - center) * vel0[:, 0]) / (r0 + 1e-10) + vphi_f = ((pos_f[:, 0] - center) * vel_f[:, 1] - (pos_f[:, 1] - center) * vel_f[:, 0]) / (rf + 1e-10) + + # Plot radial profile of azimuthal velocity + plt.figure() + rbins = np.linspace(0.5, 3.5, 30) + from scipy.stats import binned_statistic + + vphi0_binned = binned_statistic(r0, vphi0, "median", rbins)[0] + vphi_f_binned = binned_statistic(rf, vphi_f, "median", rbins)[0] + rc = 0.5 * (rbins[:-1] + rbins[1:]) + plt.plot(rc, vphi0_binned, "b-", label="Initial") + plt.plot(rc, vphi_f_binned, "r--", label="Final") + plt.plot(rc, 1.0 / np.sqrt(rc), "k:", label=r"$v_\phi \propto r^{-1/2}$") + plt.xlabel("r") + plt.ylabel(r"$v_\phi$") + plt.legend() + plt.savefig(f"test/{test_name}/vphi_profile.png") + plt.close() + + # The azimuthal velocity profile should remain close to Keplerian (v ~ r^-1/2) + good = np.isfinite(vphi0_binned) & np.isfinite(vphi_f_binned) & (rc > 0.8) & (rc < 3.0) + L1 = np.mean(np.abs(vphi_f_binned[good] - vphi0_binned[good]) / np.abs(vphi0_binned[good])) + assert L1 < 0.15, f"Azimuthal velocity profile degraded: L1 relative error {L1:.4f}" + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" diff --git a/test/kh_mcnally_2d/Config.sh b/test/kh_mcnally_2d/Config.sh new file mode 100644 index 000000000..010d23976 --- /dev/null +++ b/test/kh_mcnally_2d/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +SELFGRAVITY_OFF +EOS_GAMMA=(5.0/3.0) +KERNEL_FUNCTION=5 +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/kh_mcnally_2d/kh_mcnally_2d.params b/test/kh_mcnally_2d/kh_mcnally_2d.params new file mode 100644 index 000000000..1e1c24456 --- /dev/null +++ b/test/kh_mcnally_2d/kh_mcnally_2d.params @@ -0,0 +1,26 @@ +% Kelvin-Helmholtz instability - McNally et al. setup (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% SELFGRAVITY_OFF +% EOS_GAMMA=(5.0/3.0) +% KERNEL_FUNCTION=5 +% +InitCondFile kh_mcnally_2d_ics +OutputDir output +TimeMax 2 +BoxSize 1 +TimeBetSnapshot 0.5 +MaxSizeTimestep 0.02 +DesNumNgb 40 +ErrTolIntAccuracy 0.01 +CourantFac 0.1 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/kh_mcnally_2d/test_kh_mcnally_2d.py b/test/kh_mcnally_2d/test_kh_mcnally_2d.py new file mode 100644 index 000000000..283987785 --- /dev/null +++ b/test/kh_mcnally_2d/test_kh_mcnally_2d.py @@ -0,0 +1,60 @@ +"""Kelvin-Helmholtz instability - McNally et al. setup (Hopkins 2015) + +Tests the development of the KH instability. Since the inviscid problem +has no converged solution, we check that the instability develops +(density variance increases) and that mass/energy are conserved. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, flush_colorbar, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_kh_mcnally_2d(num_mpi_ranks, num_omp_threads): + test_name = "kh_mcnally_2d" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + rho0 = F["PartType0/Density"][:] + mass0 = F["PartType0/Masses"][:] + pos0 = F["PartType0/Coordinates"][:] + with h5py.File(snaps[-1], "r") as F: + rho_f = F["PartType0/Density"][:] + mass_f = F["PartType0/Masses"][:] + pos_f = F["PartType0/Coordinates"][:] + + # Plot final density using Meshoid slice interpolation + M = Meshoid(pos_f, boxsize=1.) + rho_slice = M.Slice(rho_f, res=1024, plane="z", center=np.array([0.5, 0.5, 0.5]), size=1., order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="viridis", extent=[0, 1, 0, 1]) + flush_colorbar(im, ax=ax, label="Density") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("KH Instability - Density") + fig.savefig(f"test/{test_name}/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" + + # KH instability should develop - density variance should increase + # as the interface gets mixed + rho_var0 = np.var(rho0) + rho_var_f = np.var(rho_f) + assert rho_var_f > 0.5 * rho_var0, "Density variance collapsed - instability may not have developed" diff --git a/test/kh_wengen/Config.sh b/test/kh_wengen/Config.sh new file mode 100644 index 000000000..33aef7a86 --- /dev/null +++ b/test/kh_wengen/Config.sh @@ -0,0 +1,9 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_LONG_X=32 +BOX_LONG_Y=32 +BOX_LONG_Z=2 +SELFGRAVITY_OFF +EOS_GAMMA=(5.0/3.0) +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/kh_wengen/kh_wengen.params b/test/kh_wengen/kh_wengen.params new file mode 100644 index 000000000..82ef21017 --- /dev/null +++ b/test/kh_wengen/kh_wengen.params @@ -0,0 +1,33 @@ +% Kelvin-Helmholtz instability - Wengen setup (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_LONG_X=32 +% BOX_LONG_Y=32 +% BOX_LONG_Z=2 +% SELFGRAVITY_OFF +% EOS_GAMMA=(5.0/3.0) +% +InitCondFile kh_wengen_ics +OutputDir output +TimeMax 4 +BoxSize 8 +TimeBetSnapshot 1 +MaxSizeTimestep 0.02 +DesNumNgb 45 +UnitLength_in_cm 3.085678e+21 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +CourantFac 0.1 +ErrTolForceAcc 0.005 +ArtCondConstant 0.25 +ViscosityAMin 0.025 +ViscosityAMax 2 +ErrTolIntAccuracy 0.01 +MaxRMSDisplacementFac 0.1 +TimeBetStatistics 0.5 +MaxMemSize 3000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/kh_wengen/test_kh_wengen.py b/test/kh_wengen/test_kh_wengen.py new file mode 100644 index 000000000..775cd3a95 --- /dev/null +++ b/test/kh_wengen/test_kh_wengen.py @@ -0,0 +1,59 @@ +"""Kelvin-Helmholtz instability - Wengen test (Hopkins 2015) + +Tests the development of the KH instability in a 3D setup from the +Wengen comparison project. Checks mass conservation and that the +instability develops (density variance evolves). +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, flush_colorbar, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_kh_wengen(num_mpi_ranks, num_omp_threads): + test_name = "kh_wengen" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + rho0 = F["PartType0/Density"][:] + mass0 = F["PartType0/Masses"][:] + with h5py.File(snaps[-1], "r") as F: + rho_f = F["PartType0/Density"][:] + mass_f = F["PartType0/Masses"][:] + pos_f = F["PartType0/Coordinates"][:] + + # Plot a slice through the midplane using Meshoid slice interpolation + # BoxSize=8, BOX_LONG_X=32, BOX_LONG_Y=32, BOX_LONG_Z=2 -> box is 256 x 256 x 16 + boxsize = 8. + box_x = boxsize * 32 + box_y = boxsize * 32 + box_z = boxsize * 2 + M = Meshoid(pos_f, boxsize=boxsize) + center = np.array([box_x / 2, box_y / 2, box_z / 2]) + rho_slice = M.Slice(rho_f, res=1024, plane="z", center=center, size=box_x, order=1) + fig, ax = plt.subplots(figsize=(8, 8)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="viridis", extent=[0, box_x, 0, box_y]) + flush_colorbar(im, ax=ax, label="Density") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("KH Wengen - Density (midplane slice)") + fig.savefig(f"test/{test_name}/Density_slice.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" diff --git a/test/mhd_blast/Config.sh b/test/mhd_blast/Config.sh new file mode 100644 index 000000000..0c1686938 --- /dev/null +++ b/test/mhd_blast/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(5.0/3.0) +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/mhd_blast/mhd_blast.params b/test/mhd_blast/mhd_blast.params new file mode 100644 index 000000000..38ca606af --- /dev/null +++ b/test/mhd_blast/mhd_blast.params @@ -0,0 +1,28 @@ +% MHD blast wave test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(5.0/3.0) +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile mhd_blast_ics +OutputDir output +TimeMax 0.2 +BoxSize 1 +TimeBetSnapshot 0.05 +DesNumNgb 20 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +MaxSizeTimestep 0.01 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/mhd_blast/test_mhd_blast.py b/test/mhd_blast/test_mhd_blast.py new file mode 100644 index 000000000..841ebbe55 --- /dev/null +++ b/test/mhd_blast/test_mhd_blast.py @@ -0,0 +1,73 @@ +"""MHD blast wave test (Hopkins & Raives 2016) + +Tests the propagation of a blast wave in a magnetized medium. The blast +should be elongated along the magnetic field direction. Checks energy +conservation and that the blast develops anisotropy. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, flush_colorbar, assert_final_time, default_omp_threads + + +def plot_mhd_blast_density_slice(coords, rho, output_dir="."): + """Plot a density slice of the MHD blast wave.""" + M = Meshoid(coords, boxsize=1.) + rho_slice = M.Slice(np.log10(rho), res=1024, plane="z", center=np.array([0.5, 0.5, 0.5]), size=1., order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="inferno", extent=[0, 1, 0, 1]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("MHD Blast - Density") + fig.savefig(output_dir + "/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_mhd_blast(num_mpi_ranks, num_omp_threads): + test_name = "mhd_blast" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load final snapshot + with h5py.File(snaps[-1], "r") as F: + pos = F["PartType0/Coordinates"][:] + rho = F["PartType0/Density"][:] + u = F["PartType0/InternalEnergy"][:] + mass = F["PartType0/Masses"][:] + B = F["PartType0/MagneticField"][:] + boxsize = F["Header"].attrs["BoxSize"] + + plot_mhd_blast_density_slice(pos, rho, output_dir=f"test/{test_name}") + + # Load initial snapshot for conservation check + with h5py.File(snaps[0], "r") as F: + mass0 = F["PartType0/Masses"][:] + u0 = F["PartType0/InternalEnergy"][:] + B0 = F["PartType0/MagneticField"][:] + vel0 = F["PartType0/Velocities"][:] + + # Mass conservation + mass_err = abs(mass.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" + + # Total energy should be approximately conserved + # (thermal + kinetic + magnetic) + with h5py.File(snaps[-1], "r") as F: + vel = F["PartType0/Velocities"][:] + Etot0 = np.sum(mass0 * u0) + 0.5 * np.sum(mass0 * np.sum(vel0**2, axis=1)) + Etot_f = np.sum(mass * u) + 0.5 * np.sum(mass * np.sum(vel**2, axis=1)) + energy_err = abs(Etot_f - Etot0) / abs(Etot0) + assert energy_err < 0.1, f"Total energy not conserved: relative error {energy_err:.4f}" diff --git a/test/mhd_wave/Config.sh b/test/mhd_wave/Config.sh new file mode 100644 index 000000000..b1f52588a --- /dev/null +++ b/test/mhd_wave/Config.sh @@ -0,0 +1,9 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=1 +EOS_GAMMA=(5.0/3.0) +MAGNETIC +SELFGRAVITY_OFF +INPUT_IN_DOUBLEPRECISION +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/mhd_wave/mhd_wave.params b/test/mhd_wave/mhd_wave.params new file mode 100644 index 000000000..27a109cfe --- /dev/null +++ b/test/mhd_wave/mhd_wave.params @@ -0,0 +1,32 @@ +% Example compile-time options: +% (HYDRO_MESHLESS_FINITE_MASS is optional, replace with your +% choice of hydro/mhd options) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=1 +% EOS_GAMMA=(5.0/3.0) +% MAGNETIC +% SELFGRAVITY_OFF +% +% +InitCondFile mhd_wave_ics +OutputDir output +TimeMax 0.5 +BoxSize 1 +TimeBetSnapshot 0.05 +MaxSizeTimestep 0.1 +DesNumNgb 4 +MaxMemSize 1000 +% -- optional numerical parameters (requires additional Config flags) +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxNumNgbDeviation 1e-6 +ErrTolTheta 0.7 +DivBcleaningParabolicSigma 0.2 +DivBcleaningHyperbolicSigma 1.0 +ResubmitOn 0 +ResubmitCommand none \ No newline at end of file diff --git a/test/mhd_wave/test_mhd_wave.py b/test/mhd_wave/test_mhd_wave.py new file mode 100644 index 000000000..3922641a3 --- /dev/null +++ b/test/mhd_wave/test_mhd_wave.py @@ -0,0 +1,20 @@ +"""MHD linear wave propagation test (Hopkins & Raives 2015)""" + +import pytest +from gizmo.test import build_and_run_test, assert_snapshots_are_close, plot_1D_snapshot_comparison, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(4),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_mhd_wave(num_mpi_ranks, num_omp_threads): + test_name = "mhd_wave" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + outputdir = f"test/{test_name}/output" + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + initial_snap = outputdir + "/snapshot_000.hdf5" + fields = ("Density", "Velocities", "InternalEnergy", "MagneticField") + assert_snapshots_are_close(initial_snap, final_snap, fields_to_compare=fields, rtol=1e-7, atol=1e-7) + plot_1D_snapshot_comparison(initial_snap, final_snap, fields_to_plot=fields, output_dir=f"test/{test_name}") diff --git a/test/mri/Config.sh b/test/mri/Config.sh new file mode 100644 index 000000000..d862e7fb0 --- /dev/null +++ b/test/mri/Config.sh @@ -0,0 +1,11 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SHEARING=1 +BOX_SHEARING_Q=(3./2.) +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(1.000001) +MAGNETIC +SELFGRAVITY_OFF +INPUT_IN_DOUBLEPRECISION +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/mri/mri.params b/test/mri/mri.params new file mode 100644 index 000000000..bcb0d0603 --- /dev/null +++ b/test/mri/mri.params @@ -0,0 +1,43 @@ +% Magneto-rotational instability test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SHEARING=1 +% BOX_SHEARING_Q=(3./2.) +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(1.000001) +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile mri_ics +OutputDir output +TimeMax 20 +BoxSize 1 +TimeBetSnapshot 5 +DesNumNgb 20 +GravityConstantInternal 1 +SofteningGas 0.05 +Softening_Type1 0 +Softening_Type2 0 +Softening_Type3 0 +Softening_Type4 0 +Softening_Type5 0 +SofteningGasMaxPhys 0.05 +Softening_Type1_MaxPhysLimit 0 +Softening_Type2_MaxPhysLimit 0 +Softening_Type3_MaxPhysLimit 0 +Softening_Type4_MaxPhysLimit 0 +Softening_Type5_MaxPhysLimit 0 +MaxSizeTimestep 0.1 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/mri/test_mri.py b/test/mri/test_mri.py new file mode 100644 index 000000000..d1c2adf9d --- /dev/null +++ b/test/mri/test_mri.py @@ -0,0 +1,55 @@ +"""Magneto-rotational instability test (Hopkins & Raives 2016) + +Tests the growth of the MRI in a shearing box. The magnetic energy +should grow exponentially from the initial seed field as the MRI develops. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_mri(num_mpi_ranks, num_omp_threads): + test_name = "mri" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Track magnetic energy over time + times = [] + Emag_list = [] + for snap in snaps: + with h5py.File(snap, "r") as F: + t = F["Header"].attrs["Time"] + B = F["PartType0/MagneticField"][:] + mass = F["PartType0/Masses"][:] + Emag = np.sum(np.sum(B**2, axis=1) * mass) + times.append(t) + Emag_list.append(Emag) + + times = np.array(times) + Emag = np.array(Emag_list) + + # Plot magnetic energy evolution + plt.figure() + plt.semilogy(times, Emag / Emag[0], "o-") + plt.xlabel("Time") + plt.ylabel("E_mag / E_mag(0)") + plt.title("MRI - Magnetic Energy Growth") + plt.savefig(f"test/{test_name}/Emag_evolution.png") + plt.close() + + # MRI should amplify the magnetic field + assert Emag[-1] > 2 * Emag[0], ( + f"MRI did not amplify B-field enough: E_mag ratio = {Emag[-1]/Emag[0]:.2f}" + ) diff --git a/test/noh/Config.sh b/test/noh/Config.sh new file mode 100644 index 000000000..4c2141507 --- /dev/null +++ b/test/noh/Config.sh @@ -0,0 +1,6 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +SELFGRAVITY_OFF +EOS_GAMMA=(5.0/3.0) +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/noh/noh.params b/test/noh/noh.params new file mode 100644 index 000000000..a8a90b77d --- /dev/null +++ b/test/noh/noh.params @@ -0,0 +1,25 @@ +% Noh implosion test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% SELFGRAVITY_OFF +% EOS_GAMMA=(5.0/3.0) +% +InitCondFile noh_ics +OutputDir output +TimeMax 1 +BoxSize 6 +TimeBetSnapshot 0.1 +DesNumNgb 60 +MaxNumNgbDeviation 0.1 +MaxMemSize 5000 +CourantFac 0.1 +MaxSizeTimestep 0.1 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +ErrTolIntAccuracy 0.01 +MaxRMSDisplacementFac 0.1 +TreeDomainUpdateFrequency 0.5 diff --git a/test/noh/test_noh.py b/test/noh/test_noh.py new file mode 100644 index 000000000..aad214f22 --- /dev/null +++ b/test/noh/test_noh.py @@ -0,0 +1,84 @@ +"""Noh implosion test (Hopkins 2015) + +The Noh problem has an analytic solution: an inward-moving flow creates a strong +accretion shock. In 3D with gamma=5/3: +- Pre-shock: rho = (1 + t/r)^2, v_r = -1 +- Post-shock (r < r_shock): rho = 64, v_r = 0 +- Shock position: r_shock = t/3 +""" + +import pytest +import numpy as np +from scipy.stats import binned_statistic +from matplotlib import pyplot as plt +import h5py + +from meshoid import Meshoid +from gizmo.test import build_and_run_test, flush_colorbar, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +def plot_noh_density_slice(coords, rho, output_dir="."): + """Plot a density slice through the Noh implosion center.""" + box_center = 3.0 + M = Meshoid(coords) + center = np.array([box_center, box_center, box_center]) + rho_slice = M.Slice(np.log10(rho), res=1024, plane="z", center=center, size=4., order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="inferno", extent=[-2, 2, -2, 2]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("Noh Implosion - Density Slice") + fig.savefig(output_dir + "/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_noh(num_mpi_ranks, num_omp_threads): + test_name = "noh" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + rho_sim = F["PartType0/Density"][:] + vel = F["PartType0/Velocities"][:] + time = F["Header"].attrs["Time"] + + # Compute radius from box center + box_center = 3.0 # BoxSize/2 + r_sim = np.sqrt(np.sum((coords - box_center) ** 2, axis=1)) + + # Analytic solution + r_shock = time / 3.0 + rho_analytic = np.where(r_sim < r_shock, 64.0, (1.0 + time / r_sim) ** 2) + + # Bin by radius + r_bins = np.linspace(0.01, 2.5, 40) + rho_binned = binned_statistic(r_sim, rho_sim, "median", r_bins)[0] + rho_analytic_binned = binned_statistic(r_sim, rho_analytic, "median", r_bins)[0] + r_centers = 0.5 * (r_bins[:-1] + r_bins[1:]) + + plot_noh_density_slice(coords, rho_sim, output_dir=f"test/{test_name}") + + # Plot + plt.figure() + plt.plot(r_centers, rho_binned, "o", markersize=3, label="GIZMO") + plt.plot(r_centers, rho_analytic_binned, "-", color="red", label="Analytic") + plt.xlabel("r") + plt.ylabel("Density") + plt.legend() + plt.savefig(f"test/{test_name}/Density.png") + plt.close() + + # Check post-shock density: median density inside shock should be close to 64 + post_shock = r_sim < r_shock * 0.8 # well inside the shock + if np.sum(post_shock) > 10: + median_post_shock_rho = np.median(rho_sim[post_shock]) + assert abs(median_post_shock_rho - 64.0) / 64.0 < 0.15, ( + f"Post-shock density {median_post_shock_rho:.1f} deviates >15% from analytic value 64" + ) diff --git a/test/orszag_tang/Config.sh b/test/orszag_tang/Config.sh new file mode 100644 index 000000000..20c05d507 --- /dev/null +++ b/test/orszag_tang/Config.sh @@ -0,0 +1,7 @@ +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +HYDRO_MESHLESS_FINITE_MASS +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/orszag_tang/orszag_tang.params b/test/orszag_tang/orszag_tang.params new file mode 100644 index 000000000..a6b94f139 --- /dev/null +++ b/test/orszag_tang/orszag_tang.params @@ -0,0 +1,27 @@ +% Orszag-Tang MHD vortex test (Hopkins & Raives 2016) +% +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% HYDRO_MESHLESS_FINITE_MASS +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile orszag_tang_ics +OutputDir output +TimeMax 0.5 +BoxSize 1 +TimeBetSnapshot 0.1 +MaxSizeTimestep 0.1 +DesNumNgb 20 +MaxNumNgbDeviation 0.1 +MaxMemSize 2000 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +DivBcleaningParabolicSigma 1.0 +DivBcleaningHyperbolicSigma 1.0 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 diff --git a/test/orszag_tang/test_orszag_tang.py b/test/orszag_tang/test_orszag_tang.py new file mode 100644 index 000000000..19b9ba43c --- /dev/null +++ b/test/orszag_tang/test_orszag_tang.py @@ -0,0 +1,61 @@ +"""Orszag-Tang MHD vortex test (Hopkins & Raives 2016) + +Classic 2D MHD turbulence problem. Tests the development of MHD shocks from +smooth initial conditions. Since there is no exact solution, this test verifies +that the simulation runs and checks energy conservation. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py + +from meshoid import Meshoid +from gizmo.test import build_and_run_test, flush_colorbar, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_orszag_tang(num_mpi_ranks, num_omp_threads): + test_name = "orszag_tang" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + final_snap = get_final_snapshot(test_name) + init_snap = outputdir + "/snapshot_000.hdf5" + assert_final_time(final_snap, test_name) + + def compute_total_energy(snapfile): + with h5py.File(snapfile, "r") as F: + mass = F["PartType0/Masses"][:] + vel = F["PartType0/Velocities"][:] + u = F["PartType0/InternalEnergy"][:] + B = F["PartType0/MagneticField"][:] + rho = F["PartType0/Density"][:] + KE = 0.5 * np.sum(mass[:, None] * vel**2) + TE = np.sum(mass * u) + # Magnetic energy: B^2 / (8*pi) * volume, where volume = mass/rho + ME = np.sum(np.sum(B**2, axis=1) / (8 * np.pi) * mass / rho) + return KE + TE + ME + + E_init = compute_total_energy(init_snap) + E_final = compute_total_energy(final_snap) + + # Plot density at final time using Meshoid slice interpolation + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + rho = F["PartType0/Density"][:] + M = Meshoid(coords, boxsize=1.) + rho_slice = M.Slice(np.log10(rho), res=1024, plane="z",center=np.array([0.5,0.5,0.5]),size=1.,order=1) + + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="viridis", extent=[coords[:,0].min(), coords[:,0].max(), coords[:,1].min(), coords[:,1].max()]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x") + ax.set_ylabel("y") + fig.savefig(f"test/{test_name}/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Energy should be conserved to within ~10% (shock dissipation is expected) + dE = abs(E_final - E_init) / abs(E_init) + assert dE < 0.1, f"Total energy changed by {dE:.4f} (>10%)" diff --git a/test/ring_collision/Config.sh b/test/ring_collision/Config.sh new file mode 100644 index 000000000..741c85db2 --- /dev/null +++ b/test/ring_collision/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_SPATIAL_DIMENSION=2 +SELFGRAVITY_OFF +EOS_TILLOTSON +EOS_ELASTIC +KERNEL_FUNCTION=6 +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/ring_collision/ring_collision.params b/test/ring_collision/ring_collision.params new file mode 100644 index 000000000..b22650a61 --- /dev/null +++ b/test/ring_collision/ring_collision.params @@ -0,0 +1,40 @@ +% Ring collision elastic/solid body test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_SPATIAL_DIMENSION=2 +% SELFGRAVITY_OFF +% EOS_TILLOTSON +% EOS_ELASTIC +% KERNEL_FUNCTION=6 +% +InitCondFile ring_collision_ics +OutputDir output +TimeMax 100 +TimeBetSnapshot 25 +MaxSizeTimestep 0.1 +DesNumNgb 32 +Tillotson_EOS_params_a 0 +Tillotson_EOS_params_b 0 +Tillotson_EOS_params_u_0 1e+20 +Tillotson_EOS_params_rho_0 1 +Tillotson_EOS_params_A 1 +Tillotson_EOS_params_B 0 +Tillotson_EOS_params_u_s 1e+20 +Tillotson_EOS_params_u_s_prime 1e+20 +Tillotson_EOS_params_alpha 0 +Tillotson_EOS_params_beta 0 +Tillotson_EOS_params_mu 0.22 +Tillotson_EOS_params_Y0 1e+20 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +UnitVelocity_in_cm_per_s 1 +UnitLength_in_cm 1 +UnitMass_in_g 1 diff --git a/test/ring_collision/test_ring_collision.py b/test/ring_collision/test_ring_collision.py new file mode 100644 index 000000000..6d46720bd --- /dev/null +++ b/test/ring_collision/test_ring_collision.py @@ -0,0 +1,70 @@ +"""Ring collision elastic/solid body test (Hopkins 2015) + +Tests elastic solid body physics by colliding two rings. The rings should +deform on impact and bounce apart, preserving their structure due to +elastic restoring forces. With the Tillotson EOS parameters set here, +P = cs^2 * (rho - rho_0) with cs = rho_0 = 1. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, flush_colorbar, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_ring_collision(num_mpi_ranks, num_omp_threads): + test_name = "ring_collision" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Plot each snapshot using Meshoid slice interpolation + for snap in snaps: + with h5py.File(snap, "r") as F: + pos = F["PartType0/Coordinates"][:] + rho = F["PartType0/Density"][:] + t = F["Header"].attrs["Time"] + center = np.array([pos[:, 0].mean(), pos[:, 1].mean(), pos[:, 2].mean() if pos.shape[1] > 2 else 0.]) + size = max(pos[:, 0].max() - pos[:, 0].min(), pos[:, 1].max() - pos[:, 1].min()) * 1.1 + M = Meshoid(pos) + rho_slice = M.Slice(rho, res=1024, plane="z", center=center, size=size, order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + extent = [center[0] - size/2, center[0] + size/2, center[1] - size/2, center[1] + size/2] + im = ax.imshow(rho_slice.T, origin="lower", cmap="viridis", extent=extent) + flush_colorbar(im, ax=ax, label="Density") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title(f"Ring Collision t={t:.0f}") + fig.savefig(f"test/{test_name}/snapshot_t{t:.0f}.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + mass0 = F["PartType0/Masses"][:] + pos0 = F["PartType0/Coordinates"][:] + with h5py.File(snaps[-1], "r") as F: + mass_f = F["PartType0/Masses"][:] + pos_f = F["PartType0/Coordinates"][:] + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" + + # After the collision, the rings should have bounced apart. + # Check that the particles span a larger x-range than initially + # (they started moving toward each other, collided, and bounced back) + x_spread0 = pos0[:, 0].max() - pos0[:, 0].min() + x_spread_f = pos_f[:, 0].max() - pos_f[:, 0].min() + assert x_spread_f > 0.5 * x_spread0, ( + "Rings appear to have collapsed rather than bouncing" + ) diff --git a/test/rotor/Config.sh b/test/rotor/Config.sh new file mode 100644 index 000000000..45c686a54 --- /dev/null +++ b/test/rotor/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(1.4000) +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/rotor/rotor.params b/test/rotor/rotor.params new file mode 100644 index 000000000..8704cab7c --- /dev/null +++ b/test/rotor/rotor.params @@ -0,0 +1,28 @@ +% MHD rotor test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(1.4000) +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile rotor_ics +OutputDir output +TimeMax 0.15 +TimeBetSnapshot 0.05 +BoxSize 1 +DesNumNgb 20 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +MaxSizeTimestep 0.005 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/test/rotor/test_rotor.py b/test/rotor/test_rotor.py new file mode 100644 index 000000000..6497125d9 --- /dev/null +++ b/test/rotor/test_rotor.py @@ -0,0 +1,73 @@ +"""MHD rotor test (Hopkins & Raives 2016) + +Tests the spinning-down of a dense rotating disk embedded in a magnetized +medium. Checks that the rotor spins down (transfers angular momentum to +the field) and that the code runs stably. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from meshoid import Meshoid +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_rotor(num_mpi_ranks, num_omp_threads): + test_name = "rotor" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + pos0 = F["PartType0/Coordinates"][:] + vel0 = F["PartType0/Velocities"][:] + mass0 = F["PartType0/Masses"][:] + boxsize = F["Header"].attrs["BoxSize"] + with h5py.File(snaps[-1], "r") as F: + pos_f = F["PartType0/Coordinates"][:] + vel_f = F["PartType0/Velocities"][:] + mass_f = F["PartType0/Masses"][:] + rho_f = F["PartType0/Density"][:] + B_f = F["PartType0/MagneticField"][:] + + # Plot final state using Meshoid slice interpolation + M = Meshoid(pos_f, boxsize=boxsize) + center = np.array([boxsize/2, boxsize/2, boxsize/2]) + Bmag = np.sqrt(np.sum(B_f**2, axis=1)) + rho_slice = M.Slice(np.log10(rho_f), res=1024, plane="z", center=center, size=boxsize, order=1) + B_slice = M.Slice(Bmag, res=1024, plane="z", center=center, size=boxsize, order=1) + extent = [0, boxsize, 0, boxsize] + fig, axes = plt.subplots(1, 2, figsize=(12, 5)) + axes[0].imshow(rho_slice.T, origin="lower", cmap="inferno", extent=extent) + axes[0].set_title("log10(Density)") + axes[1].imshow(B_slice.T, origin="lower", cmap="viridis", extent=extent) + axes[1].set_title("|B|") + for ax in axes: + ax.set_xlabel("x") + ax.set_ylabel("y") + plt.tight_layout() + plt.savefig(f"test/{test_name}/Rotor_2D.png", dpi=150) + plt.close() + + # The rotor should spin down: kinetic energy in the central region should decrease + center = boxsize / 2.0 + r0 = np.sqrt((pos0[:, 0] - center) ** 2 + (pos0[:, 1] - center) ** 2) + rf = np.sqrt((pos_f[:, 0] - center) ** 2 + (pos_f[:, 1] - center) ** 2) + KE0_center = 0.5 * np.sum(mass0[r0 < 0.15] * np.sum(vel0[r0 < 0.15] ** 2, axis=1)) + KEf_center = 0.5 * np.sum(mass_f[rf < 0.15] * np.sum(vel_f[rf < 0.15] ** 2, axis=1)) + + assert KEf_center < KE0_center, "Rotor should spin down (lose kinetic energy to field)" + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" diff --git a/test/rt/Config.sh b/test/rt/Config.sh new file mode 100644 index 000000000..307e6d399 --- /dev/null +++ b/test/rt/Config.sh @@ -0,0 +1,12 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_BND_PARTICLES +BOX_LONG_X=1 +BOX_LONG_Y=2 +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(1.4) +SELFGRAVITY_OFF +GRAVITY_ANALYTIC +GRAVITY_TESTPROBLEM_RT +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/rt/rt.params b/test/rt/rt.params new file mode 100644 index 000000000..12316a6de --- /dev/null +++ b/test/rt/rt.params @@ -0,0 +1,30 @@ +% Rayleigh-Taylor instability test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_BND_PARTICLES +% BOX_LONG_X=1 +% BOX_LONG_Y=2 +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(1.4) +% SELFGRAVITY_OFF +% GRAVITY_ANALYTIC +% GRAVITY_TESTPROBLEM_RT +% +InitCondFile rt_ics +OutputDir output +TimeMax 10 +BoxSize 0.5 +TimeBetSnapshot 1 +MaxSizeTimestep 0.1 +DesNumNgb 20 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +MaxMemSize 1000 +ErrTolTheta 0.7 +MaxNumNgbDeviation 0.1 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/rt/test_rt.py b/test/rt/test_rt.py new file mode 100644 index 000000000..944e1320f --- /dev/null +++ b/test/rt/test_rt.py @@ -0,0 +1,76 @@ +"""Rayleigh-Taylor instability test (Hopkins 2015) + +Tests the development of the RT instability with analytic gravity. +The heavy fluid on top should develop characteristic mushroom-shaped +plumes as it falls through the light fluid below. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from os import path, chdir +from urllib.request import urlretrieve +from meshoid import Meshoid +from gizmo.test import build_gizmo_for_test, download_test_files, run_test, default_mpi_ranks, clean_test_outputs, flush_colorbar, assert_final_time, default_omp_threads + + +WEBSITE = "http://www.tapir.caltech.edu/~phopkins/sims/" + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_rt(num_mpi_ranks, num_omp_threads): + test_name = "rt" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_omp_threads) + chdir(f"test/{test_name}/") + + # Download ICs (non-standard name on site: rt_ics.hdf5, but params references rt_ics) + if not path.isfile("rt_ics.hdf5"): + urlretrieve(WEBSITE + "rt_ics.hdf5", "rt_ics.hdf5") + + run_test(test_name, num_mpi_ranks, num_omp_threads) + chdir("../../") + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[0], "r") as F: + rho0 = F["PartType0/Density"][:] + mass0 = F["PartType0/Masses"][:] + with h5py.File(snaps[-1], "r") as F: + rho_f = F["PartType0/Density"][:] + mass_f = F["PartType0/Masses"][:] + pos_f = F["PartType0/Coordinates"][:] + + # Plot final density using Meshoid slice interpolation + # BoxSize=0.5, BOX_LONG_X=1, BOX_LONG_Y=2 -> box is 0.5 x 1.0 + M = Meshoid(pos_f, boxsize=0.5) + rho_slice = M.Slice(rho_f, res=512, plane="z", center=np.array([0.25, 0.5, 0.25]), size=0.5, order=1) + fig, ax = plt.subplots(figsize=(4, 8)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="viridis", extent=[0, 0.5, 0, 1.0]) + flush_colorbar(im, ax=ax, label="Density") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("Rayleigh-Taylor Instability") + fig.savefig(f"test/{test_name}/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" + + # RT instability should develop - the interface should mix, so the + # density should have intermediate values that weren't present initially + rho_median0 = np.median(rho0) + # Count particles near the median density (within 20% of median) + near_median0 = np.sum(np.abs(rho0 - rho_median0) < 0.2 * rho_median0) + near_median_f = np.sum(np.abs(rho_f - rho_median0) < 0.2 * rho_median0) + # There should be MORE particles near the median density after mixing + assert near_median_f > near_median0, "No evidence of RT mixing" diff --git a/test/sbcluster/Config.sh b/test/sbcluster/Config.sh new file mode 100644 index 000000000..6191d5340 --- /dev/null +++ b/test/sbcluster/Config.sh @@ -0,0 +1,7 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +PMGRID=32 +PM_HIRES_REGION_CLIPPING=1000 +ADAPTIVE_GRAVSOFT_FORGAS +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/sbcluster/sbcluster.params b/test/sbcluster/sbcluster.params new file mode 100644 index 000000000..535477484 --- /dev/null +++ b/test/sbcluster/sbcluster.params @@ -0,0 +1,48 @@ +% Santa Barbara cluster comparison test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% PMGRID=32 +% PM_HIRES_REGION_CLIPPING=1000 +% ADAPTIVE_GRAVSOFT_FORGAS +% +InitCondFile sbcluster_ics +OutputDir output +PartAllocFactor 5 +TimeBegin 0.02 +TimeMax 1 +ComovingIntegrationOn 1 +BoxSize 1 +Omega_Matter 1 +Omega_Lambda 0 +Omega_Baryon 0.1 +HubbleParam 0.5 +UnitLength_in_cm 3.085678e+24 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +SofteningGas 0.04 +Softening_Type1 0.04 +Softening_Type2 0 +Softening_Type3 0 +Softening_Type4 0 +Softening_Type5 0 +SofteningGasMaxPhys 0.02 +Softening_Type1_MaxPhysLimit 0.02 +Softening_Type2_MaxPhysLimit 0 +Softening_Type3_MaxPhysLimit 0 +Softening_Type4_MaxPhysLimit 0 +Softening_Type5_MaxPhysLimit 0 +TimeOfFirstSnapshot 0.02 +ScaleFac_Between_Snapshots 1.5 +MaxSizeTimestep 0.01 +DesNumNgb 32 +MaxNumNgbDeviation 0.1 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +ErrTolTheta 0.7 +TimeBetStatistics 0.5 +MaxMemSize 3000 +ResubmitOn 0 +ResubmitCommand none diff --git a/test/sbcluster/test_sbcluster.py b/test/sbcluster/test_sbcluster.py new file mode 100644 index 000000000..25129382b --- /dev/null +++ b/test/sbcluster/test_sbcluster.py @@ -0,0 +1,86 @@ +"""Santa Barbara cluster comparison test (Hopkins 2015) + +Cosmological cluster formation test. A dark matter halo collapses and +gas shock-heats to form a hot cluster. Checks that a hot, dense cluster +core forms and that baryon fraction is reasonable. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_sbcluster(num_mpi_ranks, num_omp_threads): + test_name = "sbcluster" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load final snapshot + with h5py.File(snaps[-1], "r") as F: + gas_pos = F["PartType0/Coordinates"][:] + gas_rho = F["PartType0/Density"][:] + gas_u = F["PartType0/InternalEnergy"][:] + gas_mass = F["PartType0/Masses"][:] + dm_pos = F["PartType1/Coordinates"][:] + dm_mass = F["PartType1/Masses"][:] + boxsize = F["Header"].attrs["BoxSize"] + + # Find the cluster center (densest gas region) + center = gas_pos[np.argmax(gas_rho)] + + # Compute radii from cluster center + dx = gas_pos - center + dx -= boxsize * np.round(dx / boxsize) # periodic wrapping + r_gas = np.sqrt(np.sum(dx**2, axis=1)) + + dx_dm = dm_pos - center + dx_dm -= boxsize * np.round(dx_dm / boxsize) + r_dm = np.sqrt(np.sum(dx_dm**2, axis=1)) + + # Plot radial density and temperature profiles + rbins = np.logspace(-2.5, -0.5, 20) + rc = np.sqrt(rbins[:-1] * rbins[1:]) + + from scipy.stats import binned_statistic + rho_prof = binned_statistic(r_gas, gas_rho, "median", rbins)[0] + u_prof = binned_statistic(r_gas, gas_u, "median", rbins)[0] + + fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + axes[0].loglog(rc, rho_prof, "o-") + axes[0].set_xlabel("r") + axes[0].set_ylabel("Density") + axes[0].set_title("Gas Density Profile") + axes[1].loglog(rc, u_prof, "o-") + axes[1].set_xlabel("r") + axes[1].set_ylabel("Internal Energy") + axes[1].set_title("Gas Temperature Profile") + plt.tight_layout() + plt.savefig(f"test/{test_name}/profiles.png", dpi=150) + plt.close() + + # The cluster should have formed: central density should be much higher than mean + rho_mean = gas_mass.sum() / boxsize**3 + overdensity = gas_rho.max() / rho_mean + assert overdensity > 100, ( + f"Cluster did not form: max overdensity = {overdensity:.1f}" + ) + + # Gas in the core should be hot (shock-heated) + core = r_gas < 0.05 + if np.any(core): + u_core = np.median(gas_u[core]) + u_mean = np.median(gas_u) + assert u_core > 5 * u_mean, ( + f"Core gas not hot enough: u_core/u_mean = {u_core/u_mean:.1f}" + ) diff --git a/test/sedov/Config.sh b/test/sedov/Config.sh new file mode 100644 index 000000000..4c2141507 --- /dev/null +++ b/test/sedov/Config.sh @@ -0,0 +1,6 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +SELFGRAVITY_OFF +EOS_GAMMA=(5.0/3.0) +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/sedov/sedov.params b/test/sedov/sedov.params new file mode 100644 index 000000000..ffc60e06a --- /dev/null +++ b/test/sedov/sedov.params @@ -0,0 +1,29 @@ +% Sedov-Taylor blast wave test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% SELFGRAVITY_OFF +% EOS_GAMMA=(5.0/3.0) +% +InitCondFile sedov_ics +OutputDir output +TimeMax 0.03 +BoxSize 6 +TimeBetSnapshot 0.003 +DesNumNgb 32 +MaxNumNgbDeviation 0.1 +MaxMemSize 5000 +UnitLength_in_cm 3.085678e+21 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +MinGasTemp 10 +ErrTolIntAccuracy 0.002 +CourantFac 0.05 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.0015 +TimeBetStatistics 0.001 +MaxSizeTimestep 0.01 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +TreeDomainUpdateFrequency 0.5 diff --git a/test/sedov/test_sedov.py b/test/sedov/test_sedov.py new file mode 100644 index 000000000..75bad1c67 --- /dev/null +++ b/test/sedov/test_sedov.py @@ -0,0 +1,103 @@ +"""Sedov-Taylor blast wave test (Hopkins 2015) + +Compares radial profiles of the blast wave against the analytic Sedov solution. +The exact solution file has columns: radius, temperature, density, radial_velocity. +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from scipy.stats import binned_statistic +from matplotlib import pyplot as plt +import h5py + +from meshoid import Meshoid +from gizmo.test import ( + build_and_run_test, + default_mpi_ranks, + flush_colorbar, + assert_final_time, + get_final_snapshot, + default_omp_threads, +) + + +def plot_sedov_density_slice(coords, rho, output_dir="."): + """Plot a density slice through the Sedov blast center.""" + box_center = 3.0 + M = Meshoid(coords) + center = np.array([box_center, box_center, box_center]) + rho_slice = M.Slice(np.log10(rho), res=1024, plane="z", center=center, size=4.0, order=1) + fig, ax = plt.subplots(figsize=(6, 6)) + im = ax.imshow(rho_slice.T, origin="lower", cmap="inferno", extent=[-2, 2, -2, 2]) + flush_colorbar(im, ax=ax, label="log10(Density)") + ax.set_xlabel("x") + ax.set_ylabel("y") + ax.set_title("Sedov Blast - Density Slice") + fig.savefig(output_dir + "/Density_2D.png", dpi=150, bbox_inches="tight") + plt.close(fig) + + +@pytest.mark.parametrize( + "num_mpi_ranks,num_omp_threads", + [(default_mpi_ranks(), default_omp_threads())], +) +def test_sedov(num_mpi_ranks, num_omp_threads): + test_name = "sedov" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + rho_sim = F["PartType0/Density"][:] + vel = F["PartType0/Velocities"][:] + + # Compute radius from box center + box_center = 3.0 # BoxSize/2 + r_sim = np.sqrt(np.sum((coords - box_center) ** 2, axis=1)) + vr_sim = np.sum(vel * (coords - box_center), axis=1) / (r_sim + 1e-30) + + # Load exact solution: radius, temperature, density, radial_velocity + exact = np.loadtxt(f"test/{test_name}/sedov_exact.txt") + r_exact = exact[:, 0] + rho_exact = exact[:, 2] + vr_exact = exact[:, 3] + + # Bin simulation data by radius and compute median + r_bins = np.linspace(0, r_exact.max() * 0.9, 30) + rho_binned = binned_statistic(r_sim, rho_sim, "median", r_bins)[0] + vr_binned = binned_statistic(r_sim, vr_sim, "median", r_bins)[0] + r_centers = 0.5 * (r_bins[:-1] + r_bins[1:]) + + # Interpolate exact solution to bin centers + rho_exact_interp = interp1d(r_exact, rho_exact, bounds_error=False, fill_value="extrapolate")(r_centers) + vr_exact_interp = interp1d(r_exact, vr_exact, bounds_error=False, fill_value="extrapolate")(r_centers) + + plot_sedov_density_slice(coords, rho_sim, output_dir=f"test/{test_name}") + + # Plot comparison + for label, binned, exact_vals in [ + ("Density", rho_binned, rho_exact_interp), + ("RadialVelocity", vr_binned, vr_exact_interp), + ]: + plt.figure() + plt.plot(r_centers, binned, "o", markersize=3, label="GIZMO") + plt.plot(r_centers, exact_vals, "-", color="red", label="Exact") + plt.xlabel("r") + plt.ylabel(label) + plt.legend() + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + # Check that the binned density profile roughly matches the exact solution + # Use bins where the exact solution has significant density (post-shock) + good = rho_exact_interp > 0.01 + if np.any(good): + L1_rho = np.nanmean(np.abs(rho_binned[good] - rho_exact_interp[good])) / np.nanmean( + np.abs(rho_exact_interp[good]) + ) + assert L1_rho < 0.3, f"Density profile L1 error {L1_rho:.4f} exceeds tolerance" diff --git a/test/shocktube/Config.sh b/test/shocktube/Config.sh new file mode 100644 index 000000000..86af1d89a --- /dev/null +++ b/test/shocktube/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_SPATIAL_DIMENSION=1 +BOX_PERIODIC +SELFGRAVITY_OFF +EOS_GAMMA=(1.4) +INPUT_IN_DOUBLEPRECISION +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/shocktube/shocktube.params b/test/shocktube/shocktube.params new file mode 100644 index 000000000..dda39e354 --- /dev/null +++ b/test/shocktube/shocktube.params @@ -0,0 +1,25 @@ +% Sod shock tube test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_SPATIAL_DIMENSION=1 +% BOX_PERIODIC +% SELFGRAVITY_OFF +% EOS_GAMMA=(1.4) +% +InitCondFile shocktube_ics_emass +OutputDir output +TimeMax 5 +BoxSize 80 +TimeBetSnapshot 0.5 +MaxSizeTimestep 0.001 +DesNumNgb 4 +MaxNumNgbDeviation 1e-6 +MaxMemSize 1000 +ErrTolIntAccuracy 0.0025 +CourantFac 0.05 +MaxRMSDisplacementFac 0.125 +TimeBetStatistics 0.5 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 diff --git a/test/shocktube/test_shocktube.py b/test/shocktube/test_shocktube.py new file mode 100644 index 000000000..043a3f29f --- /dev/null +++ b/test/shocktube/test_shocktube.py @@ -0,0 +1,86 @@ +"""Sod shock tube test (Hopkins 2015) + +Compares the final snapshot against the reference high-resolution PPM solution. +The exact solution file has columns: x, density, pressure, entropy, x_velocity. +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from matplotlib import pyplot as plt +import h5py +from os import chdir +from os.path import isfile +from urllib.request import urlretrieve +from gizmo.test import build_gizmo_for_test, run_test, download_test_files, clean_test_outputs, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +WEBSITE = "http://www.tapir.caltech.edu/~phopkins/sims/" + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(max_ranks=4),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_shocktube(num_mpi_ranks, num_omp_threads): + test_name = "shocktube" + clean_test_outputs(test_name) + build_gizmo_for_test(test_name, num_omp_threads) + chdir(f"test/{test_name}/") + + # Download ICs (non-standard name) and exact solution + for f in ("shocktube_ics_emass.hdf5", "shocktube_exact.txt"): + if not isfile(f): + urlretrieve(WEBSITE + f, f) + + run_test(test_name, num_mpi_ranks, num_omp_threads) + chdir("../../") + + outputdir = f"test/{test_name}/output" + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + x_sim = F["PartType0/Coordinates"][:, 0] + rho_sim = F["PartType0/Density"][:] + vel_sim = F["PartType0/Velocities"][:, 0] + u_sim = F["PartType0/InternalEnergy"][:] + + # Load exact solution: x, density, pressure, entropy, x_velocity + exact = np.loadtxt(f"test/{test_name}/shocktube_exact.txt") + x_exact = exact[:, 0] + rho_exact = exact[:, 1] + vel_exact = exact[:, 4] + P_exact = exact[:, 2] + # Internal energy from pressure: u = P / (rho * (gamma - 1)) + gamma = 1.4 + u_exact = P_exact / (rho_exact * (gamma - 1)) + + # Interpolate exact solution to particle positions + rho_interp = interp1d(x_exact, rho_exact, bounds_error=False, fill_value="extrapolate")(x_sim) + vel_interp = interp1d(x_exact, vel_exact, bounds_error=False, fill_value="extrapolate")(x_sim) + u_interp = interp1d(x_exact, u_exact, bounds_error=False, fill_value="extrapolate")(x_sim) + + # Compute L1 errors + L1_rho = np.mean(np.abs(rho_sim - rho_interp)) / np.mean(np.abs(rho_interp)) + L1_vel = np.mean(np.abs(vel_sim - vel_interp)) / (np.mean(np.abs(vel_interp)) + 1e-10) + L1_u = np.mean(np.abs(u_sim - u_interp)) / np.mean(np.abs(u_interp)) + + # Plot comparison + order = x_sim.argsort() + for label, sim, exact_vals in [ + ("Density", rho_sim, rho_interp), + ("Velocity", vel_sim, vel_interp), + ("InternalEnergy", u_sim, u_interp), + ]: + plt.figure() + plt.plot(x_sim[order], sim[order], ".", markersize=1, label="GIZMO") + plt.plot(x_sim[order], exact_vals[order], "-", color="red", linewidth=0.5, label="Exact") + plt.xlabel("x") + plt.ylabel(label) + plt.legend() + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + assert L1_rho < 0.05, f"Density L1 error {L1_rho:.4f} exceeds tolerance" + assert L1_vel < 0.1, f"Velocity L1 error {L1_vel:.4f} exceeds tolerance" + assert L1_u < 0.1, f"Internal energy L1 error {L1_u:.4f} exceeds tolerance" diff --git a/test/shu1977/Config.sh b/test/shu1977/Config.sh new file mode 100644 index 000000000..674ac2868 --- /dev/null +++ b/test/shu1977/Config.sh @@ -0,0 +1,3 @@ +SINGLE_STAR_STARFORGE_DEFAULTS +EOS_GAMMA=1.001 +EOS_ENFORCE_ADIABAT=4e4 diff --git a/test/shu1977/README.md b/test/shu1977/README.md new file mode 100644 index 000000000..0a0fc2ec5 --- /dev/null +++ b/test/shu1977/README.md @@ -0,0 +1,10 @@ +# Shu 1977 test + +This is the Shu 1977 singular isothermal sphere setup with initial density 2x the critical density. The physically-correct behavior is for the central region to undergo runaway collapse into exactly one sink (by symmetry). The test only runs for a short time, just checking that exactly 1 sink does form. + +Compile-time flags used for this setup: +``` +SINGLE_STAR_STARFORGE_DEFAULTS +EOS_ENFORCE_ADIABAT=4e4 +EOS_GAMMA=1.001 +``` diff --git a/test/shu1977/shu1977.params b/test/shu1977/shu1977.params new file mode 100755 index 000000000..1cf3b1c83 --- /dev/null +++ b/test/shu1977/shu1977.params @@ -0,0 +1,293 @@ +%------------------------------------------------------------------------- +%---- This file contains the input parameters needed at run-time for +% simulations. It is based on and closely resembles the GADGET-3 +% parameterfile (format of which and parsing routines written by +% Volker Springel [volker.springel@h-its.org]). It has been updated +% with new naming conventions and additional variables as needed by +% Phil Hopkins [phopkins@caltech.edu] for GIZMO. +%------------------------------------------------------------------------- + +%---- Relevant files (filenames and directories) +InitCondFile shu1977_ics +OutputDir output + +%---- File formats (input and output) +ICFormat 3 % 1=unformatted (gadget) binary, 3=hdf5, 4=cluster +SnapFormat 3 % 1=unformatted (gadget) binary, 3=hdf5 + + +%---- Output parameters +RestartFile restart +SnapshotFileBase snapshot +OutputListOn 0 % =1 to use list in "OutputListFilename" +OutputListFilename output_times.txt % list of times (in code units) for snaps +NumFilesPerSnapshot 1 +NumFilesWrittenInParallel 1 % must be < N_processors & power of 2 + +%---- Output frequency +TimeOfFirstSnapshot 0. % time (code units) of first snapshot +TimeBetSnapshot 1e-6 %7.656264097337446e-06 % time between (if OutputListOn=0), code units +TimeBetStatistics 1e-6 %7.656264097337446e-06 % time between additional statistics (e.g. energy) + +%---- CPU run-time and checkpointing time-limits +TimeLimitCPU 172800 % in seconds +CpuTimeBetRestartFile 7200 % in seconds +ResubmitOn 0 +ResubmitCommand my-scriptfile + +%---- Desired simulation beginning and end times (in code units) for run +TimeBegin 0.0 % Beginning of the simulation +TimeMax 2e-5 %0.0030625056389349784 % End of the simulation + +%---- Maximum and minimum timesteps allowed +MaxSizeTimestep 5e-6 %1.5312528194674893e-05 % in code units, set for your problem +MinSizeTimestep 1.0e-15 % set this very low, or get the wrong answer + + +%---- System of units +UnitLength_in_cm 3.085678e18 % 1.0pc +UnitMass_in_g 1.989e33 % solar mass +UnitVelocity_in_cm_per_s 1.0e2 % 1 m/sec +UnitMagneticField_in_gauss 1.0e4 % 1 Tesla +GravityConstantInternal %4301 %31397 %2042 %2867 %3800.5799256505575 % calculated by code if =0 + +%---- Cosmological parameters +ComovingIntegrationOn 0 % is it cosmological? (yes=1, no=0) +BoxSize 4.34 % in code units +Omega0 0. % =0 for non-cosmological +OmegaLambda 0. % =0 for non-cosmological +OmegaBaryon 0. % =0 for non-cosmological +HubbleParam 1. % little 'h'; =1 for non-cosmological runs + + +%----- Memory allocation +MaxMemSize 3500 % sets maximum MPI process memory use in MByte +PartAllocFactor 5.0 % memory load allowed for better cpu balance +BufferSize 100 % in MByte + +%---- Rebuild domains when >this fraction of particles active +TreeDomainUpdateFrequency 0.005 % 0.0005-0.05, dept on core+particle number + + +%---- (Optional) Initial hydro temperature & temperature floor (in Kelvin) +InitGasTemp 10 % set by IC file if =0 +MinGasTemp 10 % don't set <10 in explicit feedback runs, otherwise 0 + +%---- Hydro reconstruction (kernel) parameters +DesNumNgb 32 % domain-reconstruction kernel number: 32 standard, 60-114 for quintic +MaxHsml 1.0e10 % minimum gas kernel length (some very large value to prevent errors) +MinGasHsmlFractional 0 % minimum kernel length relative to gas force softening (<= 1) + + +%---- Gravitational softening lengths +%----- Softening lengths per particle type. If ADAPTIVE_GRAVSOFT is set, these +%-------- are the minimum softening allowed for each type ------- +%-------- (units are co-moving for cosmological integrations) +SofteningGas 1e-10 % gas (particle type=0) (in co-moving code units) +SofteningHalo 0.020 % dark matter/collisionless particles (type=1) +SofteningDisk 0.150 % collisionless particles (type=2) +SofteningBulge 3.66e-5 % collisionless particles (type=3) +SofteningStars 3.66e-5 % stars spawned from gas (type=4) +SofteningBndry 3.66e-6 % black holes (if active), or collisionless (type=5) + +%---- if these are set in cosmo runs, SofteningX switches from comoving to physical +%------- units when the comoving value exceeds the choice here +%------- (these are ignored, and *only* the above are used, for non-cosmo runs) +SofteningGasMaxPhys 0.0005 % e.g. switch to 0.5pc physical below z=1 +SofteningHaloMaxPhys 0.010 +SofteningDiskMaxPhys 0.075 +SofteningBulgeMaxPhys 0.250 +SofteningStarsMaxPhys 0.0005 +SofteningBndryMaxPhys 0.0005 +%----- parameters for adaptive gravitational softening +AGS_DesNumNgb 32 % neighbor number for calculating adaptive gravsoft + + + + +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- +%---------- Physics Modules ---------------------------------------------- +%------------------------------------------------------------------------- +%------------------------------------------------------------------------- + + +%------------------------------------------------------------ +%------------------ Additional Fluid Physics ---------------- +%------------------------------------------------------------ + +%---- Magneto-Hydrodynamics Parameters (MAGNETIC on) +%----- Initial B-Field Strengths (if MHD_B_SET_IN_PARAMS on, otherwise read from IC file) +BiniX 1.0e-8 % initial B_x, in code units +BiniY 1.0e-8 % initial B_y, in code units +BiniZ 1.0e-8 % initial B_z, in code units + +%---- Thermal Conduction (CONDUCTION on) +%----- set coefficient kappa [code units] or, if CONDUCTION_SPITZER on, multiplies value +ConductionCoeff 1.0 % set/multiply conduction coefficient + +%---- Navier-Stokes Viscosity (VISCOSITY on) +%--- set coefficients eta,zeta [code units] or, if VISCOSITY_BRAGINSKII on, multiplies value +ShearViscosityCoeff 1.0 % set/multiply shear viscosity coefficient +BulkViscosityCoeff 1.0 % set/multiply bulk viscosity coefficient + +%---- Turbulent Diffusion Master Switch (TURB_DIFFUSION on) +TurbDiffusionCoefficient 1.0 % Normalizes diffusion rates relative to Smagorinsky-Lilly theory [best calibration] (~0.5-2) + +%---- Cosmic Ray + Gas Fluids (COSMIC_RAYS on) +CosmicRayDiffusionCoeff 1.0 % multiplies anisotropic diffusion/streaming coefficients + +%---- Dust-Gas Mixtures (GRAIN_FLUID on) +Grain_Internal_Density 1.0 % internal/material density of grains in g/cm^3 +Grain_Size_Min 1.e-6 % minimum grain size in cm +Grain_Size_Max 1.e-4 % maximum grain size in cm +Grain_Size_Spectrum_Powerlaw 0.5 % power-law distribution of grain sizes (dm/dlnr~r^x) + + +%------------------------------------------------------------------------- +%------------------ Star, Black Hole, and Galaxy Formation --------------- +%------------------------------------------------------------------------- + + +%---- Star Formation parameters (GALSF on) +CritPhysDensity 1461452800000.0 % critical physical density for star formation (cm^(-3)) +SfEffPerFreeFall 1.0 % SFR/(Mgas/tfreefall) for gas which meets SF criteria + + +%---- sub-grid (Springel+Hernquist/GADGET/AREPO) "effective equation of state" +%------- star formation+feedback model (GALSF_EFFECTIVE_EQS on) +MaxSfrTimescale 4.0 % code units (SF timescale at 2-phase threshold) +TempSupernova 3.0e8 % in Kelvin (temp of hot gas in 2-phase model) +TempClouds 1000.0 % in Kelvin (temp of cold gas in 2-phase model) +FactorSN 0.1 % SNe coupling frac (frac of egy retained in hot) +FactorEVP 3000.0 % controls Kennicutt normalization +FactorForSofterEQS 1.0 % interpolate between 'stiff' and isothermal EOS +%------- the sub-grid "decoupled winds" model (GALSF_SUBGRID_WINDS on) +WindEfficiency 2.0 % mass-loading (Mdot_wind = SFR * WindEfficiency) +WindEnergyFraction 0.06 % fraction of SNe energy in winds (sets velocity) +WindFreeTravelMaxTime 0.1 % 'free-stream time' in units of t_Hubble(z) +WindFreeTravelDensFac 0.1 % 'free-stream' until density < this * CritPhysDensity +%------- alternative winds (set GALSF_SUBGRID_WIND_SCALING == 1 or 2) +%------- (scaling with local dark matter properties, as Dave/Oppenheimer/Mannucci/Illustris) +VariableWindVelFactor 1.0 % wind velocity relative to estimated halo v_escape +VariableWindSpecMomentum 5000. % wind momentum per unit stellar mass (code velocity units) + + + +%-------------- FIRE (PFH) explicit star formation & feedback model (FIRE on) +%--- initial metallicity of gas & stars in simulation +InitMetallicity 1.0 % initial gas+stellar metallicity (in solar) +InitStellarAge 0.0 % initial mean age (in Gyr; for stars in sim ICs) +%--- local radiation-pressure driven winds (GALSF_FB_FIRE_RT_LOCALRP) +WindMomentumLoading 1.0 % fraction of photon momentum to couple +%--- SneII Heating Model (GALSF_FB_MECHANICAL) +SNeIIEnergyFrac 1.0 % fraction of mechanical energy to couple +%--- HII region photo-heating model (GALSF_FB_FIRE_RT_HIIHEATING) +HIIRegion_fLum_Coupled 1.0 % fraction of ionizing photons allowed to see gas +%--- long-range radiation pressure acceleration (GALSF_FB_FIRE_RT_LONGRANGE) +PhotonMomentum_Coupled_Fraction 1.0 % fraction of L to allow incident +PhotonMomentum_fUV 0.0 % incident SED f(L) in UV (minimum scattering) +PhotonMomentum_fOPT 0.0 % incident SED f(L) in optical/near-IR +%--- gas return/recycling +GasReturnFraction 1.0 % fraction of gas mass returned (relative to ssp) +GasReturnEnergy 1.0 % fraction of returned gas energy+momentum (relative to ssp) +%--- cosmic rays (COSMIC_RAYS) +CosmicRay_SNeFraction 0.1 % fraction of SNe ejecta kinetic energy into cosmic rays (~10%) + + +%-------------- Black Hole accretion & formation (BLACK_HOLES on) +%--- formation/seeding +SeedBlackHoleMass 7.8125e-07 % initial mass (on-the-fly or single galaxy) +SeedAlphaDiskMass 0.0 % initial mass in the alpha disk (BH_ALPHADISK_ACCRETION) +SeedBlackHoleMinRedshift 2.0 % minimum redshift where new BH particles are seeded (lower-z ceases seeding) +SeedBlackHoleMassSigma 0.5 % lognormal standard deviation (in dex) in initial BH seed masses +%----- (specific options for on-the-fly friends-of-friends based BH seeding: FOF on) +MinFoFMassForNewSeed 10. % minimum mass of FOF group (stars or DM) to get seed, in code units +TimeBetOnTheFlyFoF 1.01 % time (in code units, e.g. scale-factor) between on-the-fly FOF searches +%--- accretion +BlackHoleAccretionFactor 1.0 % multiplier for mdot (relative to model) +BlackHoleEddingtonFactor 1e100 % fraction of eddington to cap (can be >1) +BlackHoleNgbFactor 1.0 % multiplier for kernel neighbors for BH +BlackHoleMaxAccretionRadius 4.34 % max radius for BH neighbor search/accretion (code units) +BlackHoleRadiativeEfficiency 5e-7 % radiative efficiency (for accretion and feedback) +%--- feedback +BlackHoleFeedbackFactor 1.0 % generic feedback strength multiplier +BH_FluxMomentumFactor 0.0 % multiply radiation pressure (BH_PHOTONMOMENTUM), set it to zero to avoid launching gas from rad. pressure +BAL_f_accretion 0.7 % fraction of gas swallowed by BH (BH_WIND options) +BAL_v_outflow 100000. % velocity (km/s) of BAL outflow (BH_WIND options) +BAL_internal_temperature 10. % internal temperature (K) of BAL outflow (BH_WIND_SPAWN) +BAL_wind_particle_mass 7.8125e-05 % mass of 'virtual wind particles' in code units (BH_WIND_SPAWN) + + +%------------------------------------------------------------------------- +%------------------ Grackle cooling module ----------------- +%------------------------------------------------------------------------- + +%-------------- Grackle UVB file (COOL_GRACKLE on) +GrackleDataFile CloudyData_UVB=HM2012.h5 + + + +%------------------------------------------------------------------------- +%------------------ Driven Turbulence (Large-Eddy boxes) ----------------- +%------------------------------------------------------------------------- + +%-------------- Turbulent stirring parameters (TURB_DRIVING on) +ST_decay 3.4980225402157403e+99 % decay time for driving-mode phase correlations +ST_energy 0.0 % energy of driving-scale modes: sets norm of turb +ST_DtFreq 3.4980225402157406e+98 % time interval for driving updates (set by hand) +ST_Kmin 8.98 % minimum driving-k: should be >=2.*M_PI/All.BoxSize +ST_Kmax 17.97 % maximum driving-k: set to couple times Kmin or more if cascade desired +ST_SolWeight 1.0 % fractional wt of solenoidal modes (wt*curl + (1-wt)*div) +ST_AmplFac 1.0 % multiplies turb amplitudes +ST_SpectForm 3 % driving pwr-spec: 0=Ek~const; 1=sharp-peak at kc; 2=Ek~k^(-5/3); 3=Ek~k^-2 +ST_Seed 42 % random number seed for modes (so you can reproduce it) +IsoSoundSpeed 200. % initializes gas sound speed in box to this value +TimeBetTurbSpectrum 0.5 % time (code units) between evaluations of turb pwrspec + + +%------------------------------------------------------------------------------------------------- +%------------------ Non-Standard Dark Matter, Dark Energy, Gravity, or Expansion ----------------- +%------------------------------------------------------------------------------------------------- + +%-------------- Parameters for non-standard or time-dependent Gravity/Dark Energy/Expansion (GR_TABULATED_COSMOLOGY on) +DarkEnergyConstantW -1 % time-independent DE parameter w, used only if no table +TabulatedCosmologyFile CosmoTbl % table with cosmological parameters + + +%------------------------------------------------------------- +%------------------ Solid bodies and Impacts ----------------- +%------------------------------------------------------------- + +%-------------- Parameters for custom Tillotson equation-of-state (EOS_TILLOTSON on) +%--- In ICs, set "CompositionType": 0=custom,1=granite,2=basalt,3=iron,4=ice,5=olivine/dunite,6=water; +%--- their EOS parameters will be set accordingly. If CompositionType=0, the custom parameters below +%--- are used, matched to the definitions in Table A1 of Reinhardt+Stadel 2017,MNRAS,467,4252 (below is iron) +Tillotson_EOS_params_a 0.5 % a parameter [dimensionless] +Tillotson_EOS_params_b 1.5 % b parameter [dimensionless] +Tillotson_EOS_params_u_0 9.5e10 % u_0 parameter in [erg/g] +Tillotson_EOS_params_rho_0 7.86 % rho_0 parameter in [g/cm^3] +Tillotson_EOS_params_A 1.28e12 % A parameter in [erg/cm^3] +Tillotson_EOS_params_B 1.05e12 % B parameter in [erg/cm^3] +Tillotson_EOS_params_u_s 1.42e10 % u_s parameter in [erg/g] +Tillotson_EOS_params_u_s_prime 8.45e10 % u_s^prime parameter in [erg/g] +Tillotson_EOS_params_alpha 5.0 % alpha parameter [dimensionless] +Tillotson_EOS_params_beta 5.0 % beta parameter [dimensionless] +Tillotson_EOS_params_mu 7.75e11 % elastic shear modulus in [erg/cm^3] (used if EOS_ELASTIC is on) +Tillotson_EOS_params_Y0 8.5e10 % hugoniot elastic limit in [erg/cm^3] (used if EOS_ELASTIC is on) + + + + +%--- Developer-Mode Parameters (usually hard-coded, but set manually if DEVELOPER_MODE is on) -------- +ErrTolTheta 0.21 % 0.7=standard +ErrTolForceAcc 0.0025 % 0.0025=standard +ErrTolIntAccuracy 0.01 % <0.02 +CourantFac 0.2 % <0.20 +MaxRMSDisplacementFac 0.125 % <0.25 +MaxNumNgbDeviation 0.05 % < 0.5 * contrast0, ( + f"Density contrast degraded too much: {contrast0:.2f} -> {contrast_f:.2f}" + ) + + # Mass conservation + mass_err = abs(mass_f.sum() - mass0.sum()) / mass0.sum() + assert mass_err < 1e-3, f"Mass not conserved: relative error {mass_err:.6f}" diff --git a/test/toth/Config.sh b/test/toth/Config.sh new file mode 100644 index 000000000..9f71a1f7b --- /dev/null +++ b/test/toth/Config.sh @@ -0,0 +1,11 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +BOX_LONG_X=16 +BOX_LONG_Y=1 +BOX_LONG_Z=1 +BOX_SPATIAL_DIMENSION=2 +EOS_GAMMA=(5.0/3.0) +MAGNETIC +SELFGRAVITY_OFF +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/toth/test_toth.py b/test/toth/test_toth.py new file mode 100644 index 000000000..c1d2eb72a --- /dev/null +++ b/test/toth/test_toth.py @@ -0,0 +1,49 @@ +"""Toth MHD shock tube test (Hopkins & Raives 2016) + +MHD shock tube problem from Toth (2000). Since there is no provided exact +solution file, this test verifies that the simulation runs to completion and +produces physically reasonable output. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py + +from gizmo.test import build_and_run_test, assert_final_time, default_mpi_ranks, default_omp_threads, get_final_snapshot + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_toth(num_mpi_ranks, num_omp_threads): + test_name = "toth" + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + final_snap = get_final_snapshot(test_name) + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + x_sim = F["PartType0/Coordinates"][:, 0] + rho_sim = F["PartType0/Density"][:] + vel_sim = F["PartType0/Velocities"][:, 0] + B_sim = F["PartType0/MagneticField"][:] + + # Plot profiles + order = x_sim.argsort() + for label, data in [ + ("Density", rho_sim), + ("Velocity", vel_sim), + ("By", B_sim[:, 1]), + ]: + plt.figure() + plt.plot(x_sim[order], data[order], ".", markersize=1) + plt.xlabel("x") + plt.ylabel(label) + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + # Basic sanity checks + assert np.all(np.isfinite(rho_sim)), "Non-finite density values found" + assert np.all(rho_sim > 0), "Negative density values found" + assert np.all(np.isfinite(B_sim)), "Non-finite magnetic field values found" diff --git a/test/toth/toth.params b/test/toth/toth.params new file mode 100644 index 000000000..e5921815e --- /dev/null +++ b/test/toth/toth.params @@ -0,0 +1,30 @@ +% Toth MHD shock tube test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% BOX_LONG_X=16 +% BOX_LONG_Y=1 +% BOX_LONG_Z=1 +% BOX_SPATIAL_DIMENSION=2 +% EOS_GAMMA=(5.0/3.0) +% MAGNETIC +% SELFGRAVITY_OFF +% +InitCondFile toth_ics +OutputDir output +TimeMax 0.08 +BoxSize 0.25 +TimeBetSnapshot 0.04 +DesNumNgb 20 +MaxNumNgbDeviation 0.1 +MaxMemSize 2000 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +DivBcleaningParabolicSigma 1.0 +DivBcleaningHyperbolicSigma 1.0 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 diff --git a/test/trivial/test_trivial.py b/test/trivial/test_trivial.py new file mode 100644 index 000000000..2286c69f6 --- /dev/null +++ b/test/trivial/test_trivial.py @@ -0,0 +1,4 @@ +"""A trivial test for pytest""" + +def test_trivial(): + assert True \ No newline at end of file diff --git a/test/unit/test_harness.h b/test/unit/test_harness.h new file mode 100644 index 000000000..5bae8c61e --- /dev/null +++ b/test/unit/test_harness.h @@ -0,0 +1,86 @@ +#pragma once +// Minimal C++ unit test harness. No external dependencies. +// +// Usage: +// #include "test_harness.h" +// TEST_CASE("descriptive name") { +// CHECK(1 + 1 == 2); +// CHECK_CLOSE(3.14, 3.14159, 0.01); +// } +// TEST_MAIN() // expands to main(), runs all registered tests +// +#include +#include +#include +#include +#include + +struct TestCase { + const char* name; + std::function func; +}; + +inline std::vector& test_registry() { + static std::vector cases; + return cases; +} + +struct TestRegistrar { + TestRegistrar(const char* name, std::function func) { + test_registry().push_back({name, std::move(func)}); + } +}; + +inline int& check_fail_count() { static int n = 0; return n; } +inline int& check_pass_count() { static int n = 0; return n; } + +#define TEST_PASTE_(a, b) a##b +#define TEST_PASTE(a, b) TEST_PASTE_(a, b) +#define TEST_CASE_IMPL(id, name) \ + static void TEST_PASTE(test_func_, id)(); \ + static TestRegistrar TEST_PASTE(test_reg_, id)(name, TEST_PASTE(test_func_, id)); \ + static void TEST_PASTE(test_func_, id)() +#define TEST_CASE(name) TEST_CASE_IMPL(__COUNTER__, name) + +#define CHECK(expr) do { \ + if (!(expr)) { \ + std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #expr); \ + check_fail_count()++; \ + } else { \ + check_pass_count()++; \ + } \ +} while(0) + +#define CHECK_CLOSE(a, b, tol) do { \ + double _a = (a), _b = (b), _t = (tol); \ + if (std::fabs(_a - _b) > _t) { \ + std::fprintf(stderr, " FAIL: %s:%d: %s ~= %s (%.15g != %.15g, tol=%.3g)\n", \ + __FILE__, __LINE__, #a, #b, _a, _b, _t); \ + check_fail_count()++; \ + } else { \ + check_pass_count()++; \ + } \ +} while(0) + +#define TEST_MAIN() \ +int main() { \ + int total_failures = 0; \ + for (auto& tc : test_registry()) { \ + check_fail_count() = 0; \ + check_pass_count() = 0; \ + tc.func(); \ + int f = check_fail_count(), p = check_pass_count(); \ + if (f > 0) { \ + std::fprintf(stderr, "FAIL: %s (%d/%d checks passed)\n", tc.name, p, p+f); \ + total_failures += f; \ + } else { \ + std::fprintf(stdout, " ok: %s (%d checks)\n", tc.name, p); \ + } \ + } \ + if (total_failures > 0) { \ + std::fprintf(stderr, "\n%d check(s) FAILED\n", total_failures); \ + return 1; \ + } \ + std::fprintf(stdout, "\nAll %zu test(s) passed.\n", test_registry().size()); \ + return 0; \ +} diff --git a/test/unit/test_math_types.cc b/test/unit/test_math_types.cc new file mode 100644 index 000000000..f6483045f --- /dev/null +++ b/test/unit/test_math_types.cc @@ -0,0 +1,517 @@ +// Unit tests for Vec3, Mat3, and SymmetricTensor2. +#include "test_harness.h" +#include "../../math_types/vec3.h" +#include "../../math_types/mat3.h" +#include "../../math_types/symmetric_tensor2.h" + +static constexpr double EPS = 1e-14; + +// ============================================================ +// Vec3 +// ============================================================ + +TEST_CASE("Vec3: brace init and element access") { + Vec3 v{1.0, 2.0, 3.0}; + CHECK_CLOSE(v[0], 1.0, EPS); + CHECK_CLOSE(v[1], 2.0, EPS); + CHECK_CLOSE(v[2], 3.0, EPS); +} + +TEST_CASE("Vec3: data_ptr") { + Vec3 v{4.0, 5.0, 6.0}; + double* p = v.data_ptr(); + CHECK(p == &v[0]); + CHECK_CLOSE(p[0], 4.0, EPS); + CHECK_CLOSE(p[1], 5.0, EPS); + CHECK_CLOSE(p[2], 6.0, EPS); +} + +TEST_CASE("Vec3: begin/end iteration") { + Vec3 v{1.0, 2.0, 3.0}; + double sum = 0; + for (double x : v) sum += x; + CHECK_CLOSE(sum, 6.0, EPS); +} + +TEST_CASE("Vec3: operator+= -= *= /=") { + Vec3 a{1.0, 2.0, 3.0}; + Vec3 b{4.0, 5.0, 6.0}; + + a += b; + CHECK_CLOSE(a[0], 5.0, EPS); CHECK_CLOSE(a[1], 7.0, EPS); CHECK_CLOSE(a[2], 9.0, EPS); + + a -= b; + CHECK_CLOSE(a[0], 1.0, EPS); CHECK_CLOSE(a[1], 2.0, EPS); CHECK_CLOSE(a[2], 3.0, EPS); + + a *= 2.0; + CHECK_CLOSE(a[0], 2.0, EPS); CHECK_CLOSE(a[1], 4.0, EPS); CHECK_CLOSE(a[2], 6.0, EPS); + + a /= 2.0; + CHECK_CLOSE(a[0], 1.0, EPS); CHECK_CLOSE(a[1], 2.0, EPS); CHECK_CLOSE(a[2], 3.0, EPS); +} + +TEST_CASE("Vec3: binary + - operators") { + Vec3 a{1.0, 2.0, 3.0}, b{10.0, 20.0, 30.0}; + Vec3 c = a + b; + CHECK_CLOSE(c[0], 11.0, EPS); CHECK_CLOSE(c[1], 22.0, EPS); CHECK_CLOSE(c[2], 33.0, EPS); + Vec3 d = b - a; + CHECK_CLOSE(d[0], 9.0, EPS); CHECK_CLOSE(d[1], 18.0, EPS); CHECK_CLOSE(d[2], 27.0, EPS); +} + +TEST_CASE("Vec3: unary negation") { + Vec3 a{1.0, -2.0, 3.0}; + Vec3 b = -a; + CHECK_CLOSE(b[0], -1.0, EPS); CHECK_CLOSE(b[1], 2.0, EPS); CHECK_CLOSE(b[2], -3.0, EPS); +} + +TEST_CASE("Vec3: scalar * and / (both orderings)") { + Vec3 v{2.0, 4.0, 6.0}; + Vec3 a = 3.0 * v; + Vec3 b = v * 3.0; + CHECK_CLOSE(a[0], 6.0, EPS); CHECK_CLOSE(a[1], 12.0, EPS); CHECK_CLOSE(a[2], 18.0, EPS); + CHECK_CLOSE(b[0], 6.0, EPS); CHECK_CLOSE(b[1], 12.0, EPS); CHECK_CLOSE(b[2], 18.0, EPS); + Vec3 c = v / 2.0; + CHECK_CLOSE(c[0], 1.0, EPS); CHECK_CLOSE(c[1], 2.0, EPS); CHECK_CLOSE(c[2], 3.0, EPS); +} + +TEST_CASE("Vec3: norm and norm_sq") { + Vec3 v{3.0, 4.0, 0.0}; + CHECK_CLOSE(v.norm_sq(), 25.0, EPS); + CHECK_CLOSE(v.norm(), 5.0, EPS); + + Vec3 zero{0.0, 0.0, 0.0}; + CHECK_CLOSE(zero.norm(), 0.0, EPS); +} + +TEST_CASE("Vec3: dot product") { + Vec3 a{1.0, 2.0, 3.0}, b{4.0, -5.0, 6.0}; + CHECK_CLOSE(dot(a, b), 1*4 + 2*(-5) + 3*6, EPS); // 4 - 10 + 18 = 12 +} + +TEST_CASE("Vec3: cross product") { + Vec3 x{1,0,0}, y{0,1,0}, z{0,0,1}; + Vec3 c = cross(x, y); + CHECK_CLOSE(c[0], 0.0, EPS); CHECK_CLOSE(c[1], 0.0, EPS); CHECK_CLOSE(c[2], 1.0, EPS); + c = cross(y, z); + CHECK_CLOSE(c[0], 1.0, EPS); CHECK_CLOSE(c[1], 0.0, EPS); CHECK_CLOSE(c[2], 0.0, EPS); + c = cross(z, x); + CHECK_CLOSE(c[0], 0.0, EPS); CHECK_CLOSE(c[1], 1.0, EPS); CHECK_CLOSE(c[2], 0.0, EPS); + // cross product is anti-symmetric + Vec3 a{1,2,3}, b{4,5,6}; + Vec3 ab = cross(a, b), ba = cross(b, a); + CHECK_CLOSE(ab[0], -ba[0], EPS); CHECK_CLOSE(ab[1], -ba[1], EPS); CHECK_CLOSE(ab[2], -ba[2], EPS); + // a x a = 0 + Vec3 aa = cross(a, a); + CHECK_CLOSE(aa[0], 0.0, EPS); CHECK_CLOSE(aa[1], 0.0, EPS); CHECK_CLOSE(aa[2], 0.0, EPS); +} + +TEST_CASE("Vec3: sizeof layout") { + CHECK(sizeof(Vec3) == 3 * sizeof(double)); + CHECK(sizeof(Vec3) == 3 * sizeof(float)); +} + +// ============================================================ +// Mat3 +// ============================================================ + +TEST_CASE("Mat3: brace init and element access") { + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + CHECK_CLOSE(m[0][0], 1.0, EPS); CHECK_CLOSE(m[0][1], 2.0, EPS); CHECK_CLOSE(m[0][2], 3.0, EPS); + CHECK_CLOSE(m[1][0], 4.0, EPS); CHECK_CLOSE(m[1][1], 5.0, EPS); CHECK_CLOSE(m[1][2], 6.0, EPS); + CHECK_CLOSE(m[2][0], 7.0, EPS); CHECK_CLOSE(m[2][1], 8.0, EPS); CHECK_CLOSE(m[2][2], 9.0, EPS); +} + +TEST_CASE("Mat3: trace") { + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + CHECK_CLOSE(m.trace(), 15.0, EPS); // 1 + 5 + 9 +} + +TEST_CASE("Mat3: frobenius_norm_sq and frobenius_norm") { + Mat3 m{{{1,0,0},{0,0,0},{0,0,0}}}; + CHECK_CLOSE(m.frobenius_norm_sq(), 1.0, EPS); + CHECK_CLOSE(m.frobenius_norm(), 1.0, EPS); + + // identity + Mat3 I{{{1,0,0},{0,1,0},{0,0,1}}}; + CHECK_CLOSE(I.frobenius_norm_sq(), 3.0, EPS); +} + +TEST_CASE("Mat3: matvec") { + Mat3 I{{{1,0,0},{0,1,0},{0,0,1}}}; + Vec3 v{3,4,5}; + Vec3 r = I.matvec(v); + CHECK_CLOSE(r[0], 3.0, EPS); CHECK_CLOSE(r[1], 4.0, EPS); CHECK_CLOSE(r[2], 5.0, EPS); + + // non-trivial + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + Vec3 u{1,0,0}; + r = m.matvec(u); + CHECK_CLOSE(r[0], 1.0, EPS); CHECK_CLOSE(r[1], 4.0, EPS); CHECK_CLOSE(r[2], 7.0, EPS); + + r = m.matvec(v); + CHECK_CLOSE(r[0], 1*3+2*4+3*5, EPS); // 26 + CHECK_CLOSE(r[1], 4*3+5*4+6*5, EPS); // 62 + CHECK_CLOSE(r[2], 7*3+8*4+9*5, EPS); // 98 +} + +TEST_CASE("Mat3: Tmatvec (transpose-multiply)") { + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + Vec3 v{1,1,1}; + // M^T * v: column sums + Vec3 r = m.Tmatvec(v); + CHECK_CLOSE(r[0], 1+4+7, EPS); // 12 + CHECK_CLOSE(r[1], 2+5+8, EPS); // 15 + CHECK_CLOSE(r[2], 3+6+9, EPS); // 18 + + // For symmetric matrix, matvec == Tmatvec + Mat3 s{{{1,2,3},{2,5,6},{3,6,9}}}; + Vec3 u{1,2,3}; + Vec3 a = s.matvec(u), b = s.Tmatvec(u); + CHECK_CLOSE(a[0], b[0], EPS); CHECK_CLOSE(a[1], b[1], EPS); CHECK_CLOSE(a[2], b[2], EPS); +} + +TEST_CASE("Mat3: curl") { + // curl of (x, y, z) velocity field with gradient = identity -> curl = 0 + Mat3 I{{{1,0,0},{0,1,0},{0,0,1}}}; + Vec3 c = I.curl(); + CHECK_CLOSE(c[0], 0.0, EPS); CHECK_CLOSE(c[1], 0.0, EPS); CHECK_CLOSE(c[2], 0.0, EPS); + + // solid body rotation about z-axis: v = (-y, x, 0), grad = {{0,-1,0},{1,0,0},{0,0,0}} + // curl = (0-0, 0-0, 1-(-1)) = (0, 0, 2) + Mat3 rot{{{0,-1,0},{1,0,0},{0,0,0}}}; + c = rot.curl(); + CHECK_CLOSE(c[0], 0.0, EPS); CHECK_CLOSE(c[1], 0.0, EPS); CHECK_CLOSE(c[2], 2.0, EPS); +} + +TEST_CASE("Mat3: arithmetic operators") { + Mat3 a{{{1,2,3},{4,5,6},{7,8,9}}}; + Mat3 b{{{9,8,7},{6,5,4},{3,2,1}}}; + + Mat3 c = a + b; + CHECK_CLOSE(c[0][0], 10.0, EPS); CHECK_CLOSE(c[1][1], 10.0, EPS); CHECK_CLOSE(c[2][2], 10.0, EPS); + + Mat3 d = a - b; + CHECK_CLOSE(d[0][0], -8.0, EPS); CHECK_CLOSE(d[1][1], 0.0, EPS); CHECK_CLOSE(d[2][2], 8.0, EPS); + + Mat3 e = 2.0 * a; + CHECK_CLOSE(e[0][0], 2.0, EPS); CHECK_CLOSE(e[1][2], 12.0, EPS); + + Mat3 f = a * 2.0; + CHECK_CLOSE(f[0][0], 2.0, EPS); CHECK_CLOSE(f[1][2], 12.0, EPS); + + Mat3 g = a / 2.0; + CHECK_CLOSE(g[0][0], 0.5, EPS); CHECK_CLOSE(g[2][2], 4.5, EPS); +} + +TEST_CASE("Mat3: transpose") { + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + Mat3 mt = transpose(m); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK_CLOSE(mt[i][j], m[j][i], EPS); +} + +TEST_CASE("Mat3: sizeof layout") { + CHECK(sizeof(Mat3) == 9 * sizeof(double)); + CHECK(sizeof(Mat3) == 9 * sizeof(float)); +} + +// ============================================================ +// SymmetricTensor2 +// ============================================================ + +TEST_CASE("SymmetricTensor2: storage layout [xx,yy,zz,xy,yz,xz]") { + SymmetricTensor2 s{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + // data[0]=xx, data[1]=yy, data[2]=zz, data[3]=xy, data[4]=yz, data[5]=xz + CHECK_CLOSE(s.data[0], 1.0, EPS); // xx + CHECK_CLOSE(s.data[1], 2.0, EPS); // yy + CHECK_CLOSE(s.data[2], 3.0, EPS); // zz + CHECK_CLOSE(s.data[3], 4.0, EPS); // xy + CHECK_CLOSE(s.data[4], 5.0, EPS); // yz + CHECK_CLOSE(s.data[5], 6.0, EPS); // xz +} + +TEST_CASE("SymmetricTensor2: flat_index mapping") { + // Diagonals + CHECK(SymmetricTensor2::flat_index(0, 0) == 0); + CHECK(SymmetricTensor2::flat_index(1, 1) == 1); + CHECK(SymmetricTensor2::flat_index(2, 2) == 2); + // Off-diagonals (both orderings should give same index) + CHECK(SymmetricTensor2::flat_index(0, 1) == 3); + CHECK(SymmetricTensor2::flat_index(1, 0) == 3); + CHECK(SymmetricTensor2::flat_index(1, 2) == 4); + CHECK(SymmetricTensor2::flat_index(2, 1) == 4); + CHECK(SymmetricTensor2::flat_index(0, 2) == 5); + CHECK(SymmetricTensor2::flat_index(2, 0) == 5); +} + +TEST_CASE("SymmetricTensor2: [i][j] proxy access matches data layout") { + SymmetricTensor2 s{11.0, 22.0, 33.0, 12.0, 23.0, 13.0}; + // Diagonals + CHECK_CLOSE(s[0][0], 11.0, EPS); + CHECK_CLOSE(s[1][1], 22.0, EPS); + CHECK_CLOSE(s[2][2], 33.0, EPS); + // Off-diagonals — symmetric access + CHECK_CLOSE(s[0][1], 12.0, EPS); CHECK_CLOSE(s[1][0], 12.0, EPS); + CHECK_CLOSE(s[1][2], 23.0, EPS); CHECK_CLOSE(s[2][1], 23.0, EPS); + CHECK_CLOSE(s[0][2], 13.0, EPS); CHECK_CLOSE(s[2][0], 13.0, EPS); +} + +TEST_CASE("SymmetricTensor2: [i][j] write goes to correct storage") { + SymmetricTensor2 s{}; + s[0][0] = 1; s[1][1] = 2; s[2][2] = 3; + s[0][1] = 4; s[1][2] = 5; s[0][2] = 6; + CHECK_CLOSE(s.data[0], 1.0, EPS); + CHECK_CLOSE(s.data[1], 2.0, EPS); + CHECK_CLOSE(s.data[2], 3.0, EPS); + CHECK_CLOSE(s.data[3], 4.0, EPS); + CHECK_CLOSE(s.data[4], 5.0, EPS); + CHECK_CLOSE(s.data[5], 6.0, EPS); + // Writing via transposed index hits same storage + s[1][0] = 99.0; + CHECK_CLOSE(s.data[3], 99.0, EPS); // xy slot + CHECK_CLOSE(s[0][1], 99.0, EPS); // reading back via (0,1) +} + +TEST_CASE("SymmetricTensor2: const [i][j] access") { + const SymmetricTensor2 s{1,2,3,4,5,6}; + CHECK_CLOSE(s[0][0], 1.0, EPS); + CHECK_CLOSE(s[1][0], 4.0, EPS); + CHECK_CLOSE(s[0][1], 4.0, EPS); +} + +TEST_CASE("SymmetricTensor2: trace") { + SymmetricTensor2 s{1.0, 2.0, 3.0, 99.0, 99.0, 99.0}; + CHECK_CLOSE(s.trace(), 6.0, EPS); // 1 + 2 + 3 +} + +TEST_CASE("SymmetricTensor2: set_isotropic") { + SymmetricTensor2 s{99,99,99,99,99,99}; + s.set_isotropic(1.0 / 3.0); + CHECK_CLOSE(s[0][0], 1.0/3.0, EPS); + CHECK_CLOSE(s[1][1], 1.0/3.0, EPS); + CHECK_CLOSE(s[2][2], 1.0/3.0, EPS); + CHECK_CLOSE(s[0][1], 0.0, EPS); + CHECK_CLOSE(s[1][2], 0.0, EPS); + CHECK_CLOSE(s[0][2], 0.0, EPS); + CHECK_CLOSE(s.trace(), 1.0, EPS); +} + +TEST_CASE("SymmetricTensor2: operator*= and scalar * (both orderings)") { + SymmetricTensor2 s{1,2,3,4,5,6}; + s *= 2.0; + CHECK_CLOSE(s.data[0], 2.0, EPS); CHECK_CLOSE(s.data[3], 8.0, EPS); + + SymmetricTensor2 a{1,2,3,4,5,6}; + SymmetricTensor2 b = 3.0 * a; + SymmetricTensor2 c = a * 3.0; + for (int k = 0; k < 6; k++) { + CHECK_CLOSE(b.data[k], 3.0 * a.data[k], EPS); + CHECK_CLOSE(c.data[k], b.data[k], EPS); + } +} + +TEST_CASE("SymmetricTensor2: operator/= and /") { + SymmetricTensor2 s{2,4,6,8,10,12}; + s /= 2.0; + CHECK_CLOSE(s.data[0], 1.0, EPS); CHECK_CLOSE(s.data[5], 6.0, EPS); + + SymmetricTensor2 t = SymmetricTensor2{2,4,6,8,10,12} / 2.0; + for (int k = 0; k < 6; k++) CHECK_CLOSE(t.data[k], s.data[k], EPS); +} + +TEST_CASE("SymmetricTensor2: operator+= -= and binary + -") { + SymmetricTensor2 a{1,2,3,4,5,6}; + SymmetricTensor2 b{6,5,4,3,2,1}; + + SymmetricTensor2 c = a + b; + CHECK_CLOSE(c.data[0], 7.0, EPS); CHECK_CLOSE(c.data[5], 7.0, EPS); + + SymmetricTensor2 d = a - b; + CHECK_CLOSE(d.data[0], -5.0, EPS); CHECK_CLOSE(d.data[5], 5.0, EPS); + + a += b; + for (int k = 0; k < 6; k++) CHECK_CLOSE(a.data[k], c.data[k], EPS); + + a -= b; + CHECK_CLOSE(a.data[0], 1.0, EPS); CHECK_CLOSE(a.data[5], 6.0, EPS); +} + +TEST_CASE("SymmetricTensor2: frobenius_norm_sq counts off-diag twice") { + // Identity: diag = 3*(1^2) = 3, offdiag = 0 -> total = 3 + SymmetricTensor2 I{}; I.set_isotropic(1.0); + CHECK_CLOSE(I.frobenius_norm_sq(), 3.0, EPS); + + // Single off-diagonal element: s[0][1] = s[1][0] = 1, both contribute + SymmetricTensor2 s{0,0,0,1,0,0}; // only xy = 1 + CHECK_CLOSE(s.frobenius_norm_sq(), 2.0, EPS); // 1^2 counted twice + + // Full tensor: compare with explicit 3x3 sum + SymmetricTensor2 t{1,2,3,4,5,6}; + double expected = 0; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) { + double v = t[i][j]; + expected += v * v; + } + CHECK_CLOSE(t.frobenius_norm_sq(), expected, EPS); +} + +TEST_CASE("SymmetricTensor2: frobenius_norm") { + SymmetricTensor2 I{}; I.set_isotropic(1.0); + CHECK_CLOSE(I.frobenius_norm(), std::sqrt(3.0), EPS); +} + +TEST_CASE("SymmetricTensor2: matvec — identity") { + SymmetricTensor2 I{}; I.set_isotropic(1.0); + Vec3 v{3, 4, 5}; + Vec3 r = I.matvec(v); + CHECK_CLOSE(r[0], 3.0, EPS); CHECK_CLOSE(r[1], 4.0, EPS); CHECK_CLOSE(r[2], 5.0, EPS); +} + +TEST_CASE("SymmetricTensor2: matvec — general, matches brute-force [i][j]") { + SymmetricTensor2 s{1,2,3,4,5,6}; // xx=1,yy=2,zz=3,xy=4,yz=5,xz=6 + Vec3 v{7, 8, 9}; + Vec3 r = s.matvec(v); + + // brute-force reference + for (int i = 0; i < 3; i++) { + double ref = 0; + for (int j = 0; j < 3; j++) ref += s[i][j] * v[j]; + CHECK_CLOSE(r[i], ref, EPS); + } +} + +TEST_CASE("SymmetricTensor2: matvec is symmetric (v . S . w == w . S . v)") { + SymmetricTensor2 s{2,3,5,7,11,13}; + Vec3 v{1,2,3}, w{4,5,6}; + double vSw = dot(v, s.matvec(w)); + double wSv = dot(w, s.matvec(v)); + CHECK_CLOSE(vSw, wSv, EPS); +} + +TEST_CASE("SymmetricTensor2: outer_product storage order") { + Vec3 v{2.0, 3.0, 5.0}; + SymmetricTensor2 op = outer_product(v); + CHECK_CLOSE(op.data[0], 4.0, EPS); // xx = 2*2 + CHECK_CLOSE(op.data[1], 9.0, EPS); // yy = 3*3 + CHECK_CLOSE(op.data[2], 25.0, EPS); // zz = 5*5 + CHECK_CLOSE(op.data[3], 6.0, EPS); // xy = 2*3 + CHECK_CLOSE(op.data[4], 15.0, EPS); // yz = 3*5 + CHECK_CLOSE(op.data[5], 10.0, EPS); // xz = 2*5 +} + +TEST_CASE("SymmetricTensor2: outer_product [i][j] matches v[i]*v[j]") { + Vec3 v{2.0, 3.0, 5.0}; + SymmetricTensor2 op = outer_product(v); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK_CLOSE(op[i][j], v[i] * v[j], EPS); +} + +TEST_CASE("SymmetricTensor2: outer_product matvec gives (v.w)*v") { + Vec3 v{2.0, 3.0, 5.0}, w{7.0, 11.0, 13.0}; + SymmetricTensor2 op = outer_product(v); + Vec3 r = op.matvec(w); + double vdotw = dot(v, w); + CHECK_CLOSE(r[0], vdotw * v[0], EPS); + CHECK_CLOSE(r[1], vdotw * v[1], EPS); + CHECK_CLOSE(r[2], vdotw * v[2], EPS); +} + +TEST_CASE("SymmetricTensor2: double_contract with identity") { + SymmetricTensor2 s{1,2,3,4,5,6}; + Mat3 I{{{1,0,0},{0,1,0},{0,0,1}}}; + // S : I = trace(S) = 1+2+3 = 6 + CHECK_CLOSE(s.double_contract(I), 6.0, EPS); +} + +TEST_CASE("SymmetricTensor2: double_contract with general matrix") { + SymmetricTensor2 s{1,2,3,4,5,6}; // xx=1,yy=2,zz=3,xy=4,yz=5,xz=6 + Mat3 m{{{1,2,3},{4,5,6},{7,8,9}}}; + + // brute-force: sum_ij s[i][j] * m[i][j] + double expected = 0; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + expected += s[i][j] * m[i][j]; + CHECK_CLOSE(s.double_contract(m), expected, EPS); +} + +TEST_CASE("SymmetricTensor2: double_contract with symmetric M equals S:M = S:M^T") { + SymmetricTensor2 s{2,3,5,7,11,13}; + Mat3 M{{{1,4,7},{2,5,8},{3,6,9}}}; + // For symmetric S: S:M == S:M^T + Mat3 Mt = transpose(M); + CHECK_CLOSE(s.double_contract(M), s.double_contract(Mt), EPS); +} + +TEST_CASE("SymmetricTensor2: zero initialization") { + SymmetricTensor2 s{}; + for (int k = 0; k < 6; k++) CHECK_CLOSE(s.data[k], 0.0, EPS); + CHECK_CLOSE(s.trace(), 0.0, EPS); + CHECK_CLOSE(s.frobenius_norm(), 0.0, EPS); +} + +// ============================================================ +// Cross-type: SymmetricTensor2 matvec vs Mat3 matvec +// ============================================================ + +TEST_CASE("SymmetricTensor2 vs Mat3: matvec agrees for symmetric matrix") { + SymmetricTensor2 s{2,3,5,7,11,13}; + Mat3 m{}; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + m[i][j] = s[i][j]; + + Vec3 v{17, 19, 23}; + Vec3 rs = s.matvec(v); + Vec3 rm = m.matvec(v); + CHECK_CLOSE(rs[0], rm[0], EPS); + CHECK_CLOSE(rs[1], rm[1], EPS); + CHECK_CLOSE(rs[2], rm[2], EPS); +} + +// ---- Mat3 det and invert ---- + +TEST_CASE("Mat3: det of identity is 1") { + Mat3 I = {{{1,0,0},{0,1,0},{0,0,1}}}; + CHECK_CLOSE(I.det(), 1.0, EPS); +} + +TEST_CASE("Mat3: det of known matrix") { + Mat3 A = {{{1,2,3},{0,4,5},{1,0,6}}}; + // det = 1*(4*6-5*0) - 2*(0*6-5*1) + 3*(0*0-4*1) = 24+10-12 = 22 + CHECK_CLOSE(A.det(), 22.0, EPS); +} + +TEST_CASE("Mat3: invert of identity is identity") { + Mat3 I = {{{1,0,0},{0,1,0},{0,0,1}}}; + Mat3 Iinv; + double d = I.invert(Iinv); + CHECK_CLOSE(d, 1.0, EPS); + for(int i=0;i<3;i++) for(int j=0;j<3;j++) + CHECK_CLOSE(Iinv[i][j], (i==j ? 1.0 : 0.0), EPS); +} + +TEST_CASE("Mat3: A * A^{-1} = I") { + Mat3 A = {{{1,2,3},{0,4,5},{1,0,6}}}; + Mat3 Ainv; + double d = A.invert(Ainv); + CHECK(d != 0.0); + // multiply A * Ainv, check it's identity + for(int i=0;i<3;i++) for(int j=0;j<3;j++) { + double sum = 0; + for(int k=0;k<3;k++) sum += A[i][k]*Ainv[k][j]; + CHECK_CLOSE(sum, (i==j ? 1.0 : 0.0), 1e-12); + } +} + +TEST_CASE("Mat3: invert of singular matrix returns 0") { + Mat3 S = {{{1,2,3},{2,4,6},{1,1,1}}}; // row 1 = 2*row 0 + Mat3 Sinv; + double d = S.invert(Sinv); + CHECK_CLOSE(d, 0.0, EPS); +} + +TEST_MAIN() diff --git a/test/unit/test_unit.py b/test/unit/test_unit.py new file mode 100644 index 000000000..79c7bde6f --- /dev/null +++ b/test/unit/test_unit.py @@ -0,0 +1,39 @@ +"""C++ unit tests: discovers, compiles, and runs all test_*.cc files in this directory.""" + +import subprocess +import pytest +import tempfile +from pathlib import Path + +UNIT_DIR = Path(__file__).parent +GIZMO_ROOT = UNIT_DIR.parent.parent +CXX = "c++" +CXXFLAGS = ["-std=c++17", "-O2", "-Wall", "-Werror", f"-I{UNIT_DIR}", f"-I{GIZMO_ROOT}"] + + +def discover_tests(): + """Find all test_*.cc files in the unit test directory.""" + return sorted(UNIT_DIR.glob("test_*.cc")) + + +@pytest.fixture(params=discover_tests(), ids=lambda p: p.stem) +def compiled_test(request, tmp_path): + """Compile a C++ test file and return the path to the binary.""" + src = request.param + binary = tmp_path / src.stem + result = subprocess.run( + [CXX] + CXXFLAGS + [str(src), "-o", str(binary)], + capture_output=True, text=True, + ) + if result.returncode != 0: + pytest.fail(f"Compilation failed for {src.name}:\n{result.stderr}") + return binary, src.name + + +def test_unit(compiled_test): + """Run a compiled C++ unit test binary.""" + binary, name = compiled_test + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30) + output = result.stdout + result.stderr + if result.returncode != 0: + pytest.fail(f"{name} failed:\n{output}") diff --git a/test/zeldovich/Config.sh b/test/zeldovich/Config.sh new file mode 100644 index 000000000..61d93a2cf --- /dev/null +++ b/test/zeldovich/Config.sh @@ -0,0 +1,7 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +EOS_GAMMA=(5.0/3.0) +PMGRID=256 +ADAPTIVE_GRAVSOFT_FORGAS +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/zeldovich/test_zeldovich.py b/test/zeldovich/test_zeldovich.py new file mode 100644 index 000000000..878c7a264 --- /dev/null +++ b/test/zeldovich/test_zeldovich.py @@ -0,0 +1,86 @@ +"""Zeldovich pancake cosmological test (Hopkins 2015) + +Tests cosmological integration by evolving a single-mode density perturbation +(the Zeldovich pancake) and comparing against the exact solution at z=0. +The exact solution file has columns: x-position (Mpc), log10(rho/rho_mean), +log10(temperature), velocity (km/s). +""" + +import pytest +import numpy as np +from scipy.interpolate import interp1d +from scipy.stats import binned_statistic +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, clean_test_outputs, assert_final_time, default_mpi_ranks, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_zeldovich(num_mpi_ranks, num_omp_threads): + test_name = "zeldovich" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + # Find the last snapshot (cosmological runs use ScaleFac_Between_Snapshots) + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if not snaps: + raise RuntimeError("GIZMO did not run successfully.") + final_snap = snaps[-1] + assert_final_time(final_snap, test_name) + + # Load simulation data + with h5py.File(final_snap, "r") as F: + coords = F["PartType0/Coordinates"][:] + rho_sim = F["PartType0/Density"][:] + masses = F["PartType0/Masses"][:] + vel = F["PartType0/Velocities"][:] + boxsize = F["Header"].attrs["BoxSize"] + + # Exact solution x is in Mpc centered on 0; sim box is in kpc from 0 to BoxSize + # BoxSize = 64000 kpc = 64 Mpc. Pancake is at box center, so shift by half a box. + box_mpc = boxsize / 1000.0 + x_sim_mpc = (coords[:, 0] / 1000.0 + box_mpc / 2) % box_mpc - box_mpc / 2 + + # Normalize density by the mean cosmic density + rho_mean = masses.sum() / boxsize**3 + logrho_sim = np.log10(rho_sim / rho_mean) + + # Load exact solution: x (Mpc), log10(rho/rho_mean), log10(T), velocity + exact = np.loadtxt(f"test/{test_name}/zeldovich_exact.txt") + x_exact = exact[:, 0] + logrho_exact = exact[:, 1] + vel_exact = exact[:, 3] + + # Bin simulation data to match exact solution coordinate range + x_bins = np.linspace(x_exact.min(), x_exact.max(), 60) + logrho_binned = binned_statistic(x_sim_mpc, logrho_sim, "median", x_bins)[0] + vel_binned = binned_statistic(x_sim_mpc, vel[:, 0], "median", x_bins)[0] + x_centers = 0.5 * (x_bins[:-1] + x_bins[1:]) + + # Interpolate exact solution to bin centers + logrho_exact_interp = interp1d(x_exact, logrho_exact, bounds_error=False, fill_value="extrapolate")(x_centers) + vel_exact_interp = interp1d(x_exact, vel_exact, bounds_error=False, fill_value="extrapolate")(x_centers) + + # Plot + for label, binned, exact_vals in [ + ("LogDensity", logrho_binned, logrho_exact_interp), + ("Velocity", vel_binned, vel_exact_interp), + ]: + plt.figure() + plt.plot(x_centers, binned, "o", markersize=3, label="GIZMO") + plt.plot(x_centers, exact_vals, "-", color="red", label="Exact") + plt.xlabel("x (Mpc)") + plt.ylabel(label) + plt.legend() + plt.savefig(f"test/{test_name}/{label}.png") + plt.close() + + # Check density profile - exclude the sharp caustic peak (|x| < 2 Mpc) + # where numerical smoothing prevents exact agreement + good = np.isfinite(logrho_binned) & np.isfinite(logrho_exact_interp) & (np.abs(x_centers) > 2.0) + if np.any(good): + L1_logrho = np.nanmean(np.abs(logrho_binned[good] - logrho_exact_interp[good])) + assert L1_logrho < 0.3, f"Log density profile L1 error {L1_logrho:.4f} exceeds tolerance" diff --git a/test/zeldovich/zeldovich.params b/test/zeldovich/zeldovich.params new file mode 100644 index 000000000..fd2d7f5a6 --- /dev/null +++ b/test/zeldovich/zeldovich.params @@ -0,0 +1,52 @@ +% Zeldovich pancake cosmological test (Hopkins 2015) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% EOS_GAMMA=(5.0/3.0) +% PMGRID=256 +% ADAPTIVE_GRAVSOFT_FORGAS +% +InitCondFile zeldovich_ics +OutputDir output +TimeBegin 0.00990099 +TimeMax 1 +ComovingIntegrationOn 1 +Omega_Matter 1 +Omega_Lambda 0 +Omega_Baryon 1 +HubbleParam 1 +BoxSize 64000 +TimeOfFirstSnapshot 0.00990099 +ScaleFac_Between_Snapshots 1.05 +MaxSizeTimestep 0.01 +DesNumNgb 32 +MaxNumNgbDeviation 0.1 +MaxMemSize 3000 +UnitLength_in_cm 3.085678e+21 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +SofteningGas 100 +Softening_Type1 0 +Softening_Type2 0 +Softening_Type3 0 +Softening_Type4 0 +Softening_Type5 0 +SofteningGasMaxPhys 100 +Softening_Type1_MaxPhysLimit 0 +Softening_Type2_MaxPhysLimit 0 +Softening_Type3_MaxPhysLimit 0 +Softening_Type4_MaxPhysLimit 0 +Softening_Type5_MaxPhysLimit 0 +MinGasTemp 0.001 +CourantFac 0.2 +ArtCondConstant 0.25 +ViscosityAMin 0.025 +ViscosityAMax 2 +MinGasHsmlFractional 1 +ErrTolIntAccuracy 0.01 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +TimeBetStatistics 0.5 +ResubmitOn 0 +ResubmitCommand none +ErrTolTheta 0.7 diff --git a/test/zeldovich_mhd/Config.sh b/test/zeldovich_mhd/Config.sh new file mode 100644 index 000000000..0e150ac27 --- /dev/null +++ b/test/zeldovich_mhd/Config.sh @@ -0,0 +1,8 @@ +HYDRO_MESHLESS_FINITE_MASS +BOX_PERIODIC +MAGNETIC +MHD_B_SET_IN_PARAMS +PMGRID=128 +ADAPTIVE_GRAVSOFT_FORGAS +OUTPUT_IN_DOUBLEPRECISION +DEVELOPER_MODE diff --git a/test/zeldovich_mhd/test_zeldovich_mhd.py b/test/zeldovich_mhd/test_zeldovich_mhd.py new file mode 100644 index 000000000..e0608240f --- /dev/null +++ b/test/zeldovich_mhd/test_zeldovich_mhd.py @@ -0,0 +1,66 @@ +"""MHD Zeldovich pancake test (Hopkins & Raives 2016) + +Cosmological pancake collapse with an initial magnetic field. The field +should be compressed and amplified in the pancake. Checks density +structure and magnetic field amplification. +""" + +import pytest +import numpy as np +from matplotlib import pyplot as plt +import h5py +import glob +from gizmo.test import build_and_run_test, default_mpi_ranks, clean_test_outputs, assert_final_time, default_omp_threads + + +@pytest.mark.parametrize("num_mpi_ranks", (default_mpi_ranks(),)) +@pytest.mark.parametrize("num_omp_threads", (default_omp_threads(),)) +def test_zeldovich_mhd(num_mpi_ranks, num_omp_threads): + test_name = "zeldovich_mhd" + clean_test_outputs(test_name) + build_and_run_test(test_name, num_mpi_ranks, num_omp_threads) + + outputdir = f"test/{test_name}/output" + snaps = sorted(glob.glob(outputdir + "/snapshot_*.hdf5")) + if len(snaps) < 2: + raise RuntimeError("GIZMO did not run successfully.") + assert_final_time(snaps[-1], test_name) + + # Load initial and final snapshots + with h5py.File(snaps[1], "r") as F: + B0 = F["PartType0/MagneticField"][:] + rho0 = F["PartType0/Density"][:] + with h5py.File(snaps[-1], "r") as F: + Bf = F["PartType0/MagneticField"][:] + rho_f = F["PartType0/Density"][:] + pos_f = F["PartType0/Coordinates"][:] + mass_f = F["PartType0/Masses"][:] + boxsize = F["Header"].attrs["BoxSize"] + + # Plot density and |B| vs x + x = pos_f[:, 0] + Bmag = np.sqrt(np.sum(Bf**2, axis=1)) + fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + order = x.argsort() + axes[0].semilogy(x[order], rho_f[order], ".", markersize=1) + axes[0].set_xlabel("x") + axes[0].set_ylabel("Density") + axes[0].set_title("Density") + axes[1].semilogy(x[order], Bmag[order], ".", markersize=1) + axes[1].set_xlabel("x") + axes[1].set_ylabel("|B|") + axes[1].set_title("Magnetic Field") + plt.tight_layout() + plt.savefig(f"test/{test_name}/profiles.png", dpi=150) + plt.close() + + # Pancake should have formed: density contrast should be significant + rho_contrast = rho_f.max() / rho_f.min() + assert rho_contrast > 10, ( + f"Pancake did not form: density contrast = {rho_contrast:.1f}" + ) + + # Magnetic field should be amplified in the dense pancake + Bmag0_max = np.max(np.sqrt(np.sum(B0**2, axis=1))) + Bmagf_max = np.max(Bmag) + assert Bmagf_max > Bmag0_max, "Magnetic field should be amplified in the pancake" diff --git a/test/zeldovich_mhd/zeldovich_mhd.params b/test/zeldovich_mhd/zeldovich_mhd.params new file mode 100644 index 000000000..57b26f01a --- /dev/null +++ b/test/zeldovich_mhd/zeldovich_mhd.params @@ -0,0 +1,55 @@ +% MHD Zeldovich pancake test (Hopkins & Raives 2016) +% +% HYDRO_MESHLESS_FINITE_MASS +% BOX_PERIODIC +% MAGNETIC +% MHD_B_SET_IN_PARAMS +% PMGRID=128 +% ADAPTIVE_GRAVSOFT_FORGAS +% +% BiniY=0.05517 for weak-field, BiniY=3.395 for strong-field +% +InitCondFile zeldovich_mhd_ics +OutputDir output +TimeBegin 0.047619 +TimeMax 1 +ComovingIntegrationOn 1 +Omega_Matter 1 +Omega_Lambda 0 +Omega_Baryon 1 +HubbleParam 1 +BoxSize 1 +TimeOfFirstSnapshot 0.047619 +ScaleFac_Between_Snapshots 1.05 +MaxSizeTimestep 0.01 +DesNumNgb 32 +MaxNumNgbDeviation 0.1 +BiniX 0 +BiniY 0.05517 +BiniZ 0 +SofteningGas 0.001 +Softening_Type1 0 +Softening_Type2 0 +Softening_Type3 0 +Softening_Type4 0 +Softening_Type5 0 +SofteningGasMaxPhys 0.001 +Softening_Type1_MaxPhysLimit 0 +Softening_Type2_MaxPhysLimit 0 +Softening_Type3_MaxPhysLimit 0 +Softening_Type4_MaxPhysLimit 0 +Softening_Type5_MaxPhysLimit 0 +UnitLength_in_cm 3.085678e+24 +UnitMass_in_g 1.989e+43 +UnitVelocity_in_cm_per_s 100000 +ErrTolIntAccuracy 0.01 +CourantFac 0.2 +MaxRMSDisplacementFac 0.1 +ErrTolForceAcc 0.001 +ErrTolTheta 0.7 +TimeBetStatistics 0.5 +MaxMemSize 3000 +ResubmitOn 0 +ResubmitCommand none +DivBcleaningParabolicSigma 0.1 +DivBcleaningHyperbolicSigma 1.0 diff --git a/turb/turbulent_diffusion.h b/turb/turbulent_diffusion.h index 49ddd0d64..c1ec88e68 100644 --- a/turb/turbulent_diffusion.h +++ b/turb/turbulent_diffusion.h @@ -39,7 +39,7 @@ cmag = 0.0; double grad_dot_x_ij = 0.0; double Z_j = 0; if(k_species < NUM_METAL_SPECIES) {Z_j = P[j].Metallicity[k_species];} #if defined(GALSF_ISMDUSTCHEM_MODEL) - if(k_species >= NUM_METAL_SPECIES) {Z_j = return_ismdustchem_species_of_interest_for_diffusion_and_yields(j,k_species);} + if(k_species >= NUM_METAL_SPECIES) {Z_j = return_ismdustchem_species_of_interest_for_diffusion_and_yields(j,k_species,0);} #endif d_scalar = local.Metallicity[k_species]-Z_j; // physical for(k=0;k<3;k++)