From c93ce29c38c57f887feee2d4d1e13b16a93cad4a Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Thu, 27 Nov 2025 08:40:57 +0000 Subject: [PATCH 01/71] Adding dust growth and destruction --- src/clib/dust_growth_and_destruction.cpp | 0 src/clib/dust_growth_and_destruction.hpp | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/clib/dust_growth_and_destruction.cpp create mode 100644 src/clib/dust_growth_and_destruction.hpp diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp new file mode 100644 index 000000000..e69de29bb From e833ef1f5b0c891c93441a65621e75fec21cf016 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 2 Dec 2025 16:19:34 +0800 Subject: [PATCH 02/71] Adding parameters required --- src/clib/grackle_chemistry_data_fields.def | 8 ++++++++ src/include/grackle_chemistry_data.h | 9 +++++++++ src/include/grackle_types.h | 2 ++ 3 files changed, 19 insertions(+) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 7c2887df9..6fc6d82ab 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -333,3 +333,11 @@ ENTRY(omp_nthreads, INT, 0) * 3: Newton-Raphson-only */ ENTRY(solver_method, INT, 1) + +/* Li et al 2019 dust model parameters*/ +ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) +ENTRY(sne_coeff, DOUBLE, 1.0) +ENTRY(sne_shockspeed, DOUBLE, 1.0e2) +ENTRY(dust_grainsize, DOUBLE, 1.0e-1) +ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) +ENTRY(dust_growth_tauref, DOUBLE, 1.0) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 0881952b3..5c5a65eec 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,15 @@ typedef struct */ int solver_method; + /* flag and parameters for Li+ 2019 dust growth and destruction */ + double dust_destruction_eff; + double sne_coeff; + double sne_shockspeed; + double dust_grainsize; + double dust_growth_densref; + double dust_growth_tauref; + double SolarAbundances[NUM_METAL_SPECIES_GRACKLE]; + } chemistry_data; /***************************** diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index b764fd1fa..04c42f641 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -211,6 +211,8 @@ typedef struct gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; + // dust model parameter + gr_float *SNe_ThisTimeStep; } grackle_field_data; From 711a2d1a71fc9fa00c817eb91d49badc7ade5604 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 13:41:23 +0000 Subject: [PATCH 03/71] New dust model v1.0 --- src/clib/dust_growth_and_destruction.cpp | 193 ++++++++++++++++++++++ src/clib/dust_growth_and_destruction.hpp | 36 ++++ src/clib/field_data_misc_fdatamembers.def | 3 + src/include/grackle_chemistry_data.h | 1 - 4 files changed, 232 insertions(+), 1 deletion(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index e69de29bb..374d355b6 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include "dust_growth_and_destruction.hpp" + +namespace { + const double k_boltz = 1.3806504e-16; + const double m_proton = 1.67262171e-24; + const double pi_val = 3.141592653589793; + const double sec_per_year = 3.155e7; + + const double tiny_value = 1.0e-20; + const double huge_value = 1.0e+20; + + const double t_ref = 20; + +} + +// ========================================== +// DUST GROWTH (ACCRETION) +// ========================================== +void grackle::impl::dust_growth( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + double dens_proper = internalu.density_units * internalu.a_value**3; + double dt = dt_value; + double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; + + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal= metal(i,idx_range.j,idx_range.k); + + double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); + double tau_accr = huge_value; + tau_accr = std::min(tau_accr0*(my_chemistry->SolarMetalFractionByMass/rho_metal),tau_accr); + double total_density_init = rho_metal + rho_dust; + double dM = 0; + dM = dM + std::min((1 - rho_dust/total_density_init)*(rho_dust/tau_accr)*dt, (total_density_init - rho_dust)); + double dM_tau_accr = dM; + + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + + } +} + +// ========================================== +// 2. DUST DESTRUCTION (SNe + SPUTTERING) +// ========================================== +void grackle::impl::dust_destruction( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View sne( + my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + double dt = dt_value; + double dens_proper = internalu.density_units * internalu.a_value**3; + + double Ms100 = 6800.0 * my_chemistry->sne_coeff + * (100.0 / my_chemistry->sne_shockspeed) + * (100.0 / my_chemistry->sne_shockspeed) + * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double tau_dest = 0; + double sne_this = sne(i,idx_range.j,idx_range.k); + + double total_density_init = rho_metal + rho_dust; + double dM = 0; + + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + } + + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + double dM_shock = 0.0; + if (sne_this <= 0) { + dM_shock = 0.0; + } else { + dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); + } + if (dM_shock >= rho_dust) { + if (dM_shock > rho_dust) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; + dM_shock = std::min(dM_shock, rho_dust); + } + dM = dM - rho_dust * dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, "<< dM << std::endl; + } + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } +} diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index e69de29bb..6a1971177 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -0,0 +1,36 @@ +#ifndef DUST_GROWTH_AND_DESTRUCTION_HPP +#define DUST_GROWTH_AND_DESTRUCTION_HPP + +#include "grackle_types.h" +#include "grackle_chemistry_data.h" +#include "index_helper.h" +#include "internal_units.h" +#include "phys_constants.h" + +namespace grackle::impl { + +// Calculates and applies dust growth (accretion) onto grain surfaces. +void dust_growth( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust +); + +// Calculates and applies dust destruction from SNe shocks and thermal sputtering. +void dust_destruction( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust +); + +} + +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 775ffeec2..11d9d95cb 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -97,3 +97,6 @@ ENTRY(Al2O3_dust_temperature) ENTRY(ref_org_dust_temperature) ENTRY(vol_org_dust_temperature) ENTRY(H2O_ice_dust_temperature) + + // dust model parameter + ENTRY(SNe_ThisTimeStep) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 5c5a65eec..5dce8a8a2 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -319,7 +319,6 @@ typedef struct double dust_grainsize; double dust_growth_densref; double dust_growth_tauref; - double SolarAbundances[NUM_METAL_SPECIES_GRACKLE]; } chemistry_data; From e871a1f903f26a6824ec5510e622dad560af34d0 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 14:04:43 +0000 Subject: [PATCH 04/71] New dust model v1.1 --- src/clib/dust_growth_and_destruction.cpp | 6 ++---- src/clib/dust_growth_and_destruction.hpp | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 374d355b6..fa47ed942 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -26,8 +26,7 @@ void grackle::impl::dust_growth( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust) + double* t_gas) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -97,8 +96,7 @@ void grackle::impl::dust_destruction( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust) + double* t_gas) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 6a1971177..9411e4b61 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -16,8 +16,7 @@ void dust_growth( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust + double* t_gas ); // Calculates and applies dust destruction from SNe shocks and thermal sputtering. @@ -27,8 +26,7 @@ void dust_destruction( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust + double* t_gas ); } From a0ab547c126053880a0fc133abff11370c247c8f Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 18:17:36 +0000 Subject: [PATCH 05/71] New dust model v1.2 --- src/clib/CMakeLists.txt | 1 + src/clib/cool1d_multi_g.cpp | 7 ++-- src/clib/cool1d_multi_g.hpp | 3 +- src/clib/cool_multi_time_g.cpp | 4 +-- src/clib/dust_growth_and_destruction.cpp | 44 ++++++++++++++---------- src/clib/dust_growth_and_destruction.hpp | 10 +++--- src/clib/solve_rate_cool.cpp | 3 +- src/clib/time_deriv_0d.hpp | 4 ++- 8 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/clib/CMakeLists.txt b/src/clib/CMakeLists.txt index c9c16dc38..ef0aad20b 100644 --- a/src/clib/CMakeLists.txt +++ b/src/clib/CMakeLists.txt @@ -94,6 +94,7 @@ add_library(Grackle_Grackle auto_general.hpp # C++ Source (and Private Header Files) + dust_growth_and_destruction.cpp dust_growth_and_destruction.hpp calc_tdust_3d.cpp calc_tdust_3d.h calc_temp_cloudy_g.cpp calc_temp_cloudy_g.h calc_temp1d_cloudy_g.cpp calc_temp1d_cloudy_g.hpp diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 02cc901c5..2b3bca0e3 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -29,6 +29,7 @@ #include "inject_model/grain_metal_inject_pathways.hpp" #include "internal_types.hpp" #include "utils-cpp.hpp" +#include "dust_growth_and_destruction.hpp" void grackle::impl::cool1d_multi_g( int imetal, int iter, double* edot, double* tgas, double* mmw, double* p2d, @@ -40,7 +41,8 @@ void grackle::impl::cool1d_multi_g( grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf) { + grackle::impl::CoolHeatScratchBuf coolingheating_buf, + double* dtit) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1102,7 +1104,6 @@ void grackle::impl::cool1d_multi_g( itmask_metal[i] = MASK_FALSE; } } - // Compute grain size increment if ((my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0)) { @@ -1111,6 +1112,8 @@ void grackle::impl::cool1d_multi_g( my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, my_fields, internal_dust_prop_buf); + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, dtit, tgas, true); } // Calculate dust to gas ratio AND interstellar radiation field diff --git a/src/clib/cool1d_multi_g.hpp b/src/clib/cool1d_multi_g.hpp index 6359c8084..9335b7169 100644 --- a/src/clib/cool1d_multi_g.hpp +++ b/src/clib/cool1d_multi_g.hpp @@ -96,7 +96,8 @@ void cool1d_multi_g(int imetal, int iter, double* edot, double* tgas, grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf); + grackle::impl::CoolHeatScratchBuf coolingheating_buf, + double* dtit); }; // namespace grackle::impl diff --git a/src/clib/cool_multi_time_g.cpp b/src/clib/cool_multi_time_g.cpp index c94345ea5..383f7822d 100644 --- a/src/clib/cool_multi_time_g.cpp +++ b/src/clib/cool_multi_time_g.cpp @@ -70,7 +70,7 @@ void cool_multi_time_g( // used in a number of different internal routines. Sorting these into // additional structs (or leaving them free-standing) will become more // obvious as we transcribe more routines. - + std::vector dtit(my_fields->grid_dimension[0]); std::vector p2d(my_fields->grid_dimension[0]); std::vector tgas(my_fields->grid_dimension[0]); std::vector mmw(my_fields->grid_dimension[0]); @@ -107,7 +107,7 @@ void cool_multi_time_g( dust2gas.data(), rhoH.data(), itmask.data(), itmask_metal.data(), my_chemistry, my_rates, my_fields, my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, cool1dmulti_buf, - coolingheating_buf + coolingheating_buf, dtit.data() ); // Compute the cooling time on the slice diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index fa47ed942..6785d805a 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -3,6 +3,8 @@ #include #include #include "dust_growth_and_destruction.hpp" +#include "internal_types.hpp" +#include "utils-cpp.hpp" namespace { const double k_boltz = 1.3806504e-16; @@ -25,8 +27,9 @@ void grackle::impl::dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas) + double* dt_value, + double* t_gas, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -38,8 +41,7 @@ void grackle::impl::dust_growth( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dens_proper = internalu.density_units * internalu.a_value**3; - double dt = dt_value; + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; @@ -47,9 +49,10 @@ void grackle::impl::dust_growth( for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { double rho_gas = d(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); double tau_accr = huge_value; @@ -79,11 +82,11 @@ void grackle::impl::dust_growth( if (std::abs(total_density_final - total_density_init) > 1e-8){ std::exit(21); } - - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; - + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } } } @@ -95,8 +98,9 @@ void grackle::impl::dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas) + double* dt_value, + double* t_gas, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -111,8 +115,7 @@ void grackle::impl::dust_destruction( my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dt = dt_value; - double dens_proper = internalu.density_units * internalu.a_value**3; + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double Ms100 = 6800.0 * my_chemistry->sne_coeff * (100.0 / my_chemistry->sne_shockspeed) @@ -123,11 +126,12 @@ void grackle::impl::dust_destruction( for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { double rho_gas = d(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal = metal(i,idx_range.j,idx_range.k); - double tau_dest = 0; double sne_this = sne(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; double total_density_init = rho_metal + rho_dust; double dM = 0; @@ -184,8 +188,10 @@ void grackle::impl::dust_destruction( std::exit(21); } - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } } } diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 9411e4b61..ccff2e512 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -15,8 +15,9 @@ void dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas + double* dt_value, + double* t_gas, + bool dryrun ); // Calculates and applies dust destruction from SNe shocks and thermal sputtering. @@ -25,8 +26,9 @@ void dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas + double* dt_value, + double* t_gas, + bool dryrun ); } diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 3c9d95c23..626d51e6c 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -815,7 +815,8 @@ int solve_rate_cool( *my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, - cool1dmulti_buf, coolingheating_buf + cool1dmulti_buf, coolingheating_buf, + dtit.data() ); if (my_chemistry->primordial_chemistry > 0) { diff --git a/src/clib/time_deriv_0d.hpp b/src/clib/time_deriv_0d.hpp index 5cc31592b..2bd8423bc 100644 --- a/src/clib/time_deriv_0d.hpp +++ b/src/clib/time_deriv_0d.hpp @@ -108,6 +108,7 @@ void drop_MainScratchBuf(MainScratchBuf* ptr) { /// MainScratchBuf and track pointers to previously allocated memory buffer /// for all cases struct Assorted1ElemBuf { + double dtit[1]; double p2d[1]; double tgas[1]; double tdust[1]; @@ -486,7 +487,8 @@ void derivatives( my_uvb_rates, internalu, pack.idx_range_1_element, pack.main_scratch_buf.grain_temperatures, pack.main_scratch_buf.logTlininterp_buf, pack.main_scratch_buf.cool1dmulti_buf, - pack.main_scratch_buf.coolingheating_buf + pack.main_scratch_buf.coolingheating_buf, + pack.other_scratch_buf.dtit ); } From b01c142c52f357a8d43c88ae93e1437a9d696520 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Mon, 19 Jan 2026 10:56:19 +0000 Subject: [PATCH 06/71] rebasing --- src/clib/cool1d_multi_g.cpp | 20 ++-- src/clib/cool1d_multi_g.hpp | 3 +- src/clib/cool_multi_time_g.cpp | 3 +- src/clib/dust_growth_and_destruction.cpp | 118 ++++++++++++++------- src/clib/dust_growth_and_destruction.hpp | 3 + src/clib/grackle_chemistry_data_fields.def | 2 +- src/clib/solve_rate_cool.cpp | 10 +- src/clib/time_deriv_0d.hpp | 4 +- src/python/examples/cooling_cell.py | 84 +++++++++++++-- 9 files changed, 179 insertions(+), 68 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 2b3bca0e3..abf41be51 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -41,8 +41,7 @@ void grackle::impl::cool1d_multi_g( grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf, - double* dtit) { + grackle::impl::CoolHeatScratchBuf coolingheating_buf) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1105,16 +1104,12 @@ void grackle::impl::cool1d_multi_g( } } // Compute grain size increment - if ((my_chemistry->use_dust_density_field > 0) && - (my_chemistry->dust_species > 0)) { - grackle::impl::calc_grain_size_increment_1d( - dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, my_fields, - internal_dust_prop_buf); - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, dtit, tgas, true); - } + // if ((my_chemistry->use_dust_density_field > 0) && + // (my_chemistry->dust_species > 0)) { + // grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( + // dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + // internal_dust_prop_buf); + // } // Calculate dust to gas ratio AND interstellar radiation field // -> an earlier version of this logic would store values @ indices @@ -1939,6 +1934,7 @@ void grackle::impl::cool1d_multi_g( } } + // Free memory grackle::impl::drop_InternalDustPropBuf(&internal_dust_prop_buf); grackle::impl::drop_GrainSpeciesCollection(&grain_kappa); diff --git a/src/clib/cool1d_multi_g.hpp b/src/clib/cool1d_multi_g.hpp index 9335b7169..6359c8084 100644 --- a/src/clib/cool1d_multi_g.hpp +++ b/src/clib/cool1d_multi_g.hpp @@ -96,8 +96,7 @@ void cool1d_multi_g(int imetal, int iter, double* edot, double* tgas, grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf, - double* dtit); + grackle::impl::CoolHeatScratchBuf coolingheating_buf); }; // namespace grackle::impl diff --git a/src/clib/cool_multi_time_g.cpp b/src/clib/cool_multi_time_g.cpp index 383f7822d..18a82f82d 100644 --- a/src/clib/cool_multi_time_g.cpp +++ b/src/clib/cool_multi_time_g.cpp @@ -70,7 +70,6 @@ void cool_multi_time_g( // used in a number of different internal routines. Sorting these into // additional structs (or leaving them free-standing) will become more // obvious as we transcribe more routines. - std::vector dtit(my_fields->grid_dimension[0]); std::vector p2d(my_fields->grid_dimension[0]); std::vector tgas(my_fields->grid_dimension[0]); std::vector mmw(my_fields->grid_dimension[0]); @@ -107,7 +106,7 @@ void cool_multi_time_g( dust2gas.data(), rhoH.data(), itmask.data(), itmask_metal.data(), my_chemistry, my_rates, my_fields, my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, cool1dmulti_buf, - coolingheating_buf, dtit.data() + coolingheating_buf ); // Compute the cooling time on the slice diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 6785d805a..ecff9acd2 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -27,6 +27,7 @@ void grackle::impl::dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun) @@ -47,46 +48,87 @@ void grackle::impl::dust_growth( // --- MAIN LOOP --- for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal= metal(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - - double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); - double tau_accr = huge_value; - tau_accr = std::min(tau_accr0*(my_chemistry->SolarMetalFractionByMass/rho_metal),tau_accr); - double total_density_init = rho_metal + rho_dust; - double dM = 0; - dM = dM + std::min((1 - rho_dust/total_density_init)*(rho_dust/tau_accr)*dt, (total_density_init - rho_dust)); - double dM_tau_accr = dM; - - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal= metal(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + // fprintf(stderr,"---------------\n"); + + double tau_accr0 = tau_ref * + (my_chemistry->dust_growth_densref / dens_proper) * + std::pow(t_ref / temp, 0.5); + double rho_metal_eff = std::max(rho_metal, tiny_value); + double tau_accr = tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); + tau_accr = std::min(tau_accr, huge_value); + tau_accr = std::max(tau_accr, tiny_value); + double frac_metal_available = 0.0; + if (rho_metal <= 0.0) { + frac_metal_available = 0.0; + } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { + frac_metal_available = rho_metal / rho_dust; + } else { + frac_metal_available = rho_metal / (rho_dust + rho_metal); + } + frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); + double growth = frac_metal_available * (rho_dust / tau_accr) * dt; + double dM = std::min(growth, rho_metal); + + // fprintf(stderr, + // "internal: frac=%e growth=%e dM=%e grainsize=%e\n", + // frac_metal_available, growth, dM, my_chemistry->dust_grainsize); + + double dM_tau_accr = dM; + + + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + long double change = rho_gas; + long double dM_change = (long double)dM; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + // fprintf(stderr, + // "after dM calc dust=%e gas=%e metal=%e change=%0.18Le\n", + // rho_dust, + // rho_gas, + // rho_metal, + // (change - dM_change)/change); + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + // double total_density_final = rho_metal + rho_dust; + // if (std::abs(total_density_final - total_density_init) > 1e-8){ + // std::exit(21); + // } + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + // fprintf(stderr,"------\n"); + // fprintf(stderr, + // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", + // rho_dust, + // rho_gas, + // rho_metal); + // fprintf(stderr, + // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", + // dust(i, idx_range.j, idx_range.k), + // d(i, idx_range.j, idx_range.k), + // metal(i, idx_range.j, idx_range.k)); + } } + } } diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index ccff2e512..7d9654ac0 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -6,6 +6,8 @@ #include "index_helper.h" #include "internal_units.h" #include "phys_constants.h" +#include "fortran_func_decls.h" + namespace grackle::impl { @@ -15,6 +17,7 @@ void dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 6fc6d82ab..92074f2e2 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -340,4 +340,4 @@ ENTRY(sne_coeff, DOUBLE, 1.0) ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) -ENTRY(dust_growth_tauref, DOUBLE, 1.0) +ENTRY(dust_growth_tauref, DOUBLE, 0.01) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 626d51e6c..57edc930c 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -37,6 +37,7 @@ #include "cool1d_multi_g.hpp" #include "scale_fields.hpp" #include "solve_rate_cool.hpp" +#include "dust_growth_and_destruction.hpp" /// overrides the subcycle timestep (for each index in the index-range that is /// selected by the given itmask) with the maximum allowed heating/cooling @@ -815,8 +816,7 @@ int solve_rate_cool( *my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, - cool1dmulti_buf, coolingheating_buf, - dtit.data() + cool1dmulti_buf, coolingheating_buf ); if (my_chemistry->primordial_chemistry > 0) { @@ -926,6 +926,9 @@ int solve_rate_cool( } + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), false); + // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row ttmin = huge8; @@ -940,6 +943,9 @@ int solve_rate_cool( // If all cells are done (in idx_range), break out of subcycle loop if (std::fabs(dt-ttmin) < tolerance*dt) { break; } + // grackle::impl::dust_growth( + // my_chemistry, my_fields, internalu, idx_range, dtit.data(), tgas.data(), false); + } // subcycle iteration loop (for current idx_range) // review number of iterations that were spent in the subcycle loop diff --git a/src/clib/time_deriv_0d.hpp b/src/clib/time_deriv_0d.hpp index 2bd8423bc..5cc31592b 100644 --- a/src/clib/time_deriv_0d.hpp +++ b/src/clib/time_deriv_0d.hpp @@ -108,7 +108,6 @@ void drop_MainScratchBuf(MainScratchBuf* ptr) { /// MainScratchBuf and track pointers to previously allocated memory buffer /// for all cases struct Assorted1ElemBuf { - double dtit[1]; double p2d[1]; double tgas[1]; double tdust[1]; @@ -487,8 +486,7 @@ void derivatives( my_uvb_rates, internalu, pack.idx_range_1_element, pack.main_scratch_buf.grain_temperatures, pack.main_scratch_buf.logTlininterp_buf, pack.main_scratch_buf.cool1dmulti_buf, - pack.main_scratch_buf.coolingheating_buf, - pack.other_scratch_buf.dtit + pack.main_scratch_buf.coolingheating_buf ); } diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index a6b9dd045..8da0affe3 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,25 +66,93 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 0.1 # Solar + metallicity = 0.01 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} my_chemistry = chemistry_data() my_chemistry.use_grackle = 1 - my_chemistry.with_radiative_cooling = 1 - my_chemistry.primordial_chemistry = 0 - my_chemistry.metal_cooling = 1 - my_chemistry.UVbackground = 1 my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "CloudyData_UVB=HM2012.h5") + os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=4, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=1, + multi_metals=0, + metal_abundances=0, + dust_species=3, + # dust_temperature_multi=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + HydrogenFractionByMass=0.76, + DeuteriumToHydrogenRatio=6.8e-05, + SolarMetalFractionByMass=0.01295, + local_dust_to_gas_ratio=0.009387, + NumberOfTemperatureBins=600, + CaseBRecombination=1, + TemperatureStart=1.0, + TemperatureEnd=1.0e9, + NumberOfDustTemperatureBins=250, + DustTemperatureStart=1.0, + DustTemperatureEnd=1500.0, + Compton_xray_heating=0, + LWbackground_sawtooth_suppression=0, + LWbackground_intensity=0, + UVbackground_redshift_on=-99999, + UVbackground_redshift_off=-99999, + UVbackground_redshift_fullon=-99999, + UVbackground_redshift_drop=-99999, + cloudy_electron_fraction_factor=0.00915396, + use_radiative_transfer=1, + radiative_transfer_coupled_rate_solver=0, + radiative_transfer_intermediate_step=0, + radiative_transfer_hydrogen_only=0, + self_shielding_method=0, + H2_custom_shielding=0, + H2_self_shielding=0, + radiative_transfer_H2II_diss=1, + radiative_transfer_HDI_dissociation=1, + radiative_transfer_metal_ionization=1, + radiative_transfer_metal_dissociation=1, + + use_multiple_dust_temperatures=1 + ) output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 1e6 # K + temperature = 50000 # K final_time = 100. # Myr # Set units @@ -121,4 +189,4 @@ def main(args=None): if __name__ == '__main__': - sys.exit(main()) + sys.exit(main()) \ No newline at end of file From 3a6a9affefd734e17e71d3a907e24fdaa01e190d Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Thu, 29 Jan 2026 00:18:53 +0000 Subject: [PATCH 07/71] saving changes before rebase --- src/clib/dust_growth_and_destruction.cpp | 177 ++++++++++-------- src/clib/dust_growth_and_destruction.hpp | 12 ++ src/clib/grackle_chemistry_data_fields.def | 4 +- src/clib/solve_rate_cool.cpp | 2 - src/python/examples/cooling_cell.py | 15 +- src/python/gracklepy/utilities/convenience.py | 3 + 6 files changed, 121 insertions(+), 92 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index ecff9acd2..5230d8adb 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -55,7 +55,6 @@ void grackle::impl::dust_growth( double rho_metal= metal(i,idx_range.j,idx_range.k); double temp = t_gas[i]; double dt = dt_value[i]; - // fprintf(stderr,"---------------\n"); double tau_accr0 = tau_ref * (my_chemistry->dust_growth_densref / dens_proper) * @@ -76,9 +75,6 @@ void grackle::impl::dust_growth( double growth = frac_metal_available * (rho_dust / tau_accr) * dt; double dM = std::min(growth, rho_metal); - // fprintf(stderr, - // "internal: frac=%e growth=%e dM=%e grainsize=%e\n", - // frac_metal_available, growth, dM, my_chemistry->dust_grainsize); double dM_tau_accr = dM; @@ -104,6 +100,10 @@ void grackle::impl::dust_growth( // rho_metal, // (change - dM_change)/change); rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + + // fprintf(stderr, + // "internal: frac=%e growth=%e dM=%e grainsize=%e gas=%e dust=%e metal=%e\n", + // frac_metal_available, growth, dM, my_chemistry->dust_grainsize, rho_gas, rho_dust, rho_metal); if (rho_dust < 0) { std::exit(21); } @@ -113,19 +113,10 @@ void grackle::impl::dust_growth( // } if (dryrun == false) { dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - // fprintf(stderr,"------\n"); - // fprintf(stderr, - // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", - // rho_dust, - // rho_gas, - // rho_metal); - // fprintf(stderr, - // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", - // dust(i, idx_range.j, idx_range.k), - // d(i, idx_range.j, idx_range.k), - // metal(i, idx_range.j, idx_range.k)); + + } } @@ -140,6 +131,7 @@ void grackle::impl::dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun) @@ -166,74 +158,99 @@ void grackle::impl::dust_destruction( // --- MAIN LOOP --- for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); - double sne_this = sne(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - double tau_dest = 0; - - double total_density_init = rho_metal + rho_dust; - double dM = 0; - - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; - } + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double sne_this = sne(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - double dM_shock = 0.0; - if (sne_this <= 0) { - dM_shock = 0.0; - } else { - dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); - } - if (dM_shock >= rho_dust) { - if (dM_shock > rho_dust) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + double total_density_init = rho_metal + rho_dust; + double dM = 0; + + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; - dM_shock = std::min(dM_shock, rho_dust); - } - dM = dM - rho_dust * dM_shock; - if (std::isnan(dM)) { - std::cout << "dM calculated as NaN, "<< dM << std::endl; - } - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + double dM_shock = 0.0; + if (sne_this <= 0) { + dM_shock = 0.0; + } else { + dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); + } + if (dM_shock >= rho_dust) { + if (dM_shock > rho_dust) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; + dM_shock = std::min(dM_shock, rho_dust); + } + dM = dM - rho_dust * dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, "<< dM << std::endl; + } + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + } } } } + +void grackle::impl::dust_update( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + const gr_mask_type* itmask, + double growth_rate, + double* dt) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + // recalculate metallicity + +} \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 7d9654ac0..293b193b1 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -29,11 +29,23 @@ void dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun ); +// Update the density field. +void dust_update( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + const gr_mask_type* itmask, + double growth_rate, + double*dt_value +); + } #endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 92074f2e2..c3a8e4e22 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,7 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 1) +ENTRY(solver_method, INT, 3) /* Li et al 2019 dust model parameters*/ ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) @@ -340,4 +340,4 @@ ENTRY(sne_coeff, DOUBLE, 1.0) ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) -ENTRY(dust_growth_tauref, DOUBLE, 0.01) +ENTRY(dust_growth_tauref, DOUBLE, 0.004) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 57edc930c..12a7600f5 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -943,8 +943,6 @@ int solve_rate_cool( // If all cells are done (in idx_range), break out of subcycle loop if (std::fabs(dt-ttmin) < tolerance*dt) { break; } - // grackle::impl::dust_growth( - // my_chemistry, my_fields, internalu, idx_range, dtit.data(), tgas.data(), false); } // subcycle iteration loop (for current idx_range) diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index 8da0affe3..b12c45760 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,7 +66,7 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 0.01 # Solar + metallicity = 10**-3 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} @@ -87,7 +87,7 @@ def set_opts(obj, **kwargs): my_chemistry, with_radiative_cooling=1, - primordial_chemistry=4, + primordial_chemistry=1, dust_chemistry = 1, metal_cooling=1, UVbackground=0, @@ -95,11 +95,11 @@ def set_opts(obj, **kwargs): Gamma=1.66667, h2_on_dust=1, use_dust_density_field=1, - metal_chemistry=1, + metal_chemistry=0, multi_metals=0, metal_abundances=0, - dust_species=3, - # dust_temperature_multi=0, + dust_species=0, + use_multiple_dust_temperatures=0, dust_sublimation=1, grain_growth=0, photoelectric_heating=0, @@ -143,16 +143,15 @@ def set_opts(obj, **kwargs): radiative_transfer_H2II_diss=1, radiative_transfer_HDI_dissociation=1, radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1, + radiative_transfer_metal_dissociation=1 - use_multiple_dust_temperatures=1 ) output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 50000 # K + temperature = 15000 # K final_time = 100. # Myr # Set units diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 177e2b4f9..62b5a4e59 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -34,6 +34,9 @@ def check_convergence(fc1, fc2, fields=None, tol=0.01): for field in fields: if field not in fc2: continue + if fc1[field] == 0: + print(field) + print(fc2[field]) convergence = np.max(np.abs(fc1[field] - fc2[field]) / fc1[field]) if convergence > max_val: max_val = convergence From 28d8477cac59802a37328305b06d010d2acf4eee Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Mon, 16 Feb 2026 22:54:19 +0800 Subject: [PATCH 08/71] adding dust into d(gas) v0.0 --- src/clib/calc_tdust_3d.cpp | 3 +- src/clib/cool1d_multi_g.cpp | 2 +- src/clib/dust_growth_and_destruction.cpp | 195 +++++++++--------- src/clib/dust_growth_and_destruction.hpp | 18 +- src/clib/field_data_misc_fdatamembers.def | 2 +- src/clib/grackle_chemistry_data_fields.def | 5 +- src/clib/make_consistent.cpp | 11 +- src/clib/solve_rate_cool.cpp | 18 +- src/include/grackle_chemistry_data.h | 3 + src/include/grackle_types.h | 4 +- src/python/examples/cooling_cell.py | 71 +++---- src/python/gracklepy/fluid_container.py | 2 + src/python/gracklepy/grackle_defs.pxd | 1 + src/python/gracklepy/grackle_wrapper.pyx | 2 + src/python/gracklepy/utilities/convenience.py | 12 +- src/python/gracklepy/utilities/evolve.py | 16 +- 16 files changed, 206 insertions(+), 159 deletions(-) diff --git a/src/clib/calc_tdust_3d.cpp b/src/clib/calc_tdust_3d.cpp index 2035b424d..dfce8d534 100644 --- a/src/clib/calc_tdust_3d.cpp +++ b/src/clib/calc_tdust_3d.cpp @@ -209,7 +209,8 @@ void calc_tdust_3d_g( // endif if (my_chemistry->use_dust_density_field > 0) { - dust2gas[i] = dust(i,j,k) / d(i,j,k); + dust2gas[i] = dust(i,j,k) / (d(i,j,k)); + // dust2gas[i] = dust(i,j,k) / (d(i,j,k) - dust(i,j,k)); } else { dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; } diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index abf41be51..cb656cbea 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1128,7 +1128,7 @@ void grackle::impl::cool1d_multi_g( if (itmask[i] != MASK_FALSE) { // it may be faster to remove this branching dust2gas[i] = dust(i, idx_range.j, idx_range.k) / - d(i, idx_range.j, idx_range.k); + (d(i, idx_range.j, idx_range.k)); } } } else { diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 5230d8adb..b9f1f8eda 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -30,7 +30,7 @@ void grackle::impl::dust_growth( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun) + double* growth_dM) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -41,15 +41,18 @@ void grackle::impl::dust_growth( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; // --- MAIN LOOP --- - for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + growth_dM[i] = 0.0; + if (itmask[i] != MASK_FALSE) { - + double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); @@ -72,52 +75,15 @@ void grackle::impl::dust_growth( frac_metal_available = rho_metal / (rho_dust + rho_metal); } frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); - double growth = frac_metal_available * (rho_dust / tau_accr) * dt; - double dM = std::min(growth, rho_metal); - - - double dM_tau_accr = dM; + double growth_rate = frac_metal_available * (rho_dust / tau_accr); + double dM = std::min(growth_rate, rho_metal/dt); + // Store the calculated mass change in the output array + growth_dM[i] = dM; - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - long double change = rho_gas; - long double dM_change = (long double)dM; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - // fprintf(stderr, - // "after dM calc dust=%e gas=%e metal=%e change=%0.18Le\n", - // rho_dust, - // rho_gas, - // rho_metal, - // (change - dM_change)/change); - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - // fprintf(stderr, - // "internal: frac=%e growth=%e dM=%e grainsize=%e gas=%e dust=%e metal=%e\n", - // frac_metal_available, growth, dM, my_chemistry->dust_grainsize, rho_gas, rho_dust, rho_metal); - if (rho_dust < 0) { - std::exit(21); - } - // double total_density_final = rho_metal + rho_dust; - // if (std::abs(total_density_final - total_density_init) > 1e-8){ - // std::exit(21); - // } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - - - } + // "internal: frac=%.10e growth_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", + // frac_metal_available, dM, rho_gas, rho_dust, rho_metal); } } @@ -134,7 +100,7 @@ void grackle::impl::dust_destruction( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun) + double* destruction_dM) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -145,8 +111,10 @@ void grackle::impl::dust_destruction( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( - my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], + use_sne ? my_fields->sne_rate : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); double dens_proper = internalu.urho * std::pow(internalu.a_value,3); @@ -157,77 +125,59 @@ void grackle::impl::dust_destruction( * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); // --- MAIN LOOP --- - for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + destruction_dM[i] = 0.0; + if (itmask[i] != MASK_FALSE) { double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal = metal(i,idx_range.j,idx_range.k); - double sne_this = sne(i,idx_range.j,idx_range.k); + double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; double tau_dest = 0; - double total_density_init = rho_metal + rho_dust; double dM = 0; + double dM_shock = 0.0; - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + if (use_sne) { + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + // dM_shock = 0.0; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + dM_shock = std::min(rho_dust/tau_dest, rho_dust/dt); + } } // destruction by thermal sputtering double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) * (std::pow((2.0e6/temp),2.5)+1.0); - double dM_shock = 0.0; - if (sne_this <= 0) { - dM_shock = 0.0; - } else { - dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); - } - if (dM_shock >= rho_dust) { - if (dM_shock > rho_dust) { + if (dM_shock >= rho_dust/dt) { + if (dM_shock > rho_dust/dt) { std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; } } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; - dM_shock = std::min(dM_shock, rho_dust); + dM_shock = dM_shock + rho_dust / tau_sput *3.0; + dM_shock = std::min(dM_shock, rho_dust/dt); } - dM = dM - rho_dust * dM_shock; + //dM = - rho_dust * dM_shock; + dM = -dM_shock; if (std::isnan(dM)) { std::cout << "dM calculated as NaN, "<< dM << std::endl; } - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - } + // Store the calculated mass change in the output array + destruction_dM[i] = dM; + // fprintf(stderr, + // "internal: tau_dest=%.10e tau_dest=%.10e dM_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", + // tau_dest, tau_sput, dM, rho_gas, rho_dust, rho_metal); } } } @@ -238,8 +188,10 @@ void grackle::impl::dust_update( InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - double growth_rate, - double* dt) + double* dt_value, + double* growth_dM, + double* destruction_dM, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -251,6 +203,53 @@ void grackle::impl::dust_update( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - // recalculate metallicity + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] != MASK_FALSE) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double dt = dt_value[i]; + + // Get the total mass change (growth + destruction) + + double dM_total = growth_dM[i] + destruction_dM[i]; + dM_total = dM_total * dt; + // Apply constraints to dM + dM_total = std::max(-1*rho_dust, dM_total); + dM_total = std::min(0.9*rho_metal, dM_total); + + // Apply conservation logic (from original code) + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM_total; + rho_metal = rho_metal - dM_total; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + + // Adjust gas density to conserve total mass + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Should be changed to gas_density staying constant (make_consistent.cpp) + + // Safety checks + if (rho_dust < 0) { + fprintf(stderr, "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, rho_dust); + std::exit(21); + } + + fprintf(stderr, + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e\n", + dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal); + + // Update the fields + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + } + } + } } \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 293b193b1..29eb18390 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -11,7 +11,8 @@ namespace grackle::impl { -// Calculates and applies dust growth (accretion) onto grain surfaces. +// Calculates dust growth rates (accretion) onto grain surfaces. +// Stores the mass change dM for each cell in growth_dM array. void dust_growth( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -20,10 +21,11 @@ void dust_growth( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun + double* growth_dM // output: mass change rate for each cell ); -// Calculates and applies dust destruction from SNe shocks and thermal sputtering. +// Calculates dust destruction rates from SNe shocks and thermal sputtering. +// Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -32,18 +34,20 @@ void dust_destruction( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun + double* destruction_dM // output: mass change rate for each cell ); -// Update the density field. +// Update the density fields using calculated mass changes. void dust_update( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - double growth_rate, - double*dt_value + double* dt_value, + double* growth_dM, // input: mass change from growth + double* destruction_dM, // input: mass change from destruction + bool dryrun ); } diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 11d9d95cb..870eb107a 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -99,4 +99,4 @@ ENTRY(vol_org_dust_temperature) ENTRY(H2O_ice_dust_temperature) // dust model parameter - ENTRY(SNe_ThisTimeStep) + ENTRY(sne_rate) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index c3a8e4e22..ba6efd457 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,10 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 3) +ENTRY(solver_method, INT, 2) + +/* Flag to use snetimestep */ +ENTRY(use_sne_field, INT, 0) /* Li et al 2019 dust model parameters*/ ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 8d520497e..d546e7d37 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -270,7 +270,9 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - metalfree[i] = d(i, j, k) - metal(i, j, k); + metalfree[i] = d(i, j, k) - metal(i, j, k) - dust(i, j, k); + // if (my_chemistry->dust_species > 0) + // metalfree[i] -= dust(i, j, k); } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -532,6 +534,13 @@ void make_consistent( (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { + // if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + // ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && + // (my_chemistry->dust_species == 0 || dust(i, j, k) <= 1.e-9 * d(i, j, k)) && + // (d(i, j, k) * dom < 1.e8)) || + // (((metal(i, j, k) > 1.e-9 * d(i, j, k)) || + // (my_chemistry->dust_species > 0 && dust(i, j, k) > 1.e-9 * d(i, j, k))) && + // (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + 16. / 18. * H2O(i, j, k) + O2(i, j, k) + diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 12a7600f5..f775d7872 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -740,6 +740,10 @@ int solve_rate_cool( std::vector mmw(my_fields->grid_dimension[0]); std::vector edot(my_fields->grid_dimension[0]); + // Arrays to store dust growth and destruction mass changes + std::vector growth_dM(my_fields->grid_dimension[0]); + std::vector destruction_dM(my_fields->grid_dimension[0]); + // iteration masks std::vector itmask(my_fields->grid_dimension[0]); std::vector itmask_metal(my_fields->grid_dimension[0]); @@ -926,8 +930,20 @@ int solve_rate_cool( } + // Calculate dust growth rates and store in growth_dM array grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), false); + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + tgas.data(), growth_dM.data()); + + // Calculate dust destruction rates and store in destruction_dM array + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); + + // Apply the calculated rates to update density fields + grackle::impl::dust_update( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + growth_dM.data(), destruction_dM.data(), false); // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 5dce8a8a2..1bc4bf9ff 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,9 @@ typedef struct */ int solver_method; + /* Flag to use snetimestep */ + int use_sne_field; + /* flag and parameters for Li+ 2019 dust growth and destruction */ double dust_destruction_eff; double sne_coeff; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index 04c42f641..8a01806f0 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -211,8 +211,8 @@ typedef struct gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; - // dust model parameter - gr_float *SNe_ThisTimeStep; + // use_snetimestep = 1 + gr_float *sne_rate; } grackle_field_data; diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index b12c45760..19b1111c8 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,7 +66,7 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 10**-3 # Solar + metallicity =1 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} @@ -87,7 +87,7 @@ def set_opts(obj, **kwargs): my_chemistry, with_radiative_cooling=1, - primordial_chemistry=1, + primordial_chemistry=2, dust_chemistry = 1, metal_cooling=1, UVbackground=0, @@ -112,38 +112,38 @@ def set_opts(obj, **kwargs): cie_cooling=1, h2_optical_depth_approximation=1, ih2co=1, - ipiht=1, - - HydrogenFractionByMass=0.76, - DeuteriumToHydrogenRatio=6.8e-05, - SolarMetalFractionByMass=0.01295, - local_dust_to_gas_ratio=0.009387, - NumberOfTemperatureBins=600, - CaseBRecombination=1, - TemperatureStart=1.0, - TemperatureEnd=1.0e9, - NumberOfDustTemperatureBins=250, - DustTemperatureStart=1.0, - DustTemperatureEnd=1500.0, - Compton_xray_heating=0, - LWbackground_sawtooth_suppression=0, - LWbackground_intensity=0, - UVbackground_redshift_on=-99999, - UVbackground_redshift_off=-99999, - UVbackground_redshift_fullon=-99999, - UVbackground_redshift_drop=-99999, - cloudy_electron_fraction_factor=0.00915396, - use_radiative_transfer=1, - radiative_transfer_coupled_rate_solver=0, - radiative_transfer_intermediate_step=0, - radiative_transfer_hydrogen_only=0, - self_shielding_method=0, - H2_custom_shielding=0, - H2_self_shielding=0, - radiative_transfer_H2II_diss=1, - radiative_transfer_HDI_dissociation=1, - radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1 + ipiht=1 + + # HydrogenFractionByMass=0.76, + # DeuteriumToHydrogenRatio=6.8e-05, + # SolarMetalFractionByMass=0.01295, + # # local_dust_to_gas_ratio=0.009387, + # NumberOfTemperatureBins=600, + # CaseBRecombination=1, + # TemperatureStart=1.0, + # TemperatureEnd=1.0e9, + # NumberOfDustTemperatureBins=250, + # DustTemperatureStart=1.0, + # DustTemperatureEnd=1500.0, + # Compton_xray_heating=0, + # LWbackground_sawtooth_suppression=0, + # LWbackground_intensity=0, + # UVbackground_redshift_on=-99999, + # UVbackground_redshift_off=-99999, + # UVbackground_redshift_fullon=-99999, + # UVbackground_redshift_drop=-99999, + # cloudy_electron_fraction_factor=0.00915396, + # use_radiative_transfer=1, + # radiative_transfer_coupled_rate_solver=0, + # radiative_transfer_intermediate_step=0, + # radiative_transfer_hydrogen_only=0, + # self_shielding_method=0, + # H2_custom_shielding=0, + # H2_self_shielding=0, + # radiative_transfer_H2II_diss=1, + # radiative_transfer_HDI_dissociation=1, + # radiative_transfer_metal_ionization=1, + # radiative_transfer_metal_dissociation=1 ) @@ -151,7 +151,7 @@ def set_opts(obj, **kwargs): in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 15000 # K + temperature = 5e4 # K final_time = 100. # Myr # Set units @@ -170,6 +170,7 @@ def set_opts(obj, **kwargs): density=density, temperature=temperature, metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust state="ionized", converge=True) diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index 51f02ba87..f6d3870c9 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -296,6 +296,8 @@ def _required_extra_fields(my_chemistry): my_fields.append("H2_custom_shielding_factor") if my_chemistry.use_isrf_field == 1: my_fields.append("isrf_habing") + if my_chemistry.use_sne_field == 1: + my_fields.append("sne_rate") return my_fields def _required_calculated_fields(my_chemistry): diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 7ac1eecd2..6e273b1f7 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -203,6 +203,7 @@ cdef extern from "grackle.h": gr_float *ref_org_dust_temperature; gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; + gr_float *sne_rate; ctypedef struct c_grackle_version "grackle_version": const char* version; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index d8d827f88..4bba6f0aa 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -655,6 +655,8 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.vol_org_dust_temperature = get_field(fc, "vol_org_dust_temperature") my_fields.H2O_ice_dust_temperature = get_field(fc, "H2O_ice_dust_temperature") + my_fields.sne_rate = get_field(fc, "sne_rate") + return my_fields def solve_chemistry(fc, my_dt): diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 62b5a4e59..bec44b4f0 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -85,7 +85,8 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The ratio of dust mass density to total gas density. + The dust mass fraction of total density (dust/d). + TODO: rename to dust_mass_fraction for clarity. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach @@ -126,7 +127,11 @@ def setup_fluid_container(my_chemistry, fh = my_chemistry.HydrogenFractionByMass d2h = my_chemistry.DeuteriumToHydrogenRatio - metal_free = 1 - metal_mass_fraction + # d = gas + metal + dust; dust_to_gas_ratio is really dust_fraction (dust/d) + # TODO: rename dust_to_gas_ratio to dust_mass_fraction + dust_mass_fraction = dust_to_gas_ratio * (1-metal_mass_fraction) / (1 + dust_to_gas_ratio) + # metal_free = 1 - metal_mass_fraction - dust_to_gas_ratio + metal_free = 1 - metal_mass_fraction - dust_mass_fraction H_total = fh * metal_free He_total = (1 - fh) * metal_free # someday, maybe we'll include D in the total @@ -142,7 +147,8 @@ def setup_fluid_container(my_chemistry, state_vals = { "density": fc_density, "metal_density": metal_mass_fraction * fc_density, - "dust_density": dust_to_gas_ratio * fc_density + "dust_density": dust_mass_fraction * fc_density + # "dust_density": dust_to_gas_ratio * fc_density } if my_chemistry.metal_chemistry > 0: diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 280a3fa7a..767cdd1e1 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - ((current_time * my_chemistry.time_units / sec_per_year), - (fc["density"][0] * my_chemistry.density_units), - fc["temperature"][0])) + # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + # ((current_time * my_chemistry.time_units / sec_per_year), + # (fc["density"][0] * my_chemistry.density_units), + # fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - (current_time * my_chemistry.time_units / sec_per_year, - fc["density"][0] * my_chemistry.density_units, - fc["temperature"][0])) + # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + # (current_time * my_chemistry.time_units / sec_per_year, + # fc["density"][0] * my_chemistry.density_units, + # fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From 0777f8f25d4e85cc855540a7d298a3cbbaabe69a Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 04:02:39 +0000 Subject: [PATCH 09/71] Fixing d and dust --- src/clib/dust_growth_and_destruction.cpp | 30 ++++++++++--------- src/clib/make_consistent.cpp | 4 +-- src/python/gracklepy/utilities/convenience.py | 13 +++----- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index b9f1f8eda..7b732d6a6 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -153,19 +153,21 @@ void grackle::impl::dust_destruction( } } - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - if (dM_shock >= rho_dust/dt) { - if (dM_shock > rho_dust/dt) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + if (temp >= std::pow(10,5)) { + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + if (dM_shock >= rho_dust/dt) { + if (dM_shock > rho_dust/dt) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0; + dM_shock = std::min(dM_shock, rho_dust/dt); } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0; - dM_shock = std::min(dM_shock, rho_dust/dt); } //dM = - rho_dust * dM_shock; dM = -dM_shock; @@ -240,8 +242,8 @@ void grackle::impl::dust_update( } fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e\n", - dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal); + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (dryrun == false) { diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index d546e7d37..093f93247 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -270,9 +270,7 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - metalfree[i] = d(i, j, k) - metal(i, j, k) - dust(i, j, k); - // if (my_chemistry->dust_species > 0) - // metalfree[i] -= dust(i, j, k); + metalfree[i] = d(i, j, k) - metal(i, j, k); } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index bec44b4f0..3c72a7f81 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -85,8 +85,7 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The dust mass fraction of total density (dust/d). - TODO: rename to dust_mass_fraction for clarity. + The dust-to-gas ratio (dust/d). Dust is independent of d. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach @@ -127,11 +126,8 @@ def setup_fluid_container(my_chemistry, fh = my_chemistry.HydrogenFractionByMass d2h = my_chemistry.DeuteriumToHydrogenRatio - # d = gas + metal + dust; dust_to_gas_ratio is really dust_fraction (dust/d) - # TODO: rename dust_to_gas_ratio to dust_mass_fraction - dust_mass_fraction = dust_to_gas_ratio * (1-metal_mass_fraction) / (1 + dust_to_gas_ratio) - # metal_free = 1 - metal_mass_fraction - dust_to_gas_ratio - metal_free = 1 - metal_mass_fraction - dust_mass_fraction + # d = gas + metal (dust is independent of d) + metal_free = 1 - metal_mass_fraction H_total = fh * metal_free He_total = (1 - fh) * metal_free # someday, maybe we'll include D in the total @@ -147,8 +143,7 @@ def setup_fluid_container(my_chemistry, state_vals = { "density": fc_density, "metal_density": metal_mass_fraction * fc_density, - "dust_density": dust_mass_fraction * fc_density - # "dust_density": dust_to_gas_ratio * fc_density + "dust_density": dust_to_gas_ratio * fc_density } if my_chemistry.metal_chemistry > 0: From b9956a9012f004d37eae480a3d743e882be1c27e Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 04:18:05 +0000 Subject: [PATCH 10/71] adding examples --- src/clib/calc_tdust_3d.cpp | 1 - src/python/examples/cooling_cell_test.py | 201 +++++++++++++++++++++ src/python/examples/freefall_test.py | 216 +++++++++++++++++++++++ 3 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/python/examples/cooling_cell_test.py create mode 100644 src/python/examples/freefall_test.py diff --git a/src/clib/calc_tdust_3d.cpp b/src/clib/calc_tdust_3d.cpp index dfce8d534..a4e768184 100644 --- a/src/clib/calc_tdust_3d.cpp +++ b/src/clib/calc_tdust_3d.cpp @@ -210,7 +210,6 @@ void calc_tdust_3d_g( if (my_chemistry->use_dust_density_field > 0) { dust2gas[i] = dust(i,j,k) / (d(i,j,k)); - // dust2gas[i] = dust(i,j,k) / (d(i,j,k) - dust(i,j,k)); } else { dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; } diff --git a/src/python/examples/cooling_cell_test.py b/src/python/examples/cooling_cell_test.py new file mode 100644 index 000000000..6afcb1529 --- /dev/null +++ b/src/python/examples/cooling_cell_test.py @@ -0,0 +1,201 @@ +######################################################################## +# +# Cooling cell example script +# +# This will initialize a single cell at a given temperature, +# iterate the cooling solver for a fixed time, and output the +# temperature vs. time. +# +# +# Copyright (c) 2015-2016, Grackle Development Team. +# +# Distributed under the terms of the Enzo Public Licence. +# +# The full license is in the file LICENSE, distributed with this +# software. +######################################################################## + +from matplotlib import pyplot +import os +import sys +import yt + +from gracklepy import \ + chemistry_data, \ + evolve_constant_density, \ + setup_fluid_container +from gracklepy.utilities.physical_constants import \ + mass_hydrogen_cgs, \ + sec_per_Myr, \ + cm_per_mpc + +from gracklepy.utilities.data_path import grackle_data_dir +from gracklepy.utilities.model_tests import \ + get_test_variables + +_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" + +def gen_plot(fc, data, fname): + p1, = pyplot.loglog(data["time"].to("Myr"), + data["temperature"], + color="black", label="T") + pyplot.xlabel("Time [Myr]") + pyplot.ylabel("T [K]") + pyplot.twinx() + p2, = pyplot.semilogx(data["time"].to("Myr"), + data["mean_molecular_weight"], + color="red", label="$\\mu$") + pyplot.ylabel("$\\mu$") + pyplot.legend([p1,p2],["T","$\\mu$"], fancybox=True, + loc="center left") + pyplot.tight_layout() + pyplot.savefig(fname) + + +def main(args=None): + args = sys.argv[1:] if args is None else args + if len(args) != 0: # we are using the testing framework + my_vars = get_test_variables(_MODEL_NAME, args) + + metallicity = my_vars["metallicity"] + redshift = my_vars["redshift"] + extra_attrs = my_vars["extra_attrs"] + my_chemistry = my_vars["my_chemistry"] + output_name = my_vars["output_name"] + + in_testing_framework = True + + else: # Just run the script as is. + metallicity =1 # Solar + redshift = 0. + # dictionary to store extra information in output dataset + extra_attrs = {} + + my_chemistry = chemistry_data() + my_chemistry.use_grackle = 1 + my_chemistry.grackle_data_file = \ + os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=2, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=0, + multi_metals=0, + metal_abundances=0, + dust_species=0, + use_multiple_dust_temperatures=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + # SNe dust destruction + use_sne_field=1 + + # HydrogenFractionByMass=0.76, + # DeuteriumToHydrogenRatio=6.8e-05, + # SolarMetalFractionByMass=0.01295, + # # local_dust_to_gas_ratio=0.009387, + # NumberOfTemperatureBins=600, + # CaseBRecombination=1, + # TemperatureStart=1.0, + # TemperatureEnd=1.0e9, + # NumberOfDustTemperatureBins=250, + # DustTemperatureStart=1.0, + # DustTemperatureEnd=1500.0, + # Compton_xray_heating=0, + # LWbackground_sawtooth_suppression=0, + # LWbackground_intensity=0, + # UVbackground_redshift_on=-99999, + # UVbackground_redshift_off=-99999, + # UVbackground_redshift_fullon=-99999, + # UVbackground_redshift_drop=-99999, + # cloudy_electron_fraction_factor=0.00915396, + # use_radiative_transfer=1, + # radiative_transfer_coupled_rate_solver=0, + # radiative_transfer_intermediate_step=0, + # radiative_transfer_hydrogen_only=0, + # self_shielding_method=0, + # H2_custom_shielding=0, + # H2_self_shielding=0, + # radiative_transfer_H2II_diss=1, + # radiative_transfer_HDI_dissociation=1, + # radiative_transfer_metal_ionization=1, + # radiative_transfer_metal_dissociation=1 + + ) + + output_name = _MODEL_NAME + in_testing_framework = False + + density = 0.1 * mass_hydrogen_cgs # g /cm^3 + temperature = 5e4 # K + final_time = 100. # Myr + + # Set units + my_chemistry.comoving_coordinates = 0 + my_chemistry.a_units = 1.0 + my_chemistry.a_value = 1. / (1. + redshift) / \ + my_chemistry.a_units + my_chemistry.density_units = mass_hydrogen_cgs + my_chemistry.length_units = cm_per_mpc + my_chemistry.time_units = sec_per_Myr + my_chemistry.set_velocity_units() + + metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass + fc = setup_fluid_container( + my_chemistry, + density=density, + temperature=temperature, + metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust + state="ionized", + converge=True) + + # Set SNe rate: ~0.02 SNII/yr in a MW-like disk volume (~1.3e68 cm³) + # V_disk = pi * (15kpc)^2 * 0.6kpc = 424 kpc^3 = 1.25e67 cm ^3 + # sne_rate = 1.6e-69 # per yr per cm^3 + sne_rate = 1 + fc["sne_rate"][:] = sne_rate + + # evolve gas at constant density + data = evolve_constant_density( + fc, final_time=final_time, + safety_factor=0.01) + + if not in_testing_framework: + gen_plot(fc, data, fname=f"{output_name}.png") + + # save data arrays as a yt dataset + yt.save_as_dataset({}, f"{output_name}.h5", + data=data, extra_attrs=extra_attrs) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py new file mode 100644 index 000000000..4b8244723 --- /dev/null +++ b/src/python/examples/freefall_test.py @@ -0,0 +1,216 @@ +######################################################################## +# +# Free-fall example script +# +# +# Copyright (c) 2013-2016, Grackle Development Team. +# +# Distributed under the terms of the Enzo Public Licence. +# +# The full license is in the file LICENSE, distributed with this +# software. +######################################################################## +import numpy as np +from matplotlib import pyplot as plt +import os +import sys +import yt + +from gracklepy import \ + chemistry_data, \ + evolve_constant_density, \ + evolve_freefall, \ + setup_fluid_container +from gracklepy.utilities.physical_constants import \ + mass_hydrogen_cgs, \ + sec_per_Myr, \ + cm_per_mpc + +from gracklepy.utilities.data_path import grackle_data_dir +from gracklepy.utilities.model_tests import \ + get_test_variables + +_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" + +def gen_plot(fc, data, Z): + color = plt.gca()._get_lines.get_next_color() + # plt.loglog(data["density"]/mass_hydrogen_cgs, data["metal_density"] / data["density"], + # color=color,label=f"Z={Z:g}") + plt.loglog(data["density"], data["temperature"], + color=color,label=f"Z={Z:g}") + if fc.chemistry_data.dust_chemistry == 1: + plt.loglog(data["density"], data["dust_temperature"], + color=color, linestyle="--",label="_nolegend_") + # pyplot.twinx() + # plots.extend( + # pyplot.loglog(data["density"], data["H2I_density"] / data["density"], + # color="red", label="f$_{H2}$")) + # pyplot.ylabel("H$_{2}$ fraction") + + +def finalize_plot(fname): + plt.legend(loc="upper left") + plt.tight_layout() + # plt.xlabel("$\\rho$ [g/cm$^{3}$]") + plt.xlabel("nH") + plt.ylabel("T [K]") + plt.savefig(fname, bbox_inches="tight", pad_inches=0.03, dpi=200) + + +def main(args=None): + args = sys.argv[1:] if args is None else args + if len(args) != 0: # we are using the testing framework + my_vars = get_test_variables(_MODEL_NAME, args) + + metallicity = my_vars["metallicity"] + extra_attrs = my_vars["extra_attrs"] + my_chemistry = my_vars["my_chemistry"] + output_name = my_vars["output_name"] + + in_testing_framework = True + + else: # Just run the script as is. + # Z_list = [10**(-5)] + Z_list = [0., 10**(-6), 10**(-5), 10**(-4), 10**(-3), 10**(-2), 10**(-1), 1] + # Z_list = [10**(-4)] + # metallicity = 10e-4 + # dictionary to store extra information in output dataset + extra_attrs = {} + + # Set solver parameters + my_chemistry = chemistry_data() + my_chemistry.use_grackle = 1 + my_chemistry.grackle_data_file = os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=4, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=1, + multi_metals=0, + metal_abundances=0, + dust_species=0, + # dust_temperature_multi=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + HydrogenFractionByMass=0.76, + DeuteriumToHydrogenRatio=6.8e-05, + SolarMetalFractionByMass=0.01295, + local_dust_to_gas_ratio=0.009387, + NumberOfTemperatureBins=600, + CaseBRecombination=1, + TemperatureStart=1.0, + TemperatureEnd=1.0e9, + NumberOfDustTemperatureBins=250, + DustTemperatureStart=1.0, + DustTemperatureEnd=1500.0, + Compton_xray_heating=0, + LWbackground_sawtooth_suppression=0, + LWbackground_intensity=0, + UVbackground_redshift_on=-99999, + UVbackground_redshift_off=-99999, + UVbackground_redshift_fullon=-99999, + UVbackground_redshift_drop=-99999, + cloudy_electron_fraction_factor=0.00915396, + use_radiative_transfer=1, + radiative_transfer_coupled_rate_solver=0, + radiative_transfer_intermediate_step=0, + radiative_transfer_hydrogen_only=0, + self_shielding_method=0, + H2_custom_shielding=0, + H2_self_shielding=0, + radiative_transfer_H2II_diss=1, + radiative_transfer_HDI_dissociation=1, + radiative_transfer_metal_ionization=1, + radiative_transfer_metal_dissociation=1, + use_sne_field=1, + + use_multiple_dust_temperatures=0 + ) + + output_name = _MODEL_NAME + in_testing_framework = False + + redshift = 0. + + # Set units + my_chemistry.comoving_coordinates = 0 + my_chemistry.a_units = 1.0 + my_chemistry.a_value = 1. / (1. + redshift) / \ + my_chemistry.a_units + my_chemistry.density_units = mass_hydrogen_cgs + my_chemistry.length_units = cm_per_mpc + my_chemistry.time_units = sec_per_Myr + my_chemistry.set_velocity_units() + + # set initial density and temperature + initial_temperature = 50000 + initial_density = 1e-1 * mass_hydrogen_cgs # g / cm^3 + final_density = 1e13 * mass_hydrogen_cgs + + for metallicity in Z_list: + metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass + dust_to_gas_ratio = metallicity * my_chemistry.local_dust_to_gas_ratio + fc = setup_fluid_container( + my_chemistry, + density=initial_density, + temperature=initial_temperature, + metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=dust_to_gas_ratio, + state="ionized", + converge=False) + sne_rate = 1 + fc["sne_rate"][:] = sne_rate + + # let the gas cool at constant density from the starting temperature + # down to a lower temperature to get the species fractions in a + # reasonable state. + cooling_temperature = 10**(2.5) + data0 = evolve_constant_density( # noqa: F841 + fc, final_temperature=cooling_temperature, + safety_factor=0.1) + + # evolve density and temperature according to free-fall collapse + data = evolve_freefall(fc, final_density, + safety_factor=0.01, + include_pressure=True) + if not in_testing_framework: + gen_plot(fc, data, metallicity) + + finalize_plot(f"src/python/examples/{output_name}_new.png") + # save data arrays as a yt dataset + yt.save_as_dataset({}, f"{output_name}_new.h5", + data=data, extra_attrs=extra_attrs) + # breakpoint() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From 4576a3ac0982af5793c7b866a061eb6cae236b64 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 10:29:49 +0000 Subject: [PATCH 11/71] check --- src/clib/dust_growth_and_destruction.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 7b732d6a6..54e6cbd67 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -80,10 +80,6 @@ void grackle::impl::dust_growth( // Store the calculated mass change in the output array growth_dM[i] = dM; - - // fprintf(stderr, - // "internal: frac=%.10e growth_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", - // frac_metal_available, dM, rho_gas, rho_dust, rho_metal); } } @@ -177,9 +173,6 @@ void grackle::impl::dust_destruction( // Store the calculated mass change in the output array destruction_dM[i] = dM; - // fprintf(stderr, - // "internal: tau_dest=%.10e tau_dest=%.10e dM_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", - // tau_dest, tau_sput, dM, rho_gas, rho_dust, rho_metal); } } } From ea9e33f1944be1d98f88eb6149cc4b4f6a7674a8 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 22:38:51 +0000 Subject: [PATCH 12/71] add a dust model switch --- src/clib/calc_tdust_3d.cpp | 3 ++- src/clib/cool1d_multi_g.cpp | 13 ++++----- src/clib/dust/lookup_dust_rates1d.hpp | 9 ++++--- src/clib/grackle_chemistry_data_fields.def | 6 +++++ src/clib/solve_rate_cool.cpp | 31 +++++++++++----------- src/include/grackle_chemistry_data.h | 6 +++++ src/python/examples/freefall_test.py | 6 ++--- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/clib/calc_tdust_3d.cpp b/src/clib/calc_tdust_3d.cpp index a4e768184..237d8539d 100644 --- a/src/clib/calc_tdust_3d.cpp +++ b/src/clib/calc_tdust_3d.cpp @@ -163,7 +163,8 @@ void calc_tdust_3d_g( // Compute grain size increment - if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) ) { + if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) + && (my_chemistry->dust_model == 0) ) { grackle::impl::calc_grain_size_increment_1d ( dom, idx_range, itmask_metal.data(), my_chemistry, diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index cb656cbea..655c68549 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1104,12 +1104,13 @@ void grackle::impl::cool1d_multi_g( } } // Compute grain size increment - // if ((my_chemistry->use_dust_density_field > 0) && - // (my_chemistry->dust_species > 0)) { - // grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( - // dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, - // internal_dust_prop_buf); - // } + if ((my_chemistry->use_dust_density_field > 0) && + (my_chemistry->dust_species > 0) && + (my_chemistry->dust_model == 0)) { + grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + internal_dust_prop_buf); + } // Calculate dust to gas ratio AND interstellar radiation field // -> an earlier version of this logic would store values @ indices diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index 9e216f5d3..2b50ed375 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -191,10 +191,11 @@ inline void lookup_dust_rates1d( GRIMPL_REQUIRE(my_chemistry->metal_chemistry == 1, "sanity-check!"); // Compute grain size increment - calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + if (my_chemistry->dust_model == 0) { + f_wrap::calc_grain_size_increment_1d(dom, idx_range, itmask_metal, + my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, + my_fields, internal_dust_prop_scratch_buf); + } grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index ba6efd457..21d980b3d 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -334,6 +334,12 @@ ENTRY(omp_nthreads, INT, 0) */ ENTRY(solver_method, INT, 2) +/* dust model selection +* 0: default (runs calc_grain_size_increment_1d as usual) +* 1: harrison dust model +*/ +ENTRY(dust_model, INT, 0) + /* Flag to use snetimestep */ ENTRY(use_sne_field, INT, 0) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index f775d7872..d0ee4ee72 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -929,21 +929,22 @@ int solve_rate_cool( ); } - - // Calculate dust growth rates and store in growth_dM array - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - tgas.data(), growth_dM.data()); - - // Calculate dust destruction rates and store in destruction_dM array - grackle::impl::dust_destruction( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), destruction_dM.data()); - - // Apply the calculated rates to update density fields - grackle::impl::dust_update( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM.data(), destruction_dM.data(), false); + if (my_chemistry->dust_model == 1){ + // Calculate dust growth rates and store in growth_dM array + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + tgas.data(), growth_dM.data()); + + // Calculate dust destruction rates and store in destruction_dM array + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); + + // Apply the calculated rates to update density fields + grackle::impl::dust_update( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + growth_dM.data(), destruction_dM.data(), false); + } // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 1bc4bf9ff..b68f8457b 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,12 @@ typedef struct */ int solver_method; + /* dust model selection + * 0: default (runs calc_grain_size_increment_1d as usual) + * 1: harrison dust model + */ + int dust_model; + /* Flag to use snetimestep */ int use_sne_field; diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py index 4b8244723..23cda8809 100644 --- a/src/python/examples/freefall_test.py +++ b/src/python/examples/freefall_test.py @@ -150,7 +150,7 @@ def set_opts(obj, **kwargs): radiative_transfer_HDI_dissociation=1, radiative_transfer_metal_ionization=1, radiative_transfer_metal_dissociation=1, - use_sne_field=1, + use_sne_field=0, use_multiple_dust_temperatures=0 ) @@ -186,8 +186,8 @@ def set_opts(obj, **kwargs): dust_to_gas_ratio=dust_to_gas_ratio, state="ionized", converge=False) - sne_rate = 1 - fc["sne_rate"][:] = sne_rate + # sne_rate = 1 + # fc["sne_rate"][:] = sne_rate # let the gas cool at constant density from the starting temperature # down to a lower temperature to get the species fractions in a From de18de366be4e353065f10b01b509e4a0f4aa407 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:01:04 +0000 Subject: [PATCH 13/71] rebasing fix --- src/clib/cool1d_multi_g.cpp | 6 ++++-- src/clib/dust/lookup_dust_rates1d.hpp | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 655c68549..80280aa3e 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1107,8 +1107,10 @@ void grackle::impl::cool1d_multi_g( if ((my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0)) { - grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( - dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + grackle::impl::calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, my_fields, internal_dust_prop_buf); } diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index 2b50ed375..29911abb8 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -192,9 +192,10 @@ inline void lookup_dust_rates1d( // Compute grain size increment if (my_chemistry->dust_model == 0) { - f_wrap::calc_grain_size_increment_1d(dom, idx_range, itmask_metal, - my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, + my_fields, internal_dust_prop_scratch_buf); } grackle::impl::View d( From db5b0c7855da62964bdf2f9cc6f4827fa5eb1324 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:12:00 +0000 Subject: [PATCH 14/71] final formatting --- src/clib/calc_tdust_3d.cpp | 3 - src/python/examples/cooling_cell_test.py | 201 --------------------- src/python/examples/freefall_test.py | 216 ----------------------- 3 files changed, 420 deletions(-) delete mode 100644 src/python/examples/cooling_cell_test.py delete mode 100644 src/python/examples/freefall_test.py diff --git a/src/clib/calc_tdust_3d.cpp b/src/clib/calc_tdust_3d.cpp index 237d8539d..f7c1ec8f1 100644 --- a/src/clib/calc_tdust_3d.cpp +++ b/src/clib/calc_tdust_3d.cpp @@ -162,17 +162,14 @@ void calc_tdust_3d_g( } // Compute grain size increment - if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0) ) { - grackle::impl::calc_grain_size_increment_1d ( dom, idx_range, itmask_metal.data(), my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, my_fields, internal_dust_prop_buf ); - } for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { diff --git a/src/python/examples/cooling_cell_test.py b/src/python/examples/cooling_cell_test.py deleted file mode 100644 index 6afcb1529..000000000 --- a/src/python/examples/cooling_cell_test.py +++ /dev/null @@ -1,201 +0,0 @@ -######################################################################## -# -# Cooling cell example script -# -# This will initialize a single cell at a given temperature, -# iterate the cooling solver for a fixed time, and output the -# temperature vs. time. -# -# -# Copyright (c) 2015-2016, Grackle Development Team. -# -# Distributed under the terms of the Enzo Public Licence. -# -# The full license is in the file LICENSE, distributed with this -# software. -######################################################################## - -from matplotlib import pyplot -import os -import sys -import yt - -from gracklepy import \ - chemistry_data, \ - evolve_constant_density, \ - setup_fluid_container -from gracklepy.utilities.physical_constants import \ - mass_hydrogen_cgs, \ - sec_per_Myr, \ - cm_per_mpc - -from gracklepy.utilities.data_path import grackle_data_dir -from gracklepy.utilities.model_tests import \ - get_test_variables - -_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" - -def gen_plot(fc, data, fname): - p1, = pyplot.loglog(data["time"].to("Myr"), - data["temperature"], - color="black", label="T") - pyplot.xlabel("Time [Myr]") - pyplot.ylabel("T [K]") - pyplot.twinx() - p2, = pyplot.semilogx(data["time"].to("Myr"), - data["mean_molecular_weight"], - color="red", label="$\\mu$") - pyplot.ylabel("$\\mu$") - pyplot.legend([p1,p2],["T","$\\mu$"], fancybox=True, - loc="center left") - pyplot.tight_layout() - pyplot.savefig(fname) - - -def main(args=None): - args = sys.argv[1:] if args is None else args - if len(args) != 0: # we are using the testing framework - my_vars = get_test_variables(_MODEL_NAME, args) - - metallicity = my_vars["metallicity"] - redshift = my_vars["redshift"] - extra_attrs = my_vars["extra_attrs"] - my_chemistry = my_vars["my_chemistry"] - output_name = my_vars["output_name"] - - in_testing_framework = True - - else: # Just run the script as is. - metallicity =1 # Solar - redshift = 0. - # dictionary to store extra information in output dataset - extra_attrs = {} - - my_chemistry = chemistry_data() - my_chemistry.use_grackle = 1 - my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=2, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=0, - multi_metals=0, - metal_abundances=0, - dust_species=0, - use_multiple_dust_temperatures=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1, - - # SNe dust destruction - use_sne_field=1 - - # HydrogenFractionByMass=0.76, - # DeuteriumToHydrogenRatio=6.8e-05, - # SolarMetalFractionByMass=0.01295, - # # local_dust_to_gas_ratio=0.009387, - # NumberOfTemperatureBins=600, - # CaseBRecombination=1, - # TemperatureStart=1.0, - # TemperatureEnd=1.0e9, - # NumberOfDustTemperatureBins=250, - # DustTemperatureStart=1.0, - # DustTemperatureEnd=1500.0, - # Compton_xray_heating=0, - # LWbackground_sawtooth_suppression=0, - # LWbackground_intensity=0, - # UVbackground_redshift_on=-99999, - # UVbackground_redshift_off=-99999, - # UVbackground_redshift_fullon=-99999, - # UVbackground_redshift_drop=-99999, - # cloudy_electron_fraction_factor=0.00915396, - # use_radiative_transfer=1, - # radiative_transfer_coupled_rate_solver=0, - # radiative_transfer_intermediate_step=0, - # radiative_transfer_hydrogen_only=0, - # self_shielding_method=0, - # H2_custom_shielding=0, - # H2_self_shielding=0, - # radiative_transfer_H2II_diss=1, - # radiative_transfer_HDI_dissociation=1, - # radiative_transfer_metal_ionization=1, - # radiative_transfer_metal_dissociation=1 - - ) - - output_name = _MODEL_NAME - in_testing_framework = False - - density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 5e4 # K - final_time = 100. # Myr - - # Set units - my_chemistry.comoving_coordinates = 0 - my_chemistry.a_units = 1.0 - my_chemistry.a_value = 1. / (1. + redshift) / \ - my_chemistry.a_units - my_chemistry.density_units = mass_hydrogen_cgs - my_chemistry.length_units = cm_per_mpc - my_chemistry.time_units = sec_per_Myr - my_chemistry.set_velocity_units() - - metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass - fc = setup_fluid_container( - my_chemistry, - density=density, - temperature=temperature, - metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust - state="ionized", - converge=True) - - # Set SNe rate: ~0.02 SNII/yr in a MW-like disk volume (~1.3e68 cm³) - # V_disk = pi * (15kpc)^2 * 0.6kpc = 424 kpc^3 = 1.25e67 cm ^3 - # sne_rate = 1.6e-69 # per yr per cm^3 - sne_rate = 1 - fc["sne_rate"][:] = sne_rate - - # evolve gas at constant density - data = evolve_constant_density( - fc, final_time=final_time, - safety_factor=0.01) - - if not in_testing_framework: - gen_plot(fc, data, fname=f"{output_name}.png") - - # save data arrays as a yt dataset - yt.save_as_dataset({}, f"{output_name}.h5", - data=data, extra_attrs=extra_attrs) - return 0 - - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py deleted file mode 100644 index 23cda8809..000000000 --- a/src/python/examples/freefall_test.py +++ /dev/null @@ -1,216 +0,0 @@ -######################################################################## -# -# Free-fall example script -# -# -# Copyright (c) 2013-2016, Grackle Development Team. -# -# Distributed under the terms of the Enzo Public Licence. -# -# The full license is in the file LICENSE, distributed with this -# software. -######################################################################## -import numpy as np -from matplotlib import pyplot as plt -import os -import sys -import yt - -from gracklepy import \ - chemistry_data, \ - evolve_constant_density, \ - evolve_freefall, \ - setup_fluid_container -from gracklepy.utilities.physical_constants import \ - mass_hydrogen_cgs, \ - sec_per_Myr, \ - cm_per_mpc - -from gracklepy.utilities.data_path import grackle_data_dir -from gracklepy.utilities.model_tests import \ - get_test_variables - -_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" - -def gen_plot(fc, data, Z): - color = plt.gca()._get_lines.get_next_color() - # plt.loglog(data["density"]/mass_hydrogen_cgs, data["metal_density"] / data["density"], - # color=color,label=f"Z={Z:g}") - plt.loglog(data["density"], data["temperature"], - color=color,label=f"Z={Z:g}") - if fc.chemistry_data.dust_chemistry == 1: - plt.loglog(data["density"], data["dust_temperature"], - color=color, linestyle="--",label="_nolegend_") - # pyplot.twinx() - # plots.extend( - # pyplot.loglog(data["density"], data["H2I_density"] / data["density"], - # color="red", label="f$_{H2}$")) - # pyplot.ylabel("H$_{2}$ fraction") - - -def finalize_plot(fname): - plt.legend(loc="upper left") - plt.tight_layout() - # plt.xlabel("$\\rho$ [g/cm$^{3}$]") - plt.xlabel("nH") - plt.ylabel("T [K]") - plt.savefig(fname, bbox_inches="tight", pad_inches=0.03, dpi=200) - - -def main(args=None): - args = sys.argv[1:] if args is None else args - if len(args) != 0: # we are using the testing framework - my_vars = get_test_variables(_MODEL_NAME, args) - - metallicity = my_vars["metallicity"] - extra_attrs = my_vars["extra_attrs"] - my_chemistry = my_vars["my_chemistry"] - output_name = my_vars["output_name"] - - in_testing_framework = True - - else: # Just run the script as is. - # Z_list = [10**(-5)] - Z_list = [0., 10**(-6), 10**(-5), 10**(-4), 10**(-3), 10**(-2), 10**(-1), 1] - # Z_list = [10**(-4)] - # metallicity = 10e-4 - # dictionary to store extra information in output dataset - extra_attrs = {} - - # Set solver parameters - my_chemistry = chemistry_data() - my_chemistry.use_grackle = 1 - my_chemistry.grackle_data_file = os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=4, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=1, - multi_metals=0, - metal_abundances=0, - dust_species=0, - # dust_temperature_multi=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1, - - HydrogenFractionByMass=0.76, - DeuteriumToHydrogenRatio=6.8e-05, - SolarMetalFractionByMass=0.01295, - local_dust_to_gas_ratio=0.009387, - NumberOfTemperatureBins=600, - CaseBRecombination=1, - TemperatureStart=1.0, - TemperatureEnd=1.0e9, - NumberOfDustTemperatureBins=250, - DustTemperatureStart=1.0, - DustTemperatureEnd=1500.0, - Compton_xray_heating=0, - LWbackground_sawtooth_suppression=0, - LWbackground_intensity=0, - UVbackground_redshift_on=-99999, - UVbackground_redshift_off=-99999, - UVbackground_redshift_fullon=-99999, - UVbackground_redshift_drop=-99999, - cloudy_electron_fraction_factor=0.00915396, - use_radiative_transfer=1, - radiative_transfer_coupled_rate_solver=0, - radiative_transfer_intermediate_step=0, - radiative_transfer_hydrogen_only=0, - self_shielding_method=0, - H2_custom_shielding=0, - H2_self_shielding=0, - radiative_transfer_H2II_diss=1, - radiative_transfer_HDI_dissociation=1, - radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1, - use_sne_field=0, - - use_multiple_dust_temperatures=0 - ) - - output_name = _MODEL_NAME - in_testing_framework = False - - redshift = 0. - - # Set units - my_chemistry.comoving_coordinates = 0 - my_chemistry.a_units = 1.0 - my_chemistry.a_value = 1. / (1. + redshift) / \ - my_chemistry.a_units - my_chemistry.density_units = mass_hydrogen_cgs - my_chemistry.length_units = cm_per_mpc - my_chemistry.time_units = sec_per_Myr - my_chemistry.set_velocity_units() - - # set initial density and temperature - initial_temperature = 50000 - initial_density = 1e-1 * mass_hydrogen_cgs # g / cm^3 - final_density = 1e13 * mass_hydrogen_cgs - - for metallicity in Z_list: - metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass - dust_to_gas_ratio = metallicity * my_chemistry.local_dust_to_gas_ratio - fc = setup_fluid_container( - my_chemistry, - density=initial_density, - temperature=initial_temperature, - metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=dust_to_gas_ratio, - state="ionized", - converge=False) - # sne_rate = 1 - # fc["sne_rate"][:] = sne_rate - - # let the gas cool at constant density from the starting temperature - # down to a lower temperature to get the species fractions in a - # reasonable state. - cooling_temperature = 10**(2.5) - data0 = evolve_constant_density( # noqa: F841 - fc, final_temperature=cooling_temperature, - safety_factor=0.1) - - # evolve density and temperature according to free-fall collapse - data = evolve_freefall(fc, final_density, - safety_factor=0.01, - include_pressure=True) - if not in_testing_framework: - gen_plot(fc, data, metallicity) - - finalize_plot(f"src/python/examples/{output_name}_new.png") - # save data arrays as a yt dataset - yt.save_as_dataset({}, f"{output_name}_new.h5", - data=data, extra_attrs=extra_attrs) - # breakpoint() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file From f8ff0840b81db78e49c6cc88b3f18c64bd91df6b Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:31:34 +0000 Subject: [PATCH 15/71] output printing --- src/clib/dust_growth_and_destruction.cpp | 8 ++++---- src/python/gracklepy/utilities/evolve.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 54e6cbd67..32c9f717a 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -226,7 +226,7 @@ void grackle::impl::dust_update( } // Adjust gas density to conserve total mass - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Should be changed to gas_density staying constant (make_consistent.cpp) + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Safety checks if (rho_dust < 0) { @@ -234,9 +234,9 @@ void grackle::impl::dust_update( std::exit(21); } - fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); + // fprintf(stderr, + // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (dryrun == false) { diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 767cdd1e1..280a3fa7a 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - # ((current_time * my_chemistry.time_units / sec_per_year), - # (fc["density"][0] * my_chemistry.density_units), - # fc["temperature"][0])) + print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + ((current_time * my_chemistry.time_units / sec_per_year), + (fc["density"][0] * my_chemistry.density_units), + fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - # (current_time * my_chemistry.time_units / sec_per_year, - # fc["density"][0] * my_chemistry.density_units, - # fc["temperature"][0])) + print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + (current_time * my_chemistry.time_units / sec_per_year, + fc["density"][0] * my_chemistry.density_units, + fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From 7246d2f0ccf375d837e4f1a8dbad21fda69652f9 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:41:43 +0000 Subject: [PATCH 16/71] fixing examples --- src/clib/make_consistent.cpp | 7 -- src/python/examples/cooling_cell.py | 82 ++----------------- src/python/examples/freefall.py | 2 +- src/python/gracklepy/utilities/convenience.py | 5 +- 4 files changed, 9 insertions(+), 87 deletions(-) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 093f93247..8d520497e 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -532,13 +532,6 @@ void make_consistent( (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { - // if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || - // ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && - // (my_chemistry->dust_species == 0 || dust(i, j, k) <= 1.e-9 * d(i, j, k)) && - // (d(i, j, k) * dom < 1.e8)) || - // (((metal(i, j, k) > 1.e-9 * d(i, j, k)) || - // (my_chemistry->dust_species > 0 && dust(i, j, k) > 1.e-9 * d(i, j, k))) && - // (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + 16. / 18. * H2O(i, j, k) + O2(i, j, k) + diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index 19b1111c8..049a61024 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,92 +66,25 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity =1 # Solar + metallicity = 0.1 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} my_chemistry = chemistry_data() my_chemistry.use_grackle = 1 + my_chemistry.with_radiative_cooling = 1 + my_chemistry.primordial_chemistry = 0 + my_chemistry.metal_cooling = 1 + my_chemistry.UVbackground = 1 my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=2, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=0, - multi_metals=0, - metal_abundances=0, - dust_species=0, - use_multiple_dust_temperatures=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1 - - # HydrogenFractionByMass=0.76, - # DeuteriumToHydrogenRatio=6.8e-05, - # SolarMetalFractionByMass=0.01295, - # # local_dust_to_gas_ratio=0.009387, - # NumberOfTemperatureBins=600, - # CaseBRecombination=1, - # TemperatureStart=1.0, - # TemperatureEnd=1.0e9, - # NumberOfDustTemperatureBins=250, - # DustTemperatureStart=1.0, - # DustTemperatureEnd=1500.0, - # Compton_xray_heating=0, - # LWbackground_sawtooth_suppression=0, - # LWbackground_intensity=0, - # UVbackground_redshift_on=-99999, - # UVbackground_redshift_off=-99999, - # UVbackground_redshift_fullon=-99999, - # UVbackground_redshift_drop=-99999, - # cloudy_electron_fraction_factor=0.00915396, - # use_radiative_transfer=1, - # radiative_transfer_coupled_rate_solver=0, - # radiative_transfer_intermediate_step=0, - # radiative_transfer_hydrogen_only=0, - # self_shielding_method=0, - # H2_custom_shielding=0, - # H2_self_shielding=0, - # radiative_transfer_H2II_diss=1, - # radiative_transfer_HDI_dissociation=1, - # radiative_transfer_metal_ionization=1, - # radiative_transfer_metal_dissociation=1 - - ) + os.path.join(grackle_data_dir, "CloudyData_UVB=HM2012.h5") output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 5e4 # K + temperature = 1e6 # K final_time = 100. # Myr # Set units @@ -170,7 +103,6 @@ def set_opts(obj, **kwargs): density=density, temperature=temperature, metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust state="ionized", converge=True) diff --git a/src/python/examples/freefall.py b/src/python/examples/freefall.py index aabbff225..01507821d 100644 --- a/src/python/examples/freefall.py +++ b/src/python/examples/freefall.py @@ -139,4 +139,4 @@ def main(args=None): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) \ No newline at end of file diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 3c72a7f81..b04dc7b9b 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -34,9 +34,6 @@ def check_convergence(fc1, fc2, fields=None, tol=0.01): for field in fields: if field not in fc2: continue - if fc1[field] == 0: - print(field) - print(fc2[field]) convergence = np.max(np.abs(fc1[field] - fc2[field]) / fc1[field]) if convergence > max_val: max_val = convergence @@ -85,7 +82,7 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The dust-to-gas ratio (dust/d). Dust is independent of d. + The ratio of dust mass density to total gas density. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach From 35dcdeeaaa3696b70690c44baec61b91dd0342a7 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 4 Mar 2026 00:02:49 +0000 Subject: [PATCH 17/71] pr fix --- src/clib/dust_growth_and_destruction.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 32c9f717a..453b613b7 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -53,7 +53,6 @@ void grackle::impl::dust_growth( if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); double temp = t_gas[i]; @@ -129,7 +128,6 @@ void grackle::impl::dust_destruction( double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; From 1fc13cbe5147845a8fd9c2389cdb6243af7d7382 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 4 Mar 2026 23:47:08 +0000 Subject: [PATCH 18/71] Manual fix for clang-tidy errors --- src/clib/cool1d_multi_g.cpp | 4 +- src/clib/dust/lookup_dust_rates1d.hpp | 9 +- src/clib/dust_growth_and_destruction.cpp | 386 +++++++++++------------ src/clib/dust_growth_and_destruction.hpp | 45 +-- 4 files changed, 209 insertions(+), 235 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 80280aa3e..a0de75c5b 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1105,8 +1105,7 @@ void grackle::impl::cool1d_multi_g( } // Compute grain size increment if ((my_chemistry->use_dust_density_field > 0) && - (my_chemistry->dust_species > 0) && - (my_chemistry->dust_model == 0)) { + (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0)) { grackle::impl::calc_grain_size_increment_1d( dom, idx_range, itmask_metal, my_chemistry, my_rates->opaque_storage->grain_species_info, @@ -1937,7 +1936,6 @@ void grackle::impl::cool1d_multi_g( } } - // Free memory grackle::impl::drop_InternalDustPropBuf(&internal_dust_prop_buf); grackle::impl::drop_GrainSpeciesCollection(&grain_kappa); diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index 29911abb8..b60fcb6ba 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -192,10 +192,11 @@ inline void lookup_dust_rates1d( // Compute grain size increment if (my_chemistry->dust_model == 0) { - calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, my_fields, + internal_dust_prop_scratch_buf); } grackle::impl::View d( diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 453b613b7..4072f37fa 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -7,242 +7,230 @@ #include "utils-cpp.hpp" namespace { - const double k_boltz = 1.3806504e-16; - const double m_proton = 1.67262171e-24; - const double pi_val = 3.141592653589793; - const double sec_per_year = 3.155e7; +const double k_boltz = 1.3806504e-16; +const double m_proton = 1.67262171e-24; +const double pi_val = 3.141592653589793; +const double sec_per_year = 3.155e7; - const double tiny_value = 1.0e-20; - const double huge_value = 1.0e+20; +const double tiny_value = 1.0e-20; +const double huge_value = 1.0e+20; - const double t_ref = 20; +const double t_ref = 20; -} +} // namespace // ========================================== // DUST GROWTH (ACCRETION) // ========================================== -void grackle::impl::dust_growth( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* growth_dM) -{ - grackle::impl::View d( +void grackle::impl::dust_growth(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, + double* growth_dM) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dens_proper = internalu.urho * std::pow(internalu.a_value,3); - double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; - - - // --- MAIN LOOP --- - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero - growth_dM[i] = 0.0; - - if (itmask[i] != MASK_FALSE) { - - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal= metal(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - - double tau_accr0 = tau_ref * - (my_chemistry->dust_growth_densref / dens_proper) * - std::pow(t_ref / temp, 0.5); - double rho_metal_eff = std::max(rho_metal, tiny_value); - double tau_accr = tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); - tau_accr = std::min(tau_accr, huge_value); - tau_accr = std::max(tau_accr, tiny_value); - double frac_metal_available = 0.0; - if (rho_metal <= 0.0) { - frac_metal_available = 0.0; - } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { - frac_metal_available = rho_metal / rho_dust; - } else { - frac_metal_available = rho_metal / (rho_dust + rho_metal); - } - frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); - double growth_rate = frac_metal_available * (rho_dust / tau_accr); - double dM = std::min(growth_rate, rho_metal/dt); - - // Store the calculated mass change in the output array - growth_dM[i] = dM; - } - + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + double tau_ref = + my_chemistry->dust_growth_tauref * 1e9 * sec_per_year / internalu.tbase1; + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + growth_dM[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double rho_dust = dust(i, idx_range.j, idx_range.k); + double rho_metal = metal(i, idx_range.j, idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + + double tau_accr0 = tau_ref * + (my_chemistry->dust_growth_densref / dens_proper) * + std::pow(t_ref / temp, 0.5); + double rho_metal_eff = std::max(rho_metal, tiny_value); + double tau_accr = + tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); + tau_accr = std::min(tau_accr, huge_value); + tau_accr = std::max(tau_accr, tiny_value); + double frac_metal_available = 0.0; + if (rho_metal <= 0.0) { + frac_metal_available = 0.0; + } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { + frac_metal_available = rho_metal / rho_dust; + } else { + frac_metal_available = rho_metal / (rho_dust + rho_metal); + } + frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); + double growth_rate = frac_metal_available * (rho_dust / tau_accr); + double dM = std::min(growth_rate, rho_metal / dt); + + // Store the calculated mass change in the output array + growth_dM[i] = dM; } + } } // ========================================== // 2. DUST DESTRUCTION (SNe + SPUTTERING) // ========================================== void grackle::impl::dust_destruction( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* destruction_dM) -{ - grackle::impl::View d( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* destruction_dM) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool use_sne = (my_chemistry->use_sne_field > 0); - grackle::impl::View sne( - use_sne ? my_fields->sne_rate : my_fields->density, - my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - - double dens_proper = internalu.urho * std::pow(internalu.a_value,3); - - double Ms100 = 6800.0 * my_chemistry->sne_coeff - * (100.0 / my_chemistry->sne_shockspeed) - * (100.0 / my_chemistry->sne_shockspeed) - * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); - - // --- MAIN LOOP --- - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero - destruction_dM[i] = 0.0; - - if (itmask[i] != MASK_FALSE) { - - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; - double temp = t_gas[i]; - double dt = dt_value[i]; - double tau_dest = 0; - - double dM = 0; - double dM_shock = 0.0; - - if (use_sne) { - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - // dM_shock = 0.0; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; - dM_shock = std::min(rho_dust/tau_dest, rho_dust/dt); - } - } - - if (temp >= std::pow(10,5)) { - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - if (dM_shock >= rho_dust/dt) { - if (dM_shock > rho_dust/dt) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; - } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0; - dM_shock = std::min(dM_shock, rho_dust/dt); - } - } - //dM = - rho_dust * dM_shock; - dM = -dM_shock; - if (std::isnan(dM)) { - std::cout << "dM calculated as NaN, "<< dM << std::endl; - } - - // Store the calculated mass change in the output array - destruction_dM[i] = dM; + bool use_sne = (my_chemistry->use_sne_field > 0); + grackle::impl::View sne( + use_sne ? my_fields->sne_rate : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + + double Ms100 = 6800.0 * my_chemistry->sne_coeff * + (100.0 / my_chemistry->sne_shockspeed) * + (100.0 / my_chemistry->sne_shockspeed) * SolarMass / + (internalu.urho * std::pow(internalu.uxyz, 3)); + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + destruction_dM[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust = dust(i, idx_range.j, idx_range.k); + double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; + + double dM = 0; + double dM_shock = 0.0; + + if (use_sne) { + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + // dM_shock = 0.0; + } else { + tau_dest = rho_gas / + (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * + dt; + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); } + } + + if (temp >= std::pow(10, 5)) { + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 * + (my_chemistry->dust_grainsize / 0.1) * + (1.0e-27 / (dens_proper * rho_gas)) * + (std::pow((2.0e6 / temp), 2.5) + 1.0); + + if (dM_shock >= rho_dust / dt) { + if (dM_shock > rho_dust / dt) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " + << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput * 3.0; + dM_shock = std::min(dM_shock, rho_dust / dt); + } + } + // dM = - rho_dust * dM_shock; + dM = -dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, " << dM << std::endl; + } + + // Store the calculated mass change in the output array + destruction_dM[i] = dM; } + } } -void grackle::impl::dust_update( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* growth_dM, - double* destruction_dM, - bool dryrun) -{ - grackle::impl::View d( +void grackle::impl::dust_update(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, const double* growth_dM, + const double* destruction_dM, bool dryrun) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - if (itmask[i] != MASK_FALSE) { - - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); - double dt = dt_value[i]; - - // Get the total mass change (growth + destruction) - - double dM_total = growth_dM[i] + destruction_dM[i]; - dM_total = dM_total * dt; - // Apply constraints to dM - dM_total = std::max(-1*rho_dust, dM_total); - dM_total = std::min(0.9*rho_metal, dM_total); - - // Apply conservation logic (from original code) - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM_total; - rho_metal = rho_metal - dM_total; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - - // Adjust gas density to conserve total mass - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - - // Safety checks - if (rho_dust < 0) { - fprintf(stderr, "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, rho_dust); - std::exit(21); - } - - // fprintf(stderr, - // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); - - // Update the fields - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - } - } + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] != MASK_FALSE) { + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust = dust(i, idx_range.j, idx_range.k); + double rho_metal = metal(i, idx_range.j, idx_range.k); + double dt = dt_value[i]; + + // Get the total mass change (growth + destruction) + + double dM_total = growth_dM[i] + destruction_dM[i]; + dM_total = dM_total * dt; + // Apply constraints to dM + dM_total = std::max(-1 * rho_dust, dM_total); + dM_total = std::min(0.9 * rho_metal, dM_total); + + // Apply conservation logic (from original code) + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM_total; + rho_metal = rho_metal - dM_total; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + + // Adjust gas density to conserve total mass + rho_gas = rho_gas + (rho_metal - metal(i, idx_range.j, idx_range.k)); + + // Safety checks + if (rho_dust < 0) { + fprintf(stderr, + "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, + rho_dust); + std::exit(21); + } + + // fprintf(stderr, + // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e + // dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, + // rho_dust, rho_metal, rho_dust+rho_metal); + + // Update the fields + if (!dryrun) { + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + } } - + } } \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 29eb18390..e2934192e 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -8,48 +8,35 @@ #include "phys_constants.h" #include "fortran_func_decls.h" - namespace grackle::impl { // Calculates dust growth rates (accretion) onto grain surfaces. // Stores the mass change dM for each cell in growth_dM array. -void dust_growth( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* growth_dM // output: mass change rate for each cell +void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, + const double* t_gas, + double* growth_dM // output: mass change rate for each cell ); // Calculates dust destruction rates from SNe shocks and thermal sputtering. // Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* destruction_dM // output: mass change rate for each cell ); // Update the density fields using calculated mass changes. void dust_update( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* growth_dM, // input: mass change from growth - double* destruction_dM, // input: mass change from destruction - bool dryrun -); + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, + const double* growth_dM, // input: mass change from growth + const double* destruction_dM, // input: mass change from destruction + bool dryrun); -} +} // namespace grackle::impl -#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file From 2245bc19d8e931e91fa00f227cabee546304411b Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 5 Mar 2026 00:37:26 +0000 Subject: [PATCH 19/71] setting solver_method --- src/clib/grackle_chemistry_data_fields.def | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 21d980b3d..ed640d82c 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,7 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 2) +ENTRY(solver_method, INT, 1) /* dust model selection * 0: default (runs calc_grain_size_increment_1d as usual) From 16fb96800c61a4561c760a0e7220c3ce22dcc79a Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 5 Mar 2026 01:13:12 +0000 Subject: [PATCH 20/71] adding config --- src/clib/Make.config.objects | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clib/Make.config.objects b/src/clib/Make.config.objects index e3494ebdc..7870f61d5 100644 --- a/src/clib/Make.config.objects +++ b/src/clib/Make.config.objects @@ -26,6 +26,7 @@ OBJS_CONFIG_LIB = \ cool1d_multi_g.lo \ cool_multi_time_g.lo \ dust/grain_species_info.lo \ + dust_growth_and_destruction.lo \ dynamic_api.lo \ grackle_units.lo \ index_helper.lo \ From 818222dd8db600a0df72973aacb07cde9da0acae Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:10:00 +0100 Subject: [PATCH 21/71] Removing unused variable --- src/clib/dust_growth_and_destruction.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 4072f37fa..a4b3ba4d6 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -7,9 +7,6 @@ #include "utils-cpp.hpp" namespace { -const double k_boltz = 1.3806504e-16; -const double m_proton = 1.67262171e-24; -const double pi_val = 3.141592653589793; const double sec_per_year = 3.155e7; const double tiny_value = 1.0e-20; @@ -233,4 +230,4 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, } } } -} \ No newline at end of file +} From b1c99f112ba2c15f2c8538ffea3ef5f285df6dcd Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:11:33 +0100 Subject: [PATCH 22/71] Update solve_rate_cool.cpp --- src/clib/solve_rate_cool.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index d0ee4ee72..69738f8aa 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -929,6 +929,13 @@ int solve_rate_cool( ); } + // TEMPORARY: dust growth/destruction is currently invoked here as its + // own block. Eventually, the growth and destruction rates should be + // computed alongside the other dust rates (stored together in the + // newly-created FullRxnRateBuf), and the dust density updates should + // happen alongside the other density updates rather than as a + // separate pass. The placement below is a short-term stopgap and + // will be restructured in the future. if (my_chemistry->dust_model == 1){ // Calculate dust growth rates and store in growth_dM array grackle::impl::dust_growth( From 11e46c850915218000e18bc4808766026071ea8f Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:32:28 +0100 Subject: [PATCH 23/71] Update cool1d_multi_g.cpp --- src/clib/cool1d_multi_g.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index a0de75c5b..579a0242e 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -29,7 +29,6 @@ #include "inject_model/grain_metal_inject_pathways.hpp" #include "internal_types.hpp" #include "utils-cpp.hpp" -#include "dust_growth_and_destruction.hpp" void grackle::impl::cool1d_multi_g( int imetal, int iter, double* edot, double* tgas, double* mmw, double* p2d, From 7d674987c0a16cf3dc8bd23dca6150ed282cc843 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:35:39 +0100 Subject: [PATCH 24/71] Move src/clib/dust_growth_and_destruction.cpp to src/clib/dust/dust_growth_and_destruction.cpp --- src/clib/{ => dust}/dust_growth_and_destruction.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/clib/{ => dust}/dust_growth_and_destruction.cpp (100%) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp similarity index 100% rename from src/clib/dust_growth_and_destruction.cpp rename to src/clib/dust/dust_growth_and_destruction.cpp From e369a98ee2ba7e233fae12a92f8a5d12c0882415 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:36:04 +0100 Subject: [PATCH 25/71] Move src/clib/dust_growth_and_destruction.F to src/clib/dust/dust_growth_and_destruction.F --- src/clib/{ => dust}/dust_growth_and_destruction.F | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/clib/{ => dust}/dust_growth_and_destruction.F (100%) diff --git a/src/clib/dust_growth_and_destruction.F b/src/clib/dust/dust_growth_and_destruction.F similarity index 100% rename from src/clib/dust_growth_and_destruction.F rename to src/clib/dust/dust_growth_and_destruction.F From c932db9a91eeb83953747e7f01d15e95864b91f8 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:37:02 +0100 Subject: [PATCH 26/71] Move src/clib/dust_growth_and_destruction.hpp to src/clib/dust/dust_growth_and_destruction.hpp --- src/clib/{ => dust}/dust_growth_and_destruction.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/clib/{ => dust}/dust_growth_and_destruction.hpp (97%) diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp similarity index 97% rename from src/clib/dust_growth_and_destruction.hpp rename to src/clib/dust/dust_growth_and_destruction.hpp index e2934192e..76e69ebd3 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -39,4 +39,4 @@ void dust_update( } // namespace grackle::impl -#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP From 2d41adfaec066f76086b8617ac202a82db927426 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:41:26 +0100 Subject: [PATCH 27/71] Update CMakeLists.txt --- src/clib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/CMakeLists.txt b/src/clib/CMakeLists.txt index ef0aad20b..116d5e05a 100644 --- a/src/clib/CMakeLists.txt +++ b/src/clib/CMakeLists.txt @@ -94,7 +94,7 @@ add_library(Grackle_Grackle auto_general.hpp # C++ Source (and Private Header Files) - dust_growth_and_destruction.cpp dust_growth_and_destruction.hpp + dust/dust_growth_and_destruction.cpp dust/dust_growth_and_destruction.hpp calc_tdust_3d.cpp calc_tdust_3d.h calc_temp_cloudy_g.cpp calc_temp_cloudy_g.h calc_temp1d_cloudy_g.cpp calc_temp1d_cloudy_g.hpp From f65217c244c07e1a6178403eb363516d43d59cf6 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:42:16 +0100 Subject: [PATCH 28/71] Update Make.config.objects --- src/clib/Make.config.objects | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/Make.config.objects b/src/clib/Make.config.objects index 7870f61d5..6b2357e08 100644 --- a/src/clib/Make.config.objects +++ b/src/clib/Make.config.objects @@ -26,7 +26,7 @@ OBJS_CONFIG_LIB = \ cool1d_multi_g.lo \ cool_multi_time_g.lo \ dust/grain_species_info.lo \ - dust_growth_and_destruction.lo \ + dust/dust_growth_and_destruction.lo \ dynamic_api.lo \ grackle_units.lo \ index_helper.lo \ From 40888f2b7c7fa02ea54653dfad548a4ead4c6c68 Mon Sep 17 00:00:00 2001 From: Harrison Lo <131278943+aqua-hl@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:43:20 +0100 Subject: [PATCH 29/71] Update solve_rate_cool.cpp --- src/clib/solve_rate_cool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 69738f8aa..bae56dafb 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -37,7 +37,7 @@ #include "cool1d_multi_g.hpp" #include "scale_fields.hpp" #include "solve_rate_cool.hpp" -#include "dust_growth_and_destruction.hpp" +#include "dust/dust_growth_and_destruction.hpp" /// overrides the subcycle timestep (for each index in the index-range that is /// selected by the given itmask) with the maximum allowed heating/cooling From c72d8c3831b3b56fd555951c9ae93ab7be91c222 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Thu, 27 Nov 2025 08:40:57 +0000 Subject: [PATCH 30/71] Adding dust growth and destruction --- src/clib/dust_growth_and_destruction.cpp | 0 src/clib/dust_growth_and_destruction.hpp | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/clib/dust_growth_and_destruction.cpp create mode 100644 src/clib/dust_growth_and_destruction.hpp diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp new file mode 100644 index 000000000..e69de29bb From 339e88585a18fa27d466504a3c11c7ec4047f28f Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 2 Dec 2025 16:19:34 +0800 Subject: [PATCH 31/71] Adding parameters required --- src/clib/grackle_chemistry_data_fields.def | 8 ++++++++ src/include/grackle_chemistry_data.h | 9 +++++++++ src/include/grackle_types.h | 2 ++ 3 files changed, 19 insertions(+) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 7c2887df9..6fc6d82ab 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -333,3 +333,11 @@ ENTRY(omp_nthreads, INT, 0) * 3: Newton-Raphson-only */ ENTRY(solver_method, INT, 1) + +/* Li et al 2019 dust model parameters*/ +ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) +ENTRY(sne_coeff, DOUBLE, 1.0) +ENTRY(sne_shockspeed, DOUBLE, 1.0e2) +ENTRY(dust_grainsize, DOUBLE, 1.0e-1) +ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) +ENTRY(dust_growth_tauref, DOUBLE, 1.0) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 499de6095..1abacc094 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,15 @@ typedef struct */ int solver_method; + /* flag and parameters for Li+ 2019 dust growth and destruction */ + double dust_destruction_eff; + double sne_coeff; + double sne_shockspeed; + double dust_grainsize; + double dust_growth_densref; + double dust_growth_tauref; + double SolarAbundances[NUM_METAL_SPECIES_GRACKLE]; + } chemistry_data; /***************************** diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index e65e9f81c..ab14cb783 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -203,6 +203,8 @@ typedef struct gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; + // dust model parameter + gr_float *SNe_ThisTimeStep; } grackle_field_data; From 10089fd02c7d06c62749fe3ab3ee2ada0cb729d4 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 13:41:23 +0000 Subject: [PATCH 32/71] New dust model v1.0 --- src/clib/dust_growth_and_destruction.cpp | 193 ++++++++++++++++++++++ src/clib/dust_growth_and_destruction.hpp | 36 ++++ src/clib/field_data_misc_fdatamembers.def | 3 + src/include/grackle_chemistry_data.h | 1 - 4 files changed, 232 insertions(+), 1 deletion(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index e69de29bb..374d355b6 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include "dust_growth_and_destruction.hpp" + +namespace { + const double k_boltz = 1.3806504e-16; + const double m_proton = 1.67262171e-24; + const double pi_val = 3.141592653589793; + const double sec_per_year = 3.155e7; + + const double tiny_value = 1.0e-20; + const double huge_value = 1.0e+20; + + const double t_ref = 20; + +} + +// ========================================== +// DUST GROWTH (ACCRETION) +// ========================================== +void grackle::impl::dust_growth( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + double dens_proper = internalu.density_units * internalu.a_value**3; + double dt = dt_value; + double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; + + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal= metal(i,idx_range.j,idx_range.k); + + double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); + double tau_accr = huge_value; + tau_accr = std::min(tau_accr0*(my_chemistry->SolarMetalFractionByMass/rho_metal),tau_accr); + double total_density_init = rho_metal + rho_dust; + double dM = 0; + dM = dM + std::min((1 - rho_dust/total_density_init)*(rho_dust/tau_accr)*dt, (total_density_init - rho_dust)); + double dM_tau_accr = dM; + + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + + } +} + +// ========================================== +// 2. DUST DESTRUCTION (SNe + SPUTTERING) +// ========================================== +void grackle::impl::dust_destruction( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View sne( + my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + double dt = dt_value; + double dens_proper = internalu.density_units * internalu.a_value**3; + + double Ms100 = 6800.0 * my_chemistry->sne_coeff + * (100.0 / my_chemistry->sne_shockspeed) + * (100.0 / my_chemistry->sne_shockspeed) + * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double tau_dest = 0; + double sne_this = sne(i,idx_range.j,idx_range.k); + + double total_density_init = rho_metal + rho_dust; + double dM = 0; + + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + } + + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + double dM_shock = 0.0; + if (sne_this <= 0) { + dM_shock = 0.0; + } else { + dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); + } + if (dM_shock >= rho_dust) { + if (dM_shock > rho_dust) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; + dM_shock = std::min(dM_shock, rho_dust); + } + dM = dM - rho_dust * dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, "<< dM << std::endl; + } + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } +} diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index e69de29bb..6a1971177 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -0,0 +1,36 @@ +#ifndef DUST_GROWTH_AND_DESTRUCTION_HPP +#define DUST_GROWTH_AND_DESTRUCTION_HPP + +#include "grackle_types.h" +#include "grackle_chemistry_data.h" +#include "index_helper.h" +#include "internal_units.h" +#include "phys_constants.h" + +namespace grackle::impl { + +// Calculates and applies dust growth (accretion) onto grain surfaces. +void dust_growth( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust +); + +// Calculates and applies dust destruction from SNe shocks and thermal sputtering. +void dust_destruction( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + double dt_value, + double* t_gas, + double* t_dust +); + +} + +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 3832322b2..5474b9cb1 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -79,3 +79,6 @@ ENTRY(Al2O3_dust_temperature) ENTRY(ref_org_dust_temperature) ENTRY(vol_org_dust_temperature) ENTRY(H2O_ice_dust_temperature) + + // dust model parameter + ENTRY(SNe_ThisTimeStep) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 1abacc094..9dcb9c31a 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -319,7 +319,6 @@ typedef struct double dust_grainsize; double dust_growth_densref; double dust_growth_tauref; - double SolarAbundances[NUM_METAL_SPECIES_GRACKLE]; } chemistry_data; From 71db68c25827019589dfb08b0b45c88b21ea7298 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 14:04:43 +0000 Subject: [PATCH 33/71] New dust model v1.1 --- src/clib/dust_growth_and_destruction.cpp | 6 ++---- src/clib/dust_growth_and_destruction.hpp | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 374d355b6..fa47ed942 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -26,8 +26,7 @@ void grackle::impl::dust_growth( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust) + double* t_gas) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -97,8 +96,7 @@ void grackle::impl::dust_destruction( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust) + double* t_gas) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 6a1971177..9411e4b61 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -16,8 +16,7 @@ void dust_growth( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust + double* t_gas ); // Calculates and applies dust destruction from SNe shocks and thermal sputtering. @@ -27,8 +26,7 @@ void dust_destruction( InternalGrUnits internalu, IndexRange idx_range, double dt_value, - double* t_gas, - double* t_dust + double* t_gas ); } From 57a0614eb1275f206902dfd3d26f178b1f2b3e4e Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Tue, 16 Dec 2025 18:17:36 +0000 Subject: [PATCH 34/71] New dust model v1.2 --- src/clib/CMakeLists.txt | 1 + src/clib/cool1d_multi_g.cpp | 70 +++++++++++++++++++++--- src/clib/cool1d_multi_g.hpp | 3 +- src/clib/cool_multi_time.cpp | 4 +- src/clib/dust_growth_and_destruction.cpp | 44 ++++++++------- src/clib/dust_growth_and_destruction.hpp | 10 ++-- src/clib/solve_rate_cool.cpp | 3 +- src/clib/time_deriv_0d.hpp | 4 +- 8 files changed, 104 insertions(+), 35 deletions(-) diff --git a/src/clib/CMakeLists.txt b/src/clib/CMakeLists.txt index fa0f6daff..b0c719044 100644 --- a/src/clib/CMakeLists.txt +++ b/src/clib/CMakeLists.txt @@ -101,6 +101,7 @@ add_library(Grackle_Grackle api/set_default_chemistry_parameters.cpp api/solve_chemistry.cpp calc_temp_cloudy.cpp calc_temp_cloudy.hpp + dust_growth_and_destruction.cpp dust_growth_and_destruction.hpp calc_temp1d_cloudy_g.cpp calc_temp1d_cloudy_g.hpp ceiling_species.hpp collisional_rate_props.cpp collisional_rate_props.hpp diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index c7950b1b2..44b03227a 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -31,6 +31,7 @@ #include "inject_model/grain_metal_inject_pathways.hpp" #include "internal_types.hpp" #include "utils-cpp.hpp" +#include "dust_growth_and_destruction.hpp" void grackle::impl::cool1d_multi_g( int imetal, int iter, double* edot, double* tgas, double* mmw, double* p2d, @@ -42,7 +43,8 @@ void grackle::impl::cool1d_multi_g( grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf) { + grackle::impl::CoolHeatScratchBuf coolingheating_buf, + double* dtit) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1097,13 +1099,67 @@ void grackle::impl::cool1d_multi_g( itmask_metal[i] = MASK_FALSE; } } + // Compute grain size increment + if ((my_chemistry->use_dust_density_field > 0) && + (my_chemistry->dust_species > 0)) { + grackle::impl::calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, my_fields, + internal_dust_prop_buf); + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, dtit, tgas, true); + } + + // Calculate dust to gas ratio AND interstellar radiation field + // -> an earlier version of this logic would store values @ indices + // where `itmask_metal(i) .ne. MASK_FALSE` + // -> this was undesirable, b/c these quantities are required for + // photo-electric heating, which can occur when + // `itmask_metal(i) .eq. MASK_FALSE` (we can revisit this choice + // later). Moreover, in most cases, these calculations will be + // faster when there is no branching + + if ((anydust != MASK_FALSE) || (my_chemistry->photoelectric_heating > 0)) { + if (my_chemistry->use_dust_density_field > 0) { + for (i = idx_range.i_start; i <= idx_range.i_end; i++) { + // REMINDER: use of `itmask` over `itmask_metal` is + // currently required by Photo-electric heating + if (itmask[i] != MASK_FALSE) { + // it may be faster to remove this branching + dust2gas[i] = dust(i, idx_range.j, idx_range.k) / + d(i, idx_range.j, idx_range.k); + } + } + } else { + for (i = idx_range.i_start; i <= idx_range.i_end; i++) { + dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; + } + } + } - dust_related_props(anydust, tgas, cool1dmulti_buf.mynh, metallicity, itmask, - itmask_metal, my_chemistry, my_rates, my_fields, internalu, - idx_range, logTlininterp_buf, comp2, dust2gas, tdust, - grain_temperatures, gasgr.data(), gas_grainsp_heatrate, - kappa_tot.data(), grain_kappa, cool1dmulti_buf.gasgr_tdust, - myisrf.data(), internal_dust_prop_buf); + if ((anydust != MASK_FALSE) || (my_chemistry->photoelectric_heating > 1)) { + if (my_chemistry->use_isrf_field > 0) { + for (i = idx_range.i_start; i <= idx_range.i_end; i++) { + myisrf[i] = isrf_habing(i, idx_range.j, idx_range.k); + } + } else { + for (i = idx_range.i_start; i <= idx_range.i_end; i++) { + myisrf[i] = my_chemistry->interstellar_radiation_field; + } + } + } + + // compute dust temperature and cooling due to dust + if (anydust != MASK_FALSE) { + // TODO: trad -> comp2 + grackle::impl::fortran_wrapper::calc_all_tdust_gasgr_1d_g( + comp2, tgas, tdust, metallicity, dust2gas, cool1dmulti_buf.mynh, + cool1dmulti_buf.gasgr_tdust, itmask_metal, coolunit, gasgr.data(), + myisrf.data(), kappa_tot.data(), my_chemistry, my_rates, my_fields, + idx_range, grain_temperatures, gas_grainsp_heatrate, grain_kappa, + logTlininterp_buf, internal_dust_prop_buf); + } // Calculate dust cooling rate if (anydust != MASK_FALSE) { diff --git a/src/clib/cool1d_multi_g.hpp b/src/clib/cool1d_multi_g.hpp index dc1c5c388..aca92647d 100644 --- a/src/clib/cool1d_multi_g.hpp +++ b/src/clib/cool1d_multi_g.hpp @@ -96,7 +96,8 @@ void cool1d_multi_g(int imetal, int iter, double* edot, double* tgas, grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf); + grackle::impl::CoolHeatScratchBuf coolingheating_buf, + double* dtit); }; // namespace grackle::impl diff --git a/src/clib/cool_multi_time.cpp b/src/clib/cool_multi_time.cpp index 1627ef42c..4b1c343f2 100644 --- a/src/clib/cool_multi_time.cpp +++ b/src/clib/cool_multi_time.cpp @@ -67,7 +67,7 @@ void cool_multi_time( // used in a number of different internal routines. Sorting these into // additional structs (or leaving them free-standing) will become more // obvious as we transcribe more routines. - + std::vector dtit(my_fields->grid_dimension[0]); std::vector p2d(my_fields->grid_dimension[0]); std::vector tgas(my_fields->grid_dimension[0]); std::vector mmw(my_fields->grid_dimension[0]); @@ -104,7 +104,7 @@ void cool_multi_time( dust2gas.data(), rhoH.data(), itmask.data(), itmask_metal.data(), my_chemistry, my_rates, my_fields, my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, cool1dmulti_buf, - coolingheating_buf + coolingheating_buf, dtit.data() ); // Compute the cooling time on the slice diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index fa47ed942..6785d805a 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -3,6 +3,8 @@ #include #include #include "dust_growth_and_destruction.hpp" +#include "internal_types.hpp" +#include "utils-cpp.hpp" namespace { const double k_boltz = 1.3806504e-16; @@ -25,8 +27,9 @@ void grackle::impl::dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas) + double* dt_value, + double* t_gas, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -38,8 +41,7 @@ void grackle::impl::dust_growth( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dens_proper = internalu.density_units * internalu.a_value**3; - double dt = dt_value; + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; @@ -47,9 +49,10 @@ void grackle::impl::dust_growth( for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { double rho_gas = d(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); double tau_accr = huge_value; @@ -79,11 +82,11 @@ void grackle::impl::dust_growth( if (std::abs(total_density_final - total_density_init) > 1e-8){ std::exit(21); } - - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; - + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } } } @@ -95,8 +98,9 @@ void grackle::impl::dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas) + double* dt_value, + double* t_gas, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -111,8 +115,7 @@ void grackle::impl::dust_destruction( my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dt = dt_value; - double dens_proper = internalu.density_units * internalu.a_value**3; + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double Ms100 = 6800.0 * my_chemistry->sne_coeff * (100.0 / my_chemistry->sne_shockspeed) @@ -123,11 +126,12 @@ void grackle::impl::dust_destruction( for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { double rho_gas = d(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal = metal(i,idx_range.j,idx_range.k); - double tau_dest = 0; double sne_this = sne(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; double total_density_init = rho_metal + rho_dust; double dM = 0; @@ -184,8 +188,10 @@ void grackle::impl::dust_destruction( std::exit(21); } - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = rho_dust; + metal(i,idx_range.j,idx_range.k) = rho_metal; + d(i,idx_range.j,idx_range.k) = rho_gas; + } } } diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 9411e4b61..ccff2e512 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -15,8 +15,9 @@ void dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas + double* dt_value, + double* t_gas, + bool dryrun ); // Calculates and applies dust destruction from SNe shocks and thermal sputtering. @@ -25,8 +26,9 @@ void dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - double dt_value, - double* t_gas + double* dt_value, + double* t_gas, + bool dryrun ); } diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 91e4c89e3..9136281b5 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -803,7 +803,8 @@ int solve_rate_cool( *my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, - cool1dmulti_buf, coolingheating_buf + cool1dmulti_buf, coolingheating_buf, + dtit.data() ); if (my_chemistry->primordial_chemistry > 0) { diff --git a/src/clib/time_deriv_0d.hpp b/src/clib/time_deriv_0d.hpp index 1527e31c2..a0ddf3f79 100644 --- a/src/clib/time_deriv_0d.hpp +++ b/src/clib/time_deriv_0d.hpp @@ -103,6 +103,7 @@ void drop_MainScratchBuf(MainScratchBuf* ptr) { /// MainScratchBuf and track pointers to previously allocated memory buffer /// for all cases struct Assorted1ElemBuf { + double dtit[1]; double p2d[1]; double tgas[1]; double tdust[1]; @@ -478,7 +479,8 @@ void derivatives( my_uvb_rates, internalu, pack.idx_range_1_element, pack.main_scratch_buf.grain_temperatures, pack.main_scratch_buf.logTlininterp_buf, pack.main_scratch_buf.cool1dmulti_buf, - pack.main_scratch_buf.coolingheating_buf + pack.main_scratch_buf.coolingheating_buf, + pack.other_scratch_buf.dtit ); } From 1556f81e32271a700888dd4d1fd0492323ac1673 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Mon, 19 Jan 2026 10:56:19 +0000 Subject: [PATCH 35/71] rebasing --- src/clib/cool1d_multi_g.cpp | 20 ++-- src/clib/cool1d_multi_g.hpp | 3 +- src/clib/cool_multi_time.cpp | 3 +- src/clib/dust_growth_and_destruction.cpp | 118 ++++++++++++++------- src/clib/dust_growth_and_destruction.hpp | 3 + src/clib/grackle_chemistry_data_fields.def | 2 +- src/clib/solve_rate_cool.cpp | 10 +- src/clib/time_deriv_0d.hpp | 4 +- src/python/examples/cooling_cell.py | 84 +++++++++++++-- 9 files changed, 179 insertions(+), 68 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 44b03227a..bdb62c1ce 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -43,8 +43,7 @@ void grackle::impl::cool1d_multi_g( grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf, - double* dtit) { + grackle::impl::CoolHeatScratchBuf coolingheating_buf) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1100,16 +1099,12 @@ void grackle::impl::cool1d_multi_g( } } // Compute grain size increment - if ((my_chemistry->use_dust_density_field > 0) && - (my_chemistry->dust_species > 0)) { - grackle::impl::calc_grain_size_increment_1d( - dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, my_fields, - internal_dust_prop_buf); - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, dtit, tgas, true); - } + // if ((my_chemistry->use_dust_density_field > 0) && + // (my_chemistry->dust_species > 0)) { + // grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( + // dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + // internal_dust_prop_buf); + // } // Calculate dust to gas ratio AND interstellar radiation field // -> an earlier version of this logic would store values @ indices @@ -1802,6 +1797,7 @@ void grackle::impl::cool1d_multi_g( } } + // Free memory grackle::impl::drop_InternalDustPropBuf(&internal_dust_prop_buf); grackle::impl::drop_GrainSpeciesCollection(&grain_kappa); diff --git a/src/clib/cool1d_multi_g.hpp b/src/clib/cool1d_multi_g.hpp index aca92647d..dc1c5c388 100644 --- a/src/clib/cool1d_multi_g.hpp +++ b/src/clib/cool1d_multi_g.hpp @@ -96,8 +96,7 @@ void cool1d_multi_g(int imetal, int iter, double* edot, double* tgas, grackle::impl::GrainSpeciesCollection grain_temperatures, grackle::impl::LogTLinInterpScratchBuf logTlininterp_buf, grackle::impl::Cool1DMultiScratchBuf cool1dmulti_buf, - grackle::impl::CoolHeatScratchBuf coolingheating_buf, - double* dtit); + grackle::impl::CoolHeatScratchBuf coolingheating_buf); }; // namespace grackle::impl diff --git a/src/clib/cool_multi_time.cpp b/src/clib/cool_multi_time.cpp index 4b1c343f2..dddf51f0f 100644 --- a/src/clib/cool_multi_time.cpp +++ b/src/clib/cool_multi_time.cpp @@ -67,7 +67,6 @@ void cool_multi_time( // used in a number of different internal routines. Sorting these into // additional structs (or leaving them free-standing) will become more // obvious as we transcribe more routines. - std::vector dtit(my_fields->grid_dimension[0]); std::vector p2d(my_fields->grid_dimension[0]); std::vector tgas(my_fields->grid_dimension[0]); std::vector mmw(my_fields->grid_dimension[0]); @@ -104,7 +103,7 @@ void cool_multi_time( dust2gas.data(), rhoH.data(), itmask.data(), itmask_metal.data(), my_chemistry, my_rates, my_fields, my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, cool1dmulti_buf, - coolingheating_buf, dtit.data() + coolingheating_buf ); // Compute the cooling time on the slice diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 6785d805a..ecff9acd2 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -27,6 +27,7 @@ void grackle::impl::dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun) @@ -47,46 +48,87 @@ void grackle::impl::dust_growth( // --- MAIN LOOP --- for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal= metal(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - - double tau_accr0 = tau_ref*(my_chemistry->dust_growth_densref/dens_proper)* std::pow(t_ref/temp,0.5); - double tau_accr = huge_value; - tau_accr = std::min(tau_accr0*(my_chemistry->SolarMetalFractionByMass/rho_metal),tau_accr); - double total_density_init = rho_metal + rho_dust; - double dM = 0; - dM = dM + std::min((1 - rho_dust/total_density_init)*(rho_dust/tau_accr)*dt, (total_density_init - rho_dust)); - double dM_tau_accr = dM; - - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal= metal(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + // fprintf(stderr,"---------------\n"); + + double tau_accr0 = tau_ref * + (my_chemistry->dust_growth_densref / dens_proper) * + std::pow(t_ref / temp, 0.5); + double rho_metal_eff = std::max(rho_metal, tiny_value); + double tau_accr = tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); + tau_accr = std::min(tau_accr, huge_value); + tau_accr = std::max(tau_accr, tiny_value); + double frac_metal_available = 0.0; + if (rho_metal <= 0.0) { + frac_metal_available = 0.0; + } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { + frac_metal_available = rho_metal / rho_dust; + } else { + frac_metal_available = rho_metal / (rho_dust + rho_metal); + } + frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); + double growth = frac_metal_available * (rho_dust / tau_accr) * dt; + double dM = std::min(growth, rho_metal); + + // fprintf(stderr, + // "internal: frac=%e growth=%e dM=%e grainsize=%e\n", + // frac_metal_available, growth, dM, my_chemistry->dust_grainsize); + + double dM_tau_accr = dM; + + + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + long double change = rho_gas; + long double dM_change = (long double)dM; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + // fprintf(stderr, + // "after dM calc dust=%e gas=%e metal=%e change=%0.18Le\n", + // rho_dust, + // rho_gas, + // rho_metal, + // (change - dM_change)/change); + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + // double total_density_final = rho_metal + rho_dust; + // if (std::abs(total_density_final - total_density_init) > 1e-8){ + // std::exit(21); + // } + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + // fprintf(stderr,"------\n"); + // fprintf(stderr, + // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", + // rho_dust, + // rho_gas, + // rho_metal); + // fprintf(stderr, + // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", + // dust(i, idx_range.j, idx_range.k), + // d(i, idx_range.j, idx_range.k), + // metal(i, idx_range.j, idx_range.k)); + } } + } } diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index ccff2e512..7d9654ac0 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -6,6 +6,8 @@ #include "index_helper.h" #include "internal_units.h" #include "phys_constants.h" +#include "fortran_func_decls.h" + namespace grackle::impl { @@ -15,6 +17,7 @@ void dust_growth( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 6fc6d82ab..92074f2e2 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -340,4 +340,4 @@ ENTRY(sne_coeff, DOUBLE, 1.0) ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) -ENTRY(dust_growth_tauref, DOUBLE, 1.0) +ENTRY(dust_growth_tauref, DOUBLE, 0.01) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 9136281b5..b811d7692 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -39,6 +39,7 @@ #include "cool1d_multi_g.hpp" #include "scale_fields.hpp" #include "solve_rate_cool.hpp" +#include "dust_growth_and_destruction.hpp" /// overrides the subcycle timestep (for each index in the index-range that is /// selected by the given itmask) with the maximum allowed heating/cooling @@ -803,8 +804,7 @@ int solve_rate_cool( *my_uvb_rates, internalu, idx_range, grain_temperatures, logTlininterp_buf, - cool1dmulti_buf, coolingheating_buf, - dtit.data() + cool1dmulti_buf, coolingheating_buf ); if (my_chemistry->primordial_chemistry > 0) { @@ -912,6 +912,9 @@ int solve_rate_cool( } + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), false); + // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row ttmin = huge8; @@ -926,6 +929,9 @@ int solve_rate_cool( // If all cells are done (in idx_range), break out of subcycle loop if (std::fabs(dt-ttmin) < tolerance*dt) { break; } + // grackle::impl::dust_growth( + // my_chemistry, my_fields, internalu, idx_range, dtit.data(), tgas.data(), false); + } // subcycle iteration loop (for current idx_range) // review number of iterations that were spent in the subcycle loop diff --git a/src/clib/time_deriv_0d.hpp b/src/clib/time_deriv_0d.hpp index a0ddf3f79..1527e31c2 100644 --- a/src/clib/time_deriv_0d.hpp +++ b/src/clib/time_deriv_0d.hpp @@ -103,7 +103,6 @@ void drop_MainScratchBuf(MainScratchBuf* ptr) { /// MainScratchBuf and track pointers to previously allocated memory buffer /// for all cases struct Assorted1ElemBuf { - double dtit[1]; double p2d[1]; double tgas[1]; double tdust[1]; @@ -479,8 +478,7 @@ void derivatives( my_uvb_rates, internalu, pack.idx_range_1_element, pack.main_scratch_buf.grain_temperatures, pack.main_scratch_buf.logTlininterp_buf, pack.main_scratch_buf.cool1dmulti_buf, - pack.main_scratch_buf.coolingheating_buf, - pack.other_scratch_buf.dtit + pack.main_scratch_buf.coolingheating_buf ); } diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index a6b9dd045..8da0affe3 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,25 +66,93 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 0.1 # Solar + metallicity = 0.01 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} my_chemistry = chemistry_data() my_chemistry.use_grackle = 1 - my_chemistry.with_radiative_cooling = 1 - my_chemistry.primordial_chemistry = 0 - my_chemistry.metal_cooling = 1 - my_chemistry.UVbackground = 1 my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "CloudyData_UVB=HM2012.h5") + os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=4, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=1, + multi_metals=0, + metal_abundances=0, + dust_species=3, + # dust_temperature_multi=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + HydrogenFractionByMass=0.76, + DeuteriumToHydrogenRatio=6.8e-05, + SolarMetalFractionByMass=0.01295, + local_dust_to_gas_ratio=0.009387, + NumberOfTemperatureBins=600, + CaseBRecombination=1, + TemperatureStart=1.0, + TemperatureEnd=1.0e9, + NumberOfDustTemperatureBins=250, + DustTemperatureStart=1.0, + DustTemperatureEnd=1500.0, + Compton_xray_heating=0, + LWbackground_sawtooth_suppression=0, + LWbackground_intensity=0, + UVbackground_redshift_on=-99999, + UVbackground_redshift_off=-99999, + UVbackground_redshift_fullon=-99999, + UVbackground_redshift_drop=-99999, + cloudy_electron_fraction_factor=0.00915396, + use_radiative_transfer=1, + radiative_transfer_coupled_rate_solver=0, + radiative_transfer_intermediate_step=0, + radiative_transfer_hydrogen_only=0, + self_shielding_method=0, + H2_custom_shielding=0, + H2_self_shielding=0, + radiative_transfer_H2II_diss=1, + radiative_transfer_HDI_dissociation=1, + radiative_transfer_metal_ionization=1, + radiative_transfer_metal_dissociation=1, + + use_multiple_dust_temperatures=1 + ) output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 1e6 # K + temperature = 50000 # K final_time = 100. # Myr # Set units @@ -121,4 +189,4 @@ def main(args=None): if __name__ == '__main__': - sys.exit(main()) + sys.exit(main()) \ No newline at end of file From 377ae975ab3a833a42bdbd91cdb88ead091fa6fe Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Thu, 29 Jan 2026 00:18:53 +0000 Subject: [PATCH 36/71] saving changes before rebase --- src/clib/dust_growth_and_destruction.cpp | 177 ++++++++++-------- src/clib/dust_growth_and_destruction.hpp | 12 ++ src/clib/grackle_chemistry_data_fields.def | 4 +- src/clib/solve_rate_cool.cpp | 2 - src/python/examples/cooling_cell.py | 15 +- src/python/gracklepy/utilities/convenience.py | 3 + 6 files changed, 121 insertions(+), 92 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index ecff9acd2..5230d8adb 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -55,7 +55,6 @@ void grackle::impl::dust_growth( double rho_metal= metal(i,idx_range.j,idx_range.k); double temp = t_gas[i]; double dt = dt_value[i]; - // fprintf(stderr,"---------------\n"); double tau_accr0 = tau_ref * (my_chemistry->dust_growth_densref / dens_proper) * @@ -76,9 +75,6 @@ void grackle::impl::dust_growth( double growth = frac_metal_available * (rho_dust / tau_accr) * dt; double dM = std::min(growth, rho_metal); - // fprintf(stderr, - // "internal: frac=%e growth=%e dM=%e grainsize=%e\n", - // frac_metal_available, growth, dM, my_chemistry->dust_grainsize); double dM_tau_accr = dM; @@ -104,6 +100,10 @@ void grackle::impl::dust_growth( // rho_metal, // (change - dM_change)/change); rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + + // fprintf(stderr, + // "internal: frac=%e growth=%e dM=%e grainsize=%e gas=%e dust=%e metal=%e\n", + // frac_metal_available, growth, dM, my_chemistry->dust_grainsize, rho_gas, rho_dust, rho_metal); if (rho_dust < 0) { std::exit(21); } @@ -113,19 +113,10 @@ void grackle::impl::dust_growth( // } if (dryrun == false) { dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - // fprintf(stderr,"------\n"); - // fprintf(stderr, - // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", - // rho_dust, - // rho_gas, - // rho_metal); - // fprintf(stderr, - // "dust=%0.18Le gas=%0.18Le metal=%0.18Le\n", - // dust(i, idx_range.j, idx_range.k), - // d(i, idx_range.j, idx_range.k), - // metal(i, idx_range.j, idx_range.k)); + + } } @@ -140,6 +131,7 @@ void grackle::impl::dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun) @@ -166,74 +158,99 @@ void grackle::impl::dust_destruction( // --- MAIN LOOP --- for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); - double sne_this = sne(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - double tau_dest = 0; - - double total_density_init = rho_metal + rho_dust; - double dM = 0; - - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; - } + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double sne_this = sne(i,idx_range.j,idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - double dM_shock = 0.0; - if (sne_this <= 0) { - dM_shock = 0.0; - } else { - dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); - } - if (dM_shock >= rho_dust) { - if (dM_shock > rho_dust) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + double total_density_init = rho_metal + rho_dust; + double dM = 0; + + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; - dM_shock = std::min(dM_shock, rho_dust); - } - dM = dM - rho_dust * dM_shock; - if (std::isnan(dM)) { - std::cout << "dM calculated as NaN, "<< dM << std::endl; - } - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = rho_dust; - metal(i,idx_range.j,idx_range.k) = rho_metal; - d(i,idx_range.j,idx_range.k) = rho_gas; + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + double dM_shock = 0.0; + if (sne_this <= 0) { + dM_shock = 0.0; + } else { + dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); + } + if (dM_shock >= rho_dust) { + if (dM_shock > rho_dust) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; + dM_shock = std::min(dM_shock, rho_dust); + } + dM = dM - rho_dust * dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, "<< dM << std::endl; + } + // recalculate metallicity + dM = std::max(-1*rho_dust, dM); + dM = std::min(0.9*rho_metal, dM); + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM; + rho_metal = rho_metal - dM; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); + if (rho_dust < 0) { + std::exit(21); + } + double total_density_final = rho_metal + rho_dust; + if (std::abs(total_density_final - total_density_init) > 1e-8){ + std::exit(21); + } + + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + } } } } + +void grackle::impl::dust_update( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + const gr_mask_type* itmask, + double growth_rate, + double* dt) +{ + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + // recalculate metallicity + +} \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 7d9654ac0..293b193b1 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -29,11 +29,23 @@ void dust_destruction( grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, double* dt_value, double* t_gas, bool dryrun ); +// Update the density field. +void dust_update( + chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, + IndexRange idx_range, + const gr_mask_type* itmask, + double growth_rate, + double*dt_value +); + } #endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 92074f2e2..c3a8e4e22 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,7 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 1) +ENTRY(solver_method, INT, 3) /* Li et al 2019 dust model parameters*/ ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) @@ -340,4 +340,4 @@ ENTRY(sne_coeff, DOUBLE, 1.0) ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) -ENTRY(dust_growth_tauref, DOUBLE, 0.01) +ENTRY(dust_growth_tauref, DOUBLE, 0.004) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index b811d7692..1d8128cb0 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -929,8 +929,6 @@ int solve_rate_cool( // If all cells are done (in idx_range), break out of subcycle loop if (std::fabs(dt-ttmin) < tolerance*dt) { break; } - // grackle::impl::dust_growth( - // my_chemistry, my_fields, internalu, idx_range, dtit.data(), tgas.data(), false); } // subcycle iteration loop (for current idx_range) diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index 8da0affe3..b12c45760 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,7 +66,7 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 0.01 # Solar + metallicity = 10**-3 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} @@ -87,7 +87,7 @@ def set_opts(obj, **kwargs): my_chemistry, with_radiative_cooling=1, - primordial_chemistry=4, + primordial_chemistry=1, dust_chemistry = 1, metal_cooling=1, UVbackground=0, @@ -95,11 +95,11 @@ def set_opts(obj, **kwargs): Gamma=1.66667, h2_on_dust=1, use_dust_density_field=1, - metal_chemistry=1, + metal_chemistry=0, multi_metals=0, metal_abundances=0, - dust_species=3, - # dust_temperature_multi=0, + dust_species=0, + use_multiple_dust_temperatures=0, dust_sublimation=1, grain_growth=0, photoelectric_heating=0, @@ -143,16 +143,15 @@ def set_opts(obj, **kwargs): radiative_transfer_H2II_diss=1, radiative_transfer_HDI_dissociation=1, radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1, + radiative_transfer_metal_dissociation=1 - use_multiple_dust_temperatures=1 ) output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 50000 # K + temperature = 15000 # K final_time = 100. # Myr # Set units diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 195c786a6..747865005 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -33,6 +33,9 @@ def check_convergence(fc1, fc2, fields=None, tol=0.01): for field in fields: if field not in fc2: continue + if fc1[field] == 0: + print(field) + print(fc2[field]) convergence = np.max(np.abs(fc1[field] - fc2[field]) / fc1[field]) if convergence > max_val: max_val = convergence From ffc73b41eb29a5bebcbe3bda8de4395cdf1155f9 Mon Sep 17 00:00:00 2001 From: Harrison Lo Date: Mon, 16 Feb 2026 22:54:19 +0800 Subject: [PATCH 37/71] adding dust into d(gas) v0.0 --- src/clib/cool1d_multi_g.cpp | 2 +- src/clib/dust/calc_tdust_3d.cpp | 3 +- src/clib/dust_growth_and_destruction.cpp | 195 +++++++++--------- src/clib/dust_growth_and_destruction.hpp | 18 +- src/clib/field_data_misc_fdatamembers.def | 2 +- src/clib/grackle_chemistry_data_fields.def | 5 +- src/clib/make_consistent.cpp | 11 +- src/clib/solve_rate_cool.cpp | 18 +- src/include/grackle_chemistry_data.h | 3 + src/include/grackle_types.h | 4 +- src/python/examples/cooling_cell.py | 71 +++---- src/python/gracklepy/fluid_container.py | 2 + src/python/gracklepy/grackle_defs.pxd | 1 + src/python/gracklepy/grackle_wrapper.pyx | 2 + src/python/gracklepy/utilities/convenience.py | 12 +- src/python/gracklepy/utilities/evolve.py | 16 +- 16 files changed, 206 insertions(+), 159 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index bdb62c1ce..385c050fb 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1123,7 +1123,7 @@ void grackle::impl::cool1d_multi_g( if (itmask[i] != MASK_FALSE) { // it may be faster to remove this branching dust2gas[i] = dust(i, idx_range.j, idx_range.k) / - d(i, idx_range.j, idx_range.k); + (d(i, idx_range.j, idx_range.k)); } } } else { diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 197561653..68b6d6289 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -207,7 +207,8 @@ void calc_tdust_3d( // endif if (my_chemistry->use_dust_density_field > 0) { - dust2gas[i] = dust(i,j,k) / d(i,j,k); + dust2gas[i] = dust(i,j,k) / (d(i,j,k)); + // dust2gas[i] = dust(i,j,k) / (d(i,j,k) - dust(i,j,k)); } else { dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; } diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 5230d8adb..b9f1f8eda 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -30,7 +30,7 @@ void grackle::impl::dust_growth( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun) + double* growth_dM) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -41,15 +41,18 @@ void grackle::impl::dust_growth( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - + double dens_proper = internalu.urho * std::pow(internalu.a_value,3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; // --- MAIN LOOP --- - for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + growth_dM[i] = 0.0; + if (itmask[i] != MASK_FALSE) { - + double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); @@ -72,52 +75,15 @@ void grackle::impl::dust_growth( frac_metal_available = rho_metal / (rho_dust + rho_metal); } frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); - double growth = frac_metal_available * (rho_dust / tau_accr) * dt; - double dM = std::min(growth, rho_metal); - - - double dM_tau_accr = dM; + double growth_rate = frac_metal_available * (rho_dust / tau_accr); + double dM = std::min(growth_rate, rho_metal/dt); + // Store the calculated mass change in the output array + growth_dM[i] = dM; - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - long double change = rho_gas; - long double dM_change = (long double)dM; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - // fprintf(stderr, - // "after dM calc dust=%e gas=%e metal=%e change=%0.18Le\n", - // rho_dust, - // rho_gas, - // rho_metal, - // (change - dM_change)/change); - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - // fprintf(stderr, - // "internal: frac=%e growth=%e dM=%e grainsize=%e gas=%e dust=%e metal=%e\n", - // frac_metal_available, growth, dM, my_chemistry->dust_grainsize, rho_gas, rho_dust, rho_metal); - if (rho_dust < 0) { - std::exit(21); - } - // double total_density_final = rho_metal + rho_dust; - // if (std::abs(total_density_final - total_density_init) > 1e-8){ - // std::exit(21); - // } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - - - } + // "internal: frac=%.10e growth_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", + // frac_metal_available, dM, rho_gas, rho_dust, rho_metal); } } @@ -134,7 +100,7 @@ void grackle::impl::dust_destruction( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun) + double* destruction_dM) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -145,8 +111,10 @@ void grackle::impl::dust_destruction( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( - my_fields->SNe_ThisTimeStep, my_fields->grid_dimension[0], + use_sne ? my_fields->sne_rate : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); double dens_proper = internalu.urho * std::pow(internalu.a_value,3); @@ -157,77 +125,59 @@ void grackle::impl::dust_destruction( * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); // --- MAIN LOOP --- - for (int i = idx_range.i_start; i <= idx_range.i_end; i++) { + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + destruction_dM[i] = 0.0; + if (itmask[i] != MASK_FALSE) { double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal = metal(i,idx_range.j,idx_range.k); - double sne_this = sne(i,idx_range.j,idx_range.k); + double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; double tau_dest = 0; - double total_density_init = rho_metal + rho_dust; double dM = 0; + double dM_shock = 0.0; - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + if (use_sne) { + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + // dM_shock = 0.0; + } else { + tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; + dM_shock = std::min(rho_dust/tau_dest, rho_dust/dt); + } } // destruction by thermal sputtering double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) * (std::pow((2.0e6/temp),2.5)+1.0); - double dM_shock = 0.0; - if (sne_this <= 0) { - dM_shock = 0.0; - } else { - dM_shock = std::min(rho_dust/tau_dest*dt, rho_dust); - } - if (dM_shock >= rho_dust) { - if (dM_shock > rho_dust) { + if (dM_shock >= rho_dust/dt) { + if (dM_shock > rho_dust/dt) { std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; } } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0*dt; - dM_shock = std::min(dM_shock, rho_dust); + dM_shock = dM_shock + rho_dust / tau_sput *3.0; + dM_shock = std::min(dM_shock, rho_dust/dt); } - dM = dM - rho_dust * dM_shock; + //dM = - rho_dust * dM_shock; + dM = -dM_shock; if (std::isnan(dM)) { std::cout << "dM calculated as NaN, "<< dM << std::endl; } - // recalculate metallicity - dM = std::max(-1*rho_dust, dM); - dM = std::min(0.9*rho_metal, dM); - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM; - rho_metal = rho_metal - dM; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - if (rho_dust < 0) { - std::exit(21); - } - double total_density_final = rho_metal + rho_dust; - if (std::abs(total_density_final - total_density_init) > 1e-8){ - std::exit(21); - } - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - } + // Store the calculated mass change in the output array + destruction_dM[i] = dM; + // fprintf(stderr, + // "internal: tau_dest=%.10e tau_dest=%.10e dM_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", + // tau_dest, tau_sput, dM, rho_gas, rho_dust, rho_metal); } } } @@ -238,8 +188,10 @@ void grackle::impl::dust_update( InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - double growth_rate, - double* dt) + double* dt_value, + double* growth_dM, + double* destruction_dM, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -251,6 +203,53 @@ void grackle::impl::dust_update( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - // recalculate metallicity + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] != MASK_FALSE) { + + double rho_gas = d(i,idx_range.j,idx_range.k); + double rho_dust = dust(i,idx_range.j,idx_range.k); + double rho_metal = metal(i,idx_range.j,idx_range.k); + double dt = dt_value[i]; + + // Get the total mass change (growth + destruction) + + double dM_total = growth_dM[i] + destruction_dM[i]; + dM_total = dM_total * dt; + // Apply constraints to dM + dM_total = std::max(-1*rho_dust, dM_total); + dM_total = std::min(0.9*rho_metal, dM_total); + + // Apply conservation logic (from original code) + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM_total; + rho_metal = rho_metal - dM_total; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + + // Adjust gas density to conserve total mass + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Should be changed to gas_density staying constant (make_consistent.cpp) + + // Safety checks + if (rho_dust < 0) { + fprintf(stderr, "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, rho_dust); + std::exit(21); + } + + fprintf(stderr, + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e\n", + dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal); + + // Update the fields + if (dryrun == false) { + dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; + metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; + d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; + } + } + } } \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 293b193b1..29eb18390 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -11,7 +11,8 @@ namespace grackle::impl { -// Calculates and applies dust growth (accretion) onto grain surfaces. +// Calculates dust growth rates (accretion) onto grain surfaces. +// Stores the mass change dM for each cell in growth_dM array. void dust_growth( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -20,10 +21,11 @@ void dust_growth( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun + double* growth_dM // output: mass change rate for each cell ); -// Calculates and applies dust destruction from SNe shocks and thermal sputtering. +// Calculates dust destruction rates from SNe shocks and thermal sputtering. +// Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -32,18 +34,20 @@ void dust_destruction( const gr_mask_type* itmask, double* dt_value, double* t_gas, - bool dryrun + double* destruction_dM // output: mass change rate for each cell ); -// Update the density field. +// Update the density fields using calculated mass changes. void dust_update( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - double growth_rate, - double*dt_value + double* dt_value, + double* growth_dM, // input: mass change from growth + double* destruction_dM, // input: mass change from destruction + bool dryrun ); } diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 5474b9cb1..2636bbd21 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -81,4 +81,4 @@ ENTRY(vol_org_dust_temperature) ENTRY(H2O_ice_dust_temperature) // dust model parameter - ENTRY(SNe_ThisTimeStep) + ENTRY(sne_rate) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index c3a8e4e22..ba6efd457 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,10 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 3) +ENTRY(solver_method, INT, 2) + +/* Flag to use snetimestep */ +ENTRY(use_sne_field, INT, 0) /* Li et al 2019 dust model parameters*/ ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 77b253893..921f0d5c9 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -265,7 +265,9 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - metalfree[i] = d(i, j, k) - metal(i, j, k); + metalfree[i] = d(i, j, k) - metal(i, j, k) - dust(i, j, k); + // if (my_chemistry->dust_species > 0) + // metalfree[i] -= dust(i, j, k); } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -527,6 +529,13 @@ void make_consistent( (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { + // if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + // ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && + // (my_chemistry->dust_species == 0 || dust(i, j, k) <= 1.e-9 * d(i, j, k)) && + // (d(i, j, k) * dom < 1.e8)) || + // (((metal(i, j, k) > 1.e-9 * d(i, j, k)) || + // (my_chemistry->dust_species > 0 && dust(i, j, k) > 1.e-9 * d(i, j, k))) && + // (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + 16. / 18. * H2O(i, j, k) + O2(i, j, k) + diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 1d8128cb0..d781f6dec 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -728,6 +728,10 @@ int solve_rate_cool( std::vector mmw(my_fields->grid_dimension[0]); std::vector edot(my_fields->grid_dimension[0]); + // Arrays to store dust growth and destruction mass changes + std::vector growth_dM(my_fields->grid_dimension[0]); + std::vector destruction_dM(my_fields->grid_dimension[0]); + // iteration masks std::vector itmask(my_fields->grid_dimension[0]); std::vector itmask_metal(my_fields->grid_dimension[0]); @@ -912,8 +916,20 @@ int solve_rate_cool( } + // Calculate dust growth rates and store in growth_dM array grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), false); + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + tgas.data(), growth_dM.data()); + + // Calculate dust destruction rates and store in destruction_dM array + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); + + // Apply the calculated rates to update density fields + grackle::impl::dust_update( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + growth_dM.data(), destruction_dM.data(), false); // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 9dcb9c31a..f8b14b0ba 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,9 @@ typedef struct */ int solver_method; + /* Flag to use snetimestep */ + int use_sne_field; + /* flag and parameters for Li+ 2019 dust growth and destruction */ double dust_destruction_eff; double sne_coeff; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index ab14cb783..f30f17e6a 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -203,8 +203,8 @@ typedef struct gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; - // dust model parameter - gr_float *SNe_ThisTimeStep; + // use_snetimestep = 1 + gr_float *sne_rate; } grackle_field_data; diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index b12c45760..19b1111c8 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,7 +66,7 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity = 10**-3 # Solar + metallicity =1 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} @@ -87,7 +87,7 @@ def set_opts(obj, **kwargs): my_chemistry, with_radiative_cooling=1, - primordial_chemistry=1, + primordial_chemistry=2, dust_chemistry = 1, metal_cooling=1, UVbackground=0, @@ -112,38 +112,38 @@ def set_opts(obj, **kwargs): cie_cooling=1, h2_optical_depth_approximation=1, ih2co=1, - ipiht=1, - - HydrogenFractionByMass=0.76, - DeuteriumToHydrogenRatio=6.8e-05, - SolarMetalFractionByMass=0.01295, - local_dust_to_gas_ratio=0.009387, - NumberOfTemperatureBins=600, - CaseBRecombination=1, - TemperatureStart=1.0, - TemperatureEnd=1.0e9, - NumberOfDustTemperatureBins=250, - DustTemperatureStart=1.0, - DustTemperatureEnd=1500.0, - Compton_xray_heating=0, - LWbackground_sawtooth_suppression=0, - LWbackground_intensity=0, - UVbackground_redshift_on=-99999, - UVbackground_redshift_off=-99999, - UVbackground_redshift_fullon=-99999, - UVbackground_redshift_drop=-99999, - cloudy_electron_fraction_factor=0.00915396, - use_radiative_transfer=1, - radiative_transfer_coupled_rate_solver=0, - radiative_transfer_intermediate_step=0, - radiative_transfer_hydrogen_only=0, - self_shielding_method=0, - H2_custom_shielding=0, - H2_self_shielding=0, - radiative_transfer_H2II_diss=1, - radiative_transfer_HDI_dissociation=1, - radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1 + ipiht=1 + + # HydrogenFractionByMass=0.76, + # DeuteriumToHydrogenRatio=6.8e-05, + # SolarMetalFractionByMass=0.01295, + # # local_dust_to_gas_ratio=0.009387, + # NumberOfTemperatureBins=600, + # CaseBRecombination=1, + # TemperatureStart=1.0, + # TemperatureEnd=1.0e9, + # NumberOfDustTemperatureBins=250, + # DustTemperatureStart=1.0, + # DustTemperatureEnd=1500.0, + # Compton_xray_heating=0, + # LWbackground_sawtooth_suppression=0, + # LWbackground_intensity=0, + # UVbackground_redshift_on=-99999, + # UVbackground_redshift_off=-99999, + # UVbackground_redshift_fullon=-99999, + # UVbackground_redshift_drop=-99999, + # cloudy_electron_fraction_factor=0.00915396, + # use_radiative_transfer=1, + # radiative_transfer_coupled_rate_solver=0, + # radiative_transfer_intermediate_step=0, + # radiative_transfer_hydrogen_only=0, + # self_shielding_method=0, + # H2_custom_shielding=0, + # H2_self_shielding=0, + # radiative_transfer_H2II_diss=1, + # radiative_transfer_HDI_dissociation=1, + # radiative_transfer_metal_ionization=1, + # radiative_transfer_metal_dissociation=1 ) @@ -151,7 +151,7 @@ def set_opts(obj, **kwargs): in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 15000 # K + temperature = 5e4 # K final_time = 100. # Myr # Set units @@ -170,6 +170,7 @@ def set_opts(obj, **kwargs): density=density, temperature=temperature, metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust state="ionized", converge=True) diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index 82e07cc9a..1a730c59e 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -301,6 +301,8 @@ def _required_extra_fields(my_chemistry): my_fields.append("H2_custom_shielding_factor") if my_chemistry.use_isrf_field == 1: my_fields.append("isrf_habing") + if my_chemistry.use_sne_field == 1: + my_fields.append("sne_rate") return my_fields def _required_calculated_fields(my_chemistry): diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 9b9215366..83a71b5b2 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -200,6 +200,7 @@ cdef extern from "grackle.h": gr_float *ref_org_dust_temperature; gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; + gr_float *sne_rate; ctypedef struct c_grackle_version "grackle_version": const char* version; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index a1f6a7a78..d66f0faf2 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -764,6 +764,8 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.vol_org_dust_temperature = get_field(fc, "vol_org_dust_temperature") my_fields.H2O_ice_dust_temperature = get_field(fc, "H2O_ice_dust_temperature") + my_fields.sne_rate = get_field(fc, "sne_rate") + return my_fields def solve_chemistry(fc, my_dt): diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 747865005..c425ebc2a 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -110,7 +110,8 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The ratio of dust mass density to total gas density. + The dust mass fraction of total density (dust/d). + TODO: rename to dust_mass_fraction for clarity. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach @@ -151,7 +152,11 @@ def setup_fluid_container(my_chemistry, fh = my_chemistry.HydrogenFractionByMass d2h = my_chemistry.DeuteriumToHydrogenRatio - metal_free = 1 - metal_mass_fraction + # d = gas + metal + dust; dust_to_gas_ratio is really dust_fraction (dust/d) + # TODO: rename dust_to_gas_ratio to dust_mass_fraction + dust_mass_fraction = dust_to_gas_ratio * (1-metal_mass_fraction) / (1 + dust_to_gas_ratio) + # metal_free = 1 - metal_mass_fraction - dust_to_gas_ratio + metal_free = 1 - metal_mass_fraction - dust_mass_fraction H_total = fh * metal_free He_total = (1 - fh) * metal_free # someday, maybe we'll include D in the total @@ -167,7 +172,8 @@ def setup_fluid_container(my_chemistry, state_vals = { "density": fc_density, "metal_density": metal_mass_fraction * fc_density, - "dust_density": dust_to_gas_ratio * fc_density + "dust_density": dust_mass_fraction * fc_density + # "dust_density": dust_to_gas_ratio * fc_density } _setup_inj_pathway_fields(state_vals, fc.inject_pathway_density_yield_fields) diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 280a3fa7a..767cdd1e1 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - ((current_time * my_chemistry.time_units / sec_per_year), - (fc["density"][0] * my_chemistry.density_units), - fc["temperature"][0])) + # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + # ((current_time * my_chemistry.time_units / sec_per_year), + # (fc["density"][0] * my_chemistry.density_units), + # fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - (current_time * my_chemistry.time_units / sec_per_year, - fc["density"][0] * my_chemistry.density_units, - fc["temperature"][0])) + # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + # (current_time * my_chemistry.time_units / sec_per_year, + # fc["density"][0] * my_chemistry.density_units, + # fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From a402f2427db288707cf7ff61cf719cfc7a01acbb Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 04:02:39 +0000 Subject: [PATCH 38/71] Fixing d and dust --- src/clib/dust_growth_and_destruction.cpp | 30 ++++++++++--------- src/clib/make_consistent.cpp | 4 +-- src/python/gracklepy/utilities/convenience.py | 13 +++----- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index b9f1f8eda..7b732d6a6 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -153,19 +153,21 @@ void grackle::impl::dust_destruction( } } - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - if (dM_shock >= rho_dust/dt) { - if (dM_shock > rho_dust/dt) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + if (temp >= std::pow(10,5)) { + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 + * (my_chemistry->dust_grainsize/0.1) + * (1.0e-27/(dens_proper * rho_gas)) + * (std::pow((2.0e6/temp),2.5)+1.0); + + if (dM_shock >= rho_dust/dt) { + if (dM_shock > rho_dust/dt) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput *3.0; + dM_shock = std::min(dM_shock, rho_dust/dt); } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0; - dM_shock = std::min(dM_shock, rho_dust/dt); } //dM = - rho_dust * dM_shock; dM = -dM_shock; @@ -240,8 +242,8 @@ void grackle::impl::dust_update( } fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e\n", - dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal); + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (dryrun == false) { diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 921f0d5c9..eb517e353 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -265,9 +265,7 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - metalfree[i] = d(i, j, k) - metal(i, j, k) - dust(i, j, k); - // if (my_chemistry->dust_species > 0) - // metalfree[i] -= dust(i, j, k); + metalfree[i] = d(i, j, k) - metal(i, j, k); } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index c425ebc2a..44cd1e78e 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -110,8 +110,7 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The dust mass fraction of total density (dust/d). - TODO: rename to dust_mass_fraction for clarity. + The dust-to-gas ratio (dust/d). Dust is independent of d. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach @@ -152,11 +151,8 @@ def setup_fluid_container(my_chemistry, fh = my_chemistry.HydrogenFractionByMass d2h = my_chemistry.DeuteriumToHydrogenRatio - # d = gas + metal + dust; dust_to_gas_ratio is really dust_fraction (dust/d) - # TODO: rename dust_to_gas_ratio to dust_mass_fraction - dust_mass_fraction = dust_to_gas_ratio * (1-metal_mass_fraction) / (1 + dust_to_gas_ratio) - # metal_free = 1 - metal_mass_fraction - dust_to_gas_ratio - metal_free = 1 - metal_mass_fraction - dust_mass_fraction + # d = gas + metal (dust is independent of d) + metal_free = 1 - metal_mass_fraction H_total = fh * metal_free He_total = (1 - fh) * metal_free # someday, maybe we'll include D in the total @@ -172,8 +168,7 @@ def setup_fluid_container(my_chemistry, state_vals = { "density": fc_density, "metal_density": metal_mass_fraction * fc_density, - "dust_density": dust_mass_fraction * fc_density - # "dust_density": dust_to_gas_ratio * fc_density + "dust_density": dust_to_gas_ratio * fc_density } _setup_inj_pathway_fields(state_vals, fc.inject_pathway_density_yield_fields) From 38769d7fbc5b6da0716d184ece9da38e397123ed Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 04:18:05 +0000 Subject: [PATCH 39/71] adding examples --- src/clib/dust/calc_tdust_3d.cpp | 1 - src/python/examples/cooling_cell_test.py | 201 +++++++++++++++++++++ src/python/examples/freefall_test.py | 216 +++++++++++++++++++++++ 3 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/python/examples/cooling_cell_test.py create mode 100644 src/python/examples/freefall_test.py diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 68b6d6289..53ca69d54 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -208,7 +208,6 @@ void calc_tdust_3d( if (my_chemistry->use_dust_density_field > 0) { dust2gas[i] = dust(i,j,k) / (d(i,j,k)); - // dust2gas[i] = dust(i,j,k) / (d(i,j,k) - dust(i,j,k)); } else { dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; } diff --git a/src/python/examples/cooling_cell_test.py b/src/python/examples/cooling_cell_test.py new file mode 100644 index 000000000..6afcb1529 --- /dev/null +++ b/src/python/examples/cooling_cell_test.py @@ -0,0 +1,201 @@ +######################################################################## +# +# Cooling cell example script +# +# This will initialize a single cell at a given temperature, +# iterate the cooling solver for a fixed time, and output the +# temperature vs. time. +# +# +# Copyright (c) 2015-2016, Grackle Development Team. +# +# Distributed under the terms of the Enzo Public Licence. +# +# The full license is in the file LICENSE, distributed with this +# software. +######################################################################## + +from matplotlib import pyplot +import os +import sys +import yt + +from gracklepy import \ + chemistry_data, \ + evolve_constant_density, \ + setup_fluid_container +from gracklepy.utilities.physical_constants import \ + mass_hydrogen_cgs, \ + sec_per_Myr, \ + cm_per_mpc + +from gracklepy.utilities.data_path import grackle_data_dir +from gracklepy.utilities.model_tests import \ + get_test_variables + +_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" + +def gen_plot(fc, data, fname): + p1, = pyplot.loglog(data["time"].to("Myr"), + data["temperature"], + color="black", label="T") + pyplot.xlabel("Time [Myr]") + pyplot.ylabel("T [K]") + pyplot.twinx() + p2, = pyplot.semilogx(data["time"].to("Myr"), + data["mean_molecular_weight"], + color="red", label="$\\mu$") + pyplot.ylabel("$\\mu$") + pyplot.legend([p1,p2],["T","$\\mu$"], fancybox=True, + loc="center left") + pyplot.tight_layout() + pyplot.savefig(fname) + + +def main(args=None): + args = sys.argv[1:] if args is None else args + if len(args) != 0: # we are using the testing framework + my_vars = get_test_variables(_MODEL_NAME, args) + + metallicity = my_vars["metallicity"] + redshift = my_vars["redshift"] + extra_attrs = my_vars["extra_attrs"] + my_chemistry = my_vars["my_chemistry"] + output_name = my_vars["output_name"] + + in_testing_framework = True + + else: # Just run the script as is. + metallicity =1 # Solar + redshift = 0. + # dictionary to store extra information in output dataset + extra_attrs = {} + + my_chemistry = chemistry_data() + my_chemistry.use_grackle = 1 + my_chemistry.grackle_data_file = \ + os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=2, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=0, + multi_metals=0, + metal_abundances=0, + dust_species=0, + use_multiple_dust_temperatures=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + # SNe dust destruction + use_sne_field=1 + + # HydrogenFractionByMass=0.76, + # DeuteriumToHydrogenRatio=6.8e-05, + # SolarMetalFractionByMass=0.01295, + # # local_dust_to_gas_ratio=0.009387, + # NumberOfTemperatureBins=600, + # CaseBRecombination=1, + # TemperatureStart=1.0, + # TemperatureEnd=1.0e9, + # NumberOfDustTemperatureBins=250, + # DustTemperatureStart=1.0, + # DustTemperatureEnd=1500.0, + # Compton_xray_heating=0, + # LWbackground_sawtooth_suppression=0, + # LWbackground_intensity=0, + # UVbackground_redshift_on=-99999, + # UVbackground_redshift_off=-99999, + # UVbackground_redshift_fullon=-99999, + # UVbackground_redshift_drop=-99999, + # cloudy_electron_fraction_factor=0.00915396, + # use_radiative_transfer=1, + # radiative_transfer_coupled_rate_solver=0, + # radiative_transfer_intermediate_step=0, + # radiative_transfer_hydrogen_only=0, + # self_shielding_method=0, + # H2_custom_shielding=0, + # H2_self_shielding=0, + # radiative_transfer_H2II_diss=1, + # radiative_transfer_HDI_dissociation=1, + # radiative_transfer_metal_ionization=1, + # radiative_transfer_metal_dissociation=1 + + ) + + output_name = _MODEL_NAME + in_testing_framework = False + + density = 0.1 * mass_hydrogen_cgs # g /cm^3 + temperature = 5e4 # K + final_time = 100. # Myr + + # Set units + my_chemistry.comoving_coordinates = 0 + my_chemistry.a_units = 1.0 + my_chemistry.a_value = 1. / (1. + redshift) / \ + my_chemistry.a_units + my_chemistry.density_units = mass_hydrogen_cgs + my_chemistry.length_units = cm_per_mpc + my_chemistry.time_units = sec_per_Myr + my_chemistry.set_velocity_units() + + metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass + fc = setup_fluid_container( + my_chemistry, + density=density, + temperature=temperature, + metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust + state="ionized", + converge=True) + + # Set SNe rate: ~0.02 SNII/yr in a MW-like disk volume (~1.3e68 cm³) + # V_disk = pi * (15kpc)^2 * 0.6kpc = 424 kpc^3 = 1.25e67 cm ^3 + # sne_rate = 1.6e-69 # per yr per cm^3 + sne_rate = 1 + fc["sne_rate"][:] = sne_rate + + # evolve gas at constant density + data = evolve_constant_density( + fc, final_time=final_time, + safety_factor=0.01) + + if not in_testing_framework: + gen_plot(fc, data, fname=f"{output_name}.png") + + # save data arrays as a yt dataset + yt.save_as_dataset({}, f"{output_name}.h5", + data=data, extra_attrs=extra_attrs) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py new file mode 100644 index 000000000..4b8244723 --- /dev/null +++ b/src/python/examples/freefall_test.py @@ -0,0 +1,216 @@ +######################################################################## +# +# Free-fall example script +# +# +# Copyright (c) 2013-2016, Grackle Development Team. +# +# Distributed under the terms of the Enzo Public Licence. +# +# The full license is in the file LICENSE, distributed with this +# software. +######################################################################## +import numpy as np +from matplotlib import pyplot as plt +import os +import sys +import yt + +from gracklepy import \ + chemistry_data, \ + evolve_constant_density, \ + evolve_freefall, \ + setup_fluid_container +from gracklepy.utilities.physical_constants import \ + mass_hydrogen_cgs, \ + sec_per_Myr, \ + cm_per_mpc + +from gracklepy.utilities.data_path import grackle_data_dir +from gracklepy.utilities.model_tests import \ + get_test_variables + +_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" + +def gen_plot(fc, data, Z): + color = plt.gca()._get_lines.get_next_color() + # plt.loglog(data["density"]/mass_hydrogen_cgs, data["metal_density"] / data["density"], + # color=color,label=f"Z={Z:g}") + plt.loglog(data["density"], data["temperature"], + color=color,label=f"Z={Z:g}") + if fc.chemistry_data.dust_chemistry == 1: + plt.loglog(data["density"], data["dust_temperature"], + color=color, linestyle="--",label="_nolegend_") + # pyplot.twinx() + # plots.extend( + # pyplot.loglog(data["density"], data["H2I_density"] / data["density"], + # color="red", label="f$_{H2}$")) + # pyplot.ylabel("H$_{2}$ fraction") + + +def finalize_plot(fname): + plt.legend(loc="upper left") + plt.tight_layout() + # plt.xlabel("$\\rho$ [g/cm$^{3}$]") + plt.xlabel("nH") + plt.ylabel("T [K]") + plt.savefig(fname, bbox_inches="tight", pad_inches=0.03, dpi=200) + + +def main(args=None): + args = sys.argv[1:] if args is None else args + if len(args) != 0: # we are using the testing framework + my_vars = get_test_variables(_MODEL_NAME, args) + + metallicity = my_vars["metallicity"] + extra_attrs = my_vars["extra_attrs"] + my_chemistry = my_vars["my_chemistry"] + output_name = my_vars["output_name"] + + in_testing_framework = True + + else: # Just run the script as is. + # Z_list = [10**(-5)] + Z_list = [0., 10**(-6), 10**(-5), 10**(-4), 10**(-3), 10**(-2), 10**(-1), 1] + # Z_list = [10**(-4)] + # metallicity = 10e-4 + # dictionary to store extra information in output dataset + extra_attrs = {} + + # Set solver parameters + my_chemistry = chemistry_data() + my_chemistry.use_grackle = 1 + my_chemistry.grackle_data_file = os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") + + def set_opts(obj, **kwargs): + for k, v in kwargs.items(): + if hasattr(obj, k): + setattr(obj, k, v) + else: + print(f"# warning: chemistry_data has no field '{k}' (skipped)") + + set_opts( + my_chemistry, + + with_radiative_cooling=1, + primordial_chemistry=4, + dust_chemistry = 1, + metal_cooling=1, + UVbackground=0, + cmb_temperature_floor=1, + Gamma=1.66667, + h2_on_dust=1, + use_dust_density_field=1, + metal_chemistry=1, + multi_metals=0, + metal_abundances=0, + dust_species=0, + # dust_temperature_multi=0, + dust_sublimation=1, + grain_growth=0, + photoelectric_heating=0, + photoelectric_heating_rate=0, + use_isrf_field=0, + interstellar_radiation_field=0, + use_volumetric_heating_rate=0, + use_specific_heating_rate=0, + three_body_rate=1, + cie_cooling=1, + h2_optical_depth_approximation=1, + ih2co=1, + ipiht=1, + + HydrogenFractionByMass=0.76, + DeuteriumToHydrogenRatio=6.8e-05, + SolarMetalFractionByMass=0.01295, + local_dust_to_gas_ratio=0.009387, + NumberOfTemperatureBins=600, + CaseBRecombination=1, + TemperatureStart=1.0, + TemperatureEnd=1.0e9, + NumberOfDustTemperatureBins=250, + DustTemperatureStart=1.0, + DustTemperatureEnd=1500.0, + Compton_xray_heating=0, + LWbackground_sawtooth_suppression=0, + LWbackground_intensity=0, + UVbackground_redshift_on=-99999, + UVbackground_redshift_off=-99999, + UVbackground_redshift_fullon=-99999, + UVbackground_redshift_drop=-99999, + cloudy_electron_fraction_factor=0.00915396, + use_radiative_transfer=1, + radiative_transfer_coupled_rate_solver=0, + radiative_transfer_intermediate_step=0, + radiative_transfer_hydrogen_only=0, + self_shielding_method=0, + H2_custom_shielding=0, + H2_self_shielding=0, + radiative_transfer_H2II_diss=1, + radiative_transfer_HDI_dissociation=1, + radiative_transfer_metal_ionization=1, + radiative_transfer_metal_dissociation=1, + use_sne_field=1, + + use_multiple_dust_temperatures=0 + ) + + output_name = _MODEL_NAME + in_testing_framework = False + + redshift = 0. + + # Set units + my_chemistry.comoving_coordinates = 0 + my_chemistry.a_units = 1.0 + my_chemistry.a_value = 1. / (1. + redshift) / \ + my_chemistry.a_units + my_chemistry.density_units = mass_hydrogen_cgs + my_chemistry.length_units = cm_per_mpc + my_chemistry.time_units = sec_per_Myr + my_chemistry.set_velocity_units() + + # set initial density and temperature + initial_temperature = 50000 + initial_density = 1e-1 * mass_hydrogen_cgs # g / cm^3 + final_density = 1e13 * mass_hydrogen_cgs + + for metallicity in Z_list: + metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass + dust_to_gas_ratio = metallicity * my_chemistry.local_dust_to_gas_ratio + fc = setup_fluid_container( + my_chemistry, + density=initial_density, + temperature=initial_temperature, + metal_mass_fraction=metal_mass_fraction, + dust_to_gas_ratio=dust_to_gas_ratio, + state="ionized", + converge=False) + sne_rate = 1 + fc["sne_rate"][:] = sne_rate + + # let the gas cool at constant density from the starting temperature + # down to a lower temperature to get the species fractions in a + # reasonable state. + cooling_temperature = 10**(2.5) + data0 = evolve_constant_density( # noqa: F841 + fc, final_temperature=cooling_temperature, + safety_factor=0.1) + + # evolve density and temperature according to free-fall collapse + data = evolve_freefall(fc, final_density, + safety_factor=0.01, + include_pressure=True) + if not in_testing_framework: + gen_plot(fc, data, metallicity) + + finalize_plot(f"src/python/examples/{output_name}_new.png") + # save data arrays as a yt dataset + yt.save_as_dataset({}, f"{output_name}_new.h5", + data=data, extra_attrs=extra_attrs) + # breakpoint() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From 83572659367edc5cd8038bccb7d6e7ef7c6f85ec Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 10:29:49 +0000 Subject: [PATCH 40/71] check --- src/clib/dust_growth_and_destruction.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 7b732d6a6..54e6cbd67 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -80,10 +80,6 @@ void grackle::impl::dust_growth( // Store the calculated mass change in the output array growth_dM[i] = dM; - - // fprintf(stderr, - // "internal: frac=%.10e growth_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", - // frac_metal_available, dM, rho_gas, rho_dust, rho_metal); } } @@ -177,9 +173,6 @@ void grackle::impl::dust_destruction( // Store the calculated mass change in the output array destruction_dM[i] = dM; - // fprintf(stderr, - // "internal: tau_dest=%.10e tau_dest=%.10e dM_rate=%e gas=%.15e dust=%.15e metal=%.15e\n", - // tau_dest, tau_sput, dM, rho_gas, rho_dust, rho_metal); } } } From f75a87e34194283acfec03c3be42d9355612fc4e Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 22:38:51 +0000 Subject: [PATCH 41/71] add a dust model switch --- src/clib/cool1d_multi_g.cpp | 13 ++++----- src/clib/dust/calc_tdust_3d.cpp | 3 ++- src/clib/dust/lookup_dust_rates1d.hpp | 9 ++++--- src/clib/grackle_chemistry_data_fields.def | 6 +++++ src/clib/solve_rate_cool.cpp | 31 +++++++++++----------- src/include/grackle_chemistry_data.h | 6 +++++ src/python/examples/freefall_test.py | 6 ++--- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 385c050fb..3c1fbc9c9 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1099,12 +1099,13 @@ void grackle::impl::cool1d_multi_g( } } // Compute grain size increment - // if ((my_chemistry->use_dust_density_field > 0) && - // (my_chemistry->dust_species > 0)) { - // grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( - // dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, - // internal_dust_prop_buf); - // } + if ((my_chemistry->use_dust_density_field > 0) && + (my_chemistry->dust_species > 0) && + (my_chemistry->dust_model == 0)) { + grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + internal_dust_prop_buf); + } // Calculate dust to gas ratio AND interstellar radiation field // -> an earlier version of this logic would store values @ indices diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 53ca69d54..c4c5a1375 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -161,7 +161,8 @@ void calc_tdust_3d( // Compute grain size increment - if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) ) { + if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) + && (my_chemistry->dust_model == 0) ) { calc_grain_size_increment_1d ( dom, idx_range, itmask_metal.data(), my_chemistry, diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index 5bb4ae317..31455e3a9 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -194,10 +194,11 @@ inline void lookup_dust_rates1d( GRIMPL_REQUIRE(my_chemistry->metal_chemistry == 1, "sanity-check!"); // Compute grain size increment - calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + if (my_chemistry->dust_model == 0) { + f_wrap::calc_grain_size_increment_1d(dom, idx_range, itmask_metal, + my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, + my_fields, internal_dust_prop_scratch_buf); + } grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index ba6efd457..21d980b3d 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -334,6 +334,12 @@ ENTRY(omp_nthreads, INT, 0) */ ENTRY(solver_method, INT, 2) +/* dust model selection +* 0: default (runs calc_grain_size_increment_1d as usual) +* 1: harrison dust model +*/ +ENTRY(dust_model, INT, 0) + /* Flag to use snetimestep */ ENTRY(use_sne_field, INT, 0) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index d781f6dec..ce3655cc8 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -915,21 +915,22 @@ int solve_rate_cool( ); } - - // Calculate dust growth rates and store in growth_dM array - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - tgas.data(), growth_dM.data()); - - // Calculate dust destruction rates and store in destruction_dM array - grackle::impl::dust_destruction( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), destruction_dM.data()); - - // Apply the calculated rates to update density fields - grackle::impl::dust_update( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM.data(), destruction_dM.data(), false); + if (my_chemistry->dust_model == 1){ + // Calculate dust growth rates and store in growth_dM array + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + tgas.data(), growth_dM.data()); + + // Calculate dust destruction rates and store in destruction_dM array + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); + + // Apply the calculated rates to update density fields + grackle::impl::dust_update( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + growth_dM.data(), destruction_dM.data(), false); + } // Add the timestep to the elapsed time for each cell and find // minimum elapsed time step in this row diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index f8b14b0ba..080f6c97c 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -312,6 +312,12 @@ typedef struct */ int solver_method; + /* dust model selection + * 0: default (runs calc_grain_size_increment_1d as usual) + * 1: harrison dust model + */ + int dust_model; + /* Flag to use snetimestep */ int use_sne_field; diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py index 4b8244723..23cda8809 100644 --- a/src/python/examples/freefall_test.py +++ b/src/python/examples/freefall_test.py @@ -150,7 +150,7 @@ def set_opts(obj, **kwargs): radiative_transfer_HDI_dissociation=1, radiative_transfer_metal_ionization=1, radiative_transfer_metal_dissociation=1, - use_sne_field=1, + use_sne_field=0, use_multiple_dust_temperatures=0 ) @@ -186,8 +186,8 @@ def set_opts(obj, **kwargs): dust_to_gas_ratio=dust_to_gas_ratio, state="ionized", converge=False) - sne_rate = 1 - fc["sne_rate"][:] = sne_rate + # sne_rate = 1 + # fc["sne_rate"][:] = sne_rate # let the gas cool at constant density from the starting temperature # down to a lower temperature to get the species fractions in a From 27fe4ab3ce850cca3482c12a8f75b80d9b585111 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:01:04 +0000 Subject: [PATCH 42/71] rebasing fix --- src/clib/cool1d_multi_g.cpp | 6 ++++-- src/clib/dust/lookup_dust_rates1d.hpp | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 3c1fbc9c9..d4589db38 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1102,8 +1102,10 @@ void grackle::impl::cool1d_multi_g( if ((my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0)) { - grackle::impl::fortran_wrapper::calc_grain_size_increment_1d( - dom, idx_range, itmask_metal, my_chemistry, my_rates, my_fields, + grackle::impl::calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, my_fields, internal_dust_prop_buf); } diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index 31455e3a9..bd16dda5b 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -195,9 +195,10 @@ inline void lookup_dust_rates1d( // Compute grain size increment if (my_chemistry->dust_model == 0) { - f_wrap::calc_grain_size_increment_1d(dom, idx_range, itmask_metal, - my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, + my_fields, internal_dust_prop_scratch_buf); } grackle::impl::View d( From 2623381c299f718a3f56ed0bfc16e0cec6251e54 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:12:00 +0000 Subject: [PATCH 43/71] final formatting --- src/clib/dust/calc_tdust_3d.cpp | 5 +- src/python/examples/cooling_cell_test.py | 201 --------------------- src/python/examples/freefall_test.py | 216 ----------------------- 3 files changed, 1 insertion(+), 421 deletions(-) delete mode 100644 src/python/examples/cooling_cell_test.py delete mode 100644 src/python/examples/freefall_test.py diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index c4c5a1375..36983f12e 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -160,17 +160,14 @@ void calc_tdust_3d( } // Compute grain size increment - if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0) ) { - - calc_grain_size_increment_1d ( + grackle::impl::calc_grain_size_increment_1d ( dom, idx_range, itmask_metal.data(), my_chemistry, my_rates->opaque_storage->grain_species_info, my_rates->opaque_storage->inject_pathway_props, my_fields, internal_dust_prop_buf ); - } for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { diff --git a/src/python/examples/cooling_cell_test.py b/src/python/examples/cooling_cell_test.py deleted file mode 100644 index 6afcb1529..000000000 --- a/src/python/examples/cooling_cell_test.py +++ /dev/null @@ -1,201 +0,0 @@ -######################################################################## -# -# Cooling cell example script -# -# This will initialize a single cell at a given temperature, -# iterate the cooling solver for a fixed time, and output the -# temperature vs. time. -# -# -# Copyright (c) 2015-2016, Grackle Development Team. -# -# Distributed under the terms of the Enzo Public Licence. -# -# The full license is in the file LICENSE, distributed with this -# software. -######################################################################## - -from matplotlib import pyplot -import os -import sys -import yt - -from gracklepy import \ - chemistry_data, \ - evolve_constant_density, \ - setup_fluid_container -from gracklepy.utilities.physical_constants import \ - mass_hydrogen_cgs, \ - sec_per_Myr, \ - cm_per_mpc - -from gracklepy.utilities.data_path import grackle_data_dir -from gracklepy.utilities.model_tests import \ - get_test_variables - -_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" - -def gen_plot(fc, data, fname): - p1, = pyplot.loglog(data["time"].to("Myr"), - data["temperature"], - color="black", label="T") - pyplot.xlabel("Time [Myr]") - pyplot.ylabel("T [K]") - pyplot.twinx() - p2, = pyplot.semilogx(data["time"].to("Myr"), - data["mean_molecular_weight"], - color="red", label="$\\mu$") - pyplot.ylabel("$\\mu$") - pyplot.legend([p1,p2],["T","$\\mu$"], fancybox=True, - loc="center left") - pyplot.tight_layout() - pyplot.savefig(fname) - - -def main(args=None): - args = sys.argv[1:] if args is None else args - if len(args) != 0: # we are using the testing framework - my_vars = get_test_variables(_MODEL_NAME, args) - - metallicity = my_vars["metallicity"] - redshift = my_vars["redshift"] - extra_attrs = my_vars["extra_attrs"] - my_chemistry = my_vars["my_chemistry"] - output_name = my_vars["output_name"] - - in_testing_framework = True - - else: # Just run the script as is. - metallicity =1 # Solar - redshift = 0. - # dictionary to store extra information in output dataset - extra_attrs = {} - - my_chemistry = chemistry_data() - my_chemistry.use_grackle = 1 - my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=2, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=0, - multi_metals=0, - metal_abundances=0, - dust_species=0, - use_multiple_dust_temperatures=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1, - - # SNe dust destruction - use_sne_field=1 - - # HydrogenFractionByMass=0.76, - # DeuteriumToHydrogenRatio=6.8e-05, - # SolarMetalFractionByMass=0.01295, - # # local_dust_to_gas_ratio=0.009387, - # NumberOfTemperatureBins=600, - # CaseBRecombination=1, - # TemperatureStart=1.0, - # TemperatureEnd=1.0e9, - # NumberOfDustTemperatureBins=250, - # DustTemperatureStart=1.0, - # DustTemperatureEnd=1500.0, - # Compton_xray_heating=0, - # LWbackground_sawtooth_suppression=0, - # LWbackground_intensity=0, - # UVbackground_redshift_on=-99999, - # UVbackground_redshift_off=-99999, - # UVbackground_redshift_fullon=-99999, - # UVbackground_redshift_drop=-99999, - # cloudy_electron_fraction_factor=0.00915396, - # use_radiative_transfer=1, - # radiative_transfer_coupled_rate_solver=0, - # radiative_transfer_intermediate_step=0, - # radiative_transfer_hydrogen_only=0, - # self_shielding_method=0, - # H2_custom_shielding=0, - # H2_self_shielding=0, - # radiative_transfer_H2II_diss=1, - # radiative_transfer_HDI_dissociation=1, - # radiative_transfer_metal_ionization=1, - # radiative_transfer_metal_dissociation=1 - - ) - - output_name = _MODEL_NAME - in_testing_framework = False - - density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 5e4 # K - final_time = 100. # Myr - - # Set units - my_chemistry.comoving_coordinates = 0 - my_chemistry.a_units = 1.0 - my_chemistry.a_value = 1. / (1. + redshift) / \ - my_chemistry.a_units - my_chemistry.density_units = mass_hydrogen_cgs - my_chemistry.length_units = cm_per_mpc - my_chemistry.time_units = sec_per_Myr - my_chemistry.set_velocity_units() - - metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass - fc = setup_fluid_container( - my_chemistry, - density=density, - temperature=temperature, - metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust - state="ionized", - converge=True) - - # Set SNe rate: ~0.02 SNII/yr in a MW-like disk volume (~1.3e68 cm³) - # V_disk = pi * (15kpc)^2 * 0.6kpc = 424 kpc^3 = 1.25e67 cm ^3 - # sne_rate = 1.6e-69 # per yr per cm^3 - sne_rate = 1 - fc["sne_rate"][:] = sne_rate - - # evolve gas at constant density - data = evolve_constant_density( - fc, final_time=final_time, - safety_factor=0.01) - - if not in_testing_framework: - gen_plot(fc, data, fname=f"{output_name}.png") - - # save data arrays as a yt dataset - yt.save_as_dataset({}, f"{output_name}.h5", - data=data, extra_attrs=extra_attrs) - return 0 - - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/python/examples/freefall_test.py b/src/python/examples/freefall_test.py deleted file mode 100644 index 23cda8809..000000000 --- a/src/python/examples/freefall_test.py +++ /dev/null @@ -1,216 +0,0 @@ -######################################################################## -# -# Free-fall example script -# -# -# Copyright (c) 2013-2016, Grackle Development Team. -# -# Distributed under the terms of the Enzo Public Licence. -# -# The full license is in the file LICENSE, distributed with this -# software. -######################################################################## -import numpy as np -from matplotlib import pyplot as plt -import os -import sys -import yt - -from gracklepy import \ - chemistry_data, \ - evolve_constant_density, \ - evolve_freefall, \ - setup_fluid_container -from gracklepy.utilities.physical_constants import \ - mass_hydrogen_cgs, \ - sec_per_Myr, \ - cm_per_mpc - -from gracklepy.utilities.data_path import grackle_data_dir -from gracklepy.utilities.model_tests import \ - get_test_variables - -_MODEL_NAME = os.path.basename(__file__[:-3]) # strip off ".py" - -def gen_plot(fc, data, Z): - color = plt.gca()._get_lines.get_next_color() - # plt.loglog(data["density"]/mass_hydrogen_cgs, data["metal_density"] / data["density"], - # color=color,label=f"Z={Z:g}") - plt.loglog(data["density"], data["temperature"], - color=color,label=f"Z={Z:g}") - if fc.chemistry_data.dust_chemistry == 1: - plt.loglog(data["density"], data["dust_temperature"], - color=color, linestyle="--",label="_nolegend_") - # pyplot.twinx() - # plots.extend( - # pyplot.loglog(data["density"], data["H2I_density"] / data["density"], - # color="red", label="f$_{H2}$")) - # pyplot.ylabel("H$_{2}$ fraction") - - -def finalize_plot(fname): - plt.legend(loc="upper left") - plt.tight_layout() - # plt.xlabel("$\\rho$ [g/cm$^{3}$]") - plt.xlabel("nH") - plt.ylabel("T [K]") - plt.savefig(fname, bbox_inches="tight", pad_inches=0.03, dpi=200) - - -def main(args=None): - args = sys.argv[1:] if args is None else args - if len(args) != 0: # we are using the testing framework - my_vars = get_test_variables(_MODEL_NAME, args) - - metallicity = my_vars["metallicity"] - extra_attrs = my_vars["extra_attrs"] - my_chemistry = my_vars["my_chemistry"] - output_name = my_vars["output_name"] - - in_testing_framework = True - - else: # Just run the script as is. - # Z_list = [10**(-5)] - Z_list = [0., 10**(-6), 10**(-5), 10**(-4), 10**(-3), 10**(-2), 10**(-1), 1] - # Z_list = [10**(-4)] - # metallicity = 10e-4 - # dictionary to store extra information in output dataset - extra_attrs = {} - - # Set solver parameters - my_chemistry = chemistry_data() - my_chemistry.use_grackle = 1 - my_chemistry.grackle_data_file = os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=4, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=1, - multi_metals=0, - metal_abundances=0, - dust_species=0, - # dust_temperature_multi=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1, - - HydrogenFractionByMass=0.76, - DeuteriumToHydrogenRatio=6.8e-05, - SolarMetalFractionByMass=0.01295, - local_dust_to_gas_ratio=0.009387, - NumberOfTemperatureBins=600, - CaseBRecombination=1, - TemperatureStart=1.0, - TemperatureEnd=1.0e9, - NumberOfDustTemperatureBins=250, - DustTemperatureStart=1.0, - DustTemperatureEnd=1500.0, - Compton_xray_heating=0, - LWbackground_sawtooth_suppression=0, - LWbackground_intensity=0, - UVbackground_redshift_on=-99999, - UVbackground_redshift_off=-99999, - UVbackground_redshift_fullon=-99999, - UVbackground_redshift_drop=-99999, - cloudy_electron_fraction_factor=0.00915396, - use_radiative_transfer=1, - radiative_transfer_coupled_rate_solver=0, - radiative_transfer_intermediate_step=0, - radiative_transfer_hydrogen_only=0, - self_shielding_method=0, - H2_custom_shielding=0, - H2_self_shielding=0, - radiative_transfer_H2II_diss=1, - radiative_transfer_HDI_dissociation=1, - radiative_transfer_metal_ionization=1, - radiative_transfer_metal_dissociation=1, - use_sne_field=0, - - use_multiple_dust_temperatures=0 - ) - - output_name = _MODEL_NAME - in_testing_framework = False - - redshift = 0. - - # Set units - my_chemistry.comoving_coordinates = 0 - my_chemistry.a_units = 1.0 - my_chemistry.a_value = 1. / (1. + redshift) / \ - my_chemistry.a_units - my_chemistry.density_units = mass_hydrogen_cgs - my_chemistry.length_units = cm_per_mpc - my_chemistry.time_units = sec_per_Myr - my_chemistry.set_velocity_units() - - # set initial density and temperature - initial_temperature = 50000 - initial_density = 1e-1 * mass_hydrogen_cgs # g / cm^3 - final_density = 1e13 * mass_hydrogen_cgs - - for metallicity in Z_list: - metal_mass_fraction = metallicity * my_chemistry.SolarMetalFractionByMass - dust_to_gas_ratio = metallicity * my_chemistry.local_dust_to_gas_ratio - fc = setup_fluid_container( - my_chemistry, - density=initial_density, - temperature=initial_temperature, - metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=dust_to_gas_ratio, - state="ionized", - converge=False) - # sne_rate = 1 - # fc["sne_rate"][:] = sne_rate - - # let the gas cool at constant density from the starting temperature - # down to a lower temperature to get the species fractions in a - # reasonable state. - cooling_temperature = 10**(2.5) - data0 = evolve_constant_density( # noqa: F841 - fc, final_temperature=cooling_temperature, - safety_factor=0.1) - - # evolve density and temperature according to free-fall collapse - data = evolve_freefall(fc, final_density, - safety_factor=0.01, - include_pressure=True) - if not in_testing_framework: - gen_plot(fc, data, metallicity) - - finalize_plot(f"src/python/examples/{output_name}_new.png") - # save data arrays as a yt dataset - yt.save_as_dataset({}, f"{output_name}_new.h5", - data=data, extra_attrs=extra_attrs) - # breakpoint() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file From 980f67dc5c18a1a21b245566fdbc969361cd4aec Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:31:34 +0000 Subject: [PATCH 44/71] output printing --- src/clib/dust_growth_and_destruction.cpp | 8 ++++---- src/python/gracklepy/utilities/evolve.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 54e6cbd67..32c9f717a 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -226,7 +226,7 @@ void grackle::impl::dust_update( } // Adjust gas density to conserve total mass - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Should be changed to gas_density staying constant (make_consistent.cpp) + rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); // Safety checks if (rho_dust < 0) { @@ -234,9 +234,9 @@ void grackle::impl::dust_update( std::exit(21); } - fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); + // fprintf(stderr, + // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (dryrun == false) { diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 767cdd1e1..280a3fa7a 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - # ((current_time * my_chemistry.time_units / sec_per_year), - # (fc["density"][0] * my_chemistry.density_units), - # fc["temperature"][0])) + print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + ((current_time * my_chemistry.time_units / sec_per_year), + (fc["density"][0] * my_chemistry.density_units), + fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - # (current_time * my_chemistry.time_units / sec_per_year, - # fc["density"][0] * my_chemistry.density_units, - # fc["temperature"][0])) + print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + (current_time * my_chemistry.time_units / sec_per_year, + fc["density"][0] * my_chemistry.density_units, + fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From 3065387486d704b9e3616f6077bf4b29959f15c4 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 3 Mar 2026 23:41:43 +0000 Subject: [PATCH 45/71] fixing examples --- src/clib/make_consistent.cpp | 7 -- src/python/examples/cooling_cell.py | 82 ++----------------- src/python/examples/freefall.py | 2 +- src/python/gracklepy/utilities/convenience.py | 5 +- 4 files changed, 9 insertions(+), 87 deletions(-) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index eb517e353..77b253893 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -527,13 +527,6 @@ void make_consistent( (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { - // if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || - // ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && - // (my_chemistry->dust_species == 0 || dust(i, j, k) <= 1.e-9 * d(i, j, k)) && - // (d(i, j, k) * dom < 1.e8)) || - // (((metal(i, j, k) > 1.e-9 * d(i, j, k)) || - // (my_chemistry->dust_species > 0 && dust(i, j, k) > 1.e-9 * d(i, j, k))) && - // (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + 16. / 18. * H2O(i, j, k) + O2(i, j, k) + diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index 19b1111c8..049a61024 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -66,92 +66,25 @@ def main(args=None): in_testing_framework = True else: # Just run the script as is. - metallicity =1 # Solar + metallicity = 0.1 # Solar redshift = 0. # dictionary to store extra information in output dataset extra_attrs = {} my_chemistry = chemistry_data() my_chemistry.use_grackle = 1 + my_chemistry.with_radiative_cooling = 1 + my_chemistry.primordial_chemistry = 0 + my_chemistry.metal_cooling = 1 + my_chemistry.UVbackground = 1 my_chemistry.grackle_data_file = \ - os.path.join(grackle_data_dir, "cloudy_metals_2008_3D.h5") - - def set_opts(obj, **kwargs): - for k, v in kwargs.items(): - if hasattr(obj, k): - setattr(obj, k, v) - else: - print(f"# warning: chemistry_data has no field '{k}' (skipped)") - - set_opts( - my_chemistry, - - with_radiative_cooling=1, - primordial_chemistry=2, - dust_chemistry = 1, - metal_cooling=1, - UVbackground=0, - cmb_temperature_floor=1, - Gamma=1.66667, - h2_on_dust=1, - use_dust_density_field=1, - metal_chemistry=0, - multi_metals=0, - metal_abundances=0, - dust_species=0, - use_multiple_dust_temperatures=0, - dust_sublimation=1, - grain_growth=0, - photoelectric_heating=0, - photoelectric_heating_rate=0, - use_isrf_field=0, - interstellar_radiation_field=0, - use_volumetric_heating_rate=0, - use_specific_heating_rate=0, - three_body_rate=1, - cie_cooling=1, - h2_optical_depth_approximation=1, - ih2co=1, - ipiht=1 - - # HydrogenFractionByMass=0.76, - # DeuteriumToHydrogenRatio=6.8e-05, - # SolarMetalFractionByMass=0.01295, - # # local_dust_to_gas_ratio=0.009387, - # NumberOfTemperatureBins=600, - # CaseBRecombination=1, - # TemperatureStart=1.0, - # TemperatureEnd=1.0e9, - # NumberOfDustTemperatureBins=250, - # DustTemperatureStart=1.0, - # DustTemperatureEnd=1500.0, - # Compton_xray_heating=0, - # LWbackground_sawtooth_suppression=0, - # LWbackground_intensity=0, - # UVbackground_redshift_on=-99999, - # UVbackground_redshift_off=-99999, - # UVbackground_redshift_fullon=-99999, - # UVbackground_redshift_drop=-99999, - # cloudy_electron_fraction_factor=0.00915396, - # use_radiative_transfer=1, - # radiative_transfer_coupled_rate_solver=0, - # radiative_transfer_intermediate_step=0, - # radiative_transfer_hydrogen_only=0, - # self_shielding_method=0, - # H2_custom_shielding=0, - # H2_self_shielding=0, - # radiative_transfer_H2II_diss=1, - # radiative_transfer_HDI_dissociation=1, - # radiative_transfer_metal_ionization=1, - # radiative_transfer_metal_dissociation=1 - - ) + os.path.join(grackle_data_dir, "CloudyData_UVB=HM2012.h5") output_name = _MODEL_NAME in_testing_framework = False density = 0.1 * mass_hydrogen_cgs # g /cm^3 - temperature = 5e4 # K + temperature = 1e6 # K final_time = 100. # Myr # Set units @@ -170,7 +103,6 @@ def set_opts(obj, **kwargs): density=density, temperature=temperature, metal_mass_fraction=metal_mass_fraction, - dust_to_gas_ratio=metal_mass_fraction, # 1:1 metal to dust state="ionized", converge=True) diff --git a/src/python/examples/freefall.py b/src/python/examples/freefall.py index aabbff225..01507821d 100644 --- a/src/python/examples/freefall.py +++ b/src/python/examples/freefall.py @@ -139,4 +139,4 @@ def main(args=None): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) \ No newline at end of file diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 44cd1e78e..d90eeb20d 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -33,9 +33,6 @@ def check_convergence(fc1, fc2, fields=None, tol=0.01): for field in fields: if field not in fc2: continue - if fc1[field] == 0: - print(field) - print(fc2[field]) convergence = np.max(np.abs(fc1[field] - fc2[field]) / fc1[field]) if convergence > max_val: max_val = convergence @@ -110,7 +107,7 @@ def setup_fluid_container(my_chemistry, The mass fraction of gas in gas-phase metals. Default: 1e-20. dust_to_gas_ratio : optional, float - The dust-to-gas ratio (dust/d). Dust is independent of d. + The ratio of dust mass density to total gas density. Default: 1e-20. converge : optional, bool If True, iterate the solver until the chemical species reach From 53c65cbe839dff9b63e0793c226b27ea0afd01dd Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 4 Mar 2026 00:02:49 +0000 Subject: [PATCH 46/71] pr fix --- src/clib/dust_growth_and_destruction.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 32c9f717a..453b613b7 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -53,7 +53,6 @@ void grackle::impl::dust_growth( if (itmask[i] != MASK_FALSE) { - double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); double rho_metal= metal(i,idx_range.j,idx_range.k); double temp = t_gas[i]; @@ -129,7 +128,6 @@ void grackle::impl::dust_destruction( double rho_gas = d(i,idx_range.j,idx_range.k); double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; From 4698358766bb086537e3e2ba7e9adebb6ce511d9 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 4 Mar 2026 23:47:08 +0000 Subject: [PATCH 47/71] Manual fix for clang-tidy errors --- src/clib/cool1d_multi_g.cpp | 4 +- src/clib/dust/lookup_dust_rates1d.hpp | 9 +- src/clib/dust_growth_and_destruction.cpp | 386 +++++++++++------------ src/clib/dust_growth_and_destruction.hpp | 45 +-- 4 files changed, 209 insertions(+), 235 deletions(-) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index d4589db38..c65bee9ca 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -1100,8 +1100,7 @@ void grackle::impl::cool1d_multi_g( } // Compute grain size increment if ((my_chemistry->use_dust_density_field > 0) && - (my_chemistry->dust_species > 0) && - (my_chemistry->dust_model == 0)) { + (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0)) { grackle::impl::calc_grain_size_increment_1d( dom, idx_range, itmask_metal, my_chemistry, my_rates->opaque_storage->grain_species_info, @@ -1800,7 +1799,6 @@ void grackle::impl::cool1d_multi_g( } } - // Free memory grackle::impl::drop_InternalDustPropBuf(&internal_dust_prop_buf); grackle::impl::drop_GrainSpeciesCollection(&grain_kappa); diff --git a/src/clib/dust/lookup_dust_rates1d.hpp b/src/clib/dust/lookup_dust_rates1d.hpp index bd16dda5b..8d745a8e8 100644 --- a/src/clib/dust/lookup_dust_rates1d.hpp +++ b/src/clib/dust/lookup_dust_rates1d.hpp @@ -195,10 +195,11 @@ inline void lookup_dust_rates1d( // Compute grain size increment if (my_chemistry->dust_model == 0) { - calc_grain_size_increment_1d(dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, - my_fields, internal_dust_prop_scratch_buf); + calc_grain_size_increment_1d( + dom, idx_range, itmask_metal, my_chemistry, + my_rates->opaque_storage->grain_species_info, + my_rates->opaque_storage->inject_pathway_props, my_fields, + internal_dust_prop_scratch_buf); } grackle::impl::View d( diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 453b613b7..4072f37fa 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -7,242 +7,230 @@ #include "utils-cpp.hpp" namespace { - const double k_boltz = 1.3806504e-16; - const double m_proton = 1.67262171e-24; - const double pi_val = 3.141592653589793; - const double sec_per_year = 3.155e7; +const double k_boltz = 1.3806504e-16; +const double m_proton = 1.67262171e-24; +const double pi_val = 3.141592653589793; +const double sec_per_year = 3.155e7; - const double tiny_value = 1.0e-20; - const double huge_value = 1.0e+20; +const double tiny_value = 1.0e-20; +const double huge_value = 1.0e+20; - const double t_ref = 20; +const double t_ref = 20; -} +} // namespace // ========================================== // DUST GROWTH (ACCRETION) // ========================================== -void grackle::impl::dust_growth( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* growth_dM) -{ - grackle::impl::View d( +void grackle::impl::dust_growth(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, + double* growth_dM) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - double dens_proper = internalu.urho * std::pow(internalu.a_value,3); - double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year/internalu.tbase1; - - - // --- MAIN LOOP --- - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero - growth_dM[i] = 0.0; - - if (itmask[i] != MASK_FALSE) { - - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal= metal(i,idx_range.j,idx_range.k); - double temp = t_gas[i]; - double dt = dt_value[i]; - - double tau_accr0 = tau_ref * - (my_chemistry->dust_growth_densref / dens_proper) * - std::pow(t_ref / temp, 0.5); - double rho_metal_eff = std::max(rho_metal, tiny_value); - double tau_accr = tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); - tau_accr = std::min(tau_accr, huge_value); - tau_accr = std::max(tau_accr, tiny_value); - double frac_metal_available = 0.0; - if (rho_metal <= 0.0) { - frac_metal_available = 0.0; - } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { - frac_metal_available = rho_metal / rho_dust; - } else { - frac_metal_available = rho_metal / (rho_dust + rho_metal); - } - frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); - double growth_rate = frac_metal_available * (rho_dust / tau_accr); - double dM = std::min(growth_rate, rho_metal/dt); - - // Store the calculated mass change in the output array - growth_dM[i] = dM; - } - + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + double tau_ref = + my_chemistry->dust_growth_tauref * 1e9 * sec_per_year / internalu.tbase1; + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + growth_dM[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double rho_dust = dust(i, idx_range.j, idx_range.k); + double rho_metal = metal(i, idx_range.j, idx_range.k); + double temp = t_gas[i]; + double dt = dt_value[i]; + + double tau_accr0 = tau_ref * + (my_chemistry->dust_growth_densref / dens_proper) * + std::pow(t_ref / temp, 0.5); + double rho_metal_eff = std::max(rho_metal, tiny_value); + double tau_accr = + tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); + tau_accr = std::min(tau_accr, huge_value); + tau_accr = std::max(tau_accr, tiny_value); + double frac_metal_available = 0.0; + if (rho_metal <= 0.0) { + frac_metal_available = 0.0; + } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { + frac_metal_available = rho_metal / rho_dust; + } else { + frac_metal_available = rho_metal / (rho_dust + rho_metal); + } + frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); + double growth_rate = frac_metal_available * (rho_dust / tau_accr); + double dM = std::min(growth_rate, rho_metal / dt); + + // Store the calculated mass change in the output array + growth_dM[i] = dM; } + } } // ========================================== // 2. DUST DESTRUCTION (SNe + SPUTTERING) // ========================================== void grackle::impl::dust_destruction( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* destruction_dM) -{ - grackle::impl::View d( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* destruction_dM) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool use_sne = (my_chemistry->use_sne_field > 0); - grackle::impl::View sne( - use_sne ? my_fields->sne_rate : my_fields->density, - my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - - double dens_proper = internalu.urho * std::pow(internalu.a_value,3); - - double Ms100 = 6800.0 * my_chemistry->sne_coeff - * (100.0 / my_chemistry->sne_shockspeed) - * (100.0 / my_chemistry->sne_shockspeed) - * SolarMass / (internalu.urho * std::pow(internalu.uxyz,3)); - - // --- MAIN LOOP --- - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero - destruction_dM[i] = 0.0; - - if (itmask[i] != MASK_FALSE) { - - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double sne_this = use_sne ? sne(i,idx_range.j,idx_range.k) : 0.0; - double temp = t_gas[i]; - double dt = dt_value[i]; - double tau_dest = 0; - - double dM = 0; - double dM_shock = 0.0; - - if (use_sne) { - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - // dM_shock = 0.0; - } else { - tau_dest = rho_gas/(Ms100*sne_this*my_chemistry->dust_destruction_eff) * dt; - dM_shock = std::min(rho_dust/tau_dest, rho_dust/dt); - } - } - - if (temp >= std::pow(10,5)) { - // destruction by thermal sputtering - double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 - * (my_chemistry->dust_grainsize/0.1) - * (1.0e-27/(dens_proper * rho_gas)) - * (std::pow((2.0e6/temp),2.5)+1.0); - - if (dM_shock >= rho_dust/dt) { - if (dM_shock > rho_dust/dt) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " << sne_this << ", " << tau_dest << std::endl; - } - } else { - dM_shock = dM_shock + rho_dust / tau_sput *3.0; - dM_shock = std::min(dM_shock, rho_dust/dt); - } - } - //dM = - rho_dust * dM_shock; - dM = -dM_shock; - if (std::isnan(dM)) { - std::cout << "dM calculated as NaN, "<< dM << std::endl; - } - - // Store the calculated mass change in the output array - destruction_dM[i] = dM; + bool use_sne = (my_chemistry->use_sne_field > 0); + grackle::impl::View sne( + use_sne ? my_fields->sne_rate : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + + double Ms100 = 6800.0 * my_chemistry->sne_coeff * + (100.0 / my_chemistry->sne_shockspeed) * + (100.0 / my_chemistry->sne_shockspeed) * SolarMass / + (internalu.urho * std::pow(internalu.uxyz, 3)); + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // Initialize to zero + destruction_dM[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust = dust(i, idx_range.j, idx_range.k); + double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; + double temp = t_gas[i]; + double dt = dt_value[i]; + double tau_dest = 0; + + double dM = 0; + double dM_shock = 0.0; + + if (use_sne) { + // destruction by SN shocks + if (sne_this <= 0) { + tau_dest = 1e20; + // dM_shock = 0.0; + } else { + tau_dest = rho_gas / + (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * + dt; + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); } + } + + if (temp >= std::pow(10, 5)) { + // destruction by thermal sputtering + double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 * + (my_chemistry->dust_grainsize / 0.1) * + (1.0e-27 / (dens_proper * rho_gas)) * + (std::pow((2.0e6 / temp), 2.5) + 1.0); + + if (dM_shock >= rho_dust / dt) { + if (dM_shock > rho_dust / dt) { + std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " + << sne_this << ", " << tau_dest << std::endl; + } + } else { + dM_shock = dM_shock + rho_dust / tau_sput * 3.0; + dM_shock = std::min(dM_shock, rho_dust / dt); + } + } + // dM = - rho_dust * dM_shock; + dM = -dM_shock; + if (std::isnan(dM)) { + std::cout << "dM calculated as NaN, " << dM << std::endl; + } + + // Store the calculated mass change in the output array + destruction_dM[i] = dM; } + } } -void grackle::impl::dust_update( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* growth_dM, - double* destruction_dM, - bool dryrun) -{ - grackle::impl::View d( +void grackle::impl::dust_update(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, const double* growth_dM, + const double* destruction_dM, bool dryrun) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( + grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - if (itmask[i] != MASK_FALSE) { - - double rho_gas = d(i,idx_range.j,idx_range.k); - double rho_dust = dust(i,idx_range.j,idx_range.k); - double rho_metal = metal(i,idx_range.j,idx_range.k); - double dt = dt_value[i]; - - // Get the total mass change (growth + destruction) - - double dM_total = growth_dM[i] + destruction_dM[i]; - dM_total = dM_total * dt; - // Apply constraints to dM - dM_total = std::max(-1*rho_dust, dM_total); - dM_total = std::min(0.9*rho_metal, dM_total); - - // Apply conservation logic (from original code) - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM_total; - rho_metal = rho_metal - dM_total; - } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; - } - - // Adjust gas density to conserve total mass - rho_gas = rho_gas + (rho_metal - metal(i,idx_range.j,idx_range.k)); - - // Safety checks - if (rho_dust < 0) { - fprintf(stderr, "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, rho_dust); - std::exit(21); - } - - // fprintf(stderr, - // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); - - // Update the fields - if (dryrun == false) { - dust(i,idx_range.j,idx_range.k) = (gr_float)rho_dust; - metal(i,idx_range.j,idx_range.k) = (gr_float)rho_metal; - d(i,idx_range.j,idx_range.k) = (gr_float)rho_gas; - } - } + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] != MASK_FALSE) { + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust = dust(i, idx_range.j, idx_range.k); + double rho_metal = metal(i, idx_range.j, idx_range.k); + double dt = dt_value[i]; + + // Get the total mass change (growth + destruction) + + double dM_total = growth_dM[i] + destruction_dM[i]; + dM_total = dM_total * dt; + // Apply constraints to dM + dM_total = std::max(-1 * rho_dust, dM_total); + dM_total = std::min(0.9 * rho_metal, dM_total); + + // Apply conservation logic (from original code) + double dM_conserv = 0.0; + if (rho_dust >= 0.0) { + rho_dust = rho_dust + dM_total; + rho_metal = rho_metal - dM_total; + } else { + dM_conserv = rho_dust; + rho_dust = rho_dust - dM_conserv; + rho_metal = rho_metal + dM_conserv; + } + + // Adjust gas density to conserve total mass + rho_gas = rho_gas + (rho_metal - metal(i, idx_range.j, idx_range.k)); + + // Safety checks + if (rho_dust < 0) { + fprintf(stderr, + "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, + rho_dust); + std::exit(21); + } + + // fprintf(stderr, + // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e + // dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, + // rho_dust, rho_metal, rho_dust+rho_metal); + + // Update the fields + if (!dryrun) { + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + } } - + } } \ No newline at end of file diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 29eb18390..e2934192e 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -8,48 +8,35 @@ #include "phys_constants.h" #include "fortran_func_decls.h" - namespace grackle::impl { // Calculates dust growth rates (accretion) onto grain surfaces. // Stores the mass change dM for each cell in growth_dM array. -void dust_growth( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, - double* growth_dM // output: mass change rate for each cell +void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, + const double* t_gas, + double* growth_dM // output: mass change rate for each cell ); // Calculates dust destruction rates from SNe shocks and thermal sputtering. // Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* t_gas, + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* destruction_dM // output: mass change rate for each cell ); // Update the density fields using calculated mass changes. void dust_update( - chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, - IndexRange idx_range, - const gr_mask_type* itmask, - double* dt_value, - double* growth_dM, // input: mass change from growth - double* destruction_dM, // input: mass change from destruction - bool dryrun -); + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, + const double* growth_dM, // input: mass change from growth + const double* destruction_dM, // input: mass change from destruction + bool dryrun); -} +} // namespace grackle::impl -#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file From a24679af2a83cb45c764979f5e7212848619748f Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 5 Mar 2026 00:37:26 +0000 Subject: [PATCH 48/71] setting solver_method --- src/clib/grackle_chemistry_data_fields.def | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 21d980b3d..ed640d82c 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -332,7 +332,7 @@ ENTRY(omp_nthreads, INT, 0) * 2: Gauss-Seidel-only * 3: Newton-Raphson-only */ -ENTRY(solver_method, INT, 2) +ENTRY(solver_method, INT, 1) /* dust model selection * 0: default (runs calc_grain_size_increment_1d as usual) From 614e3e4d13ecfc268c6e7ab6fb2e297ed19e0180 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 5 Mar 2026 01:13:12 +0000 Subject: [PATCH 49/71] adding config --- src/clib/Make.config.objects | 1 + 1 file changed, 1 insertion(+) diff --git a/src/clib/Make.config.objects b/src/clib/Make.config.objects index b42f2865c..57b9c40ff 100644 --- a/src/clib/Make.config.objects +++ b/src/clib/Make.config.objects @@ -31,6 +31,7 @@ OBJS_CONFIG_LIB = \ dust/calc_kappa_grain.lo \ dust/calc_tdust_1d_g.lo \ dust/grain_species_info.lo \ + dust_growth_and_destruction.lo \ dynamic_api.lo \ grackle_units.lo \ index_helper.lo \ From f5276a0693a21646678d07566389b17340b98fcd Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Mon, 16 Mar 2026 20:25:19 +0000 Subject: [PATCH 50/71] adding manual tau_dest --- src/clib/dust/calc_tdust_3d.cpp | 9 +++++++++ src/clib/dust_growth_and_destruction.cpp | 22 ++++++++++++++++------ src/clib/grackle_chemistry_data_fields.def | 3 +++ src/include/grackle_chemistry_data.h | 3 +++ src/include/grackle_types.h | 3 +++ src/python/gracklepy/fluid_container.py | 2 ++ src/python/gracklepy/grackle_defs.pxd | 1 + src/python/gracklepy/grackle_wrapper.pyx | 1 + src/python/gracklepy/utilities/evolve.py | 16 ++++++++-------- 9 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 36983f12e..0b755f478 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -159,6 +159,15 @@ void calc_tdust_3d( } } + // // Set itmask to false for dust-poor cells + // if (my_chemistry->use_dust_density_field > 0) { + // for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + // if (dust(i,j,k) < 1.e-9 * d(i,j,k)) { + // itmask_metal[i] = MASK_FALSE; + // } + // } + // } + // Compute grain size increment if ( (my_chemistry->use_dust_density_field > 0) && (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0) ) { diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 4072f37fa..c2d96d923 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -100,6 +100,11 @@ void grackle::impl::dust_destruction( use_sne ? my_fields->sne_rate : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool use_tau_dest = (my_chemistry->use_tau_dest_field > 0); + grackle::impl::View tau_dest_field( + use_tau_dest ? my_fields->tau_dest : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); @@ -124,7 +129,14 @@ void grackle::impl::dust_destruction( double dM = 0; double dM_shock = 0.0; - if (use_sne) { + if (use_tau_dest) { + // user-provided destruction timescale + tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); + if (tau_dest <= 0) { + tau_dest = 1e20; + } + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); + } else if (use_sne) { // destruction by SN shocks if (sne_this <= 0) { tau_dest = 1e20; @@ -219,11 +231,9 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, std::exit(21); } - // fprintf(stderr, - // "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e - // dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - // dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, - // rho_dust, rho_metal, rho_dust+rho_metal); + fprintf(stderr, + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (!dryrun) { diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index ed640d82c..e3b236c3c 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -343,6 +343,9 @@ ENTRY(dust_model, INT, 0) /* Flag to use snetimestep */ ENTRY(use_sne_field, INT, 0) +/* Flag to use user-provided dust destruction timescale */ +ENTRY(use_tau_dest_field, INT, 0) + /* Li et al 2019 dust model parameters*/ ENTRY(dust_destruction_eff, DOUBLE, 3.0e-1) ENTRY(sne_coeff, DOUBLE, 1.0) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 080f6c97c..3c309dc76 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -321,6 +321,9 @@ typedef struct /* Flag to use snetimestep */ int use_sne_field; + /* Flag to use user-provided dust destruction timescale field */ + int use_tau_dest_field; + /* flag and parameters for Li+ 2019 dust growth and destruction */ double dust_destruction_eff; double sne_coeff; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index f30f17e6a..b0af665ba 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -205,6 +205,9 @@ typedef struct // use_snetimestep = 1 gr_float *sne_rate; + + // use_tau_dest_field = 1 + gr_float *tau_dest; } grackle_field_data; diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index 1a730c59e..2ea25a64a 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -303,6 +303,8 @@ def _required_extra_fields(my_chemistry): my_fields.append("isrf_habing") if my_chemistry.use_sne_field == 1: my_fields.append("sne_rate") + if my_chemistry.use_tau_dest_field == 1: + my_fields.append("tau_dest") return my_fields def _required_calculated_fields(my_chemistry): diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 83a71b5b2..c538e565a 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -201,6 +201,7 @@ cdef extern from "grackle.h": gr_float *vol_org_dust_temperature; gr_float *H2O_ice_dust_temperature; gr_float *sne_rate; + gr_float *tau_dest; ctypedef struct c_grackle_version "grackle_version": const char* version; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index d66f0faf2..fdcf00dce 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -765,6 +765,7 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.H2O_ice_dust_temperature = get_field(fc, "H2O_ice_dust_temperature") my_fields.sne_rate = get_field(fc, "sne_rate") + my_fields.tau_dest = get_field(fc, "tau_dest") return my_fields diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 280a3fa7a..767cdd1e1 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - ((current_time * my_chemistry.time_units / sec_per_year), - (fc["density"][0] * my_chemistry.density_units), - fc["temperature"][0])) + # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + # ((current_time * my_chemistry.time_units / sec_per_year), + # (fc["density"][0] * my_chemistry.density_units), + # fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - (current_time * my_chemistry.time_units / sec_per_year, - fc["density"][0] * my_chemistry.density_units, - fc["temperature"][0])) + # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + # (current_time * my_chemistry.time_units / sec_per_year, + # fc["density"][0] * my_chemistry.density_units, + # fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From be4f3637ae8c7ff441e28b7e5fdf552534454505 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 18 Mar 2026 01:28:39 +0000 Subject: [PATCH 51/71] adding dust creation from sne --- src/clib/dust_growth_and_destruction.cpp | 68 ++++++++++++++++++---- src/clib/dust_growth_and_destruction.hpp | 9 +++ src/clib/grackle_chemistry_data_fields.def | 4 ++ src/clib/solve_rate_cool.cpp | 10 +++- src/include/grackle_chemistry_data.h | 5 ++ 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index c2d96d923..9dd823893 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -79,6 +79,48 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, } } +// ========================================== +// DUST CREATION (STELLAR FEEDBACK) +// ========================================== +void grackle::impl::dust_creation(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, + double* creation_dM) { + bool use_sne = (my_chemistry->use_sne_field > 0); + if (!use_sne) { + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + creation_dM[i] = 0.0; + } + return; + } + + grackle::impl::View sne( + my_fields->sne_rate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + // Metal yield per SN in code mass units + double M_yield_code = my_chemistry->sne_metal_yield * SolarMass / + (internalu.urho * std::pow(internalu.uxyz, 3)); + + double f_cond = my_chemistry->dust_condensation_eff; + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + creation_dM[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double sne_this = sne(i, idx_range.j, idx_range.k); + + if (sne_this > 0.0) { + // dM/dt = f_cond * M_metal_per_SN * sne_rate + creation_dM[i] = f_cond * M_yield_code * sne_this; + } + } + } +} + // ========================================== // 2. DUST DESTRUCTION (SNe + SPUTTERING) // ========================================== @@ -183,7 +225,8 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* growth_dM, - const double* destruction_dM, bool dryrun) { + const double* destruction_dM, + const double* creation_dM, bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -201,26 +244,27 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, double rho_metal = metal(i, idx_range.j, idx_range.k); double dt = dt_value[i]; - // Get the total mass change (growth + destruction) + // Get the mass change from growth + destruction (exchanges dust <-> metal) + double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; + // Apply constraints to exchange term + dM_exchange = std::max(-1 * rho_dust, dM_exchange); + dM_exchange = std::min(0.9 * rho_metal, dM_exchange); - double dM_total = growth_dM[i] + destruction_dM[i]; - dM_total = dM_total * dt; - // Apply constraints to dM - dM_total = std::max(-1 * rho_dust, dM_total); - dM_total = std::min(0.9 * rho_metal, dM_total); + // Get the mass change from stellar creation (externally injected) + double dM_create = creation_dM[i] * dt; // Apply conservation logic (from original code) double dM_conserv = 0.0; if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM_total; - rho_metal = rho_metal - dM_total; + rho_dust = rho_dust + dM_exchange + dM_create; + rho_metal = rho_metal - dM_exchange; } else { dM_conserv = rho_dust; rho_dust = rho_dust - dM_conserv; rho_metal = rho_metal + dM_conserv; } - // Adjust gas density to conserve total mass + // Adjust gas density to conserve total mass (only for exchange, not creation) rho_gas = rho_gas + (rho_metal - metal(i, idx_range.j, idx_range.k)); // Safety checks @@ -232,8 +276,8 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, } fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e dM_rate=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - dt, growth_dM[i], destruction_dM[i], dM_total, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); + "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e creation_dM=%.10e dM_exch=%.15e dM_crea=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", + dt, growth_dM[i], destruction_dM[i], creation_dM[i], dM_exchange, dM_create, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); // Update the fields if (!dryrun) { diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index e2934192e..547078cfc 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -19,6 +19,14 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, double* growth_dM // output: mass change rate for each cell ); +// Calculates dust creation rates from stellar feedback (SNe condensation). +// Stores the mass creation rate for each cell in creation_dM array. +void dust_creation(chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, + double* creation_dM // output: mass creation rate for each cell +); + // Calculates dust destruction rates from SNe shocks and thermal sputtering. // Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( @@ -35,6 +43,7 @@ void dust_update( const double* dt_value, const double* growth_dM, // input: mass change from growth const double* destruction_dM, // input: mass change from destruction + const double* creation_dM, // input: mass change from stellar creation bool dryrun); } // namespace grackle::impl diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index e3b236c3c..1f643ffdd 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -353,3 +353,7 @@ ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) ENTRY(dust_growth_tauref, DOUBLE, 0.004) + +/* Dust creation by stellar feedback parameters */ +ENTRY(dust_condensation_eff, DOUBLE, 1.5e-1) +ENTRY(sne_metal_yield, DOUBLE, 3.0) diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index ce3655cc8..01c5e5af8 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -728,9 +728,10 @@ int solve_rate_cool( std::vector mmw(my_fields->grid_dimension[0]); std::vector edot(my_fields->grid_dimension[0]); - // Arrays to store dust growth and destruction mass changes + // Arrays to store dust growth, destruction, and creation mass changes std::vector growth_dM(my_fields->grid_dimension[0]); std::vector destruction_dM(my_fields->grid_dimension[0]); + std::vector creation_dM(my_fields->grid_dimension[0]); // iteration masks std::vector itmask(my_fields->grid_dimension[0]); @@ -921,6 +922,11 @@ int solve_rate_cool( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), growth_dM.data()); + // Calculate dust creation rates from stellar feedback + grackle::impl::dust_creation( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), creation_dM.data()); + // Calculate dust destruction rates and store in destruction_dM array grackle::impl::dust_destruction( my_chemistry, my_fields, internalu, idx_range, itmask.data(), @@ -929,7 +935,7 @@ int solve_rate_cool( // Apply the calculated rates to update density fields grackle::impl::dust_update( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM.data(), destruction_dM.data(), false); + growth_dM.data(), destruction_dM.data(), creation_dM.data(), false); } // Add the timestep to the elapsed time for each cell and find diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 3c309dc76..c34ab8e79 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -332,6 +332,11 @@ typedef struct double dust_growth_densref; double dust_growth_tauref; + /* parameters for dust creation*/ + double dust_condensation_eff; + double sne_metal_yield; + + } chemistry_data; /***************************** From bef107a6dfab8b24d3ff2dfcb2fd2177fd2df301 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Fri, 20 Mar 2026 14:54:54 +0000 Subject: [PATCH 52/71] creation checked --- src/clib/dust_growth_and_destruction.cpp | 66 ++++++++++++++++++------ src/clib/dust_growth_and_destruction.hpp | 14 ++--- src/clib/solve_rate_cool.cpp | 8 +-- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 9dd823893..ea8135114 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -82,16 +82,29 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, // ========================================== // DUST CREATION (STELLAR FEEDBACK) // ========================================== +// Following the standard dust evolution framework (Dwek 1998, ApJ 501, 643; +// reviewed in Galliano et al. 2018, ARA&A 56, 673). Each SN injects a total +// metal mass m_Z (sne_metal_yield). A fraction delta (dust_condensation_eff) +// condenses into dust grains, while the remaining (1 - delta) fraction enters +// the gas phase as metals: +// +// dM_dust/dt = delta * m_Z * R_SN (Eq. 1 of review 2504.10585) +// dM_metal/dt = (1 - delta) * m_Z * R_SN +// +// where R_SN = sne_rate (SN rate per unit volume per unit time). +// void grackle::impl::dust_creation(chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - double* creation_dM) { + double* creation_dust_dM, + double* creation_metal_dM) { bool use_sne = (my_chemistry->use_sne_field > 0); if (!use_sne) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - creation_dM[i] = 0.0; + creation_dust_dM[i] = 0.0; + creation_metal_dM[i] = 0.0; } return; } @@ -108,14 +121,17 @@ void grackle::impl::dust_creation(chemistry_data* my_chemistry, // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - creation_dM[i] = 0.0; + creation_dust_dM[i] = 0.0; + creation_metal_dM[i] = 0.0; if (itmask[i] != MASK_FALSE) { double sne_this = sne(i, idx_range.j, idx_range.k); + double dt = dt_value[i]; if (sne_this > 0.0) { - // dM/dt = f_cond * M_metal_per_SN * sne_rate - creation_dM[i] = f_cond * M_yield_code * sne_this; + double total_metal_inject = M_yield_code * sne_this / dt; + creation_dust_dM[i] = f_cond * total_metal_inject; + creation_metal_dM[i] = (1.0 - f_cond) * total_metal_inject; } } } @@ -226,7 +242,9 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, const gr_mask_type* itmask, const double* dt_value, const double* growth_dM, const double* destruction_dM, - const double* creation_dM, bool dryrun) { + const double* creation_dust_dM, + const double* creation_metal_dM, + bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -244,28 +262,38 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, double rho_metal = metal(i, idx_range.j, idx_range.k); double dt = dt_value[i]; - // Get the mass change from growth + destruction (exchanges dust <-> metal) + // --- Growth + destruction: exchanges mass between dust <-> metal --- double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; - // Apply constraints to exchange term - dM_exchange = std::max(-1 * rho_dust, dM_exchange); + dM_exchange = std::max(-1.0 * rho_dust, dM_exchange); dM_exchange = std::min(0.9 * rho_metal, dM_exchange); - // Get the mass change from stellar creation (externally injected) - double dM_create = creation_dM[i] * dt; + // --- Stellar feedback injection (Dwek 1998 framework) --- + // SN ejecta inject total metal mass m_Z * R_SN * dt: + // delta * m_Z -> dust (separate from gas density) + // (1-delta) * m_Z -> gas-phase metals (subset of gas density) + double dM_create_dust = creation_dust_dM[i] * dt; + double dM_create_metal = creation_metal_dM[i] * dt; // Apply conservation logic (from original code) double dM_conserv = 0.0; if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM_exchange + dM_create; - rho_metal = rho_metal - dM_exchange; + rho_dust = rho_dust + dM_exchange + dM_create_dust; + rho_metal = rho_metal - dM_exchange + dM_create_metal; } else { dM_conserv = rho_dust; rho_dust = rho_dust - dM_conserv; rho_metal = rho_metal + dM_conserv; } - // Adjust gas density to conserve total mass (only for exchange, not creation) - rho_gas = rho_gas + (rho_metal - metal(i, idx_range.j, idx_range.k)); + // Gas density update: + // density includes metal_density as a subset, but NOT dust_density. + // Exchange: metal<->dust moves mass in/out of gas, so density + // changes by the same amount as metal (-dM_exchange). + // Stellar injection: only (1-delta)*m_Z enters gas (as metals). + // The delta*m_Z portion goes directly to dust (outside gas). + // Both effects are captured by tracking how metal changed: + double old_metal = metal(i, idx_range.j, idx_range.k); + rho_gas = rho_gas + (rho_metal - old_metal); // Safety checks if (rho_dust < 0) { @@ -276,8 +304,12 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, } fprintf(stderr, - "internal: dt=%e growth_dM=%.10e destruction_dM=%.10e creation_dM=%.10e dM_exch=%.15e dM_crea=%.15e gas=%.15e dust=%.15e metal=%.15e consv.=%.15e\n", - dt, growth_dM[i], destruction_dM[i], creation_dM[i], dM_exchange, dM_create, rho_gas, rho_dust, rho_metal, rho_dust+rho_metal); + "internal: dt=%e growth=%.10e destruct=%.10e " + "cre_dust=%.10e cre_metal=%.10e " + "gas=%.15e dust=%.15e metal=%.15e\n", + dt, growth_dM[i], destruction_dM[i], + creation_dust_dM[i], creation_metal_dM[i], + rho_gas, rho_dust, rho_metal); // Update the fields if (!dryrun) { diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust_growth_and_destruction.hpp index 547078cfc..8a28d2356 100644 --- a/src/clib/dust_growth_and_destruction.hpp +++ b/src/clib/dust_growth_and_destruction.hpp @@ -19,12 +19,13 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, double* growth_dM // output: mass change rate for each cell ); -// Calculates dust creation rates from stellar feedback (SNe condensation). -// Stores the mass creation rate for each cell in creation_dM array. +// Calculates stellar feedback injection rates (Dwek 1998 framework). +// Each SN injects m_Z metals: fraction delta -> dust, (1-delta) -> gas metals. void dust_creation(chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - double* creation_dM // output: mass creation rate for each cell + double* creation_dust_dM, // output: dust creation rate + double* creation_metal_dM // output: gas-phase metal injection rate ); // Calculates dust destruction rates from SNe shocks and thermal sputtering. @@ -41,9 +42,10 @@ void dust_update( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - const double* growth_dM, // input: mass change from growth - const double* destruction_dM, // input: mass change from destruction - const double* creation_dM, // input: mass change from stellar creation + const double* growth_dM, // input: mass change from growth + const double* destruction_dM, // input: mass change from destruction + const double* creation_dust_dM, // input: dust created by stellar feedback + const double* creation_metal_dM, // input: gas-phase metals from stellar feedback bool dryrun); } // namespace grackle::impl diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 01c5e5af8..6deb9b90b 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -731,7 +731,8 @@ int solve_rate_cool( // Arrays to store dust growth, destruction, and creation mass changes std::vector growth_dM(my_fields->grid_dimension[0]); std::vector destruction_dM(my_fields->grid_dimension[0]); - std::vector creation_dM(my_fields->grid_dimension[0]); + std::vector creation_dust_dM(my_fields->grid_dimension[0]); + std::vector creation_metal_dM(my_fields->grid_dimension[0]); // iteration masks std::vector itmask(my_fields->grid_dimension[0]); @@ -925,7 +926,7 @@ int solve_rate_cool( // Calculate dust creation rates from stellar feedback grackle::impl::dust_creation( my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), creation_dM.data()); + dtit.data(), creation_dust_dM.data(), creation_metal_dM.data()); // Calculate dust destruction rates and store in destruction_dM array grackle::impl::dust_destruction( @@ -935,7 +936,8 @@ int solve_rate_cool( // Apply the calculated rates to update density fields grackle::impl::dust_update( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM.data(), destruction_dM.data(), creation_dM.data(), false); + growth_dM.data(), destruction_dM.data(), + creation_dust_dM.data(), creation_metal_dM.data(), false); } // Add the timestep to the elapsed time for each cell and find From e5d8f9b4c9e8a0e6b0b805b66730c98161bc2984 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 1 Apr 2026 01:34:39 +0100 Subject: [PATCH 53/71] minor change to support enzo run --- src/clib/dust_growth_and_destruction.cpp | 36 +++++++++++++++++++----- src/clib/solve_rate_cool.cpp | 8 +++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index ea8135114..9ad6b14a0 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -17,6 +17,15 @@ const double huge_value = 1.0e+20; const double t_ref = 20; +// Gate thresholds for dust evolution: skip cells where both dust and metals +// are negligible fractions of the baryon density. The metal threshold matches +// the 1e-9 ratio used in make_consistent (make_consistent.cpp ~line 530) to +// distinguish metal-poor from metal-rich cells. The dust threshold uses the +// same ratio for consistency — below 1 ppb of baryon density, any dust +// evolution would just integrate numerical noise. +const double dust_gate_threshold = 1.0e-9; +const double metal_gate_threshold = 1.0e-9; + } // namespace // ========================================== @@ -50,6 +59,13 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, if (itmask[i] != MASK_FALSE) { double rho_dust = dust(i, idx_range.j, idx_range.k); double rho_metal = metal(i, idx_range.j, idx_range.k); + double rho_d = d(i, idx_range.j, idx_range.k); + + // No metals to accrete onto grains — skip growth + if (rho_metal < metal_gate_threshold * rho_d) { + continue; + } + double temp = t_gas[i]; double dt = dt_value[i]; @@ -179,6 +195,12 @@ void grackle::impl::dust_destruction( if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); double rho_dust = dust(i, idx_range.j, idx_range.k); + + // No dust to destroy — skip destruction + if (rho_dust < dust_gate_threshold * rho_gas) { + continue; + } + double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; @@ -303,13 +325,13 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, std::exit(21); } - fprintf(stderr, - "internal: dt=%e growth=%.10e destruct=%.10e " - "cre_dust=%.10e cre_metal=%.10e " - "gas=%.15e dust=%.15e metal=%.15e\n", - dt, growth_dM[i], destruction_dM[i], - creation_dust_dM[i], creation_metal_dM[i], - rho_gas, rho_dust, rho_metal); + // fprintf(stderr, + // "internal: dt=%e growth=%.10e destruct=%.10e " + // "cre_dust=%.10e cre_metal=%.10e " + // "gas=%.15e dust=%.15e metal=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], + // creation_dust_dM[i], creation_metal_dM[i], + // rho_gas, rho_dust, rho_metal); // Update the fields if (!dryrun) { diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 6deb9b90b..a3dbcb2a4 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -923,10 +923,10 @@ int solve_rate_cool( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), growth_dM.data()); - // Calculate dust creation rates from stellar feedback - grackle::impl::dust_creation( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), creation_dust_dM.data(), creation_metal_dM.data()); + // // Calculate dust creation rates from stellar feedback + // grackle::impl::dust_creation( + // my_chemistry, my_fields, internalu, idx_range, itmask.data(), + // dtit.data(), creation_dust_dM.data(), creation_metal_dM.data()); // Calculate dust destruction rates and store in destruction_dM array grackle::impl::dust_destruction( From 41c4ac3e9519e6d3854c676df0396f1ac3d9c16d Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Fri, 3 Apr 2026 01:49:00 +0100 Subject: [PATCH 54/71] Tracking C and O --- src/clib/api/calculate_temperature.cpp | 9 +- src/clib/calc_temp1d_cloudy_g.cpp | 25 +- src/clib/calc_temp_cloudy.cpp | 22 +- src/clib/cool1d_multi_g.cpp | 40 ++- src/clib/dust/calc_tdust_3d.cpp | 23 +- src/clib/dust_growth_and_destruction.cpp | 236 ++++++++++++++++-- src/clib/field_data_misc_fdatamembers.def | 26 ++ src/clib/grackle_chemistry_data_fields.def | 17 ++ src/clib/initialize_chemistry_data.cpp | 11 + src/clib/make_consistent.cpp | 21 ++ src/include/grackle_chemistry_data.h | 17 ++ src/include/grackle_types.h | 8 + src/python/gracklepy/fluid_container.py | 7 + src/python/gracklepy/grackle_defs.pxd | 4 + src/python/gracklepy/grackle_wrapper.pyx | 4 + src/python/gracklepy/utilities/convenience.py | 23 +- 16 files changed, 456 insertions(+), 37 deletions(-) diff --git a/src/clib/api/calculate_temperature.cpp b/src/clib/api/calculate_temperature.cpp index a5adeb324..50fd06935 100644 --- a/src/clib/api/calculate_temperature.cpp +++ b/src/clib/api/calculate_temperature.cpp @@ -97,7 +97,14 @@ extern "C" int local_calculate_temperature(chemistry_data *my_chemistry, } if (imetal) { - number_density += my_fields->metal_density[index] * inv_metal_mol; + double total_metal = my_fields->metal_density[index]; + if (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != NULL && + my_fields->metal_density_oxygen != NULL) { + total_metal += my_fields->metal_density_carbon[index] + + my_fields->metal_density_oxygen[index]; + } + number_density += total_metal * inv_metal_mol; } /* Ignore deuterium. */ diff --git a/src/clib/calc_temp1d_cloudy_g.cpp b/src/clib/calc_temp1d_cloudy_g.cpp index 7e4f97e04..65457dd7e 100644 --- a/src/clib/calc_temp1d_cloudy_g.cpp +++ b/src/clib/calc_temp1d_cloudy_g.cpp @@ -36,6 +36,22 @@ void grackle::impl::calc_temp1d_cloudy_g( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + bool track_elements_1d = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_1d( + track_elements_1d ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_1d( + track_elements_1d ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View e( my_fields->internal_energy, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -178,11 +194,16 @@ void grackle::impl::calc_temp1d_cloudy_g( // Add metal species to mean molecular weight if (imetal == 1) { + double total_metal_1d = metal(i, idx_range.j, idx_range.k); + if (track_elements_1d) { + total_metal_1d += metal_C_1d(i, idx_range.j, idx_range.k) + + metal_O_1d(i, idx_range.j, idx_range.k); + } munew = d(i, idx_range.j, idx_range.k) / ((d(i, idx_range.j, idx_range.k) - - metal(i, idx_range.j, idx_range.k)) / + total_metal_1d) / munew + - metal(i, idx_range.j, idx_range.k) / mu_metal); + total_metal_1d / mu_metal); tgas[i] = tgas[i] * munew / muold; } diff --git a/src/clib/calc_temp_cloudy.cpp b/src/clib/calc_temp_cloudy.cpp index ea71c0955..b6405ccaa 100644 --- a/src/clib/calc_temp_cloudy.cpp +++ b/src/clib/calc_temp_cloudy.cpp @@ -71,6 +71,21 @@ void calc_temp_cloudy(gr_float* temperature_data_, int imetal, my_fields->grid_dimension[1], my_fields->grid_dimension[2]); } + bool track_elements_temp = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + View metal_C_temp( + track_elements_temp ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + View metal_O_temp( + track_elements_temp ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + View temperature( temperature_data_, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -97,8 +112,13 @@ void calc_temp_cloudy(gr_float* temperature_data_, int imetal, itmask[i] = MASK_TRUE; if (imetal == 1) { + gr_float total_metal = metal(i, idx_range.j, idx_range.k); + if (track_elements_temp) { + total_metal += metal_C_temp(i, idx_range.j, idx_range.k) + + metal_O_temp(i, idx_range.j, idx_range.k); + } gr_float metal_free_density = (d(i, idx_range.j, idx_range.k) - - metal(i, idx_range.j, idx_range.k)); + total_metal); rhoH[i] = f_H * metal_free_density; } else { rhoH[i] = f_H * d(i, idx_range.j, idx_range.k); diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index c65bee9ca..57eac2e3f 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -83,6 +83,23 @@ void grackle::impl::cool1d_multi_g( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + // When dust_model1_track_elements=1, metal_density excludes C and O. + // We need Views for these fields to reconstruct total metallicity + // for the Cloudy cooling table lookup. + bool track_elements_cool = (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_cool( + track_elements_cool ? my_fields->metal_density_carbon : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_cool( + track_elements_cool ? my_fields->metal_density_oxygen : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View Vheat( my_fields->volumetric_heating_rate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -337,7 +354,12 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { - mmw[i] = mmw[i] + metal(i, idx_range.j, idx_range.k) / mu_metal; + double total_metal_mmw = metal(i, idx_range.j, idx_range.k); + if (track_elements_cool) { + total_metal_mmw += metal_C_cool(i, idx_range.j, idx_range.k) + + metal_O_cool(i, idx_range.j, idx_range.k); + } + mmw[i] = mmw[i] + total_metal_mmw / mu_metal; } } } @@ -428,7 +450,14 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { - metallicity[i] = metal(i, idx_range.j, idx_range.k) / + double total_metal_i = metal(i, idx_range.j, idx_range.k); + // When tracking elements, metal_density excludes C and O — + // add them back for the Cloudy cooling table lookup + if (track_elements_cool) { + total_metal_i += metal_C_cool(i, idx_range.j, idx_range.k) + + metal_O_cool(i, idx_range.j, idx_range.k); + } + metallicity[i] = total_metal_i / d(i, idx_range.j, idx_range.k) / my_chemistry->SolarMetalFractionByMass; } @@ -1431,9 +1460,14 @@ void grackle::impl::cool1d_multi_g( 1 - mmw[i] * (3.0 * my_chemistry->HydrogenFractionByMass + 1.0) / 4.0; if (imetal == 1) { + double total_metal_myde = metal(i, idx_range.j, idx_range.k); + if (track_elements_cool) { + total_metal_myde += metal_C_cool(i, idx_range.j, idx_range.k) + + metal_O_cool(i, idx_range.j, idx_range.k); + } cool1dmulti_buf.myde[i] = cool1dmulti_buf.myde[i] - - mmw[i] * metal(i, idx_range.j, idx_range.k) / + mmw[i] * total_metal_myde / (d(i, idx_range.j, idx_range.k) * mu_metal); } cool1dmulti_buf.myde[i] = diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 0b755f478..49732b42d 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -76,6 +76,17 @@ void calc_tdust_3d( View dust_temp(dust_temp_data_, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View isrf_habing(my_fields->isrf_habing, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View metal(my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool track_elements_tdust = (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + View metal_C_tdust( + track_elements_tdust ? my_fields->metal_density_carbon : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + View metal_O_tdust( + track_elements_tdust ? my_fields->metal_density_oxygen : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); View dust(my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View SiM_temp(my_fields->SiM_dust_temperature, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -153,7 +164,11 @@ void calc_tdust_3d( // Set itmask to false for metal-poor cells if (imetal == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - if (metal(i,j,k) < 1.e-9 * d(i,j,k)) { + double total_metal_mask = metal(i,j,k); + if (track_elements_tdust) { + total_metal_mask += metal_C_tdust(i,j,k) + metal_O_tdust(i,j,k); + } + if (total_metal_mask < 1.e-9 * d(i,j,k)) { itmask_metal[i] = MASK_FALSE; } } @@ -184,7 +199,11 @@ void calc_tdust_3d( // Calculate metallicity if (imetal == 1) { - metallicity[i] = metal(i,j,k) / d(i,j,k) / my_chemistry->SolarMetalFractionByMass; + double total_metal_i = metal(i,j,k); + if (track_elements_tdust) { + total_metal_i += metal_C_tdust(i,j,k) + metal_O_tdust(i,j,k); + } + metallicity[i] = total_metal_i / d(i,j,k) / my_chemistry->SolarMetalFractionByMass; } // Calculate dust to gas ratio diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust_growth_and_destruction.cpp index 9ad6b14a0..07dd21473 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust_growth_and_destruction.cpp @@ -47,6 +47,18 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool track_elements = (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C( + track_elements ? my_fields->metal_density_carbon : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O( + track_elements ? my_fields->metal_density_oxygen : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year / internalu.tbase1; @@ -59,6 +71,12 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, if (itmask[i] != MASK_FALSE) { double rho_dust = dust(i, idx_range.j, idx_range.k); double rho_metal = metal(i, idx_range.j, idx_range.k); + // When tracking elements, rho_metal must include C and O for + // correct accretion timescale and metal availability + if (track_elements) { + rho_metal += metal_C(i, idx_range.j, idx_range.k) + + metal_O(i, idx_range.j, idx_range.k); + } double rho_d = d(i, idx_range.j, idx_range.k); // No metals to accrete onto grains — skip growth @@ -277,45 +295,192 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + // --- Element-resolved tracking setup --- + bool track_elements = (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr && + my_fields->dust_density_carbon != nullptr && + my_fields->dust_density_oxygen != nullptr); + + grackle::impl::View metal_C( + track_elements ? my_fields->metal_density_carbon : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O( + track_elements ? my_fields->metal_density_oxygen : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_C( + track_elements ? my_fields->dust_density_carbon : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_O( + track_elements ? my_fields->dust_density_oxygen : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); double rho_dust = dust(i, idx_range.j, idx_range.k); - double rho_metal = metal(i, idx_range.j, idx_range.k); + double rho_metal_other = metal(i, idx_range.j, idx_range.k); double dt = dt_value[i]; + // Load element-resolved fields (zero when not tracking) + double rho_metal_C = 0.0, rho_metal_O = 0.0; + double rho_dust_C = 0.0, rho_dust_O = 0.0; + if (track_elements) { + rho_metal_C = metal_C(i, idx_range.j, idx_range.k); + rho_metal_O = metal_O(i, idx_range.j, idx_range.k); + rho_dust_C = dust_C(i, idx_range.j, idx_range.k); + rho_dust_O = dust_O(i, idx_range.j, idx_range.k); + } + + // Total gas-phase metals (needed for clamping and gas density update) + double rho_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; + double f_C = (rho_metal_total > 0) ? rho_metal_C / rho_metal_total : 0.0; + double f_O = (rho_metal_total > 0) ? rho_metal_O / rho_metal_total : 0.0; + double f_other = (rho_metal_total > 0) ? rho_metal_other / rho_metal_total : 0.0; + double f_dust_C = (rho_dust > 0) ? rho_dust_C / rho_dust : 0.0; + double f_dust_O = (rho_dust > 0) ? rho_dust_O / rho_dust : 0.0; + double f_dust_other = (rho_dust > 0) ? 1.0 - (rho_dust_C + rho_dust_O) / rho_dust : 0.0; + // --- Growth + destruction: exchanges mass between dust <-> metal --- + // dM_exchange > 0 means net growth (metal -> dust) + // dM_exchange < 0 means net destruction (dust -> metal) double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; dM_exchange = std::max(-1.0 * rho_dust, dM_exchange); - dM_exchange = std::min(0.9 * rho_metal, dM_exchange); + dM_exchange = std::min(0.9 * rho_metal_total, dM_exchange); // --- Stellar feedback injection (Dwek 1998 framework) --- - // SN ejecta inject total metal mass m_Z * R_SN * dt: - // delta * m_Z -> dust (separate from gas density) - // (1-delta) * m_Z -> gas-phase metals (subset of gas density) double dM_create_dust = creation_dust_dM[i] * dt; double dM_create_metal = creation_metal_dM[i] * dt; - // Apply conservation logic (from original code) + // Apply conservation logic double dM_conserv = 0.0; if (rho_dust >= 0.0) { - rho_dust = rho_dust + dM_exchange + dM_create_dust; - rho_metal = rho_metal - dM_exchange + dM_create_metal; + if (track_elements) { + // --- GROWTH partitioning (Aoyama et al. 2017 proportional approach): + // deplete gas-phase C, O, other metals proportionally to their + + // Compute intended subtractions from original dM_exchange + double dM_C = ((dM_exchange > 0) ? f_C : f_dust_C) * dM_exchange; + double dM_O = ((dM_exchange > 0) ? f_O : f_dust_O) * dM_exchange; + double dM_other = ((dM_exchange > 0) ? f_other : f_dust_other) * dM_exchange; + + // Cap each independently + if (dM_C > ((dM_exchange > 0) ? rho_metal_C : -rho_dust_C)) { + dM_C = (dM_exchange > 0) ? rho_metal_C : dM_C; + rho_metal_C -= (dM_exchange > 0) ? rho_metal_C : dM_C; + } else { + rho_metal_C -= (dM_exchange > 0) ? dM_C : -rho_dust_C; + dM_C = (dM_exchange > 0) ? dM_C : -rho_dust_C; + } + + if (dM_O > ((dM_exchange > 0) ? rho_metal_O : -rho_dust_O)) { + dM_O = (dM_exchange > 0) ? rho_metal_O : dM_O; + rho_metal_O -= (dM_exchange > 0) ? rho_metal_O : dM_O; + } else { + rho_metal_O -= (dM_exchange > 0) ? dM_O : -rho_dust_O; + dM_O = (dM_exchange > 0) ? dM_O : -rho_dust_O; + } + + if (dM_other > ((dM_exchange > 0) ? rho_metal_other : -(rho_dust - rho_dust_C - rho_dust_O))) { + dM_other = (dM_exchange > 0) ? rho_metal_other : dM_other; + rho_metal_other -= (dM_exchange > 0) ? rho_metal_other : dM_other; + } else { + rho_metal_other -= (dM_exchange > 0) ? dM_other : -(rho_dust - rho_dust_C - rho_dust_O); + dM_other = (dM_exchange > 0) ? dM_other : -(rho_dust - rho_dust_C - rho_dust_O); + } + + // Dust receives exactly what was actually taken + rho_dust_C += dM_C; + rho_dust_O += dM_O; + dM_exchange = dM_C + dM_O + dM_other; + rho_dust = rho_dust + dM_exchange + dM_create_dust; + rho_metal_C += dM_create_metal * f_C; + rho_metal_O += dM_create_metal * f_O; + rho_metal_other += dM_create_metal * f_other; + + } else { + // Backward-compatible: all exchange with bulk metal_density + rho_metal_other = rho_metal_other - dM_exchange + dM_create_metal; + rho_dust = rho_dust + dM_exchange + dM_create_dust; + + } } else { - dM_conserv = rho_dust; - rho_dust = rho_dust - dM_conserv; - rho_metal = rho_metal + dM_conserv; + dM_conserv = -rho_dust; + rho_dust = 0.0; + + if (track_elements) { + bool converged = false; + if (dM_conserv > rho_metal_total) { + // Not enough dust species to cover + converged = false; + fprintf(stderr, + "[DustConservation] WARNING: dust species deficit — " + "need %.6e but only %.6e available " + "(C=%.6e, O=%.6e). Capping transfer.\n", + dM_conserv, rho_metal_total, + rho_dust_C, rho_dust_O); + std::exit(22); + } else { + if (rho_metal_C >= f_C * dM_conserv && rho_metal_O >= f_O * dM_conserv && rho_metal_other >= f_other * dM_conserv) { + rho_metal_C -= f_C*dM_conserv; + rho_metal_O -= f_O*dM_conserv; + rho_metal_other -= f_other*dM_conserv; + } else { + bool active[3] = {true, true, true}; + double f_back[3] = {f_C, f_O, f_other}; + double* rho_metal_arr[3] = { &rho_metal_C, &rho_metal_O, &rho_metal_other }; + double dM_back[3] = {f_C*dM_conserv,f_O*dM_conserv,f_other*dM_conserv}; + while (!converged) { + converged = true; + double shortfall = 0.0; + double f_sum = 0.0; + + for (int j = 0; j < 3; j++) { + if (*rho_metal_arr[j] < dM_back[j]) { + shortfall += dM_back[j] - *rho_metal_arr[j]; + dM_back[j] = *rho_metal_arr[j]; + active[j] = false; + } + } + + if (shortfall > 0.0) { + for (int j = 0; j < 3; j++) { + f_sum += active[j]*f_back[j]; + } + if (f_sum > 0.0) { + converged = false; + for (int j = 0; j < 3; j++) { + if (active[j]) { + dM_back[j] += (f_back[j]/f_sum)*shortfall; + } + } + } + } + } + rho_metal_C -= dM_back[0]; + rho_metal_O -= dM_back[1]; + rho_metal_other -= dM_back[2]; + } + } + } else { + rho_metal_other -= dM_conserv; + } } // Gas density update: - // density includes metal_density as a subset, but NOT dust_density. - // Exchange: metal<->dust moves mass in/out of gas, so density - // changes by the same amount as metal (-dM_exchange). - // Stellar injection: only (1-delta)*m_Z enters gas (as metals). - // The delta*m_Z portion goes directly to dust (outside gas). - // Both effects are captured by tracking how metal changed: - double old_metal = metal(i, idx_range.j, idx_range.k); - rho_gas = rho_gas + (rho_metal - old_metal); + // density includes all metal fields as a subset, but NOT dust_density. + // Track how total gas-phase metals changed to update gas density. + double old_metal_total = metal(i, idx_range.j, idx_range.k); + if (track_elements) { + old_metal_total += metal_C(i, idx_range.j, idx_range.k) + + metal_O(i, idx_range.j, idx_range.k); + } + double new_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; + rho_gas = rho_gas + (new_metal_total - old_metal_total); // Safety checks if (rho_dust < 0) { @@ -325,19 +490,36 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, std::exit(21); } - // fprintf(stderr, - // "internal: dt=%e growth=%.10e destruct=%.10e " - // "cre_dust=%.10e cre_metal=%.10e " - // "gas=%.15e dust=%.15e metal=%.15e\n", - // dt, growth_dM[i], destruction_dM[i], - // creation_dust_dM[i], creation_metal_dM[i], - // rho_gas, rho_dust, rho_metal); + // Clamp element fields to prevent negative values from round-off + if (track_elements) { + rho_metal_C = std::max(0.0, rho_metal_C); + rho_metal_O = std::max(0.0, rho_metal_O); + rho_dust_C = std::max(0.0, rho_dust_C); + rho_dust_O = std::max(0.0, rho_dust_O); + // Ensure dust sub-components don't exceed total dust + rho_dust_C = std::min(rho_dust_C, rho_dust); + rho_dust_O = std::min(rho_dust_O, rho_dust - rho_dust_C); + } + + fprintf(stderr, + "internal: dt=%e growth=%.10e destruct=%.10e " + "cre_dust=%.10e cre_metal=%.10e " + "gas=%.15e dust=%.15e metal=%.15e\n", + dt, growth_dM[i], destruction_dM[i], + creation_dust_dM[i], creation_metal_dM[i], + rho_gas, rho_dust, rho_metal_other + rho_metal_C + rho_metal_O); // Update the fields if (!dryrun) { dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; - metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_other; d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + if (track_elements) { + metal_C(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_C; + metal_O(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_O; + dust_C(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_C; + dust_O(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_O; + } } } } diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 2636bbd21..63a3d6c98 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -28,9 +28,35 @@ ENTRY(z_velocity) // metal_cooling = 1 ENTRY(metal_density) + // dust_model1_track_elements = 1 +ENTRY(metal_density_carbon) +ENTRY(metal_density_oxygen) + // use_dust_density_field = 1 ENTRY(dust_density) + // dust_model1_track_elements = 1 +ENTRY(dust_density_carbon) +ENTRY(dust_density_oxygen) + +// the following are included in this list because we don't evolve these species + + // metal_chemistry = 1 + // multi_metals = 0, metal_abundances = 0-11 selects one of below + // multi_metals = 1, all of below +ENTRY(local_ISM_metal_density) +ENTRY(ccsn13_metal_density) +ENTRY(ccsn20_metal_density) +ENTRY(ccsn25_metal_density) +ENTRY(ccsn30_metal_density) +ENTRY(fsn13_metal_density) +ENTRY(fsn15_metal_density) +ENTRY(fsn50_metal_density) +ENTRY(fsn80_metal_density) +ENTRY(pisn170_metal_density) +ENTRY(pisn200_metal_density) +ENTRY(y19_metal_density) + // use_volumetric_heating_rate = 1 ENTRY(volumetric_heating_rate) // use_specific_heating_rate = 1 diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 1f643ffdd..7099a74e6 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -340,6 +340,23 @@ ENTRY(solver_method, INT, 1) */ ENTRY(dust_model, INT, 0) +/* Track gas-phase C and O as separate element density fields, + with corresponding dust-phase composition bookkeeping. + Requires dust_model=1. + 0) off — bulk metal_density behaviour (default) + 1) on — element-resolved C/O tracking */ +ENTRY(dust_model1_track_elements, INT, 0) + +/* Element mass fractions for initialising C/O fields (Pollack et al. 1994). + "total" = fraction of total metals (gas + dust) that is the element. + "gas" = fraction of total metals that is gas-phase element. + Dust-phase fraction = total - gas. + Defaults are local ISM values (inject_pathway_props pathway 0). */ +ENTRY(dust_model1_C_total_fraction, DOUBLE, 1.79042e-01) +ENTRY(dust_model1_C_gas_fraction, DOUBLE, 5.01317e-02) +ENTRY(dust_model1_O_total_fraction, DOUBLE, 5.11524e-01) +ENTRY(dust_model1_O_gas_fraction, DOUBLE, 2.78491e-01) + /* Flag to use snetimestep */ ENTRY(use_sne_field, INT, 0) diff --git a/src/clib/initialize_chemistry_data.cpp b/src/clib/initialize_chemistry_data.cpp index 4ddcae002..434c07c24 100644 --- a/src/clib/initialize_chemistry_data.cpp +++ b/src/clib/initialize_chemistry_data.cpp @@ -232,6 +232,17 @@ static int local_initialize_chemistry_data_( return GR_FAIL; } + if (my_chemistry->dust_model1_track_elements > 0) { + if (my_chemistry->dust_model != 1) { + fprintf(stderr, "ERROR: dust_model1_track_elements requires dust_model=1.\n"); + return GR_FAIL; + } + if (my_chemistry->metal_cooling != 1) { + fprintf(stderr, "ERROR: dust_model1_track_elements requires metal_cooling=1.\n"); + return GR_FAIL; + } + } + // Default photo-electric heating to off if unset. if (my_chemistry->photoelectric_heating < 0) { my_chemistry->photoelectric_heating = 0; diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 77b253893..b2093192a 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -59,6 +59,24 @@ void make_consistent( const_cast(my_fields->metal_density), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + bool track_elements_mc = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_mc( + const_cast( + track_elements_mc ? my_fields->metal_density_carbon + : my_fields->density), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_mc( + const_cast( + track_elements_mc ? my_fields->metal_density_oxygen + : my_fields->density), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HM( my_fields->HM_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -266,6 +284,9 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metalfree[i] = d(i, j, k) - metal(i, j, k); + if (track_elements_mc) { + metalfree[i] -= metal_C_mc(i, j, k) + metal_O_mc(i, j, k); + } } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index c34ab8e79..da4abf554 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -318,6 +318,23 @@ typedef struct */ int dust_model; + /* Track gas-phase C and O as separate element density fields, + with corresponding dust-phase composition bookkeeping. + Requires dust_model=1. + 0) off — bulk metal_density behaviour (default) + 1) on — element-resolved C/O tracking */ + int dust_model1_track_elements; + + /* Element mass fractions for initialising C/O fields (Pollack et al. 1994). + "total" = fraction of total metals (gas + dust) that is the element. + "gas" = fraction of total metals that is gas-phase element. + Dust-phase fraction = total - gas. + Defaults are local ISM values (inject_pathway_props pathway 0). */ + double dust_model1_C_total_fraction; + double dust_model1_C_gas_fraction; + double dust_model1_O_total_fraction; + double dust_model1_O_gas_fraction; + /* Flag to use snetimestep */ int use_sne_field; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index b0af665ba..9eb988bd9 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -72,9 +72,17 @@ typedef struct // metal_cooling = 1 gr_float *metal_density; + // dust_model1_track_elements = 1 + gr_float *metal_density_carbon; // gas-phase C (all ionisation states) + gr_float *metal_density_oxygen; // gas-phase O (all ionisation states) + // use_dust_density_field = 1 gr_float *dust_density; + // dust_model1_track_elements = 1 + gr_float *dust_density_carbon; // C mass locked in dust grains + gr_float *dust_density_oxygen; // O mass locked in dust grains + // primordial_chemistry = 1 gr_float *e_density; gr_float *HI_density; diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index 2ea25a64a..a109135a6 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -266,6 +266,13 @@ def _required_density_fields(my_chemistry): my_fields.append("metal_density") if my_chemistry.dust_chemistry == 1: my_fields.append("dust_density") + if my_chemistry.dust_model1_track_elements == 1: + my_fields.extend([ + "metal_density_carbon", + "metal_density_oxygen", + "dust_density_carbon", + "dust_density_oxygen", + ]) if my_chemistry.metal_chemistry > 0: my_fields.extend(_dust_metal_densities[my_chemistry.dust_species]) my_fields.extend(_dust_densities[my_chemistry.dust_species]) diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index c538e565a..9f5f53dd6 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -109,7 +109,11 @@ cdef extern from "grackle.h": gr_float *y_velocity; gr_float *z_velocity; gr_float *metal_density; + gr_float *metal_density_carbon; + gr_float *metal_density_oxygen; gr_float *dust_density; + gr_float *dust_density_carbon; + gr_float *dust_density_oxygen; gr_float *e_density; gr_float *HI_density; gr_float *HII_density; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index fdcf00dce..1d25161d3 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -655,7 +655,11 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.z_velocity = get_field(fc, "z_velocity") my_fields.metal_density = get_field(fc, "metal_density") + my_fields.metal_density_carbon = get_field(fc, "metal_density_carbon") + my_fields.metal_density_oxygen = get_field(fc, "metal_density_oxygen") my_fields.dust_density = get_field(fc, "dust_density") + my_fields.dust_density_carbon = get_field(fc, "dust_density_carbon") + my_fields.dust_density_oxygen = get_field(fc, "dust_density_oxygen") my_fields.e_density = get_field(fc, "e_density") my_fields.HI_density = get_field(fc, "HI_density") diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index d90eeb20d..b7b772cac 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -219,9 +219,11 @@ def setup_fluid_container(my_chemistry, fc["z_velocity"][:] = 0.0 fc_last = fc.copy() - # disable cooling to iterate to equilibrium + # disable cooling and element tracking to iterate to equilibrium val = fc.chemistry_data.with_radiative_cooling fc.chemistry_data.with_radiative_cooling = 0 + val_track = fc.chemistry_data.dust_model1_track_elements + fc.chemistry_data.dust_model1_track_elements = 0 my_time = 0.0 i = 0 @@ -247,8 +249,27 @@ def setup_fluid_container(my_chemistry, i += 1 fc.chemistry_data.with_radiative_cooling = val + fc.chemistry_data.dust_model1_track_elements = val_track if i >= max_iterations: raise RuntimeError( f"ERROR: solver did not converge in {max_iterations} iterations.") + # Element-resolved C/O initialisation for dust_model=1. + # Deferred until after convergence so that tracking and non-tracking + # runs converge from identical bulk fields. + if my_chemistry.dust_model1_track_elements == 1: + M_total = fc["metal_density"] + fc["dust_density"] + f_C_total = my_chemistry.dust_model1_C_total_fraction + f_C_gas = my_chemistry.dust_model1_C_gas_fraction + f_O_total = my_chemistry.dust_model1_O_total_fraction + f_O_gas = my_chemistry.dust_model1_O_gas_fraction + + fc["metal_density_carbon"][:] = f_C_gas * M_total + fc["metal_density_oxygen"][:] = f_O_gas * M_total + fc["metal_density"][:] = (fc["metal_density"] + - fc["metal_density_carbon"] + - fc["metal_density_oxygen"]) + fc["dust_density_carbon"][:] = (f_C_total - f_C_gas) * M_total + fc["dust_density_oxygen"][:] = (f_O_total - f_O_gas) * M_total + return fc From 47f995b88a1926307ea056e4fa43c5a25e0ee644 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Fri, 3 Apr 2026 02:11:10 +0100 Subject: [PATCH 55/71] Bug fixing --- src/clib/ceiling_species.hpp | 34 +++++++++++--- src/clib/cool1d_multi_g.cpp | 8 +++- src/clib/make_consistent.cpp | 8 +++- src/clib/rate_timestep_g.cpp | 28 +++++++++++- src/clib/scale_fields.cpp | 36 +++++++++++++++ src/clib/scale_fields.hpp | 87 +++++++++++++++++++++++++++++------- 6 files changed, 174 insertions(+), 27 deletions(-) diff --git a/src/clib/ceiling_species.hpp b/src/clib/ceiling_species.hpp index 8cc78a79b..9de4302a2 100644 --- a/src/clib/ceiling_species.hpp +++ b/src/clib/ceiling_species.hpp @@ -69,7 +69,21 @@ inline void ceiling_species(int imetal, chemistry_data* my_chemistry, GRIMPL_NS::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - GRIMPL_NS::View dust( + bool track_elements_cs = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_cs( + track_elements_cs ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_cs( + track_elements_cs ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); GRIMPL_NS::View DM( @@ -247,10 +261,20 @@ inline void ceiling_species(int imetal, chemistry_data* my_chemistry, for (j = my_fields->grid_start[1]; j <= my_fields->grid_end[1]; j++) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metal(i, j, k) = std::fmax(metal(i, j, k), tiny_fortran_val); - if (metal(i, j, k) > d(i, j, k)) { - eprintf("WARNING: metal density exceeds total density!\n"); - eprintf("i, j, k, metal, density = %d %d %d %g %g\n", i, j, k, - metal(i, j, k), d(i, j, k)); + if (track_elements_cs) { + metal_C_cs(i, j, k) = std::fmax(metal_C_cs(i, j, k), tiny_fortran_val); + metal_O_cs(i, j, k) = std::fmax(metal_O_cs(i, j, k), tiny_fortran_val); + } + { + double total_metal_cs = metal(i, j, k); + if (track_elements_cs) { + total_metal_cs += metal_C_cs(i, j, k) + metal_O_cs(i, j, k); + } + if (total_metal_cs > d(i, j, k)) { + eprintf("WARNING: metal density exceeds total density!\n"); + eprintf("i, j, k, metal, density = %d %d %d %g %g\n", i, j, k, + total_metal_cs, d(i, j, k)); + } } // if( immulti .gt. 0 ) then // metal_loc(i,j,k) = max(metal_loc(i,j,k), tiny) diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 57eac2e3f..470667027 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -298,9 +298,13 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { + double total_metal_rhoH = metal(i, idx_range.j, idx_range.k); + if (track_elements_cool) { + total_metal_rhoH += metal_C_cool(i, idx_range.j, idx_range.k) + + metal_O_cool(i, idx_range.j, idx_range.k); + } rhoH[i] = my_chemistry->HydrogenFractionByMass * - (d(i, idx_range.j, idx_range.k) - - metal(i, idx_range.j, idx_range.k)); + (d(i, idx_range.j, idx_range.k) - total_metal_rhoH); } } } else { diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index b2093192a..6b6dfc107 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -543,10 +543,14 @@ void make_consistent( // ! if (d(i,j,k)*dom .lt. // ! & min(1.e6_DKIND/(metal(i,j,k)/d(i,j,k)/0.02d-4)**2 // ! & ,1.e6_DKIND)) then + double total_metal_mc = metal(i, j, k); + if (track_elements_mc) { + total_metal_mc += metal_C_mc(i, j, k) + metal_O_mc(i, j, k); + } if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || - ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && + ((imetal == 1) && (((total_metal_mc <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || - ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && + ((total_metal_mc > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + diff --git a/src/clib/rate_timestep_g.cpp b/src/clib/rate_timestep_g.cpp index e8f7cc82a..069eacaae 100644 --- a/src/clib/rate_timestep_g.cpp +++ b/src/clib/rate_timestep_g.cpp @@ -68,6 +68,20 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool track_elements_rt = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_rt( + track_elements_rt ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_rt( + track_elements_rt ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); // Radiative Transfer Fields grackle::impl::View kphHI( @@ -241,7 +255,12 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, // Add H2 formation on dust grains if (anydust != MASK_FALSE) { - if (metal(i, idx_range.j, idx_range.k) > + double total_metal_rt = metal(i, idx_range.j, idx_range.k); + if (track_elements_rt) { + total_metal_rt += metal_C_rt(i, idx_range.j, idx_range.k) + + metal_O_rt(i, idx_range.j, idx_range.k); + } + if (total_metal_rt > 1.e-9 * d(i, idx_range.j, idx_range.k)) { HIdot[i] = HIdot[i] - 2. * h2dust[i] * rhoH[i] * HI(i, idx_range.j, idx_range.k); @@ -350,7 +369,12 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, // ! endif if (anydust != MASK_FALSE) { - if (metal(i, idx_range.j, idx_range.k) > + double total_metal_rt2 = metal(i, idx_range.j, idx_range.k); + if (track_elements_rt) { + total_metal_rt2 += metal_C_rt(i, idx_range.j, idx_range.k) + + metal_O_rt(i, idx_range.j, idx_range.k); + } + if (total_metal_rt2 > 1.e-9 * d(i, idx_range.j, idx_range.k)) { H2delta[i] = H2delta[i] + h2dust[i] * HI(i, idx_range.j, idx_range.k) * diff --git a/src/clib/scale_fields.cpp b/src/clib/scale_fields.cpp index a36833d35..fcfe91228 100644 --- a/src/clib/scale_fields.cpp +++ b/src/clib/scale_fields.cpp @@ -106,9 +106,33 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool track_elements_sf = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_sf( + track_elements_sf ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_sf( + track_elements_sf ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_C_sf( + track_elements_sf ? my_fields->dust_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_O_sf( + track_elements_sf ? my_fields->dust_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View CI( my_fields->CI_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -272,6 +296,12 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metal(i, j, k) = metal(i, j, k) * factor; } + if (track_elements_sf) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + metal_C_sf(i, j, k) = metal_C_sf(i, j, k) * factor; + metal_O_sf(i, j, k) = metal_O_sf(i, j, k) * factor; + } + } if (my_chemistry->metal_chemistry == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -319,6 +349,12 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { dust(i, j, k) = dust(i, j, k) * factor; } + if (track_elements_sf) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + dust_C_sf(i, j, k) = dust_C_sf(i, j, k) * factor; + dust_O_sf(i, j, k) = dust_O_sf(i, j, k) * factor; + } + } if ((my_chemistry->grain_growth == 1) || (my_chemistry->dust_sublimation == 1)) { diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index 58ff282c5..37d791b90 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -53,31 +53,64 @@ inline void scale_fields_table(grackle_field_data* my_fields, double factor) { } } } + if (my_fields->metal_density_carbon != nullptr) { + grackle::impl::View metal_C( + my_fields->metal_density_carbon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + metal_C(i, j, k) = metal_C(i, j, k) * factor; + } + } + } + } + if (my_fields->metal_density_oxygen != nullptr) { + grackle::impl::View metal_O( + my_fields->metal_density_oxygen, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + metal_O(i, j, k) = metal_O(i, j, k) * factor; + } + } + } + } } -/// A helper function for scaling the injection pathway metal density fields -/// -/// @param[inout] my_fields holds the fields that will be updated in-place -/// @param[in] factor The factor that is multiplied by the fields -/// @param[in] n_inj_path_ptrs The number of pointers tracked by -/// `my_fields->inject_pathway_metal_density` -void scale_inject_path_metal_densities_(grackle_field_data* my_fields, - gr_float factor, int n_inj_path_ptrs); - /// Scales fields related to computing dust temperature -/// -/// @param[in] my_chemistry holds a number of configuration parameters -/// @param[inout] my_fields holds the fields that will be updated in-place -/// @param[in] imetal Specifies whether the metal_density was specified -/// @param[in] factor The factor that is multiplied by the fields -/// @param[in] n_inj_path_ptrs The number of pointers tracked by -/// `my_fields->inject_pathway_metal_density` + inline void scale_fields_dust(chemistry_data* my_chemistry, grackle_field_data* my_fields, int imetal, gr_float factor, int n_inj_path_ptrs) { grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool track_elements_sfd = + (my_chemistry->dust_model1_track_elements > 0 && + my_fields->metal_density_carbon != nullptr && + my_fields->metal_density_oxygen != nullptr); + grackle::impl::View metal_C_sfd( + track_elements_sfd ? my_fields->metal_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View metal_O_sfd( + track_elements_sfd ? my_fields->metal_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_C_sfd( + track_elements_sfd ? my_fields->dust_density_carbon + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_O_sfd( + track_elements_sfd ? my_fields->dust_density_oxygen + : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -133,11 +166,33 @@ inline void scale_fields_dust(chemistry_data* my_chemistry, if (imetal == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { metal(i, j, k) = metal(i, j, k) * factor; + if (track_elements_sfd) { + metal_C_sfd(i, j, k) = metal_C_sfd(i, j, k) * factor; + metal_O_sfd(i, j, k) = metal_O_sfd(i, j, k) * factor; + } + // if (my_chemistry->multi_metals > 0) { + // metal_loc(i, j, k) = metal_loc(i, j, k) * factor; + // metal_C13(i, j, k) = metal_C13(i, j, k) * factor; + // metal_C20(i, j, k) = metal_C20(i, j, k) * factor; + // metal_C25(i, j, k) = metal_C25(i, j, k) * factor; + // metal_C30(i, j, k) = metal_C30(i, j, k) * factor; + // metal_F13(i, j, k) = metal_F13(i, j, k) * factor; + // metal_F15(i, j, k) = metal_F15(i, j, k) * factor; + // metal_F50(i, j, k) = metal_F50(i, j, k) * factor; + // metal_F80(i, j, k) = metal_F80(i, j, k) * factor; + // metal_P170(i, j, k) = metal_P170(i, j, k) * factor; + // metal_P200(i, j, k) = metal_P200(i, j, k) * factor; + // metal_Y19(i, j, k) = metal_Y19(i, j, k) * factor; + // } } } if (my_chemistry->use_dust_density_field == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { dust(i, j, k) = dust(i, j, k) * factor; + if (track_elements_sfd) { + dust_C_sfd(i, j, k) = dust_C_sfd(i, j, k) * factor; + dust_O_sfd(i, j, k) = dust_O_sfd(i, j, k) * factor; + } if ((my_chemistry->grain_growth == 1) || (my_chemistry->dust_sublimation == 1)) { // ! if (metal(i,j,k) .gt. 1.d-9 * d(i,j,k)) then From 3a51672a074ef9b4cae0d853749f6662bb79c367 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Sat, 4 Apr 2026 02:54:50 +0100 Subject: [PATCH 56/71] Fixing bug --- src/clib/make_consistent.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 6b6dfc107..74475e006 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -76,6 +76,18 @@ void make_consistent( : my_fields->density), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_C_mc( + const_cast( + track_elements_mc ? my_fields->dust_density_carbon + : my_fields->density), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_O_mc( + const_cast( + track_elements_mc ? my_fields->dust_density_oxygen + : my_fields->density), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View HM( my_fields->HM_density, my_fields->grid_dimension[0], @@ -388,6 +400,18 @@ void make_consistent( Sg[i] = Sg[i] + onlygas_metal_yields.S[iSN] * cur_val; Feg[i] = Feg[i] + onlygas_metal_yields.Fe[iSN] * cur_val; } + + // When dust_model1_track_elements is active, metal_density_carbon + // and metal_density_oxygen directly track the gas-phase C and O + // mass (updated by dust_growth/dust_destruction each subcycle). + // Use these as the conservation targets instead of the yield-based + // values, which don't account for dust locking up C and O. + if (track_elements_mc) { + Cg[i] = metal_C_mc(i, j, k); + Og[i] = metal_O_mc(i, j, k); + Ct[i] = metal_C_mc(i, j, k) + dust_C_mc(i, j, k); + Ot[i] = metal_O_mc(i, j, k) + dust_O_mc(i, j, k); + } } for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -547,7 +571,11 @@ void make_consistent( if (track_elements_mc) { total_metal_mc += metal_C_mc(i, j, k) + metal_O_mc(i, j, k); } - if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + // When track_elements is active, Cg/Og come from the tracked + // fields (always valid), so bypass the density cutoff that + // exists for the yield-based computation. + if (track_elements_mc || + ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || ((imetal == 1) && (((total_metal_mc <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || ((total_metal_mc > 1.e-9 * d(i, j, k)) && From 8dde35af9e612075e734ed50edd4325d920345cf Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 22 Apr 2026 20:20:33 +0100 Subject: [PATCH 57/71] Make metal_density total; move dust_growth_and_destruction to dust/ metal_density now represents total gas-phase metals, with C/O as subsets (mirroring the dust_density/dust_density_carbon/oxygen pattern). Drops the add-C/O-into-metal arithmetic throughout cool/temp paths. Also moves dust_growth_and_destruction.{cpp,hpp} into src/clib/dust/ and removes the dead Fortran source. --- src/clib/Make.config.objects | 2 +- src/clib/api/calculate_temperature.cpp | 9 +- src/clib/calc_temp1d_cloudy_g.cpp | 19 - src/clib/calc_temp_cloudy.cpp | 22 +- src/clib/ceiling_species.hpp | 14 +- src/clib/dust/calc_tdust_3d.cpp | 23 +- .../dust_growth_and_destruction.cpp | 70 ++-- .../dust_growth_and_destruction.hpp | 0 src/clib/dust_growth_and_destruction.F | 361 ------------------ src/clib/field_data_misc_fdatamembers.def | 18 - src/clib/make_consistent.cpp | 14 +- src/clib/rate_timestep_g.cpp | 28 +- src/clib/scale_fields.hpp | 10 +- src/clib/solve_rate_cool.cpp | 9 +- src/include/grackle_types.h | 10 +- src/python/gracklepy/utilities/convenience.py | 8 +- 16 files changed, 66 insertions(+), 551 deletions(-) rename src/clib/{ => dust}/dust_growth_and_destruction.cpp (91%) rename src/clib/{ => dust}/dust_growth_and_destruction.hpp (100%) delete mode 100644 src/clib/dust_growth_and_destruction.F diff --git a/src/clib/Make.config.objects b/src/clib/Make.config.objects index 57b9c40ff..afeee8a9b 100644 --- a/src/clib/Make.config.objects +++ b/src/clib/Make.config.objects @@ -30,8 +30,8 @@ OBJS_CONFIG_LIB = \ dust/calc_all_tdust_gasgr_1d_g.lo \ dust/calc_kappa_grain.lo \ dust/calc_tdust_1d_g.lo \ + dust/dust_growth_and_destruction.lo \ dust/grain_species_info.lo \ - dust_growth_and_destruction.lo \ dynamic_api.lo \ grackle_units.lo \ index_helper.lo \ diff --git a/src/clib/api/calculate_temperature.cpp b/src/clib/api/calculate_temperature.cpp index 50fd06935..a5adeb324 100644 --- a/src/clib/api/calculate_temperature.cpp +++ b/src/clib/api/calculate_temperature.cpp @@ -97,14 +97,7 @@ extern "C" int local_calculate_temperature(chemistry_data *my_chemistry, } if (imetal) { - double total_metal = my_fields->metal_density[index]; - if (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != NULL && - my_fields->metal_density_oxygen != NULL) { - total_metal += my_fields->metal_density_carbon[index] - + my_fields->metal_density_oxygen[index]; - } - number_density += total_metal * inv_metal_mol; + number_density += my_fields->metal_density[index] * inv_metal_mol; } /* Ignore deuterium. */ diff --git a/src/clib/calc_temp1d_cloudy_g.cpp b/src/clib/calc_temp1d_cloudy_g.cpp index 65457dd7e..95c37d17b 100644 --- a/src/clib/calc_temp1d_cloudy_g.cpp +++ b/src/clib/calc_temp1d_cloudy_g.cpp @@ -37,21 +37,6 @@ void grackle::impl::calc_temp1d_cloudy_g( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_1d = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_1d( - track_elements_1d ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_1d( - track_elements_1d ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View e( my_fields->internal_energy, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -195,10 +180,6 @@ void grackle::impl::calc_temp1d_cloudy_g( if (imetal == 1) { double total_metal_1d = metal(i, idx_range.j, idx_range.k); - if (track_elements_1d) { - total_metal_1d += metal_C_1d(i, idx_range.j, idx_range.k) - + metal_O_1d(i, idx_range.j, idx_range.k); - } munew = d(i, idx_range.j, idx_range.k) / ((d(i, idx_range.j, idx_range.k) - total_metal_1d) / diff --git a/src/clib/calc_temp_cloudy.cpp b/src/clib/calc_temp_cloudy.cpp index b6405ccaa..ea71c0955 100644 --- a/src/clib/calc_temp_cloudy.cpp +++ b/src/clib/calc_temp_cloudy.cpp @@ -71,21 +71,6 @@ void calc_temp_cloudy(gr_float* temperature_data_, int imetal, my_fields->grid_dimension[1], my_fields->grid_dimension[2]); } - bool track_elements_temp = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - View metal_C_temp( - track_elements_temp ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - View metal_O_temp( - track_elements_temp ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - View temperature( temperature_data_, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -112,13 +97,8 @@ void calc_temp_cloudy(gr_float* temperature_data_, int imetal, itmask[i] = MASK_TRUE; if (imetal == 1) { - gr_float total_metal = metal(i, idx_range.j, idx_range.k); - if (track_elements_temp) { - total_metal += metal_C_temp(i, idx_range.j, idx_range.k) - + metal_O_temp(i, idx_range.j, idx_range.k); - } gr_float metal_free_density = (d(i, idx_range.j, idx_range.k) - - total_metal); + metal(i, idx_range.j, idx_range.k)); rhoH[i] = f_H * metal_free_density; } else { rhoH[i] = f_H * d(i, idx_range.j, idx_range.k); diff --git a/src/clib/ceiling_species.hpp b/src/clib/ceiling_species.hpp index 9de4302a2..bd3e2f237 100644 --- a/src/clib/ceiling_species.hpp +++ b/src/clib/ceiling_species.hpp @@ -265,16 +265,10 @@ inline void ceiling_species(int imetal, chemistry_data* my_chemistry, metal_C_cs(i, j, k) = std::fmax(metal_C_cs(i, j, k), tiny_fortran_val); metal_O_cs(i, j, k) = std::fmax(metal_O_cs(i, j, k), tiny_fortran_val); } - { - double total_metal_cs = metal(i, j, k); - if (track_elements_cs) { - total_metal_cs += metal_C_cs(i, j, k) + metal_O_cs(i, j, k); - } - if (total_metal_cs > d(i, j, k)) { - eprintf("WARNING: metal density exceeds total density!\n"); - eprintf("i, j, k, metal, density = %d %d %d %g %g\n", i, j, k, - total_metal_cs, d(i, j, k)); - } + if (metal(i, j, k) > d(i, j, k)) { + eprintf("WARNING: metal density exceeds total density!\n"); + eprintf("i, j, k, metal, density = %d %d %d %g %g\n", i, j, k, + metal(i, j, k), d(i, j, k)); } // if( immulti .gt. 0 ) then // metal_loc(i,j,k) = max(metal_loc(i,j,k), tiny) diff --git a/src/clib/dust/calc_tdust_3d.cpp b/src/clib/dust/calc_tdust_3d.cpp index 49732b42d..0b755f478 100644 --- a/src/clib/dust/calc_tdust_3d.cpp +++ b/src/clib/dust/calc_tdust_3d.cpp @@ -76,17 +76,6 @@ void calc_tdust_3d( View dust_temp(dust_temp_data_, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View isrf_habing(my_fields->isrf_habing, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View metal(my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_tdust = (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - View metal_C_tdust( - track_elements_tdust ? my_fields->metal_density_carbon : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - View metal_O_tdust( - track_elements_tdust ? my_fields->metal_density_oxygen : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); View dust(my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); View SiM_temp(my_fields->SiM_dust_temperature, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -164,11 +153,7 @@ void calc_tdust_3d( // Set itmask to false for metal-poor cells if (imetal == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - double total_metal_mask = metal(i,j,k); - if (track_elements_tdust) { - total_metal_mask += metal_C_tdust(i,j,k) + metal_O_tdust(i,j,k); - } - if (total_metal_mask < 1.e-9 * d(i,j,k)) { + if (metal(i,j,k) < 1.e-9 * d(i,j,k)) { itmask_metal[i] = MASK_FALSE; } } @@ -199,11 +184,7 @@ void calc_tdust_3d( // Calculate metallicity if (imetal == 1) { - double total_metal_i = metal(i,j,k); - if (track_elements_tdust) { - total_metal_i += metal_C_tdust(i,j,k) + metal_O_tdust(i,j,k); - } - metallicity[i] = total_metal_i / d(i,j,k) / my_chemistry->SolarMetalFractionByMass; + metallicity[i] = metal(i,j,k) / d(i,j,k) / my_chemistry->SolarMetalFractionByMass; } // Calculate dust to gas ratio diff --git a/src/clib/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp similarity index 91% rename from src/clib/dust_growth_and_destruction.cpp rename to src/clib/dust/dust_growth_and_destruction.cpp index 07dd21473..a944c1425 100644 --- a/src/clib/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -7,9 +7,6 @@ #include "utils-cpp.hpp" namespace { -const double k_boltz = 1.3806504e-16; -const double m_proton = 1.67262171e-24; -const double pi_val = 3.141592653589793; const double sec_per_year = 3.155e7; const double tiny_value = 1.0e-20; @@ -47,18 +44,6 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements = (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C( - track_elements ? my_fields->metal_density_carbon : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O( - track_elements ? my_fields->metal_density_oxygen : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year / internalu.tbase1; @@ -70,13 +55,8 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, if (itmask[i] != MASK_FALSE) { double rho_dust = dust(i, idx_range.j, idx_range.k); + // metal_density holds total gas-phase metals (C, O, and all others) double rho_metal = metal(i, idx_range.j, idx_range.k); - // When tracking elements, rho_metal must include C and O for - // correct accretion timescale and metal availability - if (track_elements) { - rho_metal += metal_C(i, idx_range.j, idx_range.k) - + metal_O(i, idx_range.j, idx_range.k); - } double rho_d = d(i, idx_range.j, idx_range.k); // No metals to accrete onto grains — skip growth @@ -323,10 +303,11 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); double rho_dust = dust(i, idx_range.j, idx_range.k); - double rho_metal_other = metal(i, idx_range.j, idx_range.k); + // metal_density holds the total gas-phase metals (C + O + others) + double rho_metal_total = metal(i, idx_range.j, idx_range.k); double dt = dt_value[i]; - // Load element-resolved fields (zero when not tracking) + // Load element-resolved subsets (zero when not tracking) double rho_metal_C = 0.0, rho_metal_O = 0.0; double rho_dust_C = 0.0, rho_dust_O = 0.0; if (track_elements) { @@ -335,9 +316,9 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, rho_dust_C = dust_C(i, idx_range.j, idx_range.k); rho_dust_O = dust_O(i, idx_range.j, idx_range.k); } + // Non-C, non-O gas metals, derived from the total + double rho_metal_other = rho_metal_total - rho_metal_C - rho_metal_O; - // Total gas-phase metals (needed for clamping and gas density update) - double rho_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; double f_C = (rho_metal_total > 0) ? rho_metal_C / rho_metal_total : 0.0; double f_O = (rho_metal_total > 0) ? rho_metal_O / rho_metal_total : 0.0; double f_other = (rho_metal_total > 0) ? rho_metal_other / rho_metal_total : 0.0; @@ -471,17 +452,6 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, } } - // Gas density update: - // density includes all metal fields as a subset, but NOT dust_density. - // Track how total gas-phase metals changed to update gas density. - double old_metal_total = metal(i, idx_range.j, idx_range.k); - if (track_elements) { - old_metal_total += metal_C(i, idx_range.j, idx_range.k) - + metal_O(i, idx_range.j, idx_range.k); - } - double new_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; - rho_gas = rho_gas + (new_metal_total - old_metal_total); - // Safety checks if (rho_dust < 0) { fprintf(stderr, @@ -501,18 +471,28 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, rho_dust_O = std::min(rho_dust_O, rho_dust - rho_dust_C); } - fprintf(stderr, - "internal: dt=%e growth=%.10e destruct=%.10e " - "cre_dust=%.10e cre_metal=%.10e " - "gas=%.15e dust=%.15e metal=%.15e\n", - dt, growth_dM[i], destruction_dM[i], - creation_dust_dM[i], creation_metal_dM[i], - rho_gas, rho_dust, rho_metal_other + rho_metal_C + rho_metal_O); + // Gas density update: + // density includes all metal fields as a subset, but NOT dust_density. + // Track how total gas-phase metals changed to update gas density. + // Compute after clamping so metal_C/O remain subsets of the total. + double old_metal_total = metal(i, idx_range.j, idx_range.k); + double new_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; + rho_gas = rho_gas + (new_metal_total - old_metal_total); - // Update the fields + // fprintf(stderr, + // "internal: dt=%e growth=%.10e destruct=%.10e " + // "cre_dust=%.10e cre_metal=%.10e " + // "gas=%.15e dust=%.15e metal=%.15e\n", + // dt, growth_dM[i], destruction_dM[i], + // creation_dust_dM[i], creation_metal_dM[i], + // rho_gas, rho_dust, new_metal_total); + fprintf(stderr, "checking: %e\n", rho_gas + rho_dust); + + // Update the fields. metal_density now stores the *total* gas-phase + // metals; metal_density_carbon / oxygen are subsets of it. if (!dryrun) { dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; - metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_other; + metal(i, idx_range.j, idx_range.k) = (gr_float)new_metal_total; d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; if (track_elements) { metal_C(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_C; diff --git a/src/clib/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp similarity index 100% rename from src/clib/dust_growth_and_destruction.hpp rename to src/clib/dust/dust_growth_and_destruction.hpp diff --git a/src/clib/dust_growth_and_destruction.F b/src/clib/dust_growth_and_destruction.F deleted file mode 100644 index bd74989aa..000000000 --- a/src/clib/dust_growth_and_destruction.F +++ /dev/null @@ -1,361 +0,0 @@ -#include "phys_const.def" -#include "dust_evol.def" - -!//////////////////////////////////////////////////////////// -! calculate characteristic timescales used for subcycling -!//////////////////////////////////////////////////////////// - subroutine dust_tau_for_timestep(in, jn, kn, i, j, k, - & Density, Tgas, Tdust, metal_Density, - & gas_metal1, gas_metal2, gas_metal3, - & gas_metal4, gas_metal5, gas_metal6, - & gas_metal7, gas_metal8, gas_metal9, - & gas_metal10, SNe_ThisTimeStep, - & dust_destruction_eff, - & sne_coeff, sne_shockspeed, - & dust_grainsize, dust_growth_densref, - & dust_growth_tauref, - & tau_accr, tau_dest, tau_sput, - & z_solar, SolarAbundances, - & time_units, dens_units, len_units, aye, dtime) - implicit NONE -#include "grackle_fortran_types.def" - - character(80) :: userinp - integer in, jn, kn, i, j, k - real*8 tau_accr(in), tau_dest(in), tau_sput(in), - & z_solar, SolarAbundances(NUM_METAL_SPECIES_GRACKLE), - & dust_destruction_eff, sne_coeff, sne_shockspeed, - & dust_grainsize, dust_growth_densref, - & dust_growth_tauref, - & time_units, dens_units, len_units, aye, dtime - R_PREC Density(in,jn,kn), Tgas(in,jn,kn), Tdust(in,jn,kn), metal_Density(in,jn,kn), - & gas_metal1(in,jn,kn), gas_metal2(in,jn,kn), - & gas_metal3(in,jn,kn), gas_metal4(in,jn,kn), - & gas_metal5(in,jn,kn), gas_metal6(in,jn,kn), - & gas_metal7(in,jn,kn), gas_metal8(in,jn,kn), - & gas_metal9(in,jn,kn), gas_metal10(in,jn,kn), - & SNe_ThisTimeStep(in,jn,kn) - -! local variables - real*8 tau_ref, Ms100, tau_accr0, dens_proper - - dens_proper = dens_units * aye**3 - - Ms100 = 6800.0*sne_coeff - & *(100.0/sne_shockspeed)*(100.0/sne_shockspeed) - & * SolarMass / (dens_units * len_units**3) ! gas mass shocked per SNe (Sedov-Taylor) -! check: - tau_ref = dust_growth_tauref * 1e9 * SEC_PER_YEAR_GRACKLE / time_units - -! growth - tau_accr0 = tau_ref * (dust_growth_densref / dens_proper) - & * (Tdust(i,j,k)/Tgas(i,j,k))**0.5 - tau_accr(i) = 1.d+20 - tau_accr(i) = min(tau_accr0 * (z_solar - & / metal_Density(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(1) - & / gas_metal1(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(2) - & / gas_metal2(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(3) - & / gas_metal3(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(4) - & / gas_metal4(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(5) - & / gas_metal5(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(6) - & / gas_metal6(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(7) - & / gas_metal7(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(8) - & / gas_metal8(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(9) - & / gas_metal9(i,j,k)), tau_accr(i)) - tau_accr(i) = min(tau_accr0 * (SolarAbundances(10) - & / gas_metal10(i,j,k)), tau_accr(i)) - - if (tau_accr(i) .le. 0) then - write(*,*) "-- tau_accr neagative! --", tau_accr(i) - write(*,*) "tau_accr0=", tau_accr0 - write(*,*) "metal_density=", metal_density(i,j,k) - write(*,*) "gas_metal1=", gas_metal1(i,j,k) - write(*,*) "gas_metal2=", gas_metal2(i,j,k) - write(*,*) "gas_metal3=", gas_metal3(i,j,k) - write(*,*) "gas_metal4=", gas_metal4(i,j,k) - write(*,*) "gas_metal5=", gas_metal5(i,j,k) - write(*,*) "gas_metal6=", gas_metal6(i,j,k) - write(*,*) "gas_metal7=", gas_metal7(i,j,k) - write(*,*) "gas_metal8=", gas_metal8(i,j,k) - write(*,*) "gas_metal9=", gas_metal9(i,j,k) - write(*,*) "gas_metal10=", gas_metal10(i,j,k) - write(*,*) "SolarAbundances1=", SolarAbundances(1) - write(*,*) "SolarAbundances2=", SolarAbundances(2) - write(*,*) "SolarAbundances3=", SolarAbundances(3) - write(*,*) "SolarAbundances4=", SolarAbundances(4) - write(*,*) "SolarAbundances5=", SolarAbundances(5) - write(*,*) "SolarAbundances6=", SolarAbundances(6) - write(*,*) "SolarAbundances7=", SolarAbundances(7) - write(*,*) "SolarAbundances8=", SolarAbundances(8) - write(*,*) "SolarAbundances9=", SolarAbundances(9) - write(*,*) "SolarAbundances10=", SolarAbundances(10) - call exit(74) - endif - -! destruction by SN shocks - if (SNe_ThisTimeStep(i,j,k).le.0.0) then - tau_dest(i) = 1.d+20 - else - tau_dest(i) = Density(i,j,k)/(Ms100*SNe_ThisTimeStep(i,j,k) - & *dust_destruction_eff) * dtime - endif - -! destruction by thermal sputtering - tau_sput(i) = 1.7e8 * SEC_PER_YEAR_GRACKLE / time_units - & * (dust_grainsize/0.1) * (1e-27/(dens_proper*Density(i,j,k))) - & * ((2e6/Tgas(i,j,k))**2.5+1.0) ! Tsai & Mathews (1995) - - return - end ! end of dust_tau_for_timestep - - -!//////////////////////////////////////////////////////////// -! main routine for dust growth and destruction -!//////////////////////////////////////////////////////////// - subroutine dust_growth_and_destruction(in, jn, kn, i, j, k, - & gas_Density, dust_Density, metal_Density, Tgas, Tdust, - & gas_metal1, gas_metal2, gas_metal3, - & gas_metal4, gas_metal5, gas_metal6, - & gas_metal7, gas_metal8, gas_metal9, - & gas_metal10, - & dust_metal1, dust_metal2, dust_metal3, - & dust_metal4, dust_metal5, dust_metal6, - & dust_metal7, dust_metal8, dust_metal9, - & dust_metal10, - & SNe_ThisTimeStep, dust_destruction_eff, - & sne_coeff, sne_shockspeed, - & dust_grainsize, dust_growth_densref, - & dust_growth_tauref, - & tau_dest, tau_sput, - & z_solar, SolarAbundances, - & time_units, dens_units, len_units, aye, dtime, metpt, dmetpt) - implicit NONE -#include "grackle_fortran_types.def" -! arguments - integer in, jn, kn, i, j, k - real*8 tau_dest(in), tau_sput(in), - & z_solar, SolarAbundances(NUM_METAL_SPECIES_GRACKLE), - & dust_destruction_eff, sne_coeff, sne_shockspeed, - & dust_grainsize, dust_growth_densref, - & dust_growth_tauref, - & time_units, dens_units, len_units, aye, dtime, dM_conserv - R_PREC Tgas(in,jn,kn), Tdust(in,jn,kn), - & gas_metal1(in,jn,kn), gas_metal2(in,jn,kn), - & gas_metal3(in,jn,kn), gas_metal4(in,jn,kn), - & gas_metal5(in,jn,kn), gas_metal6(in,jn,kn), - & gas_metal7(in,jn,kn), gas_metal8(in,jn,kn), - & gas_metal9(in,jn,kn), gas_metal10(in,jn,kn), - & dust_metal1(in,jn,kn), dust_metal2(in,jn,kn), - & dust_metal3(in,jn,kn), dust_metal4(in,jn,kn), - & dust_metal5(in,jn,kn), dust_metal6(in,jn,kn), - & dust_metal7(in,jn,kn), dust_metal8(in,jn,kn), - & dust_metal9(in,jn,kn), dust_metal10(in,jn,kn), - & metal_Density(in,jn,kn), dust_Density(in,jn,kn), - & gas_Density(in,jn,kn), SNe_ThisTimeStep(in,jn,kn), - & metpt(NUM_METAL_SPECIES_GRACKLE+1), dmetpt(NUM_METAL_SPECIES_GRACKLE+1) - -! local variables - integer n - character(80) :: userinp - real*8 tau_ref, Ms100, tau_accr0, tau_accr - R_PREC dM(NUM_METAL_SPECIES_GRACKLE+1), dMs, - & Mmet(NUM_METAL_SPECIES_GRACKLE+1), - & dens_proper - -! debug variables - real*8 dM_tau_accr - R_PREC total_density_init, total_density_final - - dens_proper = dens_units * aye**3 - -! copy metallicites into a single array - metpt(1) = metal_Density(i,j,k) - metpt(2) = gas_metal1(i,j,k) - metpt(3) = gas_metal2(i,j,k) - metpt(4) = gas_metal3(i,j,k) - metpt(5) = gas_metal4(i,j,k) - metpt(6) = gas_metal5(i,j,k) - metpt(7) = gas_metal6(i,j,k) - metpt(8) = gas_metal7(i,j,k) - metpt(9) = gas_metal8(i,j,k) - metpt(10) = gas_metal9(i,j,k) - metpt(11) = gas_metal10(i,j,k) - dmetpt(1) = dust_Density(i,j,k) - dmetpt(2) = dust_metal1(i,j,k) - dmetpt(3) = dust_metal2(i,j,k) - dmetpt(4) = dust_metal3(i,j,k) - dmetpt(5) = dust_metal4(i,j,k) - dmetpt(6) = dust_metal5(i,j,k) - dmetpt(7) = dust_metal6(i,j,k) - dmetpt(8) = dust_metal7(i,j,k) - dmetpt(9) = dust_metal8(i,j,k) - dmetpt(10) = dust_metal9(i,j,k) - dmetpt(11) = dust_metal10(i,j,k) - - total_density_init = metpt(1) + dmetpt(1) - - Ms100 = 6800.0*sne_coeff - & *(100.0/sne_shockspeed)*(100.0/sne_shockspeed) - & * SolarMass / (dens_units * len_units**3) ! gas mass shocked per SNe (Sedov-Taylor) - tau_ref = dust_growth_tauref * 1e9 * SEC_PER_YEAR_GRACKLE / time_units - - do n=0+1,NUM_METAL_SPECIES_GRACKLE+1 - dM(n) = 0. - enddo - -! metals accreted from gas to dust - tau_accr0 = tau_ref * (dust_growth_densref/dens_proper) - & * (Tdust(i,j,k)/Tgas(i,j,k))**0.5 - do n=1+1,NUM_METAL_SPECIES_GRACKLE+1 - Mmet(n) = metpt(n) + dmetpt(n) - tau_accr = tau_accr0 * SolarAbundances(n-1) / metpt(n) - - if (dmetpt(n) .ne. dmetpt(n)) then - write(*,*) "dmetpt at index", n, - & "calculated as NaN in line 174 of dust model", dmetpt(n) - endif - - if (Mmet(n) .ne. Mmet(n)) then - write(*,*) "Mmet at index", n, - & "calculated as NaN in line 174 of dust model", Mmet(n) - endif - - if (tau_accr .ne. tau_accr) then - write(*,*) "tau_accr", - & "calculated as NaN in line 174 of dust model", tau_accr - endif - - if (dtime .ne. dtime) then - write(*,*) "dtime", - & "calculated as NaN in line 174 of dust model", dtime - endif - - dM(n) = dM(n) + min((1 - dmetpt(n) / Mmet(n)) - & * (dmetpt(n)/tau_accr) * dtime, - & Mmet(n) - dmetpt(n)) ! growth capped by gas in a single cell - - if (dM(n) .ne. dM(n)) then - write(*,*) "dM at index", n, - & "calculated as NaN in line 174 of dust model", dM(n) - endif - - dM(1) = dM(1) + dM(n) - enddo - - dM_tau_accr = dM(1) - -! SNe shock and thermal sputtering - if (SNe_ThisTimeStep(i,j,k) .le. 0.0) then - dMs = 0.0 - else - dMs = min(dust_Density(i,j,k)/tau_dest(i)*dtime, - & dust_Density(i,j,k)) - endif - if (dMs .ge. dust_Density(i,j,k)) then - if (dMs .gt. dust_Density(i,j,k)) then - write (*,*) "WARNING: dMs > Mdust SNe shock destruction:", - & SNe_ThisTimeStep(i,j,k), tau_dest(i) - endif - else - dMs = dMs + dust_Density(i,j,k) / tau_sput(i) *3.0* dtime - dMs = min(dMs,dust_Density(i,j,k)) - endif - do n=0+1,NUM_METAL_SPECIES_GRACKLE+1 - dM(n) = dM(n) - dmetpt(n) * dMs - - if (dM(n) .ne. dM(n)) then - write(*,*) "dM at index", n, - & "calculated as NaN in line 202 of dust model", dM(n) - endif - enddo - -! recalculate metallicity - do n=0+1,NUM_METAL_SPECIES_GRACKLE+1 -! 1. Place conditions on dM such that mass is conserved but dust and gas -! mass cannot be negative -! 2. Make sure that only 90% of the gaseous metals can become dust - dM(n) = max(-1*dmetpt(n), dM(n)) - dM(n) = min(0.9 * metpt(n), dM(n)) - -! Transfer metals between gaseous and dust phase only if dust present - dM_conserv = 0.0 - if (dust_Density(i,j,k) .gt. 0.0) then - dmetpt(n) = dmetpt(n) + dM(n) - metpt(n) = metpt(n) - dM(n) - else -! If dust density is <= 0 then makes sure metals are conserved accordingly - dM_conserv = dmetpt(n) - dmetpt(n) = dmetpt(n) - dM_conserv - metpt(n) = metpt(n) + dM_conserv - endif -! surely this should be only happen if you add to the dust metallicity, -! else you are removing gas metallicity and breaking conservation - ! metpt(n) = metpt(n) - dM(n) - enddo - -! update gas density to account for changes in metal density - gas_Density(i,j,k) = gas_Density(i,j,k) + (metpt(1) - metal_Density(i,j,k)) - - if (metpt(1) .lt. 0) then - write(*,*) "metpt(1)=", metpt(1), - & "dM(1)=", dM(1) - endif - - if (dmetpt(1) .lt. 0) then - write(*,*) "final metal density = ", metpt(1) - write(*,*) "inital metal density = ", metal_Density(i,j,k) - write(*,*) "DELTA metal density = ", -1*dM(1) - write(*,*) "final dust density = ", dmetpt(1) - write(*,*) "initial dust density = ", dust_Density(i,j,k) - write(*,*) "DELTA dust density = ", dM(1) - write(*,*) "------------" - write(*,*) "dM_tau_accr = ", dM_tau_accr - call exit(21) - endif - - total_density_final = metpt(1) + dmetpt(1) - - if (abs(total_density_final - total_density_init) .gt. 1d-8) then - write(*,*) "Total dust + metal density not conserved!" - write(*,*) "Initial value --> ", total_density_init - write(*,*) "Final value --> ", total_density_final - write(*,*) "dmetpt = ", dmetpt(1), ", metpt = ", metpt(1), ", dM = ", dM(1) - write(*,*) "Ending run!!" - call exit(21) - endif - -! copy metallicites back into fields arrays - metal_Density(i,j,k) = metpt(1) - gas_metal1(i,j,k) = metpt(2) - gas_metal2(i,j,k) = metpt(3) - gas_metal3(i,j,k) = metpt(4) - gas_metal4(i,j,k) = metpt(5) - gas_metal5(i,j,k) = metpt(6) - gas_metal6(i,j,k) = metpt(7) - gas_metal7(i,j,k) = metpt(8) - gas_metal8(i,j,k) = metpt(9) - gas_metal9(i,j,k) = metpt(10) - gas_metal10(i,j,k) = metpt(11) - dust_Density(i,j,k) = dmetpt(1) - dust_metal1(i,j,k) = dmetpt(2) - dust_metal2(i,j,k) = dmetpt(3) - dust_metal3(i,j,k) = dmetpt(4) - dust_metal4(i,j,k) = dmetpt(5) - dust_metal5(i,j,k) = dmetpt(6) - dust_metal6(i,j,k) = dmetpt(7) - dust_metal7(i,j,k) = dmetpt(8) - dust_metal8(i,j,k) = dmetpt(9) - dust_metal9(i,j,k) = dmetpt(10) - dust_metal10(i,j,k) = dmetpt(11) - - return - end ! end of dust growth and destruction module diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 63a3d6c98..d40b441e4 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -39,24 +39,6 @@ ENTRY(dust_density) ENTRY(dust_density_carbon) ENTRY(dust_density_oxygen) -// the following are included in this list because we don't evolve these species - - // metal_chemistry = 1 - // multi_metals = 0, metal_abundances = 0-11 selects one of below - // multi_metals = 1, all of below -ENTRY(local_ISM_metal_density) -ENTRY(ccsn13_metal_density) -ENTRY(ccsn20_metal_density) -ENTRY(ccsn25_metal_density) -ENTRY(ccsn30_metal_density) -ENTRY(fsn13_metal_density) -ENTRY(fsn15_metal_density) -ENTRY(fsn50_metal_density) -ENTRY(fsn80_metal_density) -ENTRY(pisn170_metal_density) -ENTRY(pisn200_metal_density) -ENTRY(y19_metal_density) - // use_volumetric_heating_rate = 1 ENTRY(volumetric_heating_rate) // use_specific_heating_rate = 1 diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 74475e006..840cbb915 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -296,9 +296,6 @@ void make_consistent( if ((imetal) == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metalfree[i] = d(i, j, k) - metal(i, j, k); - if (track_elements_mc) { - metalfree[i] -= metal_C_mc(i, j, k) + metal_O_mc(i, j, k); - } } } else { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -567,18 +564,15 @@ void make_consistent( // ! if (d(i,j,k)*dom .lt. // ! & min(1.e6_DKIND/(metal(i,j,k)/d(i,j,k)/0.02d-4)**2 // ! & ,1.e6_DKIND)) then - double total_metal_mc = metal(i, j, k); - if (track_elements_mc) { - total_metal_mc += metal_C_mc(i, j, k) + metal_O_mc(i, j, k); - } // When track_elements is active, Cg/Og come from the tracked // fields (always valid), so bypass the density cutoff that // exists for the yield-based computation. - if (track_elements_mc || + // if (track_elements_mc || + if ( ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || - ((imetal == 1) && (((total_metal_mc <= 1.e-9 * d(i, j, k)) && + ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || - ((total_metal_mc > 1.e-9 * d(i, j, k)) && + ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e6))))) { totalOg = 16. / 28. * CO(i, j, k) + 32. / 44. * CO2(i, j, k) + OI(i, j, k) + 16. / 17. * OH(i, j, k) + diff --git a/src/clib/rate_timestep_g.cpp b/src/clib/rate_timestep_g.cpp index 069eacaae..e8f7cc82a 100644 --- a/src/clib/rate_timestep_g.cpp +++ b/src/clib/rate_timestep_g.cpp @@ -68,20 +68,6 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_rt = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_rt( - track_elements_rt ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_rt( - track_elements_rt ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); // Radiative Transfer Fields grackle::impl::View kphHI( @@ -255,12 +241,7 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, // Add H2 formation on dust grains if (anydust != MASK_FALSE) { - double total_metal_rt = metal(i, idx_range.j, idx_range.k); - if (track_elements_rt) { - total_metal_rt += metal_C_rt(i, idx_range.j, idx_range.k) - + metal_O_rt(i, idx_range.j, idx_range.k); - } - if (total_metal_rt > + if (metal(i, idx_range.j, idx_range.k) > 1.e-9 * d(i, idx_range.j, idx_range.k)) { HIdot[i] = HIdot[i] - 2. * h2dust[i] * rhoH[i] * HI(i, idx_range.j, idx_range.k); @@ -369,12 +350,7 @@ void rate_timestep_g(double* dedot, double* HIdot, gr_mask_type anydust, // ! endif if (anydust != MASK_FALSE) { - double total_metal_rt2 = metal(i, idx_range.j, idx_range.k); - if (track_elements_rt) { - total_metal_rt2 += metal_C_rt(i, idx_range.j, idx_range.k) - + metal_O_rt(i, idx_range.j, idx_range.k); - } - if (total_metal_rt2 > + if (metal(i, idx_range.j, idx_range.k) > 1.e-9 * d(i, idx_range.j, idx_range.k)) { H2delta[i] = H2delta[i] + h2dust[i] * HI(i, idx_range.j, idx_range.k) * diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index 37d791b90..a6b2ad6ae 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -79,8 +79,16 @@ inline void scale_fields_table(grackle_field_data* my_fields, double factor) { } } -/// Scales fields related to computing dust temperature +/// A helper function for scaling the injection pathway metal density fields +/// +/// @param[inout] my_fields holds the fields that will be updated in-place +/// @param[in] factor The factor that is multiplied by the fields +/// @param[in] n_inj_path_ptrs The number of pointers tracked by +/// `my_fields->inject_pathway_metal_density` +void scale_inject_path_metal_densities_(grackle_field_data* my_fields, + gr_float factor, int n_inj_path_ptrs); +/// Scales fields related to computing dust temperature inline void scale_fields_dust(chemistry_data* my_chemistry, grackle_field_data* my_fields, int imetal, gr_float factor, int n_inj_path_ptrs) { diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index a3dbcb2a4..bf162b7ec 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -39,7 +39,7 @@ #include "cool1d_multi_g.hpp" #include "scale_fields.hpp" #include "solve_rate_cool.hpp" -#include "dust_growth_and_destruction.hpp" +#include "dust/dust_growth_and_destruction.hpp" /// overrides the subcycle timestep (for each index in the index-range that is /// selected by the given itmask) with the maximum allowed heating/cooling @@ -917,6 +917,13 @@ int solve_rate_cool( ); } + // TEMPORARY: dust growth/destruction is currently invoked here as its + // own block. Eventually, the growth and destruction rates should be + // computed alongside the other dust rates (stored together in the + // newly-created FullRxnRateBuf), and the dust density updates should + // happen alongside the other density updates rather than as a + // separate pass. The placement below is a short-term stopgap and + // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ // Calculate dust growth rates and store in growth_dM array grackle::impl::dust_growth( diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index 9eb988bd9..3bbc80b59 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -73,15 +73,17 @@ typedef struct gr_float *metal_density; // dust_model1_track_elements = 1 - gr_float *metal_density_carbon; // gas-phase C (all ionisation states) - gr_float *metal_density_oxygen; // gas-phase O (all ionisation states) + // These are *subsets* of metal_density (not separate fields). + gr_float *metal_density_carbon; // gas-phase C (subset of metal_density) + gr_float *metal_density_oxygen; // gas-phase O (subset of metal_density) // use_dust_density_field = 1 gr_float *dust_density; // dust_model1_track_elements = 1 - gr_float *dust_density_carbon; // C mass locked in dust grains - gr_float *dust_density_oxygen; // O mass locked in dust grains + // These are *subsets* of dust_density (not separate fields). + gr_float *dust_density_carbon; // C in dust (subset of dust_density) + gr_float *dust_density_oxygen; // O in dust (subset of dust_density) // primordial_chemistry = 1 gr_float *e_density; diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index b7b772cac..d39fb3fa2 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -255,8 +255,9 @@ def setup_fluid_container(my_chemistry, f"ERROR: solver did not converge in {max_iterations} iterations.") # Element-resolved C/O initialisation for dust_model=1. - # Deferred until after convergence so that tracking and non-tracking - # runs converge from identical bulk fields. + # metal_density holds the *total* gas-phase metal budget; C and O are + # subsets of it (not separate). Same convention as dust_density / + # dust_density_carbon / dust_density_oxygen. if my_chemistry.dust_model1_track_elements == 1: M_total = fc["metal_density"] + fc["dust_density"] f_C_total = my_chemistry.dust_model1_C_total_fraction @@ -266,9 +267,6 @@ def setup_fluid_container(my_chemistry, fc["metal_density_carbon"][:] = f_C_gas * M_total fc["metal_density_oxygen"][:] = f_O_gas * M_total - fc["metal_density"][:] = (fc["metal_density"] - - fc["metal_density_carbon"] - - fc["metal_density_oxygen"]) fc["dust_density_carbon"][:] = (f_C_total - f_C_gas) * M_total fc["dust_density_oxygen"][:] = (f_O_total - f_O_gas) * M_total From a429432b2762ed396a878e01c15c7a3649dc7858 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Fri, 1 May 2026 03:38:20 +0100 Subject: [PATCH 58/71] rebasing fix --- src/clib/CMakeLists.txt | 2 +- src/clib/cool1d_multi_g.cpp | 70 ++----------------- src/clib/dust/dust_growth_and_destruction.cpp | 16 ++--- src/clib/make_consistent.cpp | 5 +- 4 files changed, 19 insertions(+), 74 deletions(-) diff --git a/src/clib/CMakeLists.txt b/src/clib/CMakeLists.txt index b0c719044..7eabe8a1a 100644 --- a/src/clib/CMakeLists.txt +++ b/src/clib/CMakeLists.txt @@ -101,7 +101,7 @@ add_library(Grackle_Grackle api/set_default_chemistry_parameters.cpp api/solve_chemistry.cpp calc_temp_cloudy.cpp calc_temp_cloudy.hpp - dust_growth_and_destruction.cpp dust_growth_and_destruction.hpp + dust/dust_growth_and_destruction.cpp dust/dust_growth_and_destruction.hpp calc_temp1d_cloudy_g.cpp calc_temp1d_cloudy_g.hpp ceiling_species.hpp collisional_rate_props.cpp collisional_rate_props.hpp diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 470667027..3dc8bfe0e 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -31,7 +31,7 @@ #include "inject_model/grain_metal_inject_pathways.hpp" #include "internal_types.hpp" #include "utils-cpp.hpp" -#include "dust_growth_and_destruction.hpp" +#include "dust/dust_growth_and_destruction.hpp" void grackle::impl::cool1d_multi_g( int imetal, int iter, double* edot, double* tgas, double* mmw, double* p2d, @@ -97,9 +97,6 @@ void grackle::impl::cool1d_multi_g( track_elements_cool ? my_fields->metal_density_oxygen : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( - my_fields->dust_density, my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View Vheat( my_fields->volumetric_heating_rate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1131,65 +1128,12 @@ void grackle::impl::cool1d_multi_g( itmask_metal[i] = MASK_FALSE; } } - // Compute grain size increment - if ((my_chemistry->use_dust_density_field > 0) && - (my_chemistry->dust_species > 0) && (my_chemistry->dust_model == 0)) { - grackle::impl::calc_grain_size_increment_1d( - dom, idx_range, itmask_metal, my_chemistry, - my_rates->opaque_storage->grain_species_info, - my_rates->opaque_storage->inject_pathway_props, my_fields, - internal_dust_prop_buf); - } - - // Calculate dust to gas ratio AND interstellar radiation field - // -> an earlier version of this logic would store values @ indices - // where `itmask_metal(i) .ne. MASK_FALSE` - // -> this was undesirable, b/c these quantities are required for - // photo-electric heating, which can occur when - // `itmask_metal(i) .eq. MASK_FALSE` (we can revisit this choice - // later). Moreover, in most cases, these calculations will be - // faster when there is no branching - - if ((anydust != MASK_FALSE) || (my_chemistry->photoelectric_heating > 0)) { - if (my_chemistry->use_dust_density_field > 0) { - for (i = idx_range.i_start; i <= idx_range.i_end; i++) { - // REMINDER: use of `itmask` over `itmask_metal` is - // currently required by Photo-electric heating - if (itmask[i] != MASK_FALSE) { - // it may be faster to remove this branching - dust2gas[i] = dust(i, idx_range.j, idx_range.k) / - (d(i, idx_range.j, idx_range.k)); - } - } - } else { - for (i = idx_range.i_start; i <= idx_range.i_end; i++) { - dust2gas[i] = my_chemistry->local_dust_to_gas_ratio * metallicity[i]; - } - } - } - - if ((anydust != MASK_FALSE) || (my_chemistry->photoelectric_heating > 1)) { - if (my_chemistry->use_isrf_field > 0) { - for (i = idx_range.i_start; i <= idx_range.i_end; i++) { - myisrf[i] = isrf_habing(i, idx_range.j, idx_range.k); - } - } else { - for (i = idx_range.i_start; i <= idx_range.i_end; i++) { - myisrf[i] = my_chemistry->interstellar_radiation_field; - } - } - } - - // compute dust temperature and cooling due to dust - if (anydust != MASK_FALSE) { - // TODO: trad -> comp2 - grackle::impl::fortran_wrapper::calc_all_tdust_gasgr_1d_g( - comp2, tgas, tdust, metallicity, dust2gas, cool1dmulti_buf.mynh, - cool1dmulti_buf.gasgr_tdust, itmask_metal, coolunit, gasgr.data(), - myisrf.data(), kappa_tot.data(), my_chemistry, my_rates, my_fields, - idx_range, grain_temperatures, gas_grainsp_heatrate, grain_kappa, - logTlininterp_buf, internal_dust_prop_buf); - } + dust_related_props(anydust, tgas, cool1dmulti_buf.mynh, metallicity, itmask, + itmask_metal, my_chemistry, my_rates, my_fields, internalu, + idx_range, logTlininterp_buf, comp2, dust2gas, tdust, + grain_temperatures, gasgr.data(), gas_grainsp_heatrate, + kappa_tot.data(), grain_kappa, cool1dmulti_buf.gasgr_tdust, + myisrf.data(), internal_dust_prop_buf); // Calculate dust cooling rate if (anydust != MASK_FALSE) { diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index a944c1425..f4871e4e7 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -479,14 +479,14 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, double new_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; rho_gas = rho_gas + (new_metal_total - old_metal_total); - // fprintf(stderr, - // "internal: dt=%e growth=%.10e destruct=%.10e " - // "cre_dust=%.10e cre_metal=%.10e " - // "gas=%.15e dust=%.15e metal=%.15e\n", - // dt, growth_dM[i], destruction_dM[i], - // creation_dust_dM[i], creation_metal_dM[i], - // rho_gas, rho_dust, new_metal_total); - fprintf(stderr, "checking: %e\n", rho_gas + rho_dust); + fprintf(stderr, + "internal: dt=%e growth=%.10e destruct=%.10e " + "cre_dust=%.10e cre_metal=%.10e " + "gas=%.15e dust=%.15e metal=%.15e\n", + dt, growth_dM[i], destruction_dM[i], + creation_dust_dM[i], creation_metal_dM[i], + rho_gas, rho_dust, new_metal_total); + // fprintf(stderr, "checking: %e\n", rho_gas + rho_dust); // Update the fields. metal_density now stores the *total* gas-phase // metals; metal_density_carbon / oxygen are subsets of it. diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 840cbb915..f50ef5908 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -14,6 +14,7 @@ // make_consistent_g function from FORTRAN to C++ #include +#include #include #include "grackle.h" @@ -567,8 +568,8 @@ void make_consistent( // When track_elements is active, Cg/Og come from the tracked // fields (always valid), so bypass the density cutoff that // exists for the yield-based computation. - // if (track_elements_mc || - if ( + if (track_elements_mc || + // if ( ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || From 29adb1943e5070927722e79d24dc6dba7b7c56fa Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Fri, 1 May 2026 03:54:21 +0100 Subject: [PATCH 59/71] rebasing backup --- src/clib/dust/dust_growth_and_destruction.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 8a28d2356..211927232 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -4,7 +4,7 @@ #include "grackle_types.h" #include "grackle_chemistry_data.h" #include "index_helper.h" -#include "internal_units.h" +#include "internal_units.hpp" #include "phys_constants.h" #include "fortran_func_decls.h" From 6f5635d19cd8dbb21928c128373f81b5c79d0ebc Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Sun, 3 May 2026 23:56:11 +0100 Subject: [PATCH 60/71] Adding new tracking species & related parameters --- src/clib/field_data_misc_fdatamembers.def | 11 +++++++- src/clib/grackle_chemistry_data_fields.def | 32 ++++++++++++++++++++++ src/include/grackle_chemistry_data.h | 26 ++++++++++++++++++ src/include/grackle_types.h | 15 +++++++++- src/python/gracklepy/fluid_container.py | 12 ++++++++ src/python/gracklepy/grackle_defs.pxd | 5 ++++ src/python/gracklepy/grackle_wrapper.pyx | 5 ++++ 7 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index d40b441e4..047ec369a 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -28,10 +28,15 @@ ENTRY(z_velocity) // metal_cooling = 1 ENTRY(metal_density) - // dust_model1_track_elements = 1 + // dust_model1_track_elements = 1 OR dust_species_track = 1 ENTRY(metal_density_carbon) ENTRY(metal_density_oxygen) + // dust_species_track = 1 (5-element gas tracking) +ENTRY(metal_density_magnesium) +ENTRY(metal_density_silicon) +ENTRY(metal_density_iron) + // use_dust_density_field = 1 ENTRY(dust_density) @@ -39,6 +44,10 @@ ENTRY(dust_density) ENTRY(dust_density_carbon) ENTRY(dust_density_oxygen) + // dust_species_track = 1 (two-species dust) +ENTRY(dust_density_silicate) +ENTRY(dust_density_carbonaceous) + // use_volumetric_heating_rate = 1 ENTRY(volumetric_heating_rate) // use_specific_heating_rate = 1 diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 7099a74e6..d2cded106 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -374,3 +374,35 @@ ENTRY(dust_growth_tauref, DOUBLE, 0.004) /* Dust creation by stellar feedback parameters */ ENTRY(dust_condensation_eff, DOUBLE, 1.5e-1) ENTRY(sne_metal_yield, DOUBLE, 3.0) + +/* Two-species dust tracking (silicate + carbonaceous). + Requires dust_model=1. Mutually exclusive with dust_model1_track_elements. + 0) off (default — bulk dust_density) + 1) on — evolves dust_density_silicate, dust_density_carbonaceous and + 5-element gas tracking (C, O, Mg, Si, Fe). + REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937; + McKinnon+2018 MNRAS 478, 2851 */ +ENTRY(dust_species_track, INT, 0) + +/* Species-specific growth (accretion) reference timescales [Gyr]. + Used in tau = tau_ref * (a/0.1um) * (n_ref/n_H) * sqrt(T_ref/T) * (Z_sun_X/rho_X). + REF: Hirashita 2011 ApJ 743, 159 Table 1; Asano+2013 EP&S 65, 213 */ +ENTRY(dust_growth_tauref_silicate, DOUBLE, 0.4) +ENTRY(dust_growth_tauref_carbon, DOUBLE, 2.0) + +/* Species-specific thermal sputtering reference timescales [yr]. + Same scaling as Draine & Salpeter 1979 ApJ 231, 77; Tielens+1994 ApJ 431, 321: + tau_sp = tau_ref * (a/0.1) * (1e-27/(rho*n)) * ((2e6/T)^2.5 + 1). + REF (silicate): Tsai & Mathews 1995 ApJ 448, 84 + REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435 */ +ENTRY(dust_sputter_tauref_silicate, DOUBLE, 1.7e8) +ENTRY(dust_sputter_tauref_carbon, DOUBLE, 3.4e8) + +/* Silicate stoichiometric mass fractions (50/50 olivine MgFeSiO4 + + pyroxene MgSiO3 by mass). Used for both growth depletion and + destruction release of Mg/Fe/Si/O. Sum = 1.000. + REF: Draine 2003 ARA&A 41, 241; Dwek 1998 ApJ 501, 643 */ +ENTRY(dust_silicate_f_Mg, DOUBLE, 0.190) +ENTRY(dust_silicate_f_Fe, DOUBLE, 0.163) +ENTRY(dust_silicate_f_Si, DOUBLE, 0.221) +ENTRY(dust_silicate_f_O, DOUBLE, 0.426) diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index da4abf554..3efb6cc5a 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -353,6 +353,32 @@ typedef struct double dust_condensation_eff; double sne_metal_yield; + /* Two-species dust tracking (silicate + carbonaceous). + Requires dust_model=1. Mutually exclusive with dust_model1_track_elements. + 0) off — bulk dust_density (default) + 1) on — evolves dust_density_silicate, dust_density_carbonaceous and + 5-element gas tracking (C, O, Mg, Si, Fe). + REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 */ + int dust_species_track; + + /* Species-specific growth (accretion) reference timescales [Gyr]. + REF: Hirashita 2011 ApJ 743, 159 Table 1; Asano+2013 EP&S 65, 213 */ + double dust_growth_tauref_silicate; + double dust_growth_tauref_carbon; + + /* Species-specific thermal sputtering reference timescales [yr]. + REF (silicate): Tsai & Mathews 1995 ApJ 448, 84 + REF (carbon): Nozawa+2006 ApJ 648, 435 */ + double dust_sputter_tauref_silicate; + double dust_sputter_tauref_carbon; + + /* Silicate stoichiometric mass fractions (50/50 olivine MgFeSiO4 + + pyroxene MgSiO3 by mass). Sum = 1.000. + REF: Draine 2003 ARA&A 41, 241; Dwek 1998 ApJ 501, 643 */ + double dust_silicate_f_Mg; + double dust_silicate_f_Fe; + double dust_silicate_f_Si; + double dust_silicate_f_O; } chemistry_data; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index 3bbc80b59..69f58bbe1 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -72,11 +72,18 @@ typedef struct // metal_cooling = 1 gr_float *metal_density; - // dust_model1_track_elements = 1 + // dust_model1_track_elements = 1 OR dust_species_track = 1 // These are *subsets* of metal_density (not separate fields). gr_float *metal_density_carbon; // gas-phase C (subset of metal_density) gr_float *metal_density_oxygen; // gas-phase O (subset of metal_density) + // dust_species_track = 1 + // 5-element gas tracking adds Mg, Si, Fe alongside C, O above. + // Subsets of metal_density. REF: Choban+2022 MNRAS 514, 4506 + gr_float *metal_density_magnesium; + gr_float *metal_density_silicon; + gr_float *metal_density_iron; + // use_dust_density_field = 1 gr_float *dust_density; @@ -85,6 +92,12 @@ typedef struct gr_float *dust_density_carbon; // C in dust (subset of dust_density) gr_float *dust_density_oxygen; // O in dust (subset of dust_density) + // dust_species_track = 1 + // Two-species dust: bulk dust_density = silicate + carbonaceous. + // REF: Hirashita 2015 MNRAS 447, 2937; McKinnon+2018 MNRAS 478, 2851 + gr_float *dust_density_silicate; + gr_float *dust_density_carbonaceous; + // primordial_chemistry = 1 gr_float *e_density; gr_float *HI_density; diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index a109135a6..a0026f67e 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -273,6 +273,18 @@ def _required_density_fields(my_chemistry): "dust_density_carbon", "dust_density_oxygen", ]) + if my_chemistry.dust_species_track == 1: + # Two-species dust (silicate + carbonaceous) with 5-element gas tracking. + # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 + my_fields.extend([ + "metal_density_carbon", + "metal_density_oxygen", + "metal_density_magnesium", + "metal_density_silicon", + "metal_density_iron", + "dust_density_silicate", + "dust_density_carbonaceous", + ]) if my_chemistry.metal_chemistry > 0: my_fields.extend(_dust_metal_densities[my_chemistry.dust_species]) my_fields.extend(_dust_densities[my_chemistry.dust_species]) diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 9f5f53dd6..ec7729c10 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -111,9 +111,14 @@ cdef extern from "grackle.h": gr_float *metal_density; gr_float *metal_density_carbon; gr_float *metal_density_oxygen; + gr_float *metal_density_magnesium; + gr_float *metal_density_silicon; + gr_float *metal_density_iron; gr_float *dust_density; gr_float *dust_density_carbon; gr_float *dust_density_oxygen; + gr_float *dust_density_silicate; + gr_float *dust_density_carbonaceous; gr_float *e_density; gr_float *HI_density; gr_float *HII_density; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index 1d25161d3..be8484849 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -657,9 +657,14 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.metal_density = get_field(fc, "metal_density") my_fields.metal_density_carbon = get_field(fc, "metal_density_carbon") my_fields.metal_density_oxygen = get_field(fc, "metal_density_oxygen") + my_fields.metal_density_magnesium = get_field(fc, "metal_density_magnesium") + my_fields.metal_density_silicon = get_field(fc, "metal_density_silicon") + my_fields.metal_density_iron = get_field(fc, "metal_density_iron") my_fields.dust_density = get_field(fc, "dust_density") my_fields.dust_density_carbon = get_field(fc, "dust_density_carbon") my_fields.dust_density_oxygen = get_field(fc, "dust_density_oxygen") + my_fields.dust_density_silicate = get_field(fc, "dust_density_silicate") + my_fields.dust_density_carbonaceous = get_field(fc, "dust_density_carbonaceous") my_fields.e_density = get_field(fc, "e_density") my_fields.HI_density = get_field(fc, "HI_density") From ef76d177b7d910cc3fc0e0986231f44d1575ad93 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Mon, 4 May 2026 01:15:49 +0100 Subject: [PATCH 61/71] Adding dust growth by species --- src/clib/dust/dust_growth_and_destruction.cpp | 119 ++++++++++++++++++ src/clib/dust/dust_growth_and_destruction.hpp | 16 +++ src/clib/solve_rate_cool.cpp | 21 +++- src/python/gracklepy/fluid_container.py | 4 + 4 files changed, 156 insertions(+), 4 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index f4871e4e7..39c03bf2e 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -93,6 +93,125 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, } } +// ========================================== +// DUST GROWTH (SPECIES-SPECIFIC: silicate + carbonaceous) +// ========================================== +// Two parallel accretion rates onto pre-existing dust seeds, gated by +// dust_species_track == 1. Carbonaceous growth is rate-limited by gas-phase +// carbon. Silicate growth is rate-limited by the least-available key +// reactant in {Mg, Si, Fe, O} weighted by stoichiometric mass fraction +// f_X (Choban+2022 MNRAS 514, 4506 §2.2). The structural form of tau_accr +// follows Hirashita 2011 ApJ 743, 159 Eq. (16)-(17): +// tau_accr = tau_ref · (rho_ref / rho) · (T_ref/T)^0.5 · (Z_sun / Z_X) +// reusing the same dust_growth_densref / SolarMetalFractionByMass scaling +// convention as the bulk dust_growth() above. +void grackle::impl::dust_growth_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, const double* t_gas, + double* growth_dM_silicate, double* growth_dM_carbon) { + + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_carb( + my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mC( + my_fields->metal_density_carbon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mO( + my_fields->metal_density_oxygen, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mMg( + my_fields->metal_density_magnesium, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mSi( + my_fields->metal_density_silicon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mFe( + my_fields->metal_density_iron, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + // tau_ref values are stored in Gyr (Hirashita 2011 Table 1 convention) + double tau_ref_sil = my_chemistry->dust_growth_tauref_silicate * 1e9 * + sec_per_year / internalu.tbase1; + double tau_ref_carb = my_chemistry->dust_growth_tauref_carbon * 1e9 * + sec_per_year / internalu.tbase1; + + double f_Mg = my_chemistry->dust_silicate_f_Mg; + double f_Fe = my_chemistry->dust_silicate_f_Fe; + double f_Si = my_chemistry->dust_silicate_f_Si; + double f_O = my_chemistry->dust_silicate_f_O; + + // --- MAIN LOOP --- + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + growth_dM_silicate[i] = 0.0; + growth_dM_carbon[i] = 0.0; + + if (itmask[i] != MASK_FALSE) { + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); + + double rho_C = mC(i, idx_range.j, idx_range.k); + double rho_O = mO(i, idx_range.j, idx_range.k); + double rho_Mg = mMg(i, idx_range.j, idx_range.k); + double rho_Si = mSi(i, idx_range.j, idx_range.k); + double rho_Fe = mFe(i, idx_range.j, idx_range.k); + + double T = t_gas[i]; + double dt = dt_value[i]; + + double accr_struct = (my_chemistry->dust_growth_densref / dens_proper) * + std::pow(t_ref / T, 0.5); + + // ---------- Carbonaceous: rate-limited by gas-phase C ---------- + if (rho_C >= metal_gate_threshold * rho_gas && rho_dust_carb_i > 0.0) { + double rho_C_eff = std::max(rho_C, tiny_value); + double tau_accr_carb = tau_ref_carb * accr_struct * + (my_chemistry->SolarMetalFractionByMass / + rho_C_eff); + tau_accr_carb = std::min(std::max(tau_accr_carb, tiny_value), + huge_value); + double frac_avail = rho_C / (rho_dust_carb_i + rho_C); + frac_avail = std::clamp(frac_avail, 0.0, 1.0); + double rate = frac_avail * (rho_dust_carb_i / tau_accr_carb); + growth_dM_carbon[i] = std::min(rate, rho_C / dt); + } + + // ---------- Silicate: Choban+2022 key-reactant ---------- + // For each key element X, the maximum silicate dust mass that could + // be assembled from X is rho_X / f_X. The bottleneck element sets + // the rate. + double mass_from_Mg = (f_Mg > 0.0) ? rho_Mg / f_Mg : huge_value; + double mass_from_Fe = (f_Fe > 0.0) ? rho_Fe / f_Fe : huge_value; + double mass_from_Si = (f_Si > 0.0) ? rho_Si / f_Si : huge_value; + double mass_from_O = (f_O > 0.0) ? rho_O / f_O : huge_value; + double rho_sil_pool = std::min(std::min(mass_from_Mg, mass_from_Fe), + std::min(mass_from_Si, mass_from_O)); + + if (rho_sil_pool >= metal_gate_threshold * rho_gas && + rho_dust_sil_i > 0.0) { + double rho_pool_eff = std::max(rho_sil_pool, tiny_value); + double tau_accr_sil = tau_ref_sil * accr_struct * + (my_chemistry->SolarMetalFractionByMass / + rho_pool_eff); + tau_accr_sil = std::min(std::max(tau_accr_sil, tiny_value), + huge_value); + double frac_avail = rho_sil_pool / (rho_dust_sil_i + rho_sil_pool); + frac_avail = std::clamp(frac_avail, 0.0, 1.0); + double rate = frac_avail * (rho_dust_sil_i / tau_accr_sil); + growth_dM_silicate[i] = std::min(rate, rho_sil_pool / dt); + } + } + } +} + // ========================================== // DUST CREATION (STELLAR FEEDBACK) // ========================================== diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 211927232..6367982ac 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -19,6 +19,22 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, double* growth_dM // output: mass change rate for each cell ); +// Species-specific accretion onto two pre-existing dust populations +// (silicate + carbonaceous). Active when dust_species_track == 1. +// - carbonaceous: rate-limited by gas-phase carbon +// - silicate: rate-limited by min over {Mg, Si, Fe, O} of (rho_X / f_X), +// following the Choban+2022 MNRAS 514, 4506 §2.2 key-reactant approach +// Per-species tau_accr structural form follows Hirashita 2011 ApJ 743, 159 +// Eq. (16)-(17). No bulk dM, no partitioning — Phase D wires the species +// outputs into dust_update(). +void dust_growth_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, const double* t_gas, + double* growth_dM_silicate, // output: silicate accretion rate + double* growth_dM_carbon // output: carbonaceous accretion rate +); + // Calculates stellar feedback injection rates (Dwek 1998 framework). // Each SN injects m_Z metals: fraction delta -> dust, (1-delta) -> gas metals. void dust_creation(chemistry_data* my_chemistry, grackle_field_data* my_fields, diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index bf162b7ec..8e3248018 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -733,6 +733,10 @@ int solve_rate_cool( std::vector destruction_dM(my_fields->grid_dimension[0]); std::vector creation_dust_dM(my_fields->grid_dimension[0]); std::vector creation_metal_dM(my_fields->grid_dimension[0]); + // Phase B: species-specific accretion outputs (used when + // dust_species_track == 1; Phase D wires them into dust_update()) + std::vector growth_dM_silicate(my_fields->grid_dimension[0]); + std::vector growth_dM_carbon(my_fields->grid_dimension[0]); // iteration masks std::vector itmask(my_fields->grid_dimension[0]); @@ -925,10 +929,19 @@ int solve_rate_cool( // separate pass. The placement below is a short-term stopgap and // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ - // Calculate dust growth rates and store in growth_dM array - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - tgas.data(), growth_dM.data()); + // Calculate dust growth rates: bulk path (legacy) or species path + // (silicate + carbonaceous, Phase B). dust_update() still consumes + // the bulk growth_dM until Phase D rewires it. + if (my_chemistry->dust_species_track == 1) { + grackle::impl::dust_growth_species( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), + growth_dM_silicate.data(), growth_dM_carbon.data()); + } else { + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), growth_dM.data()); + } // // Calculate dust creation rates from stellar feedback // grackle::impl::dust_creation( diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index a0026f67e..f727df456 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -276,12 +276,16 @@ def _required_density_fields(my_chemistry): if my_chemistry.dust_species_track == 1: # Two-species dust (silicate + carbonaceous) with 5-element gas tracking. # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 + # Bulk metal_density / dust_density are also required because + # destruction + update still operate on bulk until Phase D rewires. my_fields.extend([ + "metal_density", "metal_density_carbon", "metal_density_oxygen", "metal_density_magnesium", "metal_density_silicon", "metal_density_iron", + "dust_density", "dust_density_silicate", "dust_density_carbonaceous", ]) From a289ccb7e714a4ed3ba446789b01b1075afbba2e Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Mon, 4 May 2026 03:02:10 +0100 Subject: [PATCH 62/71] Adding dust destruction by species and dust update by species. Remove dust creation --- src/clib/ceiling_species.hpp | 18 - src/clib/cool1d_multi_g.cpp | 44 +- src/clib/dust/dust_growth_and_destruction.cpp | 580 +++++++++--------- src/clib/dust/dust_growth_and_destruction.hpp | 50 +- src/clib/field_data_misc_fdatamembers.def | 6 +- src/clib/grackle_chemistry_data_fields.def | 19 +- src/clib/initialize_chemistry_data.cpp | 11 - src/clib/make_consistent.cpp | 47 +- src/clib/scale_fields.cpp | 37 -- src/clib/scale_fields.hpp | 32 - src/clib/solve_rate_cool.cpp | 52 +- src/include/grackle_chemistry_data.h | 19 +- src/include/grackle_types.h | 7 +- src/python/gracklepy/fluid_container.py | 7 - src/python/gracklepy/grackle_defs.pxd | 2 - src/python/gracklepy/grackle_wrapper.pyx | 2 - src/python/gracklepy/utilities/convenience.py | 21 +- 17 files changed, 383 insertions(+), 571 deletions(-) diff --git a/src/clib/ceiling_species.hpp b/src/clib/ceiling_species.hpp index bd3e2f237..c0ae42eff 100644 --- a/src/clib/ceiling_species.hpp +++ b/src/clib/ceiling_species.hpp @@ -69,20 +69,6 @@ inline void ceiling_species(int imetal, chemistry_data* my_chemistry, GRIMPL_NS::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_cs = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_cs( - track_elements_cs ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_cs( - track_elements_cs ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -261,10 +247,6 @@ inline void ceiling_species(int imetal, chemistry_data* my_chemistry, for (j = my_fields->grid_start[1]; j <= my_fields->grid_end[1]; j++) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metal(i, j, k) = std::fmax(metal(i, j, k), tiny_fortran_val); - if (track_elements_cs) { - metal_C_cs(i, j, k) = std::fmax(metal_C_cs(i, j, k), tiny_fortran_val); - metal_O_cs(i, j, k) = std::fmax(metal_O_cs(i, j, k), tiny_fortran_val); - } if (metal(i, j, k) > d(i, j, k)) { eprintf("WARNING: metal density exceeds total density!\n"); eprintf("i, j, k, metal, density = %d %d %d %g %g\n", i, j, k, diff --git a/src/clib/cool1d_multi_g.cpp b/src/clib/cool1d_multi_g.cpp index 3dc8bfe0e..7be3f9539 100644 --- a/src/clib/cool1d_multi_g.cpp +++ b/src/clib/cool1d_multi_g.cpp @@ -83,20 +83,6 @@ void grackle::impl::cool1d_multi_g( grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - // When dust_model1_track_elements=1, metal_density excludes C and O. - // We need Views for these fields to reconstruct total metallicity - // for the Cloudy cooling table lookup. - bool track_elements_cool = (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_cool( - track_elements_cool ? my_fields->metal_density_carbon : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_cool( - track_elements_cool ? my_fields->metal_density_oxygen : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View Vheat( my_fields->volumetric_heating_rate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -295,13 +281,8 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { - double total_metal_rhoH = metal(i, idx_range.j, idx_range.k); - if (track_elements_cool) { - total_metal_rhoH += metal_C_cool(i, idx_range.j, idx_range.k) - + metal_O_cool(i, idx_range.j, idx_range.k); - } rhoH[i] = my_chemistry->HydrogenFractionByMass * - (d(i, idx_range.j, idx_range.k) - total_metal_rhoH); + (d(i, idx_range.j, idx_range.k) - metal(i, idx_range.j, idx_range.k)); } } } else { @@ -355,12 +336,7 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { - double total_metal_mmw = metal(i, idx_range.j, idx_range.k); - if (track_elements_cool) { - total_metal_mmw += metal_C_cool(i, idx_range.j, idx_range.k) - + metal_O_cool(i, idx_range.j, idx_range.k); - } - mmw[i] = mmw[i] + total_metal_mmw / mu_metal; + mmw[i] = mmw[i] + metal(i, idx_range.j, idx_range.k) / mu_metal; } } } @@ -451,14 +427,7 @@ void grackle::impl::cool1d_multi_g( if (imetal == 1) { for (i = idx_range.i_start; i <= idx_range.i_end; i++) { if (itmask[i] != MASK_FALSE) { - double total_metal_i = metal(i, idx_range.j, idx_range.k); - // When tracking elements, metal_density excludes C and O — - // add them back for the Cloudy cooling table lookup - if (track_elements_cool) { - total_metal_i += metal_C_cool(i, idx_range.j, idx_range.k) - + metal_O_cool(i, idx_range.j, idx_range.k); - } - metallicity[i] = total_metal_i / + metallicity[i] = metal(i, idx_range.j, idx_range.k) / d(i, idx_range.j, idx_range.k) / my_chemistry->SolarMetalFractionByMass; } @@ -1408,14 +1377,9 @@ void grackle::impl::cool1d_multi_g( 1 - mmw[i] * (3.0 * my_chemistry->HydrogenFractionByMass + 1.0) / 4.0; if (imetal == 1) { - double total_metal_myde = metal(i, idx_range.j, idx_range.k); - if (track_elements_cool) { - total_metal_myde += metal_C_cool(i, idx_range.j, idx_range.k) - + metal_O_cool(i, idx_range.j, idx_range.k); - } cool1dmulti_buf.myde[i] = cool1dmulti_buf.myde[i] - - mmw[i] * total_metal_myde / + mmw[i] * metal(i, idx_range.j, idx_range.k) / (d(i, idx_range.j, idx_range.k) * mu_metal); } cool1dmulti_buf.myde[i] = diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index 39c03bf2e..ec6df4b36 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -212,64 +212,6 @@ void grackle::impl::dust_growth_species( } } -// ========================================== -// DUST CREATION (STELLAR FEEDBACK) -// ========================================== -// Following the standard dust evolution framework (Dwek 1998, ApJ 501, 643; -// reviewed in Galliano et al. 2018, ARA&A 56, 673). Each SN injects a total -// metal mass m_Z (sne_metal_yield). A fraction delta (dust_condensation_eff) -// condenses into dust grains, while the remaining (1 - delta) fraction enters -// the gas phase as metals: -// -// dM_dust/dt = delta * m_Z * R_SN (Eq. 1 of review 2504.10585) -// dM_metal/dt = (1 - delta) * m_Z * R_SN -// -// where R_SN = sne_rate (SN rate per unit volume per unit time). -// -void grackle::impl::dust_creation(chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, - const double* dt_value, - double* creation_dust_dM, - double* creation_metal_dM) { - bool use_sne = (my_chemistry->use_sne_field > 0); - if (!use_sne) { - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - creation_dust_dM[i] = 0.0; - creation_metal_dM[i] = 0.0; - } - return; - } - - grackle::impl::View sne( - my_fields->sne_rate, my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - - // Metal yield per SN in code mass units - double M_yield_code = my_chemistry->sne_metal_yield * SolarMass / - (internalu.urho * std::pow(internalu.uxyz, 3)); - - double f_cond = my_chemistry->dust_condensation_eff; - - // --- MAIN LOOP --- - for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - creation_dust_dM[i] = 0.0; - creation_metal_dM[i] = 0.0; - - if (itmask[i] != MASK_FALSE) { - double sne_this = sne(i, idx_range.j, idx_range.k); - double dt = dt_value[i]; - - if (sne_this > 0.0) { - double total_metal_inject = M_yield_code * sne_this / dt; - creation_dust_dM[i] = f_cond * total_metal_inject; - creation_metal_dM[i] = (1.0 - f_cond) * total_metal_inject; - } - } - } -} - // ========================================== // 2. DUST DESTRUCTION (SNe + SPUTTERING) // ========================================== @@ -375,251 +317,331 @@ void grackle::impl::dust_destruction( } } -void grackle::impl::dust_update(chemistry_data* my_chemistry, - grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, - const double* dt_value, const double* growth_dM, - const double* destruction_dM, - const double* creation_dust_dM, - const double* creation_metal_dM, - bool dryrun) { +// ========================================== +// DUST DESTRUCTION (SPECIES-SPECIFIC: silicate + carbonaceous) +// ========================================== +// SN-shock destruction + thermal sputtering applied independently to each +// dust species, gated by dust_species_track == 1. Carbonaceous (graphite) +// is the shock-vulnerability baseline; silicates are ~1.5x more easily +// shattered by SN shocks under canonical ISM conditions +// [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740]. +// Thermal sputtering uses species-specific tau_ref values +// [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; +// REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] +// with the same Draine & Salpeter 1979 / Tielens+1994 scaling form +// (a/0.1) * (1e-27/(rho*n)) * ((2e6/T)^2.5 + 1) used by the bulk path. +// Phase D wires the species outputs into dust_update(). +void grackle::impl::dust_destruction_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, const double* dt_value, const double* t_gas, + double* destruction_dM_silicate, double* destruction_dM_carbon) { + grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust( - my_fields->dust_density, my_fields->grid_dimension[0], + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( - my_fields->metal_density, my_fields->grid_dimension[0], + grackle::impl::View dust_carb( + my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - - // --- Element-resolved tracking setup --- - bool track_elements = (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr && - my_fields->dust_density_carbon != nullptr && - my_fields->dust_density_oxygen != nullptr); - - grackle::impl::View metal_C( - track_elements ? my_fields->metal_density_carbon : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O( - track_elements ? my_fields->metal_density_oxygen : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_C( - track_elements ? my_fields->dust_density_carbon : my_fields->density, + bool use_sne = (my_chemistry->use_sne_field > 0); + grackle::impl::View sne( + use_sne ? my_fields->sne_rate : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_O( - track_elements ? my_fields->dust_density_oxygen : my_fields->density, + bool use_tau_dest = (my_chemistry->use_tau_dest_field > 0); + grackle::impl::View tau_dest_field( + use_tau_dest ? my_fields->tau_dest : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + + // Base shock-yield mass coefficient (Li+2019 framework). + double Ms100 = 6800.0 * my_chemistry->sne_coeff * + (100.0 / my_chemistry->sne_shockspeed) * + (100.0 / my_chemistry->sne_shockspeed) * SolarMass / + (internalu.urho * std::pow(internalu.uxyz, 3)); + + // Species-specific shock-vulnerability multipliers. Graphite is the + // baseline (1.0); silicate is ~1.5x more easily destroyed under + // canonical SN-shock conditions. [REF: Slavin+2015 ApJ 803, 7] + const double shock_factor_carbon = 1.0; + const double shock_factor_silicate = 1.5; + + // Species-specific thermal sputtering tau_ref (params stored in years) + double tau_sput_ref_sil = my_chemistry->dust_sputter_tauref_silicate * + sec_per_year / internalu.tbase1; + double tau_sput_ref_carb = my_chemistry->dust_sputter_tauref_carbon * + sec_per_year / internalu.tbase1; + + // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + destruction_dM_silicate[i] = 0.0; + destruction_dM_carbon[i] = 0.0; + if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust = dust(i, idx_range.j, idx_range.k); - // metal_density holds the total gas-phase metals (C + O + others) - double rho_metal_total = metal(i, idx_range.j, idx_range.k); + double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); + + bool sil_active = (rho_dust_sil_i >= dust_gate_threshold * rho_gas); + bool carb_active = (rho_dust_carb_i >= dust_gate_threshold * rho_gas); + if (!sil_active && !carb_active) continue; + + double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; + double temp = t_gas[i]; double dt = dt_value[i]; - // Load element-resolved subsets (zero when not tracking) - double rho_metal_C = 0.0, rho_metal_O = 0.0; - double rho_dust_C = 0.0, rho_dust_O = 0.0; - if (track_elements) { - rho_metal_C = metal_C(i, idx_range.j, idx_range.k); - rho_metal_O = metal_O(i, idx_range.j, idx_range.k); - rho_dust_C = dust_C(i, idx_range.j, idx_range.k); - rho_dust_O = dust_O(i, idx_range.j, idx_range.k); - } - // Non-C, non-O gas metals, derived from the total - double rho_metal_other = rho_metal_total - rho_metal_C - rho_metal_O; - - double f_C = (rho_metal_total > 0) ? rho_metal_C / rho_metal_total : 0.0; - double f_O = (rho_metal_total > 0) ? rho_metal_O / rho_metal_total : 0.0; - double f_other = (rho_metal_total > 0) ? rho_metal_other / rho_metal_total : 0.0; - double f_dust_C = (rho_dust > 0) ? rho_dust_C / rho_dust : 0.0; - double f_dust_O = (rho_dust > 0) ? rho_dust_O / rho_dust : 0.0; - double f_dust_other = (rho_dust > 0) ? 1.0 - (rho_dust_C + rho_dust_O) / rho_dust : 0.0; - - // --- Growth + destruction: exchanges mass between dust <-> metal --- - // dM_exchange > 0 means net growth (metal -> dust) - // dM_exchange < 0 means net destruction (dust -> metal) - double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; - dM_exchange = std::max(-1.0 * rho_dust, dM_exchange); - dM_exchange = std::min(0.9 * rho_metal_total, dM_exchange); - - // --- Stellar feedback injection (Dwek 1998 framework) --- - double dM_create_dust = creation_dust_dM[i] * dt; - double dM_create_metal = creation_metal_dM[i] * dt; - - // Apply conservation logic - double dM_conserv = 0.0; - if (rho_dust >= 0.0) { - if (track_elements) { - // --- GROWTH partitioning (Aoyama et al. 2017 proportional approach): - // deplete gas-phase C, O, other metals proportionally to their - - // Compute intended subtractions from original dM_exchange - double dM_C = ((dM_exchange > 0) ? f_C : f_dust_C) * dM_exchange; - double dM_O = ((dM_exchange > 0) ? f_O : f_dust_O) * dM_exchange; - double dM_other = ((dM_exchange > 0) ? f_other : f_dust_other) * dM_exchange; - - // Cap each independently - if (dM_C > ((dM_exchange > 0) ? rho_metal_C : -rho_dust_C)) { - dM_C = (dM_exchange > 0) ? rho_metal_C : dM_C; - rho_metal_C -= (dM_exchange > 0) ? rho_metal_C : dM_C; - } else { - rho_metal_C -= (dM_exchange > 0) ? dM_C : -rho_dust_C; - dM_C = (dM_exchange > 0) ? dM_C : -rho_dust_C; + // Common (species-independent) sputtering structural factor + double sput_struct = (my_chemistry->dust_grainsize / 0.1) * + (1.0e-27 / (dens_proper * rho_gas)) * + (std::pow((2.0e6 / temp), 2.5) + 1.0); + + // Helper computing destruction rate for one species + auto compute_dM = [&](double rho_dust, double shock_factor, + double tau_sput_ref) -> double { + double dM_shock = 0.0; + double tau_dest = 1e20; + + if (use_tau_dest) { + tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); + if (tau_dest <= 0) tau_dest = 1e20; + // Apply species multiplier on top of user-supplied tau_dest: + // higher shock_factor -> shorter tau -> faster destruction. + tau_dest = tau_dest / shock_factor; + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); + } else if (use_sne) { + if (sne_this > 0.0) { + tau_dest = rho_gas / + (Ms100 * shock_factor * sne_this * + my_chemistry->dust_destruction_eff) * dt; + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); } + } - if (dM_O > ((dM_exchange > 0) ? rho_metal_O : -rho_dust_O)) { - dM_O = (dM_exchange > 0) ? rho_metal_O : dM_O; - rho_metal_O -= (dM_exchange > 0) ? rho_metal_O : dM_O; - } else { - rho_metal_O -= (dM_exchange > 0) ? dM_O : -rho_dust_O; - dM_O = (dM_exchange > 0) ? dM_O : -rho_dust_O; + if (temp >= std::pow(10, 5)) { + double tau_sput = tau_sput_ref * sput_struct; + if (dM_shock < rho_dust / dt) { + dM_shock = dM_shock + rho_dust / tau_sput * 3.0; + dM_shock = std::min(dM_shock, rho_dust / dt); } - - if (dM_other > ((dM_exchange > 0) ? rho_metal_other : -(rho_dust - rho_dust_C - rho_dust_O))) { - dM_other = (dM_exchange > 0) ? rho_metal_other : dM_other; - rho_metal_other -= (dM_exchange > 0) ? rho_metal_other : dM_other; - } else { - rho_metal_other -= (dM_exchange > 0) ? dM_other : -(rho_dust - rho_dust_C - rho_dust_O); - dM_other = (dM_exchange > 0) ? dM_other : -(rho_dust - rho_dust_C - rho_dust_O); - } - - // Dust receives exactly what was actually taken - rho_dust_C += dM_C; - rho_dust_O += dM_O; - dM_exchange = dM_C + dM_O + dM_other; - rho_dust = rho_dust + dM_exchange + dM_create_dust; - rho_metal_C += dM_create_metal * f_C; - rho_metal_O += dM_create_metal * f_O; - rho_metal_other += dM_create_metal * f_other; - - } else { - // Backward-compatible: all exchange with bulk metal_density - rho_metal_other = rho_metal_other - dM_exchange + dM_create_metal; - rho_dust = rho_dust + dM_exchange + dM_create_dust; - } - } else { - dM_conserv = -rho_dust; - rho_dust = 0.0; - - if (track_elements) { - bool converged = false; - if (dM_conserv > rho_metal_total) { - // Not enough dust species to cover - converged = false; - fprintf(stderr, - "[DustConservation] WARNING: dust species deficit — " - "need %.6e but only %.6e available " - "(C=%.6e, O=%.6e). Capping transfer.\n", - dM_conserv, rho_metal_total, - rho_dust_C, rho_dust_O); - std::exit(22); - } else { - if (rho_metal_C >= f_C * dM_conserv && rho_metal_O >= f_O * dM_conserv && rho_metal_other >= f_other * dM_conserv) { - rho_metal_C -= f_C*dM_conserv; - rho_metal_O -= f_O*dM_conserv; - rho_metal_other -= f_other*dM_conserv; - } else { - bool active[3] = {true, true, true}; - double f_back[3] = {f_C, f_O, f_other}; - double* rho_metal_arr[3] = { &rho_metal_C, &rho_metal_O, &rho_metal_other }; - double dM_back[3] = {f_C*dM_conserv,f_O*dM_conserv,f_other*dM_conserv}; - while (!converged) { - converged = true; - double shortfall = 0.0; - double f_sum = 0.0; - - for (int j = 0; j < 3; j++) { - if (*rho_metal_arr[j] < dM_back[j]) { - shortfall += dM_back[j] - *rho_metal_arr[j]; - dM_back[j] = *rho_metal_arr[j]; - active[j] = false; - } - } - - if (shortfall > 0.0) { - for (int j = 0; j < 3; j++) { - f_sum += active[j]*f_back[j]; - } - if (f_sum > 0.0) { - converged = false; - for (int j = 0; j < 3; j++) { - if (active[j]) { - dM_back[j] += (f_back[j]/f_sum)*shortfall; - } - } - } - } - } - rho_metal_C -= dM_back[0]; - rho_metal_O -= dM_back[1]; - rho_metal_other -= dM_back[2]; - } - } - } else { - rho_metal_other -= dM_conserv; + + double dM = -dM_shock; + if (std::isnan(dM)) { + std::cout << "dM (species) calculated as NaN, " << dM << std::endl; + dM = 0.0; } - } + return dM; + }; - // Safety checks - if (rho_dust < 0) { - fprintf(stderr, - "ERROR: Negative dust density at cell %d: rho_dust=%e\n", i, - rho_dust); - std::exit(21); + if (carb_active) { + destruction_dM_carbon[i] = compute_dM( + rho_dust_carb_i, shock_factor_carbon, tau_sput_ref_carb); } - - // Clamp element fields to prevent negative values from round-off - if (track_elements) { - rho_metal_C = std::max(0.0, rho_metal_C); - rho_metal_O = std::max(0.0, rho_metal_O); - rho_dust_C = std::max(0.0, rho_dust_C); - rho_dust_O = std::max(0.0, rho_dust_O); - // Ensure dust sub-components don't exceed total dust - rho_dust_C = std::min(rho_dust_C, rho_dust); - rho_dust_O = std::min(rho_dust_O, rho_dust - rho_dust_C); + if (sil_active) { + destruction_dM_silicate[i] = compute_dM( + rho_dust_sil_i, shock_factor_silicate, tau_sput_ref_sil); } + } + } +} - // Gas density update: - // density includes all metal fields as a subset, but NOT dust_density. - // Track how total gas-phase metals changed to update gas density. - // Compute after clamping so metal_C/O remain subsets of the total. - double old_metal_total = metal(i, idx_range.j, idx_range.k); - double new_metal_total = rho_metal_other + rho_metal_C + rho_metal_O; - rho_gas = rho_gas + (new_metal_total - old_metal_total); - - fprintf(stderr, - "internal: dt=%e growth=%.10e destruct=%.10e " - "cre_dust=%.10e cre_metal=%.10e " - "gas=%.15e dust=%.15e metal=%.15e\n", - dt, growth_dM[i], destruction_dM[i], - creation_dust_dM[i], creation_metal_dM[i], - rho_gas, rho_dust, new_metal_total); - // fprintf(stderr, "checking: %e\n", rho_gas + rho_dust); - - // Update the fields. metal_density now stores the *total* gas-phase - // metals; metal_density_carbon / oxygen are subsets of it. - if (!dryrun) { - dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; - metal(i, idx_range.j, idx_range.k) = (gr_float)new_metal_total; - d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; - if (track_elements) { - metal_C(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_C; - metal_O(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_O; - dust_C(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_C; - dust_O(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_O; - } - } +// ========================================== +// DUST UPDATE (SPECIES-SPECIFIC: silicate + carbonaceous) +// ========================================== +// Per-channel mass exchange between dust species and their corresponding +// gas-phase reactant pools. Active when dust_species_track == 1. +// +// carbon channel: rho_dust_carbonaceous <-> rho_metal_carbon +// silicate channel: rho_dust_silicate <-> {Mg, Fe, Si, O} at fractions f_X +// (Choban+2022 §2.2; 50/50 olivine+pyroxene per +// Draine 2003 / Dwek 1998). +// +// Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] +// shortfall iteration: growth is capped by the limiting reactant, destruction +// is capped by available dust mass. No SN injection on this path — host code +// seeds the species directly. +void grackle::impl::dust_update_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* growth_dM_silicate, + const double* growth_dM_carbon, const double* destruction_dM_silicate, + const double* destruction_dM_carbon, bool dryrun) { + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_carb( + my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mC( + my_fields->metal_density_carbon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mO( + my_fields->metal_density_oxygen, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mMg( + my_fields->metal_density_magnesium, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mSi( + my_fields->metal_density_silicon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View mFe( + my_fields->metal_density_iron, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + const double f_Mg = my_chemistry->dust_silicate_f_Mg; + const double f_Fe = my_chemistry->dust_silicate_f_Fe; + const double f_Si = my_chemistry->dust_silicate_f_Si; + const double f_O = my_chemistry->dust_silicate_f_O; + + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] == MASK_FALSE) continue; + + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); + double rho_metal_total = metal(i, idx_range.j, idx_range.k); + double rho_C = mC(i, idx_range.j, idx_range.k); + double rho_O = mO(i, idx_range.j, idx_range.k); + double rho_Mg = mMg(i, idx_range.j, idx_range.k); + double rho_Si = mSi(i, idx_range.j, idx_range.k); + double rho_Fe = mFe(i, idx_range.j, idx_range.k); + double dt = dt_value[i]; + + // dM > 0 means net growth (gas reactants -> dust); + // dM < 0 means net destruction (dust -> gas reactants). + double dM_carb = (growth_dM_carbon[i] + destruction_dM_carbon[i]) * dt; + double dM_sil = (growth_dM_silicate[i] + destruction_dM_silicate[i]) * dt; + + // ---------- Per-channel pre-cap ---------- + // Carbon channel: bounded by rho_C (growth) or rho_dust_carb (destruction). + if (dM_carb > 0.0) { + dM_carb = std::min(dM_carb, rho_C); + } else { + dM_carb = std::max(dM_carb, -rho_dust_carb_i); + } + + // Silicate channel: growth limited by least-available reactant per + // stoichiometric coefficient; destruction limited by silicate dust mass. + if (dM_sil > 0.0) { + double cap = dM_sil; + if (f_Mg > 0.0) cap = std::min(cap, rho_Mg / f_Mg); + if (f_Fe > 0.0) cap = std::min(cap, rho_Fe / f_Fe); + if (f_Si > 0.0) cap = std::min(cap, rho_Si / f_Si); + if (f_O > 0.0) cap = std::min(cap, rho_O / f_O); + dM_sil = std::max(0.0, cap); + } else { + dM_sil = std::max(dM_sil, -rho_dust_sil_i); + } + + // ---------- Apply ---------- + rho_dust_carb_i += dM_carb; + rho_C -= dM_carb; + + rho_dust_sil_i += dM_sil; + rho_Mg -= dM_sil * f_Mg; + rho_Fe -= dM_sil * f_Fe; + rho_Si -= dM_sil * f_Si; + rho_O -= dM_sil * f_O; + + // Bulk dust = silicate + carbonaceous (Phase E will enforce as invariant + // in make_consistent; computing it here keeps the bulk field in sync). + double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; + + // Total gas-phase metal change: -(dM_carb + dM_sil) (metals lost when + // dust grows). metal_density_other (= total - C - O - Mg - Si - Fe) is + // unchanged on the dust loop per Choban+2022 §2.2. + double delta_metal_total = -(dM_carb + dM_sil); + double rho_metal_new = rho_metal_total + delta_metal_total; + + // Gas density tracks metals as a subset (dust is not part of `density`). + rho_gas += delta_metal_total; + + // Floors / NaN guard + rho_dust_carb_i = std::max(0.0, rho_dust_carb_i); + rho_dust_sil_i = std::max(0.0, rho_dust_sil_i); + rho_dust_new = std::max(0.0, rho_dust_new); + rho_C = std::max(0.0, rho_C); + rho_O = std::max(0.0, rho_O); + rho_Mg = std::max(0.0, rho_Mg); + rho_Si = std::max(0.0, rho_Si); + rho_Fe = std::max(0.0, rho_Fe); + rho_metal_new = std::max(0.0, rho_metal_new); + + if (std::isnan(rho_dust_new) || std::isnan(rho_metal_new)) { + std::cout << "dust_update_species: NaN at cell " << i + << " dM_carb=" << dM_carb << " dM_sil=" << dM_sil << std::endl; + continue; + } + + if (!dryrun) { + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_new; + dust_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_sil_i; + dust_carb(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_carb_i; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_new; + mC(i, idx_range.j, idx_range.k) = (gr_float)rho_C; + mO(i, idx_range.j, idx_range.k) = (gr_float)rho_O; + mMg(i, idx_range.j, idx_range.k) = (gr_float)rho_Mg; + mSi(i, idx_range.j, idx_range.k) = (gr_float)rho_Si; + mFe(i, idx_range.j, idx_range.k) = (gr_float)rho_Fe; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + } + } +} + +void grackle::impl::dust_update(chemistry_data* my_chemistry, + grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, + const gr_mask_type* itmask, + const double* dt_value, const double* growth_dM, + const double* destruction_dM, bool dryrun) { + grackle::impl::View d( + my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + + for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { + if (itmask[i] == MASK_FALSE) continue; + + double rho_gas = d(i, idx_range.j, idx_range.k); + double rho_dust = dust(i, idx_range.j, idx_range.k); + double rho_metal_total = metal(i, idx_range.j, idx_range.k); + double dt = dt_value[i]; + + // dM_exchange > 0: net growth (metal -> dust); < 0: destruction (dust -> metal) + double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; + dM_exchange = std::max(-1.0 * rho_dust, dM_exchange); + dM_exchange = std::min(0.9 * rho_metal_total, dM_exchange); + + rho_dust += dM_exchange; + rho_metal_total -= dM_exchange; + rho_gas -= dM_exchange; // gas tracks metals as a subset + + rho_dust = std::max(0.0, rho_dust); + rho_metal_total = std::max(0.0, rho_metal_total); + + if (!dryrun) { + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_total; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; } } } \ No newline at end of file diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 6367982ac..3e76ff1b4 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -35,15 +35,6 @@ void dust_growth_species( double* growth_dM_carbon // output: carbonaceous accretion rate ); -// Calculates stellar feedback injection rates (Dwek 1998 framework). -// Each SN injects m_Z metals: fraction delta -> dust, (1-delta) -> gas metals. -void dust_creation(chemistry_data* my_chemistry, grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, const double* dt_value, - double* creation_dust_dM, // output: dust creation rate - double* creation_metal_dM // output: gas-phase metal injection rate -); - // Calculates dust destruction rates from SNe shocks and thermal sputtering. // Stores the mass change dM for each cell in destruction_dM array. void dust_destruction( @@ -53,6 +44,25 @@ void dust_destruction( double* destruction_dM // output: mass change rate for each cell ); +// Species-specific destruction (SN shocks + thermal sputtering) onto the two +// dust populations. Active when dust_species_track == 1. +// - shock yield: graphite is baseline (factor 1.0); silicate ~1.5x more +// easily destroyed [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; +// Jones+1996 ApJ 469, 740] +// - thermal sputtering: species-specific tau_ref +// [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; +// REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] +// with Draine & Salpeter 1979 / Tielens+1994 scaling form +// No bulk dM, no partitioning — Phase D wires the species outputs into +// dust_update(). +void dust_destruction_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, + double* destruction_dM_silicate, // output: silicate destruction rate + double* destruction_dM_carbon // output: carbonaceous destruction rate +); + // Update the density fields using calculated mass changes. void dust_update( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -60,8 +70,26 @@ void dust_update( const double* dt_value, const double* growth_dM, // input: mass change from growth const double* destruction_dM, // input: mass change from destruction - const double* creation_dust_dM, // input: dust created by stellar feedback - const double* creation_metal_dM, // input: gas-phase metals from stellar feedback + bool dryrun); + +// Species-specific field update for the two-species path (dust_species_track==1). +// Per-channel mass exchange: +// - carbon channel: rho_dust_carbonaceous <-> metal_density_carbon +// - silicate channel: rho_dust_silicate <-> {Mg, Fe, Si, O} at stoichiometric +// mass fractions f_X (Choban+2022 §2.2; 50/50 olivine + +// pyroxene mix, Draine 2003 / Dwek 1998). +// Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] +// shortfall mask. No SN injection here — Phase D drops the in-Grackle +// dust_creation pathway from the species branch; host code seeds dust species +// directly (or via inject_pathway machinery in make_consistent). +void dust_update_species( + chemistry_data* my_chemistry, grackle_field_data* my_fields, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, + const double* growth_dM_silicate, // input: silicate accretion rate + const double* growth_dM_carbon, // input: carbonaceous accretion rate + const double* destruction_dM_silicate, // input: silicate destruction rate (<=0) + const double* destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) bool dryrun); } // namespace grackle::impl diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 047ec369a..95721be86 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -28,7 +28,7 @@ ENTRY(z_velocity) // metal_cooling = 1 ENTRY(metal_density) - // dust_model1_track_elements = 1 OR dust_species_track = 1 + // dust_species_track = 1 ENTRY(metal_density_carbon) ENTRY(metal_density_oxygen) @@ -40,10 +40,6 @@ ENTRY(metal_density_iron) // use_dust_density_field = 1 ENTRY(dust_density) - // dust_model1_track_elements = 1 -ENTRY(dust_density_carbon) -ENTRY(dust_density_oxygen) - // dust_species_track = 1 (two-species dust) ENTRY(dust_density_silicate) ENTRY(dust_density_carbonaceous) diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index d2cded106..6faba9732 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -340,23 +340,6 @@ ENTRY(solver_method, INT, 1) */ ENTRY(dust_model, INT, 0) -/* Track gas-phase C and O as separate element density fields, - with corresponding dust-phase composition bookkeeping. - Requires dust_model=1. - 0) off — bulk metal_density behaviour (default) - 1) on — element-resolved C/O tracking */ -ENTRY(dust_model1_track_elements, INT, 0) - -/* Element mass fractions for initialising C/O fields (Pollack et al. 1994). - "total" = fraction of total metals (gas + dust) that is the element. - "gas" = fraction of total metals that is gas-phase element. - Dust-phase fraction = total - gas. - Defaults are local ISM values (inject_pathway_props pathway 0). */ -ENTRY(dust_model1_C_total_fraction, DOUBLE, 1.79042e-01) -ENTRY(dust_model1_C_gas_fraction, DOUBLE, 5.01317e-02) -ENTRY(dust_model1_O_total_fraction, DOUBLE, 5.11524e-01) -ENTRY(dust_model1_O_gas_fraction, DOUBLE, 2.78491e-01) - /* Flag to use snetimestep */ ENTRY(use_sne_field, INT, 0) @@ -376,7 +359,7 @@ ENTRY(dust_condensation_eff, DOUBLE, 1.5e-1) ENTRY(sne_metal_yield, DOUBLE, 3.0) /* Two-species dust tracking (silicate + carbonaceous). - Requires dust_model=1. Mutually exclusive with dust_model1_track_elements. + Requires dust_model=1. 0) off (default — bulk dust_density) 1) on — evolves dust_density_silicate, dust_density_carbonaceous and 5-element gas tracking (C, O, Mg, Si, Fe). diff --git a/src/clib/initialize_chemistry_data.cpp b/src/clib/initialize_chemistry_data.cpp index 434c07c24..4ddcae002 100644 --- a/src/clib/initialize_chemistry_data.cpp +++ b/src/clib/initialize_chemistry_data.cpp @@ -232,17 +232,6 @@ static int local_initialize_chemistry_data_( return GR_FAIL; } - if (my_chemistry->dust_model1_track_elements > 0) { - if (my_chemistry->dust_model != 1) { - fprintf(stderr, "ERROR: dust_model1_track_elements requires dust_model=1.\n"); - return GR_FAIL; - } - if (my_chemistry->metal_cooling != 1) { - fprintf(stderr, "ERROR: dust_model1_track_elements requires metal_cooling=1.\n"); - return GR_FAIL; - } - } - // Default photo-electric heating to off if unset. if (my_chemistry->photoelectric_heating < 0) { my_chemistry->photoelectric_heating = 0; diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index f50ef5908..b133bd147 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -61,35 +61,6 @@ void make_consistent( my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_mc = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_mc( - const_cast( - track_elements_mc ? my_fields->metal_density_carbon - : my_fields->density), - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_mc( - const_cast( - track_elements_mc ? my_fields->metal_density_oxygen - : my_fields->density), - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_C_mc( - const_cast( - track_elements_mc ? my_fields->dust_density_carbon - : my_fields->density), - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_O_mc( - const_cast( - track_elements_mc ? my_fields->dust_density_oxygen - : my_fields->density), - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HM( my_fields->HM_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -399,17 +370,6 @@ void make_consistent( Feg[i] = Feg[i] + onlygas_metal_yields.Fe[iSN] * cur_val; } - // When dust_model1_track_elements is active, metal_density_carbon - // and metal_density_oxygen directly track the gas-phase C and O - // mass (updated by dust_growth/dust_destruction each subcycle). - // Use these as the conservation targets instead of the yield-based - // values, which don't account for dust locking up C and O. - if (track_elements_mc) { - Cg[i] = metal_C_mc(i, j, k); - Og[i] = metal_O_mc(i, j, k); - Ct[i] = metal_C_mc(i, j, k) + dust_C_mc(i, j, k); - Ot[i] = metal_O_mc(i, j, k) + dust_O_mc(i, j, k); - } } for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -565,12 +525,7 @@ void make_consistent( // ! if (d(i,j,k)*dom .lt. // ! & min(1.e6_DKIND/(metal(i,j,k)/d(i,j,k)/0.02d-4)**2 // ! & ,1.e6_DKIND)) then - // When track_elements is active, Cg/Og come from the tracked - // fields (always valid), so bypass the density cutoff that - // exists for the yield-based computation. - if (track_elements_mc || - // if ( - ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && diff --git a/src/clib/scale_fields.cpp b/src/clib/scale_fields.cpp index fcfe91228..f3f4cae0f 100644 --- a/src/clib/scale_fields.cpp +++ b/src/clib/scale_fields.cpp @@ -106,33 +106,9 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_sf = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_sf( - track_elements_sf ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_sf( - track_elements_sf ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_C_sf( - track_elements_sf ? my_fields->dust_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_O_sf( - track_elements_sf ? my_fields->dust_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View CI( my_fields->CI_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -296,13 +272,6 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metal(i, j, k) = metal(i, j, k) * factor; } - if (track_elements_sf) { - for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - metal_C_sf(i, j, k) = metal_C_sf(i, j, k) * factor; - metal_O_sf(i, j, k) = metal_O_sf(i, j, k) * factor; - } - } - if (my_chemistry->metal_chemistry == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { CI(i, j, k) = CI(i, j, k) * factor; @@ -349,12 +318,6 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { dust(i, j, k) = dust(i, j, k) * factor; } - if (track_elements_sf) { - for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - dust_C_sf(i, j, k) = dust_C_sf(i, j, k) * factor; - dust_O_sf(i, j, k) = dust_O_sf(i, j, k) * factor; - } - } if ((my_chemistry->grain_growth == 1) || (my_chemistry->dust_sublimation == 1)) { diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index a6b2ad6ae..c24025d66 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -95,30 +95,6 @@ inline void scale_fields_dust(chemistry_data* my_chemistry, grackle::impl::View metal( my_fields->metal_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool track_elements_sfd = - (my_chemistry->dust_model1_track_elements > 0 && - my_fields->metal_density_carbon != nullptr && - my_fields->metal_density_oxygen != nullptr); - grackle::impl::View metal_C_sfd( - track_elements_sfd ? my_fields->metal_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View metal_O_sfd( - track_elements_sfd ? my_fields->metal_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_C_sfd( - track_elements_sfd ? my_fields->dust_density_carbon - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View dust_O_sfd( - track_elements_sfd ? my_fields->dust_density_oxygen - : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -174,10 +150,6 @@ inline void scale_fields_dust(chemistry_data* my_chemistry, if (imetal == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { metal(i, j, k) = metal(i, j, k) * factor; - if (track_elements_sfd) { - metal_C_sfd(i, j, k) = metal_C_sfd(i, j, k) * factor; - metal_O_sfd(i, j, k) = metal_O_sfd(i, j, k) * factor; - } // if (my_chemistry->multi_metals > 0) { // metal_loc(i, j, k) = metal_loc(i, j, k) * factor; // metal_C13(i, j, k) = metal_C13(i, j, k) * factor; @@ -197,10 +169,6 @@ inline void scale_fields_dust(chemistry_data* my_chemistry, if (my_chemistry->use_dust_density_field == 1) { for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { dust(i, j, k) = dust(i, j, k) * factor; - if (track_elements_sfd) { - dust_C_sfd(i, j, k) = dust_C_sfd(i, j, k) * factor; - dust_O_sfd(i, j, k) = dust_O_sfd(i, j, k) * factor; - } if ((my_chemistry->grain_growth == 1) || (my_chemistry->dust_sublimation == 1)) { // ! if (metal(i,j,k) .gt. 1.d-9 * d(i,j,k)) then diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 8e3248018..a9dbc34fc 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -728,15 +728,15 @@ int solve_rate_cool( std::vector mmw(my_fields->grid_dimension[0]); std::vector edot(my_fields->grid_dimension[0]); - // Arrays to store dust growth, destruction, and creation mass changes + // Arrays to store dust growth and destruction mass changes std::vector growth_dM(my_fields->grid_dimension[0]); std::vector destruction_dM(my_fields->grid_dimension[0]); - std::vector creation_dust_dM(my_fields->grid_dimension[0]); - std::vector creation_metal_dM(my_fields->grid_dimension[0]); - // Phase B: species-specific accretion outputs (used when + // Phase B/C: species-specific growth & destruction outputs (used when // dust_species_track == 1; Phase D wires them into dust_update()) std::vector growth_dM_silicate(my_fields->grid_dimension[0]); std::vector growth_dM_carbon(my_fields->grid_dimension[0]); + std::vector destruction_dM_silicate(my_fields->grid_dimension[0]); + std::vector destruction_dM_carbon(my_fields->grid_dimension[0]); // iteration masks std::vector itmask(my_fields->grid_dimension[0]); @@ -943,21 +943,35 @@ int solve_rate_cool( dtit.data(), tgas.data(), growth_dM.data()); } - // // Calculate dust creation rates from stellar feedback - // grackle::impl::dust_creation( - // my_chemistry, my_fields, internalu, idx_range, itmask.data(), - // dtit.data(), creation_dust_dM.data(), creation_metal_dM.data()); - - // Calculate dust destruction rates and store in destruction_dM array - grackle::impl::dust_destruction( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), destruction_dM.data()); - - // Apply the calculated rates to update density fields - grackle::impl::dust_update( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM.data(), destruction_dM.data(), - creation_dust_dM.data(), creation_metal_dM.data(), false); + // Calculate dust destruction rates: bulk path (legacy) or species + // path (silicate + carbonaceous, Phase C). dust_update() still + // consumes the bulk destruction_dM until Phase D rewires it. + if (my_chemistry->dust_species_track == 1) { + grackle::impl::dust_destruction_species( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), + destruction_dM_silicate.data(), destruction_dM_carbon.data()); + } else { + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); + } + + // Apply the calculated rates to update density fields. Species + // path runs the per-channel update (no SN injection on this branch); + // legacy bulk path is unchanged. + if (my_chemistry->dust_species_track == 1) { + grackle::impl::dust_update_species( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), + growth_dM_silicate.data(), growth_dM_carbon.data(), + destruction_dM_silicate.data(), destruction_dM_carbon.data(), + false); + } else { + grackle::impl::dust_update( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), + growth_dM.data(), destruction_dM.data(), false); + } } // Add the timestep to the elapsed time for each cell and find diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index 3efb6cc5a..afc8d8675 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -318,23 +318,6 @@ typedef struct */ int dust_model; - /* Track gas-phase C and O as separate element density fields, - with corresponding dust-phase composition bookkeeping. - Requires dust_model=1. - 0) off — bulk metal_density behaviour (default) - 1) on — element-resolved C/O tracking */ - int dust_model1_track_elements; - - /* Element mass fractions for initialising C/O fields (Pollack et al. 1994). - "total" = fraction of total metals (gas + dust) that is the element. - "gas" = fraction of total metals that is gas-phase element. - Dust-phase fraction = total - gas. - Defaults are local ISM values (inject_pathway_props pathway 0). */ - double dust_model1_C_total_fraction; - double dust_model1_C_gas_fraction; - double dust_model1_O_total_fraction; - double dust_model1_O_gas_fraction; - /* Flag to use snetimestep */ int use_sne_field; @@ -354,7 +337,7 @@ typedef struct double sne_metal_yield; /* Two-species dust tracking (silicate + carbonaceous). - Requires dust_model=1. Mutually exclusive with dust_model1_track_elements. + Requires dust_model=1. 0) off — bulk dust_density (default) 1) on — evolves dust_density_silicate, dust_density_carbonaceous and 5-element gas tracking (C, O, Mg, Si, Fe). diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index 69f58bbe1..9e911ad6f 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -72,7 +72,7 @@ typedef struct // metal_cooling = 1 gr_float *metal_density; - // dust_model1_track_elements = 1 OR dust_species_track = 1 + // dust_species_track = 1 // These are *subsets* of metal_density (not separate fields). gr_float *metal_density_carbon; // gas-phase C (subset of metal_density) gr_float *metal_density_oxygen; // gas-phase O (subset of metal_density) @@ -87,11 +87,6 @@ typedef struct // use_dust_density_field = 1 gr_float *dust_density; - // dust_model1_track_elements = 1 - // These are *subsets* of dust_density (not separate fields). - gr_float *dust_density_carbon; // C in dust (subset of dust_density) - gr_float *dust_density_oxygen; // O in dust (subset of dust_density) - // dust_species_track = 1 // Two-species dust: bulk dust_density = silicate + carbonaceous. // REF: Hirashita 2015 MNRAS 447, 2937; McKinnon+2018 MNRAS 478, 2851 diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index f727df456..e86bfff16 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -266,13 +266,6 @@ def _required_density_fields(my_chemistry): my_fields.append("metal_density") if my_chemistry.dust_chemistry == 1: my_fields.append("dust_density") - if my_chemistry.dust_model1_track_elements == 1: - my_fields.extend([ - "metal_density_carbon", - "metal_density_oxygen", - "dust_density_carbon", - "dust_density_oxygen", - ]) if my_chemistry.dust_species_track == 1: # Two-species dust (silicate + carbonaceous) with 5-element gas tracking. # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index ec7729c10..94dca3e92 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -115,8 +115,6 @@ cdef extern from "grackle.h": gr_float *metal_density_silicon; gr_float *metal_density_iron; gr_float *dust_density; - gr_float *dust_density_carbon; - gr_float *dust_density_oxygen; gr_float *dust_density_silicate; gr_float *dust_density_carbonaceous; gr_float *e_density; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index be8484849..1c5b2072e 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -661,8 +661,6 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.metal_density_silicon = get_field(fc, "metal_density_silicon") my_fields.metal_density_iron = get_field(fc, "metal_density_iron") my_fields.dust_density = get_field(fc, "dust_density") - my_fields.dust_density_carbon = get_field(fc, "dust_density_carbon") - my_fields.dust_density_oxygen = get_field(fc, "dust_density_oxygen") my_fields.dust_density_silicate = get_field(fc, "dust_density_silicate") my_fields.dust_density_carbonaceous = get_field(fc, "dust_density_carbonaceous") diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index d39fb3fa2..d90eeb20d 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -219,11 +219,9 @@ def setup_fluid_container(my_chemistry, fc["z_velocity"][:] = 0.0 fc_last = fc.copy() - # disable cooling and element tracking to iterate to equilibrium + # disable cooling to iterate to equilibrium val = fc.chemistry_data.with_radiative_cooling fc.chemistry_data.with_radiative_cooling = 0 - val_track = fc.chemistry_data.dust_model1_track_elements - fc.chemistry_data.dust_model1_track_elements = 0 my_time = 0.0 i = 0 @@ -249,25 +247,8 @@ def setup_fluid_container(my_chemistry, i += 1 fc.chemistry_data.with_radiative_cooling = val - fc.chemistry_data.dust_model1_track_elements = val_track if i >= max_iterations: raise RuntimeError( f"ERROR: solver did not converge in {max_iterations} iterations.") - # Element-resolved C/O initialisation for dust_model=1. - # metal_density holds the *total* gas-phase metal budget; C and O are - # subsets of it (not separate). Same convention as dust_density / - # dust_density_carbon / dust_density_oxygen. - if my_chemistry.dust_model1_track_elements == 1: - M_total = fc["metal_density"] + fc["dust_density"] - f_C_total = my_chemistry.dust_model1_C_total_fraction - f_C_gas = my_chemistry.dust_model1_C_gas_fraction - f_O_total = my_chemistry.dust_model1_O_total_fraction - f_O_gas = my_chemistry.dust_model1_O_gas_fraction - - fc["metal_density_carbon"][:] = f_C_gas * M_total - fc["metal_density_oxygen"][:] = f_O_gas * M_total - fc["dust_density_carbon"][:] = (f_C_total - f_C_gas) * M_total - fc["dust_density_oxygen"][:] = (f_O_total - f_O_gas) * M_total - return fc From 7c3256b9e26729823f5e4f13ecbd4d5fee3b1828 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Mon, 4 May 2026 11:06:50 +0100 Subject: [PATCH 63/71] Adding dust check --- src/clib/make_consistent.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index b133bd147..fd0cefc55 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -982,6 +982,32 @@ void make_consistent( } } + // Phase E invariant: bulk dust_density = silicate + carbonaceous. + // dust_update_species() maintains this per-cell, but external mutations + // (host injection, inject_pathway writes) can break it before make_consistent. + // Re-derive here so downstream consumers (calc_tdust_3d.cpp, cooling tables) + // see a consistent bulk field. + if (my_chemistry->dust_species_track == 1) { + grackle::impl::View dust_bulk( + my_fields->dust_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_sil( + const_cast(my_fields->dust_density_silicate), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_carb( + const_cast(my_fields->dust_density_carbonaceous), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + for (k = my_fields->grid_start[2]; k <= my_fields->grid_end[2]; k++) { + for (j = my_fields->grid_start[1]; j <= my_fields->grid_end[1]; j++) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + dust_bulk(i, j, k) = dust_sil(i, j, k) + dust_carb(i, j, k); + } + } + } + } + return; } From 15a868c4d23c158cab08142f182aa190f8fcad42 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 5 May 2026 03:59:32 +0100 Subject: [PATCH 64/71] Bug fixing --- src/clib/dust/dust_growth_and_destruction.cpp | 236 ++++++++++++++---- src/clib/dust/dust_growth_and_destruction.hpp | 15 +- src/clib/grackle_chemistry_data_fields.def | 14 +- src/clib/make_consistent.cpp | 73 +++++- src/clib/scale_fields.cpp | 99 +++++--- src/clib/scale_fields.hpp | 60 +++++ src/clib/solve_rate_cool.cpp | 30 +-- src/include/grackle_chemistry_data.h | 8 +- src/python/examples/cooling_cell.py | 2 + src/python/gracklepy/fluid_container.py | 10 +- src/python/gracklepy/utilities/convenience.py | 64 ++++- src/python/gracklepy/utilities/evolve.py | 16 +- 12 files changed, 503 insertions(+), 124 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index ec6df4b36..323ef80cc 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -13,6 +13,17 @@ const double tiny_value = 1.0e-20; const double huge_value = 1.0e+20; const double t_ref = 20; +const double species_t_ref = 50; +const double species_nH_ref = 1.0e3; + +// Solar metal mass fractions for the tracked dust-forming elements, relative +// to total solar metals. These match gracklepy.utilities.convenience, which +// seeds the dust_species_track gas reservoirs from metal_density. +const double solar_frac_C = 0.15925782394660776; +const double solar_frac_O = 0.4242932765702842; +const double solar_frac_Mg = 0.045644817372018066; +const double solar_frac_Si = 0.052744600629574714; +const double solar_frac_Fe = 0.08523143041944482; // Gate thresholds for dust evolution: skip cells where both dust and metals // are negligible fractions of the baryon density. The metal threshold matches @@ -98,13 +109,21 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, // ========================================== // Two parallel accretion rates onto pre-existing dust seeds, gated by // dust_species_track == 1. Carbonaceous growth is rate-limited by gas-phase -// carbon. Silicate growth is rate-limited by the least-available key -// reactant in {Mg, Si, Fe, O} weighted by stoichiometric mass fraction -// f_X (Choban+2022 MNRAS 514, 4506 §2.2). The structural form of tau_accr -// follows Hirashita 2011 ApJ 743, 159 Eq. (16)-(17): -// tau_accr = tau_ref · (rho_ref / rho) · (T_ref/T)^0.5 · (Z_sun / Z_X) -// reusing the same dust_growth_densref / SolarMetalFractionByMass scaling -// convention as the bulk dust_growth() above. +// carbon. Silicate growth is rate-limited by the least-available reactant in +// {Mg, Si, Fe, O} weighted by stoichiometric mass fraction f_X. +// +// The species tau_ref values follow Hirashita 2011 MNRAS 416, 1340 section +// 2.6: they are normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, +// a = 0.1 micron, and solar abundance of the relevant key species. This branch +// rescales the paper's S_0.3 factor through dust_growth_sticking_coeff and +// a_0.1 through dust_grainsize / 0.1. +// It therefore computes the density factor from local hydrogen number density, +// not from the bulk SIMBA +// dust_growth_densref parameter. Since this path tracks five silicate +// elements, we apply the same key-species logic to the limiting stoichiometric +// pool and normalize by that pool's solar value. This preserves tau_ref for a +// solar mixture while slowing growth when any required silicate reactant is +// depleted. void grackle::impl::dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, @@ -120,6 +139,45 @@ void grackle::impl::dust_growth_species( grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool use_H_fields = (my_chemistry->primordial_chemistry > 0); + bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); + bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); + bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); + grackle::impl::View HI( + use_H_fields ? my_fields->HI_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HII( + use_H_fields ? my_fields->HII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HM( + use_H2_fields ? my_fields->HM_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2I( + use_H2_fields ? my_fields->H2I_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2II( + use_H2_fields ? my_fields->H2II_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HDI( + use_HD_fields ? my_fields->HDI_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HDII( + use_HeH_fields ? my_fields->HDII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HeHII( + use_HeH_fields ? my_fields->HeHII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View mC( my_fields->metal_density_carbon, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -137,17 +195,44 @@ void grackle::impl::dust_growth_species( my_fields->grid_dimension[1], my_fields->grid_dimension[2]); double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); - // tau_ref values are stored in Gyr (Hirashita 2011 Table 1 convention) + // tau_ref values are stored in Gyr (Hirashita 2011 section 2.6). double tau_ref_sil = my_chemistry->dust_growth_tauref_silicate * 1e9 * sec_per_year / internalu.tbase1; double tau_ref_carb = my_chemistry->dust_growth_tauref_carbon * 1e9 * sec_per_year / internalu.tbase1; + double sticking_factor = + (my_chemistry->dust_growth_sticking_coeff > 0.0) + ? 0.3 / my_chemistry->dust_growth_sticking_coeff + : huge_value; + double grain_size_factor = + (my_chemistry->dust_grainsize > 0.0) + ? my_chemistry->dust_grainsize / 0.1 + : huge_value; double f_Mg = my_chemistry->dust_silicate_f_Mg; double f_Fe = my_chemistry->dust_silicate_f_Fe; double f_Si = my_chemistry->dust_silicate_f_Si; double f_O = my_chemistry->dust_silicate_f_O; + double solar_nonmetal = + std::max(1.0 - my_chemistry->SolarMetalFractionByMass, tiny_value); + double solar_H = std::max(my_chemistry->HydrogenFractionByMass * + solar_nonmetal, tiny_value); + double solar_C = my_chemistry->SolarMetalFractionByMass * + solar_frac_C / solar_H; + double solar_O = my_chemistry->SolarMetalFractionByMass * + solar_frac_O / solar_H; + double solar_Mg = my_chemistry->SolarMetalFractionByMass * + solar_frac_Mg / solar_H; + double solar_Si = my_chemistry->SolarMetalFractionByMass * + solar_frac_Si / solar_H; + double solar_Fe = my_chemistry->SolarMetalFractionByMass * + solar_frac_Fe / solar_H; + + double solar_pool_Mg = (f_Mg > 0.0) ? solar_Mg / f_Mg : huge_value; + double solar_pool_Fe = (f_Fe > 0.0) ? solar_Fe / f_Fe : huge_value; + double solar_pool_Si = (f_Si > 0.0) ? solar_Si / f_Si : huge_value; + double solar_pool_O = (f_O > 0.0) ? solar_O / f_O : huge_value; // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { growth_dM_silicate[i] = 0.0; @@ -157,6 +242,7 @@ void grackle::impl::dust_growth_species( double rho_gas = d(i, idx_range.j, idx_range.k); double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); + double rho_metal_total = metal(i, idx_range.j, idx_range.k); double rho_C = mC(i, idx_range.j, idx_range.k); double rho_O = mO(i, idx_range.j, idx_range.k); @@ -164,18 +250,48 @@ void grackle::impl::dust_growth_species( double rho_Si = mSi(i, idx_range.j, idx_range.k); double rho_Fe = mFe(i, idx_range.j, idx_range.k); - double T = t_gas[i]; + double T = std::max(t_gas[i], tiny_value); double dt = dt_value[i]; + double rho_nonmetal = rho_gas - rho_metal_total; + if (rho_nonmetal <= 0.0) { + continue; + } + double rho_H_nuclei = my_chemistry->HydrogenFractionByMass * + rho_nonmetal; + if (use_H_fields) { + rho_H_nuclei = HI(i, idx_range.j, idx_range.k) + + HII(i, idx_range.j, idx_range.k); + if (use_H2_fields) { + rho_H_nuclei += HM(i, idx_range.j, idx_range.k) + + H2I(i, idx_range.j, idx_range.k) + + H2II(i, idx_range.j, idx_range.k); + } + if (use_HD_fields) { + rho_H_nuclei += HDI(i, idx_range.j, idx_range.k) / 3.0; + } + if (use_HeH_fields) { + rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + + HeHII(i, idx_range.j, idx_range.k) / 5.0; + } + } + double nH = rho_H_nuclei * dens_proper / mh; + if (rho_gas <= 0.0 || rho_H_nuclei <= 0.0 || nH <= 0.0) { + continue; + } - double accr_struct = (my_chemistry->dust_growth_densref / dens_proper) * - std::pow(t_ref / T, 0.5); + double accr_struct = (species_nH_ref / nH) * + std::pow(species_t_ref / T, 0.5); // ---------- Carbonaceous: rate-limited by gas-phase C ---------- - if (rho_C >= metal_gate_threshold * rho_gas && rho_dust_carb_i > 0.0) { - double rho_C_eff = std::max(rho_C, tiny_value); + if (rho_C >= metal_gate_threshold * rho_gas && + rho_dust_carb_i > 0.0 && solar_C > 0.0 && dt > 0.0) { + double rho_C_total = rho_C + rho_dust_carb_i; + double z_C_total_eff = std::max(rho_C_total / rho_H_nuclei, + tiny_value); double tau_accr_carb = tau_ref_carb * accr_struct * - (my_chemistry->SolarMetalFractionByMass / - rho_C_eff); + (solar_C / z_C_total_eff) * + grain_size_factor * + sticking_factor; tau_accr_carb = std::min(std::max(tau_accr_carb, tiny_value), huge_value); double frac_avail = rho_C / (rho_dust_carb_i + rho_C); @@ -187,20 +303,40 @@ void grackle::impl::dust_growth_species( // ---------- Silicate: Choban+2022 key-reactant ---------- // For each key element X, the maximum silicate dust mass that could // be assembled from X is rho_X / f_X. The bottleneck element sets - // the rate. + // the rate, and the solar normalization must use that same element. double mass_from_Mg = (f_Mg > 0.0) ? rho_Mg / f_Mg : huge_value; double mass_from_Fe = (f_Fe > 0.0) ? rho_Fe / f_Fe : huge_value; double mass_from_Si = (f_Si > 0.0) ? rho_Si / f_Si : huge_value; double mass_from_O = (f_O > 0.0) ? rho_O / f_O : huge_value; - double rho_sil_pool = std::min(std::min(mass_from_Mg, mass_from_Fe), - std::min(mass_from_Si, mass_from_O)); + + double rho_sil_pool = huge_value; + double solar_sil_pool = 0.0; + if (f_Mg > 0.0 && mass_from_Mg < rho_sil_pool) { + rho_sil_pool = mass_from_Mg; + solar_sil_pool = solar_pool_Mg; + } + if (f_Fe > 0.0 && mass_from_Fe < rho_sil_pool) { + rho_sil_pool = mass_from_Fe; + solar_sil_pool = solar_pool_Fe; + } + if (f_Si > 0.0 && mass_from_Si < rho_sil_pool) { + rho_sil_pool = mass_from_Si; + solar_sil_pool = solar_pool_Si; + } + if (f_O > 0.0 && mass_from_O < rho_sil_pool) { + rho_sil_pool = mass_from_O; + solar_sil_pool = solar_pool_O; + } if (rho_sil_pool >= metal_gate_threshold * rho_gas && - rho_dust_sil_i > 0.0) { - double rho_pool_eff = std::max(rho_sil_pool, tiny_value); + rho_dust_sil_i > 0.0 && solar_sil_pool > 0.0 && dt > 0.0) { + double rho_sil_total_pool = rho_sil_pool + rho_dust_sil_i; + double z_pool_total_eff = std::max(rho_sil_total_pool / rho_H_nuclei, + tiny_value); double tau_accr_sil = tau_ref_sil * accr_struct * - (my_chemistry->SolarMetalFractionByMass / - rho_pool_eff); + (solar_sil_pool / z_pool_total_eff) * + grain_size_factor * + sticking_factor; tau_accr_sil = std::min(std::max(tau_accr_sil, tiny_value), huge_value); double frac_avail = rho_sil_pool / (rho_dust_sil_i + rho_sil_pool); @@ -322,14 +458,15 @@ void grackle::impl::dust_destruction( // ========================================== // SN-shock destruction + thermal sputtering applied independently to each // dust species, gated by dust_species_track == 1. Carbonaceous (graphite) -// is the shock-vulnerability baseline; silicates are ~1.5x more easily -// shattered by SN shocks under canonical ISM conditions +// is the shock-vulnerability baseline; Slavin+2015 Table 2 gives gas-cleared +// masses of 990 Msun for silicates and 600 Msun for carbonaceous grains in +// their standard SNR model, so silicates are destroyed about 1.65x faster // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740]. // Thermal sputtering uses species-specific tau_ref values // [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; // REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] // with the same Draine & Salpeter 1979 / Tielens+1994 scaling form -// (a/0.1) * (1e-27/(rho*n)) * ((2e6/T)^2.5 + 1) used by the bulk path. +// (a/0.1) * (1e-27/rho_gas) * ((2e6/T)^2.5 + 1) used by the bulk path. // Phase D wires the species outputs into dust_update(). void grackle::impl::dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -366,10 +503,10 @@ void grackle::impl::dust_destruction_species( (internalu.urho * std::pow(internalu.uxyz, 3)); // Species-specific shock-vulnerability multipliers. Graphite is the - // baseline (1.0); silicate is ~1.5x more easily destroyed under - // canonical SN-shock conditions. [REF: Slavin+2015 ApJ 803, 7] + // baseline (1.0); silicate follows the standard Slavin+2015 SNR + // gas-cleared mass ratio, 990/600 = 1.65. const double shock_factor_carbon = 1.0; - const double shock_factor_silicate = 1.5; + const double shock_factor_silicate = 1.65; // Species-specific thermal sputtering tau_ref (params stored in years) double tau_sput_ref_sil = my_chemistry->dust_sputter_tauref_silicate * @@ -392,8 +529,11 @@ void grackle::impl::dust_destruction_species( if (!sil_active && !carb_active) continue; double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; - double temp = t_gas[i]; + if (rho_gas <= 0.0) continue; + + double temp = std::max(t_gas[i], tiny_value); double dt = dt_value[i]; + if (dt <= 0.0) continue; // Common (species-independent) sputtering structural factor double sput_struct = (my_chemistry->dust_grainsize / 0.1) * @@ -520,6 +660,10 @@ void grackle::impl::dust_update_species( double rho_Si = mSi(i, idx_range.j, idx_range.k); double rho_Fe = mFe(i, idx_range.j, idx_range.k); double dt = dt_value[i]; + if (dt <= 0.0) continue; + + double rho_tracked_before = rho_C + rho_O + rho_Mg + rho_Si + rho_Fe; + double rho_metal_other = rho_metal_total - rho_tracked_before; // dM > 0 means net growth (gas reactants -> dust); // dM < 0 means net destruction (dust -> gas reactants). @@ -557,31 +701,33 @@ void grackle::impl::dust_update_species( rho_Si -= dM_sil * f_Si; rho_O -= dM_sil * f_O; - // Bulk dust = silicate + carbonaceous (Phase E will enforce as invariant - // in make_consistent; computing it here keeps the bulk field in sync). - double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; - - // Total gas-phase metal change: -(dM_carb + dM_sil) (metals lost when - // dust grows). metal_density_other (= total - C - O - Mg - Si - Fe) is - // unchanged on the dust loop per Choban+2022 §2.2. - double delta_metal_total = -(dM_carb + dM_sil); - double rho_metal_new = rho_metal_total + delta_metal_total; - - // Gas density tracks metals as a subset (dust is not part of `density`). - rho_gas += delta_metal_total; - // Floors / NaN guard rho_dust_carb_i = std::max(0.0, rho_dust_carb_i); rho_dust_sil_i = std::max(0.0, rho_dust_sil_i); - rho_dust_new = std::max(0.0, rho_dust_new); rho_C = std::max(0.0, rho_C); rho_O = std::max(0.0, rho_O); rho_Mg = std::max(0.0, rho_Mg); rho_Si = std::max(0.0, rho_Si); rho_Fe = std::max(0.0, rho_Fe); + + // Bulk dust = silicate + carbonaceous (Phase E will enforce as invariant + // in make_consistent; computing it here keeps the bulk field in sync). + double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; + + // metal_density_other (= total - C - O - Mg - Si - Fe) is unchanged on + // the dust loop. Rebuilding total metals from the updated tracked fields + // preserves consistency even if user-edited silicate fractions do not sum + // to exactly one. + double rho_tracked_after = rho_C + rho_O + rho_Mg + rho_Si + rho_Fe; + double rho_metal_new = rho_metal_other + rho_tracked_after; rho_metal_new = std::max(0.0, rho_metal_new); + double delta_metal_total = rho_metal_new - rho_metal_total; + + // Gas density tracks metals as a subset (dust is not part of `density`). + rho_gas += delta_metal_total; - if (std::isnan(rho_dust_new) || std::isnan(rho_metal_new)) { + if (std::isnan(rho_dust_new) || std::isnan(rho_metal_new) || + std::isnan(rho_gas)) { std::cout << "dust_update_species: NaN at cell " << i << " dM_carb=" << dM_carb << " dM_sil=" << dM_sil << std::endl; continue; @@ -644,4 +790,4 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; } } -} \ No newline at end of file +} diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 3e76ff1b4..3424e52a8 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -24,8 +24,11 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, // - carbonaceous: rate-limited by gas-phase carbon // - silicate: rate-limited by min over {Mg, Si, Fe, O} of (rho_X / f_X), // following the Choban+2022 MNRAS 514, 4506 §2.2 key-reactant approach -// Per-species tau_accr structural form follows Hirashita 2011 ApJ 743, 159 -// Eq. (16)-(17). No bulk dM, no partitioning — Phase D wires the species +// Per-species tau_accr uses the Hirashita 2011 section 2.6 normalization: +// n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, and solar key-species +// abundance, with S rescaled by dust_growth_sticking_coeff and a rescaled by +// dust_grainsize / 0.1. This is independent of the bulk SIMBA +// dust_growth_densref. No bulk dM, no partitioning — Phase D wires the species // outputs into dust_update(). void dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, @@ -46,9 +49,9 @@ void dust_destruction( // Species-specific destruction (SN shocks + thermal sputtering) onto the two // dust populations. Active when dust_species_track == 1. -// - shock yield: graphite is baseline (factor 1.0); silicate ~1.5x more -// easily destroyed [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; -// Jones+1996 ApJ 469, 740] +// - shock yield: graphite is baseline (factor 1.0); silicate follows the +// Slavin+2015 standard SNR gas-cleared mass ratio 990/600 = 1.65 +// [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740] // - thermal sputtering: species-specific tau_ref // [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; // REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] @@ -94,4 +97,4 @@ void dust_update_species( } // namespace grackle::impl -#endif // DUST_GROWTH_AND_DESTRUCTION_HPP \ No newline at end of file +#endif // DUST_GROWTH_AND_DESTRUCTION_HPP diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 6faba9732..db30ed36c 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -353,6 +353,7 @@ ENTRY(sne_shockspeed, DOUBLE, 1.0e2) ENTRY(dust_grainsize, DOUBLE, 1.0e-1) ENTRY(dust_growth_densref, DOUBLE, 2.3e-22) ENTRY(dust_growth_tauref, DOUBLE, 0.004) +ENTRY(dust_growth_sticking_coeff, DOUBLE, 3.0e-1) /* Dust creation by stellar feedback parameters */ ENTRY(dust_condensation_eff, DOUBLE, 1.5e-1) @@ -368,14 +369,17 @@ ENTRY(sne_metal_yield, DOUBLE, 3.0) ENTRY(dust_species_track, INT, 0) /* Species-specific growth (accretion) reference timescales [Gyr]. - Used in tau = tau_ref * (a/0.1um) * (n_ref/n_H) * sqrt(T_ref/T) * (Z_sun_X/rho_X). - REF: Hirashita 2011 ApJ 743, 159 Table 1; Asano+2013 EP&S 65, 213 */ -ENTRY(dust_growth_tauref_silicate, DOUBLE, 0.4) -ENTRY(dust_growth_tauref_carbon, DOUBLE, 2.0) + Normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, and + solar key-species abundance for the carbon / limiting silicate reactant + pools. dust_growth_species rescales these values by + (dust_grainsize / 0.1) * (0.3 / dust_growth_sticking_coeff). + REF: Hirashita 2011 MNRAS 416, 1340 section 2.6; Asano+2013 EP&S 65, 213 */ +ENTRY(dust_growth_tauref_silicate, DOUBLE, 6.3e-2) +ENTRY(dust_growth_tauref_carbon, DOUBLE, 5.59e-2) /* Species-specific thermal sputtering reference timescales [yr]. Same scaling as Draine & Salpeter 1979 ApJ 231, 77; Tielens+1994 ApJ 431, 321: - tau_sp = tau_ref * (a/0.1) * (1e-27/(rho*n)) * ((2e6/T)^2.5 + 1). + tau_sp = tau_ref * (a/0.1) * (1e-27/rho_gas) * ((2e6/T)^2.5 + 1). REF (silicate): Tsai & Mathews 1995 ApJ 448, 84 REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435 */ ENTRY(dust_sputter_tauref_silicate, DOUBLE, 1.7e8) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index fd0cefc55..70775ec15 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -236,6 +236,43 @@ void make_consistent( } } + // Phase F hijack: when dust_species_track == 1, the per-element + // gas/dust/total nuclide arrays (Cg/Cd/Ct etc.) are populated directly + // from our tracked metal_density_X + dust species fields, overriding the + // SN-yield-derived computation below. Views are constructed once here. + grackle::impl::View mC_view, mO_view, mMg_view, + mSi_view, mFe_view, dust_sil_view, dust_carb_view; + if (my_chemistry->dust_species_track == 1) { + mC_view = grackle::impl::View( + const_cast(my_fields->metal_density_carbon), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + mO_view = grackle::impl::View( + const_cast(my_fields->metal_density_oxygen), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + mMg_view = grackle::impl::View( + const_cast(my_fields->metal_density_magnesium), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + mSi_view = grackle::impl::View( + const_cast(my_fields->metal_density_silicon), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + mFe_view = grackle::impl::View( + const_cast(my_fields->metal_density_iron), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + dust_sil_view = grackle::impl::View( + const_cast(my_fields->dust_density_silicate), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + dust_carb_view = grackle::impl::View( + const_cast(my_fields->dust_density_carbonaceous), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + } + std::vector Ct(my_fields->grid_dimension[0]); std::vector Ot(my_fields->grid_dimension[0]); std::vector Mgt(my_fields->grid_dimension[0]); @@ -382,6 +419,39 @@ void make_consistent( Fed[i] = Fet[i] - Feg[i]; } + // Phase F hijack: replace SN-yield-derived per-element arrays with + // values pulled directly from our tracked fields. Carbonaceous dust + // is pure C; silicate dust contributes Mg/Fe/Si/O at stoichiometric + // mass fractions f_X. Al and S are not part of the two-species + // architecture, so their per-element arrays are zeroed. + if (my_chemistry->dust_species_track == 1) { + const double f_Mg = my_chemistry->dust_silicate_f_Mg; + const double f_Fe = my_chemistry->dust_silicate_f_Fe; + const double f_Si = my_chemistry->dust_silicate_f_Si; + const double f_O = my_chemistry->dust_silicate_f_O; + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + const double sil = dust_sil_view(i, j, k); + const double carb = dust_carb_view(i, j, k); + Cg[i] = mC_view(i, j, k); + Og[i] = mO_view(i, j, k); + Mgg[i] = mMg_view(i, j, k); + Sig[i] = mSi_view(i, j, k); + Feg[i] = mFe_view(i, j, k); + Cd[i] = carb; + Od[i] = sil * f_O; + Mgd[i] = sil * f_Mg; + Sid[i] = sil * f_Si; + Fed[i] = sil * f_Fe; + Ct[i] = Cg[i] + Cd[i]; + Ot[i] = Og[i] + Od[i]; + Mgt[i] = Mgg[i] + Mgd[i]; + Sit[i] = Sig[i] + Sid[i]; + Fet[i] = Feg[i] + Fed[i]; + Alt[i] = 0.; Alg[i] = 0.; Ald[i] = 0.; + St[i] = 0.; Sg[i] = 0.; Sd[i] = 0.; + } + } + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { // if (itmask_metal(i)) then OH(i, j, k) = std::fabs(OH(i, j, k)); @@ -525,7 +595,8 @@ void make_consistent( // ! if (d(i,j,k)*dom .lt. // ! & min(1.e6_DKIND/(metal(i,j,k)/d(i,j,k)/0.02d-4)**2 // ! & ,1.e6_DKIND)) then - if (((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + if (my_chemistry->dust_species_track || + ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && diff --git a/src/clib/scale_fields.cpp b/src/clib/scale_fields.cpp index f3f4cae0f..7898a3356 100644 --- a/src/clib/scale_fields.cpp +++ b/src/clib/scale_fields.cpp @@ -109,6 +109,27 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal_C( + my_fields->metal_density_carbon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal_O( + my_fields->metal_density_oxygen, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal_Mg( + my_fields->metal_density_magnesium, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal_Si( + my_fields->metal_density_silicon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal_Fe( + my_fields->metal_density_iron, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_carb( + my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View CI( my_fields->CI_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -268,10 +289,21 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, } } - if (imetal == 1) { + if ((imetal == 1) || (my_chemistry->dust_species_track == 1)) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { metal(i, j, k) = metal(i, j, k) * factor; } + } + if (my_chemistry->dust_species_track == 1) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + metal_C(i, j, k) = metal_C(i, j, k) * factor; + metal_O(i, j, k) = metal_O(i, j, k) * factor; + metal_Mg(i, j, k) = metal_Mg(i, j, k) * factor; + metal_Si(i, j, k) = metal_Si(i, j, k) * factor; + metal_Fe(i, j, k) = metal_Fe(i, j, k) * factor; + } + } + if (imetal == 1) { if (my_chemistry->metal_chemistry == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { CI(i, j, k) = CI(i, j, k) * factor; @@ -313,41 +345,48 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, } } } + } - if (my_chemistry->use_dust_density_field == 1) { + if ((my_chemistry->use_dust_density_field == 1) || + (my_chemistry->dust_species_track == 1)) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { + dust(i, j, k) = dust(i, j, k) * factor; + } + if (my_chemistry->dust_species_track == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - dust(i, j, k) = dust(i, j, k) * factor; + dust_sil(i, j, k) = dust_sil(i, j, k) * factor; + dust_carb(i, j, k) = dust_carb(i, j, k) * factor; } + } - if ((my_chemistry->grain_growth == 1) || - (my_chemistry->dust_sublimation == 1)) { - if (my_chemistry->dust_species > 0) { - for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; - i++) { - MgSiO3(i, j, k) = MgSiO3(i, j, k) * factor; - AC(i, j, k) = AC(i, j, k) * factor; - } + if ((my_chemistry->grain_growth == 1) || + (my_chemistry->dust_sublimation == 1)) { + if (my_chemistry->dust_species > 0) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; + i++) { + MgSiO3(i, j, k) = MgSiO3(i, j, k) * factor; + AC(i, j, k) = AC(i, j, k) * factor; } - if (my_chemistry->dust_species > 1) { - for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; - i++) { - SiM(i, j, k) = SiM(i, j, k) * factor; - FeM(i, j, k) = FeM(i, j, k) * factor; - Mg2SiO4(i, j, k) = Mg2SiO4(i, j, k) * factor; - Fe3O4(i, j, k) = Fe3O4(i, j, k) * factor; - SiO2D(i, j, k) = SiO2D(i, j, k) * factor; - MgO(i, j, k) = MgO(i, j, k) * factor; - FeS(i, j, k) = FeS(i, j, k) * factor; - Al2O3(i, j, k) = Al2O3(i, j, k) * factor; - } + } + if (my_chemistry->dust_species > 1) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; + i++) { + SiM(i, j, k) = SiM(i, j, k) * factor; + FeM(i, j, k) = FeM(i, j, k) * factor; + Mg2SiO4(i, j, k) = Mg2SiO4(i, j, k) * factor; + Fe3O4(i, j, k) = Fe3O4(i, j, k) * factor; + SiO2D(i, j, k) = SiO2D(i, j, k) * factor; + MgO(i, j, k) = MgO(i, j, k) * factor; + FeS(i, j, k) = FeS(i, j, k) * factor; + Al2O3(i, j, k) = Al2O3(i, j, k) * factor; } - if (my_chemistry->dust_species > 2) { - for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; - i++) { - reforg(i, j, k) = reforg(i, j, k) * factor; - volorg(i, j, k) = volorg(i, j, k) * factor; - H2Oice(i, j, k) = H2Oice(i, j, k) * factor; - } + } + if (my_chemistry->dust_species > 2) { + for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; + i++) { + reforg(i, j, k) = reforg(i, j, k) * factor; + volorg(i, j, k) = volorg(i, j, k) * factor; + H2Oice(i, j, k) = H2Oice(i, j, k) * factor; } } } diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index c24025d66..ad8293dad 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -77,6 +77,66 @@ inline void scale_fields_table(grackle_field_data* my_fields, double factor) { } } } + if (my_fields->metal_density_magnesium != nullptr) { + grackle::impl::View metal_Mg( + my_fields->metal_density_magnesium, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + metal_Mg(i, j, k) = metal_Mg(i, j, k) * factor; + } + } + } + } + if (my_fields->metal_density_silicon != nullptr) { + grackle::impl::View metal_Si( + my_fields->metal_density_silicon, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + metal_Si(i, j, k) = metal_Si(i, j, k) * factor; + } + } + } + } + if (my_fields->metal_density_iron != nullptr) { + grackle::impl::View metal_Fe( + my_fields->metal_density_iron, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + metal_Fe(i, j, k) = metal_Fe(i, j, k) * factor; + } + } + } + } + if (my_fields->dust_density_silicate != nullptr) { + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + dust_sil(i, j, k) = dust_sil(i, j, k) * factor; + } + } + } + } + if (my_fields->dust_density_carbonaceous != nullptr) { + grackle::impl::View dust_carb( + my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + dust_carb(i, j, k) = dust_carb(i, j, k) * factor; + } + } + } + } } /// A helper function for scaling the injection pathway metal density fields diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index a9dbc34fc..86d516bd4 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -929,38 +929,16 @@ int solve_rate_cool( // separate pass. The placement below is a short-term stopgap and // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ - // Calculate dust growth rates: bulk path (legacy) or species path - // (silicate + carbonaceous, Phase B). dust_update() still consumes - // the bulk growth_dM until Phase D rewires it. if (my_chemistry->dust_species_track == 1) { + // Calculate and apply the silicate + carbonaceous rates. grackle::impl::dust_growth_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), growth_dM_silicate.data(), growth_dM_carbon.data()); - } else { - grackle::impl::dust_growth( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), growth_dM.data()); - } - - // Calculate dust destruction rates: bulk path (legacy) or species - // path (silicate + carbonaceous, Phase C). dust_update() still - // consumes the bulk destruction_dM until Phase D rewires it. - if (my_chemistry->dust_species_track == 1) { grackle::impl::dust_destruction_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), destruction_dM_silicate.data(), destruction_dM_carbon.data()); - } else { - grackle::impl::dust_destruction( - my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), destruction_dM.data()); - } - - // Apply the calculated rates to update density fields. Species - // path runs the per-channel update (no SN injection on this branch); - // legacy bulk path is unchanged. - if (my_chemistry->dust_species_track == 1) { grackle::impl::dust_update_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), @@ -968,6 +946,12 @@ int solve_rate_cool( destruction_dM_silicate.data(), destruction_dM_carbon.data(), false); } else { + grackle::impl::dust_growth( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), growth_dM.data()); + grackle::impl::dust_destruction( + my_chemistry, my_fields, internalu, idx_range, itmask.data(), + dtit.data(), tgas.data(), destruction_dM.data()); grackle::impl::dust_update( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), growth_dM.data(), destruction_dM.data(), false); diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index afc8d8675..a14ca7191 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -331,6 +331,9 @@ typedef struct double dust_grainsize; double dust_growth_densref; double dust_growth_tauref; + /* Sticking coefficient S for species-specific dust growth. + dust_growth_species rescales tau_ref by 0.3 / S. */ + double dust_growth_sticking_coeff; /* parameters for dust creation*/ double dust_condensation_eff; @@ -345,7 +348,10 @@ typedef struct int dust_species_track; /* Species-specific growth (accretion) reference timescales [Gyr]. - REF: Hirashita 2011 ApJ 743, 159 Table 1; Asano+2013 EP&S 65, 213 */ + Normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, + and solar key-species abundance. Rescaled by dust_grainsize and + dust_growth_sticking_coeff. + REF: Hirashita 2011 MNRAS 416, 1340 section 2.6; Asano+2013 EP&S 65, 213 */ double dust_growth_tauref_silicate; double dust_growth_tauref_carbon; diff --git a/src/python/examples/cooling_cell.py b/src/python/examples/cooling_cell.py index 049a61024..ba59f0c51 100644 --- a/src/python/examples/cooling_cell.py +++ b/src/python/examples/cooling_cell.py @@ -77,6 +77,8 @@ def main(args=None): my_chemistry.primordial_chemistry = 0 my_chemistry.metal_cooling = 1 my_chemistry.UVbackground = 1 + my_chemistry.dust_model = 1 + my_chemistry.dust_species_track = 0 my_chemistry.grackle_data_file = \ os.path.join(grackle_data_dir, "CloudyData_UVB=HM2012.h5") diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index e86bfff16..4cda46e1e 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -269,16 +269,18 @@ def _required_density_fields(my_chemistry): if my_chemistry.dust_species_track == 1: # Two-species dust (silicate + carbonaceous) with 5-element gas tracking. # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 - # Bulk metal_density / dust_density are also required because - # destruction + update still operate on bulk until Phase D rewires. + # Bulk metal_density and dust_density are required invariants for this + # path, even when cooling itself is disabled. + if "metal_density" not in my_fields: + my_fields.append("metal_density") + if "dust_density" not in my_fields: + my_fields.append("dust_density") my_fields.extend([ - "metal_density", "metal_density_carbon", "metal_density_oxygen", "metal_density_magnesium", "metal_density_silicon", "metal_density_iron", - "dust_density", "dust_density_silicate", "dust_density_carbonaceous", ]) diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index d90eeb20d..9e5ab08a0 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -18,11 +18,69 @@ _element_masses, \ FluidContainer -from gracklepy.utilities.atomic import solar_abundance +from gracklepy.utilities.atomic import solar_abundance, atomic_mass from gracklepy.utilities.physical_constants import \ mass_hydrogen_cgs, \ sec_per_Myr +_DUST_SPECIES_ELEMENT_FIELDS = { + "C": "metal_density_carbon", + "O": "metal_density_oxygen", + "Mg": "metal_density_magnesium", + "Si": "metal_density_silicon", + "Fe": "metal_density_iron", +} + +# Default silicate / carbonaceous mass split for IC seeding. +# REF: Draine 2003 ARA&A 41, 241; Zubko, Dwek & Arendt 2004 ApJS 152, 211 — +# canonical MW diffuse-ISM split is ~0.6-0.7 silicate, ~0.3-0.4 carbonaceous. +_DUST_SPECIES_FRACTIONS = { + "silicate": 0.65, + "carbonaceous": 0.35, +} + + +def solar_metal_mass_fractions(elements=("C", "O", "Mg", "Si", "Fe")): + """ + Mass fractions of given elements relative to total solar metal mass. + + Converts number-density ratios n_X/n_H from atomic.solar_abundance into + mass fractions of the total metal mass via + f_X = (n_X * A_X) / sum_Y (n_Y * A_Y) + where the sum runs over all metals (everything except H, He) so the + fractions sum to <= 1 (the 5 dust-relevant elements account for ~80% + of solar metal mass). + """ + metals = [el for el in solar_abundance if el not in ("H", "He")] + total = sum(solar_abundance[el] * atomic_mass[el] for el in metals) + return {el: solar_abundance[el] * atomic_mass[el] / total for el in elements} + + +def seed_dust_species_metal_elements(fc): + """ + Fill metal_density_carbon/oxygen/magnesium/silicon/iron from + fc['metal_density'] using solar mass fractions. Required when + dust_species_track==1: dust_update_species() reads these as the + gas-phase reservoir for silicate accretion (Mg/Fe/Si/O via Choban+2022 + key-reactant scheme) and feeds shock destruction back into them. + """ + fractions = solar_metal_mass_fractions(_DUST_SPECIES_ELEMENT_FIELDS.keys()) + for el, field in _DUST_SPECIES_ELEMENT_FIELDS.items(): + fc[field][:] = fractions[el] * fc["metal_density"] + + +def seed_dust_species_dust(fc): + """ + Split fc['dust_density'] into silicate / carbonaceous reservoirs using + canonical MW diffuse-ISM mass fractions (Draine 2003). + """ + fc["dust_density_silicate"][:] = ( + _DUST_SPECIES_FRACTIONS["silicate"] * fc["dust_density"] + ) + fc["dust_density_carbonaceous"][:] = ( + _DUST_SPECIES_FRACTIONS["carbonaceous"] * fc["dust_density"] + ) + def check_convergence(fc1, fc2, fields=None, tol=0.01): "Check for fields to be different by less than tol." @@ -210,6 +268,10 @@ def setup_fluid_container(my_chemistry, for field in fc.density_fields: fc[field][:] = state_vals.get(field, tiny_density) + if my_chemistry.dust_species_track == 1: + seed_dust_species_metal_elements(fc) + seed_dust_species_dust(fc) + fc.calculate_mean_molecular_weight() fc["internal_energy"] = temperature / \ fc.chemistry_data.temperature_units / \ diff --git a/src/python/gracklepy/utilities/evolve.py b/src/python/gracklepy/utilities/evolve.py index 767cdd1e1..280a3fa7a 100644 --- a/src/python/gracklepy/utilities/evolve.py +++ b/src/python/gracklepy/utilities/evolve.py @@ -59,10 +59,10 @@ def evolve_freefall(fc, final_density, safety_factor=0.01, (0.5 * freefall_time_constant * dt * np.power((1 - force_factor), 0.5))), -2.) - # print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % - # ((current_time * my_chemistry.time_units / sec_per_year), - # (fc["density"][0] * my_chemistry.density_units), - # fc["temperature"][0])) + print("Evolve Freefall - t: %e yr, rho: %e g/cm^3, T: %e K." % + ((current_time * my_chemistry.time_units / sec_per_year), + (fc["density"][0] * my_chemistry.density_units), + fc["temperature"][0])) # use this to multiply by elemental densities if you are tracking those density_ratio = new_density / fc["density"][0] @@ -134,10 +134,10 @@ def evolve_constant_density(fc, final_temperature=None, break fc.calculate_temperature() - # print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % - # (current_time * my_chemistry.time_units / sec_per_year, - # fc["density"][0] * my_chemistry.density_units, - # fc["temperature"][0])) + print("Evolve constant density - t: %e yr, rho: %e g/cm^3, T: %e K." % + (current_time * my_chemistry.time_units / sec_per_year, + fc["density"][0] * my_chemistry.density_units, + fc["temperature"][0])) fc.solve_chemistry(dt) add_to_data(fc, data, extra={"time": current_time}) From 629c6dbc1d58cbb7716ed7f32f873e1024e416fb Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Wed, 6 May 2026 19:37:12 +0100 Subject: [PATCH 65/71] Seperate olivine and pyroxene --- src/clib/dust/dust_growth_and_destruction.cpp | 307 ++++++++++++------ src/clib/dust/dust_growth_and_destruction.hpp | 32 +- src/clib/field_data_misc_fdatamembers.def | 4 +- src/clib/grackle_chemistry_data_fields.def | 31 +- src/clib/make_consistent.cpp | 76 ++++- src/clib/scale_fields.cpp | 8 + src/clib/scale_fields.hpp | 24 ++ src/clib/solve_rate_cool.cpp | 20 +- src/include/grackle_chemistry_data.h | 31 +- src/include/grackle_types.h | 8 +- src/python/gracklepy/fluid_container.py | 6 +- src/python/gracklepy/grackle_defs.pxd | 2 + src/python/gracklepy/grackle_wrapper.pyx | 2 + src/python/gracklepy/utilities/convenience.py | 24 +- 14 files changed, 402 insertions(+), 173 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index 323ef80cc..f0cd07e1c 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -105,12 +105,13 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, } // ========================================== -// DUST GROWTH (SPECIES-SPECIFIC: silicate + carbonaceous) +// DUST GROWTH (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) // ========================================== -// Two parallel accretion rates onto pre-existing dust seeds, gated by +// Parallel accretion rates onto pre-existing dust seeds, gated by // dust_species_track == 1. Carbonaceous growth is rate-limited by gas-phase -// carbon. Silicate growth is rate-limited by the least-available reactant in -// {Mg, Si, Fe, O} weighted by stoichiometric mass fraction f_X. +// carbon. Olivine growth is rate-limited by the least-available reactant in +// {Mg, Si, Fe, O}; pyroxene growth is limited by {Mg, Si, O} because its Fe +// stoichiometric coefficient is zero. // // The species tau_ref values follow Hirashita 2011 MNRAS 416, 1340 section // 2.6: they are normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, @@ -120,21 +121,24 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, // It therefore computes the density factor from local hydrogen number density, // not from the bulk SIMBA // dust_growth_densref parameter. Since this path tracks five silicate -// elements, we apply the same key-species logic to the limiting stoichiometric -// pool and normalize by that pool's solar value. This preserves tau_ref for a -// solar mixture while slowing growth when any required silicate reactant is -// depleted. +// elements, we apply the same key-species logic separately to olivine and +// pyroxene. This preserves tau_ref for a solar mixture while slowing each +// channel only when a reactant required by that channel is depleted. void grackle::impl::dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* growth_dM_silicate, double* growth_dM_carbon) { + double* growth_dM_olivine, double* growth_dM_pyroxene, + double* growth_dM_carbon) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_sil( - my_fields->dust_density_silicate, my_fields->grid_dimension[0], + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], @@ -209,10 +213,14 @@ void grackle::impl::dust_growth_species( ? my_chemistry->dust_grainsize / 0.1 : huge_value; - double f_Mg = my_chemistry->dust_silicate_f_Mg; - double f_Fe = my_chemistry->dust_silicate_f_Fe; - double f_Si = my_chemistry->dust_silicate_f_Si; - double f_O = my_chemistry->dust_silicate_f_O; + double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; + double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; + double f_ol_Si = my_chemistry->dust_olivine_f_Si; + double f_ol_O = my_chemistry->dust_olivine_f_O; + double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; + double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; + double f_py_Si = my_chemistry->dust_pyroxene_f_Si; + double f_py_O = my_chemistry->dust_pyroxene_f_O; double solar_nonmetal = std::max(1.0 - my_chemistry->SolarMetalFractionByMass, tiny_value); @@ -229,18 +237,16 @@ void grackle::impl::dust_growth_species( double solar_Fe = my_chemistry->SolarMetalFractionByMass * solar_frac_Fe / solar_H; - double solar_pool_Mg = (f_Mg > 0.0) ? solar_Mg / f_Mg : huge_value; - double solar_pool_Fe = (f_Fe > 0.0) ? solar_Fe / f_Fe : huge_value; - double solar_pool_Si = (f_Si > 0.0) ? solar_Si / f_Si : huge_value; - double solar_pool_O = (f_O > 0.0) ? solar_O / f_O : huge_value; // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - growth_dM_silicate[i] = 0.0; + growth_dM_olivine[i] = 0.0; + growth_dM_pyroxene[i] = 0.0; growth_dM_carbon[i] = 0.0; if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); + double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); double rho_metal_total = metal(i, idx_range.j, idx_range.k); @@ -300,50 +306,52 @@ void grackle::impl::dust_growth_species( growth_dM_carbon[i] = std::min(rate, rho_C / dt); } - // ---------- Silicate: Choban+2022 key-reactant ---------- - // For each key element X, the maximum silicate dust mass that could - // be assembled from X is rho_X / f_X. The bottleneck element sets - // the rate, and the solar normalization must use that same element. - double mass_from_Mg = (f_Mg > 0.0) ? rho_Mg / f_Mg : huge_value; - double mass_from_Fe = (f_Fe > 0.0) ? rho_Fe / f_Fe : huge_value; - double mass_from_Si = (f_Si > 0.0) ? rho_Si / f_Si : huge_value; - double mass_from_O = (f_O > 0.0) ? rho_O / f_O : huge_value; - - double rho_sil_pool = huge_value; - double solar_sil_pool = 0.0; - if (f_Mg > 0.0 && mass_from_Mg < rho_sil_pool) { - rho_sil_pool = mass_from_Mg; - solar_sil_pool = solar_pool_Mg; - } - if (f_Fe > 0.0 && mass_from_Fe < rho_sil_pool) { - rho_sil_pool = mass_from_Fe; - solar_sil_pool = solar_pool_Fe; - } - if (f_Si > 0.0 && mass_from_Si < rho_sil_pool) { - rho_sil_pool = mass_from_Si; - solar_sil_pool = solar_pool_Si; - } - if (f_O > 0.0 && mass_from_O < rho_sil_pool) { - rho_sil_pool = mass_from_O; - solar_sil_pool = solar_pool_O; - } + // ---------- Silicates: Choban+2022 key-reactant by species ---------- + // For each required element X, the maximum dust mass that could be + // assembled from X is rho_X / f_X. The bottleneck element sets the + // rate, and the solar normalization must use that same element. + auto silicate_growth_rate = [&](double rho_dust, double f_Mg, + double f_Fe, double f_Si, + double f_O) -> double { + if (rho_dust <= 0.0 || dt <= 0.0) return 0.0; + + double rho_pool = huge_value; + double solar_pool = 0.0; + auto consider_pool = [&](double rho_X, double f_X, double solar_X) { + if (f_X <= 0.0) return; + double mass_from_X = rho_X / f_X; + if (mass_from_X < rho_pool) { + rho_pool = mass_from_X; + solar_pool = solar_X / f_X; + } + }; + consider_pool(rho_Mg, f_Mg, solar_Mg); + consider_pool(rho_Fe, f_Fe, solar_Fe); + consider_pool(rho_Si, f_Si, solar_Si); + consider_pool(rho_O, f_O, solar_O); + + if (rho_pool < metal_gate_threshold * rho_gas || solar_pool <= 0.0) { + return 0.0; + } - if (rho_sil_pool >= metal_gate_threshold * rho_gas && - rho_dust_sil_i > 0.0 && solar_sil_pool > 0.0 && dt > 0.0) { - double rho_sil_total_pool = rho_sil_pool + rho_dust_sil_i; - double z_pool_total_eff = std::max(rho_sil_total_pool / rho_H_nuclei, + double rho_total_pool = rho_pool + rho_dust; + double z_pool_total_eff = std::max(rho_total_pool / rho_H_nuclei, tiny_value); - double tau_accr_sil = tau_ref_sil * accr_struct * - (solar_sil_pool / z_pool_total_eff) * - grain_size_factor * - sticking_factor; - tau_accr_sil = std::min(std::max(tau_accr_sil, tiny_value), - huge_value); - double frac_avail = rho_sil_pool / (rho_dust_sil_i + rho_sil_pool); + double tau_accr = tau_ref_sil * accr_struct * + (solar_pool / z_pool_total_eff) * + grain_size_factor * + sticking_factor; + tau_accr = std::min(std::max(tau_accr, tiny_value), huge_value); + double frac_avail = rho_pool / (rho_dust + rho_pool); frac_avail = std::clamp(frac_avail, 0.0, 1.0); - double rate = frac_avail * (rho_dust_sil_i / tau_accr_sil); - growth_dM_silicate[i] = std::min(rate, rho_sil_pool / dt); - } + double rate = frac_avail * (rho_dust / tau_accr); + return std::min(rate, rho_pool / dt); + }; + + growth_dM_olivine[i] = silicate_growth_rate( + rho_dust_oliv_i, f_ol_Mg, f_ol_Fe, f_ol_Si, f_ol_O); + growth_dM_pyroxene[i] = silicate_growth_rate( + rho_dust_pyro_i, f_py_Mg, f_py_Fe, f_py_Si, f_py_O); } } } @@ -454,11 +462,12 @@ void grackle::impl::dust_destruction( } // ========================================== -// DUST DESTRUCTION (SPECIES-SPECIFIC: silicate + carbonaceous) +// DUST DESTRUCTION (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) // ========================================== // SN-shock destruction + thermal sputtering applied independently to each -// dust species, gated by dust_species_track == 1. Carbonaceous (graphite) -// is the shock-vulnerability baseline; Slavin+2015 Table 2 gives gas-cleared +// dust species, gated by dust_species_track == 1. Carbonaceous (graphite) is +// the shock-vulnerability baseline; olivine and pyroxene use the same +// silicate destruction coefficients. Slavin+2015 Table 2 gives gas-cleared // masses of 990 Msun for silicates and 600 Msun for carbonaceous grains in // their standard SNR model, so silicates are destroyed about 1.65x faster // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740]. @@ -472,13 +481,17 @@ void grackle::impl::dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* destruction_dM_silicate, double* destruction_dM_carbon) { + double* destruction_dM_olivine, double* destruction_dM_pyroxene, + double* destruction_dM_carbon) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_sil( - my_fields->dust_density_silicate, my_fields->grid_dimension[0], + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], @@ -516,17 +529,20 @@ void grackle::impl::dust_destruction_species( // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - destruction_dM_silicate[i] = 0.0; + destruction_dM_olivine[i] = 0.0; + destruction_dM_pyroxene[i] = 0.0; destruction_dM_carbon[i] = 0.0; if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); + double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); - bool sil_active = (rho_dust_sil_i >= dust_gate_threshold * rho_gas); + bool oliv_active = (rho_dust_oliv_i >= dust_gate_threshold * rho_gas); + bool pyro_active = (rho_dust_pyro_i >= dust_gate_threshold * rho_gas); bool carb_active = (rho_dust_carb_i >= dust_gate_threshold * rho_gas); - if (!sil_active && !carb_active) continue; + if (!oliv_active && !pyro_active && !carb_active) continue; double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; if (rho_gas <= 0.0) continue; @@ -582,24 +598,27 @@ void grackle::impl::dust_destruction_species( destruction_dM_carbon[i] = compute_dM( rho_dust_carb_i, shock_factor_carbon, tau_sput_ref_carb); } - if (sil_active) { - destruction_dM_silicate[i] = compute_dM( - rho_dust_sil_i, shock_factor_silicate, tau_sput_ref_sil); + if (oliv_active) { + destruction_dM_olivine[i] = compute_dM( + rho_dust_oliv_i, shock_factor_silicate, tau_sput_ref_sil); + } + if (pyro_active) { + destruction_dM_pyroxene[i] = compute_dM( + rho_dust_pyro_i, shock_factor_silicate, tau_sput_ref_sil); } } } } // ========================================== -// DUST UPDATE (SPECIES-SPECIFIC: silicate + carbonaceous) +// DUST UPDATE (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) // ========================================== // Per-channel mass exchange between dust species and their corresponding // gas-phase reactant pools. Active when dust_species_track == 1. // // carbon channel: rho_dust_carbonaceous <-> rho_metal_carbon -// silicate channel: rho_dust_silicate <-> {Mg, Fe, Si, O} at fractions f_X -// (Choban+2022 §2.2; 50/50 olivine+pyroxene per -// Draine 2003 / Dwek 1998). +// olivine channel: rho_dust_olivine <-> {Mg, Fe, Si, O} as MgFeSiO4 +// pyroxene channel: rho_dust_pyroxene <-> {Mg, Si, O} as MgSiO3 // // Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] // shortfall iteration: growth is capped by the limiting reactant, destruction @@ -608,8 +627,10 @@ void grackle::impl::dust_destruction_species( void grackle::impl::dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - const double* dt_value, const double* growth_dM_silicate, - const double* growth_dM_carbon, const double* destruction_dM_silicate, + const double* dt_value, const double* growth_dM_olivine, + const double* growth_dM_pyroxene, const double* growth_dM_carbon, + const double* destruction_dM_olivine, + const double* destruction_dM_pyroxene, const double* destruction_dM_carbon, bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -620,6 +641,12 @@ void grackle::impl::dust_update_species( grackle::impl::View dust_sil( my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -642,16 +669,21 @@ void grackle::impl::dust_update_species( my_fields->metal_density_iron, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - const double f_Mg = my_chemistry->dust_silicate_f_Mg; - const double f_Fe = my_chemistry->dust_silicate_f_Fe; - const double f_Si = my_chemistry->dust_silicate_f_Si; - const double f_O = my_chemistry->dust_silicate_f_O; + const double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; + const double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; + const double f_ol_Si = my_chemistry->dust_olivine_f_Si; + const double f_ol_O = my_chemistry->dust_olivine_f_O; + const double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; + const double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; + const double f_py_Si = my_chemistry->dust_pyroxene_f_Si; + const double f_py_O = my_chemistry->dust_pyroxene_f_O; for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { if (itmask[i] == MASK_FALSE) continue; double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_sil_i = dust_sil(i, idx_range.j, idx_range.k); + double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); + double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); double rho_metal_total = metal(i, idx_range.j, idx_range.k); double rho_C = mC(i, idx_range.j, idx_range.k); @@ -668,7 +700,10 @@ void grackle::impl::dust_update_species( // dM > 0 means net growth (gas reactants -> dust); // dM < 0 means net destruction (dust -> gas reactants). double dM_carb = (growth_dM_carbon[i] + destruction_dM_carbon[i]) * dt; - double dM_sil = (growth_dM_silicate[i] + destruction_dM_silicate[i]) * dt; + double dM_oliv = (growth_dM_olivine[i] + + destruction_dM_olivine[i]) * dt; + double dM_pyro = (growth_dM_pyroxene[i] + + destruction_dM_pyroxene[i]) * dt; // ---------- Per-channel pre-cap ---------- // Carbon channel: bounded by rho_C (growth) or rho_dust_carb (destruction). @@ -678,40 +713,96 @@ void grackle::impl::dust_update_species( dM_carb = std::max(dM_carb, -rho_dust_carb_i); } - // Silicate channel: growth limited by least-available reactant per - // stoichiometric coefficient; destruction limited by silicate dust mass. - if (dM_sil > 0.0) { - double cap = dM_sil; - if (f_Mg > 0.0) cap = std::min(cap, rho_Mg / f_Mg); - if (f_Fe > 0.0) cap = std::min(cap, rho_Fe / f_Fe); - if (f_Si > 0.0) cap = std::min(cap, rho_Si / f_Si); - if (f_O > 0.0) cap = std::min(cap, rho_O / f_O); - dM_sil = std::max(0.0, cap); + // Olivine and pyroxene channels share Mg/Si/O but not Fe. First cap + // destruction by each species' own dust mass. + if (dM_oliv < 0.0) { + dM_oliv = std::max(dM_oliv, -rho_dust_oliv_i); + } + if (dM_pyro < 0.0) { + dM_pyro = std::max(dM_pyro, -rho_dust_pyro_i); + } + + // Then cap positive growth jointly against the shared gas reservoirs. + // This preserves pyroxene growth when Fe alone limits olivine, while still + // preventing double-use of Mg/Si/O if both channels grow together. + double dM_oliv_grow = std::max(dM_oliv, 0.0); + double dM_pyro_grow = std::max(dM_pyro, 0.0); + for (int cap_iter = 0; cap_iter < 4; cap_iter++) { + double scale = 1.0; + int limiter = -1; // 0=Mg, 1=Fe, 2=Si, 3=O + auto consider_element = [&](double rho_X, double c_ol, + double c_py, int element_id) { + double need = dM_oliv_grow * c_ol + dM_pyro_grow * c_py; + if (need > rho_X && need > 0.0) { + double trial = std::max(0.0, rho_X / need); + if (trial < scale) { + scale = trial; + limiter = element_id; + } + } + }; + consider_element(rho_Mg, f_ol_Mg, f_py_Mg, 0); + consider_element(rho_Fe, f_ol_Fe, f_py_Fe, 1); + consider_element(rho_Si, f_ol_Si, f_py_Si, 2); + consider_element(rho_O, f_ol_O, f_py_O, 3); + if (limiter < 0 || scale >= 1.0) break; + + if (limiter == 0) { + if (f_ol_Mg > 0.0) dM_oliv_grow *= scale; + if (f_py_Mg > 0.0) dM_pyro_grow *= scale; + } else if (limiter == 1) { + if (f_ol_Fe > 0.0) dM_oliv_grow *= scale; + if (f_py_Fe > 0.0) dM_pyro_grow *= scale; + } else if (limiter == 2) { + if (f_ol_Si > 0.0) dM_oliv_grow *= scale; + if (f_py_Si > 0.0) dM_pyro_grow *= scale; + } else { + if (f_ol_O > 0.0) dM_oliv_grow *= scale; + if (f_py_O > 0.0) dM_pyro_grow *= scale; + } + } + if (dM_oliv > 0.0) { + dM_oliv = dM_oliv_grow; + } + if (dM_pyro > 0.0) { + dM_pyro = dM_pyro_grow; + } + + if (dM_oliv < 0.0) { + dM_oliv = std::max(dM_oliv, -rho_dust_oliv_i); + } else { + dM_oliv = std::max(dM_oliv, 0.0); + } + if (dM_pyro < 0.0) { + dM_pyro = std::max(dM_pyro, -rho_dust_pyro_i); } else { - dM_sil = std::max(dM_sil, -rho_dust_sil_i); + dM_pyro = std::max(dM_pyro, 0.0); } // ---------- Apply ---------- rho_dust_carb_i += dM_carb; rho_C -= dM_carb; - rho_dust_sil_i += dM_sil; - rho_Mg -= dM_sil * f_Mg; - rho_Fe -= dM_sil * f_Fe; - rho_Si -= dM_sil * f_Si; - rho_O -= dM_sil * f_O; + rho_dust_oliv_i += dM_oliv; + rho_dust_pyro_i += dM_pyro; + rho_Mg -= dM_oliv * f_ol_Mg + dM_pyro * f_py_Mg; + rho_Fe -= dM_oliv * f_ol_Fe + dM_pyro * f_py_Fe; + rho_Si -= dM_oliv * f_ol_Si + dM_pyro * f_py_Si; + rho_O -= dM_oliv * f_ol_O + dM_pyro * f_py_O; // Floors / NaN guard rho_dust_carb_i = std::max(0.0, rho_dust_carb_i); - rho_dust_sil_i = std::max(0.0, rho_dust_sil_i); + rho_dust_oliv_i = std::max(0.0, rho_dust_oliv_i); + rho_dust_pyro_i = std::max(0.0, rho_dust_pyro_i); rho_C = std::max(0.0, rho_C); rho_O = std::max(0.0, rho_O); rho_Mg = std::max(0.0, rho_Mg); rho_Si = std::max(0.0, rho_Si); rho_Fe = std::max(0.0, rho_Fe); - // Bulk dust = silicate + carbonaceous (Phase E will enforce as invariant - // in make_consistent; computing it here keeps the bulk field in sync). + // Bulk dust = olivine + pyroxene + carbonaceous. The compatibility + // silicate field is olivine + pyroxene. + double rho_dust_sil_i = rho_dust_oliv_i + rho_dust_pyro_i; double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; // metal_density_other (= total - C - O - Mg - Si - Fe) is unchanged on @@ -729,13 +820,17 @@ void grackle::impl::dust_update_species( if (std::isnan(rho_dust_new) || std::isnan(rho_metal_new) || std::isnan(rho_gas)) { std::cout << "dust_update_species: NaN at cell " << i - << " dM_carb=" << dM_carb << " dM_sil=" << dM_sil << std::endl; + << " dM_carb=" << dM_carb + << " dM_oliv=" << dM_oliv + << " dM_pyro=" << dM_pyro << std::endl; continue; } if (!dryrun) { dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_new; dust_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_sil_i; + dust_oliv(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_oliv_i; + dust_pyro(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_pyro_i; dust_carb(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_carb_i; metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_new; mC(i, idx_range.j, idx_range.k) = (gr_float)rho_C; diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 3424e52a8..6f1649b7f 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -19,10 +19,11 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, double* growth_dM // output: mass change rate for each cell ); -// Species-specific accretion onto two pre-existing dust populations -// (silicate + carbonaceous). Active when dust_species_track == 1. +// Species-specific accretion onto three pre-existing dust populations +// (olivine + pyroxene + carbonaceous). Active when dust_species_track == 1. // - carbonaceous: rate-limited by gas-phase carbon -// - silicate: rate-limited by min over {Mg, Si, Fe, O} of (rho_X / f_X), +// - olivine: rate-limited by min over {Mg, Fe, Si, O} of (rho_X / f_X) +// - pyroxene: rate-limited by min over {Mg, Si, O} of (rho_X / f_X) // following the Choban+2022 MNRAS 514, 4506 §2.2 key-reactant approach // Per-species tau_accr uses the Hirashita 2011 section 2.6 normalization: // n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, and solar key-species @@ -34,7 +35,8 @@ void dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* growth_dM_silicate, // output: silicate accretion rate + double* growth_dM_olivine, // output: olivine accretion rate + double* growth_dM_pyroxene, // output: pyroxene accretion rate double* growth_dM_carbon // output: carbonaceous accretion rate ); @@ -47,9 +49,10 @@ void dust_destruction( double* destruction_dM // output: mass change rate for each cell ); -// Species-specific destruction (SN shocks + thermal sputtering) onto the two +// Species-specific destruction (SN shocks + thermal sputtering) onto the three // dust populations. Active when dust_species_track == 1. -// - shock yield: graphite is baseline (factor 1.0); silicate follows the +// - shock yield: graphite is baseline (factor 1.0); olivine and pyroxene +// both follow the // Slavin+2015 standard SNR gas-cleared mass ratio 990/600 = 1.65 // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740] // - thermal sputtering: species-specific tau_ref @@ -62,7 +65,8 @@ void dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* destruction_dM_silicate, // output: silicate destruction rate + double* destruction_dM_olivine, // output: olivine destruction rate + double* destruction_dM_pyroxene, // output: pyroxene destruction rate double* destruction_dM_carbon // output: carbonaceous destruction rate ); @@ -75,12 +79,12 @@ void dust_update( const double* destruction_dM, // input: mass change from destruction bool dryrun); -// Species-specific field update for the two-species path (dust_species_track==1). +// Species-specific field update for the split-silicate path +// (dust_species_track==1). // Per-channel mass exchange: // - carbon channel: rho_dust_carbonaceous <-> metal_density_carbon -// - silicate channel: rho_dust_silicate <-> {Mg, Fe, Si, O} at stoichiometric -// mass fractions f_X (Choban+2022 §2.2; 50/50 olivine + -// pyroxene mix, Draine 2003 / Dwek 1998). +// - olivine channel: rho_dust_olivine <-> {Mg, Fe, Si, O} as MgFeSiO4 +// - pyroxene channel: rho_dust_pyroxene <-> {Mg, Si, O} as MgSiO3 // Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] // shortfall mask. No SN injection here — Phase D drops the in-Grackle // dust_creation pathway from the species branch; host code seeds dust species @@ -89,9 +93,11 @@ void dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - const double* growth_dM_silicate, // input: silicate accretion rate + const double* growth_dM_olivine, // input: olivine accretion rate + const double* growth_dM_pyroxene, // input: pyroxene accretion rate const double* growth_dM_carbon, // input: carbonaceous accretion rate - const double* destruction_dM_silicate, // input: silicate destruction rate (<=0) + const double* destruction_dM_olivine, // input: olivine destruction rate (<=0) + const double* destruction_dM_pyroxene, // input: pyroxene destruction rate (<=0) const double* destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) bool dryrun); diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index 95721be86..a5104efe5 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -40,8 +40,10 @@ ENTRY(metal_density_iron) // use_dust_density_field = 1 ENTRY(dust_density) - // dust_species_track = 1 (two-species dust) + // dust_species_track = 1 (olivine + pyroxene + carbonaceous dust) ENTRY(dust_density_silicate) +ENTRY(dust_density_olivine) +ENTRY(dust_density_pyroxene) ENTRY(dust_density_carbonaceous) // use_volumetric_heating_rate = 1 diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index db30ed36c..22917a75f 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -359,11 +359,13 @@ ENTRY(dust_growth_sticking_coeff, DOUBLE, 3.0e-1) ENTRY(dust_condensation_eff, DOUBLE, 1.5e-1) ENTRY(sne_metal_yield, DOUBLE, 3.0) -/* Two-species dust tracking (silicate + carbonaceous). +/* Species-resolved dust tracking. Requires dust_model=1. 0) off (default — bulk dust_density) - 1) on — evolves dust_density_silicate, dust_density_carbonaceous and - 5-element gas tracking (C, O, Mg, Si, Fe). + 1) on — evolves dust_density_olivine, dust_density_pyroxene, + dust_density_carbonaceous and 5-element gas tracking + (C, O, Mg, Si, Fe). dust_density_silicate is maintained as + olivine + pyroxene for compatibility. REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937; McKinnon+2018 MNRAS 478, 2851 */ ENTRY(dust_species_track, INT, 0) @@ -385,11 +387,18 @@ ENTRY(dust_growth_tauref_carbon, DOUBLE, 5.59e-2) ENTRY(dust_sputter_tauref_silicate, DOUBLE, 1.7e8) ENTRY(dust_sputter_tauref_carbon, DOUBLE, 3.4e8) -/* Silicate stoichiometric mass fractions (50/50 olivine MgFeSiO4 + - pyroxene MgSiO3 by mass). Used for both growth depletion and - destruction release of Mg/Fe/Si/O. Sum = 1.000. - REF: Draine 2003 ARA&A 41, 241; Dwek 1998 ApJ 501, 643 */ -ENTRY(dust_silicate_f_Mg, DOUBLE, 0.190) -ENTRY(dust_silicate_f_Fe, DOUBLE, 0.163) -ENTRY(dust_silicate_f_Si, DOUBLE, 0.221) -ENTRY(dust_silicate_f_O, DOUBLE, 0.426) +/* Initial/fallback mass fraction of silicate dust in olivine. The live + olivine/pyroxene ratio evolves once the split fields are active. */ +ENTRY(dust_silicate_olivine_fraction, DOUBLE, 0.5) + +/* Olivine stoichiometric mass fractions for MgFeSiO4. Sum = 1.000. */ +ENTRY(dust_olivine_f_Mg, DOUBLE, 0.141) +ENTRY(dust_olivine_f_Fe, DOUBLE, 0.324) +ENTRY(dust_olivine_f_Si, DOUBLE, 0.163) +ENTRY(dust_olivine_f_O, DOUBLE, 0.372) + +/* Pyroxene stoichiometric mass fractions for MgSiO3. Sum = 1.000. */ +ENTRY(dust_pyroxene_f_Mg, DOUBLE, 0.242) +ENTRY(dust_pyroxene_f_Fe, DOUBLE, 0.0) +ENTRY(dust_pyroxene_f_Si, DOUBLE, 0.280) +ENTRY(dust_pyroxene_f_O, DOUBLE, 0.478) diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 70775ec15..8c29bc15f 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -13,6 +13,7 @@ // This file was initially generated automatically during conversion of the // make_consistent_g function from FORTRAN to C++ +#include #include #include #include @@ -241,7 +242,8 @@ void make_consistent( // from our tracked metal_density_X + dust species fields, overriding the // SN-yield-derived computation below. Views are constructed once here. grackle::impl::View mC_view, mO_view, mMg_view, - mSi_view, mFe_view, dust_sil_view, dust_carb_view; + mSi_view, mFe_view, dust_sil_view, dust_oliv_view, dust_pyro_view, + dust_carb_view; if (my_chemistry->dust_species_track == 1) { mC_view = grackle::impl::View( const_cast(my_fields->metal_density_carbon), @@ -267,6 +269,14 @@ void make_consistent( const_cast(my_fields->dust_density_silicate), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + dust_oliv_view = grackle::impl::View( + const_cast(my_fields->dust_density_olivine), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + dust_pyro_view = grackle::impl::View( + const_cast(my_fields->dust_density_pyroxene), + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); dust_carb_view = grackle::impl::View( const_cast(my_fields->dust_density_carbonaceous), my_fields->grid_dimension[0], my_fields->grid_dimension[1], @@ -421,16 +431,28 @@ void make_consistent( // Phase F hijack: replace SN-yield-derived per-element arrays with // values pulled directly from our tracked fields. Carbonaceous dust - // is pure C; silicate dust contributes Mg/Fe/Si/O at stoichiometric - // mass fractions f_X. Al and S are not part of the two-species + // is pure C; olivine and pyroxene contribute Mg/Fe/Si/O at their own + // stoichiometric mass fractions. Al and S are not part of this // architecture, so their per-element arrays are zeroed. if (my_chemistry->dust_species_track == 1) { - const double f_Mg = my_chemistry->dust_silicate_f_Mg; - const double f_Fe = my_chemistry->dust_silicate_f_Fe; - const double f_Si = my_chemistry->dust_silicate_f_Si; - const double f_O = my_chemistry->dust_silicate_f_O; + const double f_ol = std::clamp( + my_chemistry->dust_silicate_olivine_fraction, 0.0, 1.0); + const double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; + const double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; + const double f_ol_Si = my_chemistry->dust_olivine_f_Si; + const double f_ol_O = my_chemistry->dust_olivine_f_O; + const double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; + const double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; + const double f_py_Si = my_chemistry->dust_pyroxene_f_Si; + const double f_py_O = my_chemistry->dust_pyroxene_f_O; for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - const double sil = dust_sil_view(i, j, k); + double oliv = dust_oliv_view(i, j, k); + double pyro = dust_pyro_view(i, j, k); + const double sil_compat = dust_sil_view(i, j, k); + if (oliv + pyro <= 0.0 && sil_compat > 0.0) { + oliv = f_ol * sil_compat; + pyro = (1.0 - f_ol) * sil_compat; + } const double carb = dust_carb_view(i, j, k); Cg[i] = mC_view(i, j, k); Og[i] = mO_view(i, j, k); @@ -438,10 +460,10 @@ void make_consistent( Sig[i] = mSi_view(i, j, k); Feg[i] = mFe_view(i, j, k); Cd[i] = carb; - Od[i] = sil * f_O; - Mgd[i] = sil * f_Mg; - Sid[i] = sil * f_Si; - Fed[i] = sil * f_Fe; + Od[i] = oliv * f_ol_O + pyro * f_py_O; + Mgd[i] = oliv * f_ol_Mg + pyro * f_py_Mg; + Sid[i] = oliv * f_ol_Si + pyro * f_py_Si; + Fed[i] = oliv * f_ol_Fe + pyro * f_py_Fe; Ct[i] = Cg[i] + Cd[i]; Ot[i] = Og[i] + Od[i]; Mgt[i] = Mgg[i] + Mgd[i]; @@ -1053,17 +1075,28 @@ void make_consistent( } } - // Phase E invariant: bulk dust_density = silicate + carbonaceous. + // Phase E invariant: bulk dust_density = olivine + pyroxene + carbonaceous, + // with dust_density_silicate maintained as olivine + pyroxene. // dust_update_species() maintains this per-cell, but external mutations // (host injection, inject_pathway writes) can break it before make_consistent. // Re-derive here so downstream consumers (calc_tdust_3d.cpp, cooling tables) // see a consistent bulk field. if (my_chemistry->dust_species_track == 1) { + const double f_ol = std::clamp( + my_chemistry->dust_silicate_olivine_fraction, 0.0, 1.0); grackle::impl::View dust_bulk( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_sil( - const_cast(my_fields->dust_density_silicate), + grackle::impl::View dust_sil( + my_fields->dust_density_silicate, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( @@ -1073,7 +1106,18 @@ void make_consistent( for (k = my_fields->grid_start[2]; k <= my_fields->grid_end[2]; k++) { for (j = my_fields->grid_start[1]; j <= my_fields->grid_end[1]; j++) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - dust_bulk(i, j, k) = dust_sil(i, j, k) + dust_carb(i, j, k); + double oliv = dust_oliv(i, j, k); + double pyro = dust_pyro(i, j, k); + const double sil_compat = dust_sil(i, j, k); + if (oliv + pyro <= 0.0 && sil_compat > 0.0) { + oliv = f_ol * sil_compat; + pyro = (1.0 - f_ol) * sil_compat; + dust_oliv(i, j, k) = (gr_float)oliv; + dust_pyro(i, j, k) = (gr_float)pyro; + } + const double sil = oliv + pyro; + dust_sil(i, j, k) = (gr_float)sil; + dust_bulk(i, j, k) = (gr_float)(sil + dust_carb(i, j, k)); } } } diff --git a/src/clib/scale_fields.cpp b/src/clib/scale_fields.cpp index 7898a3356..9e807be40 100644 --- a/src/clib/scale_fields.cpp +++ b/src/clib/scale_fields.cpp @@ -127,6 +127,12 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, grackle::impl::View dust_sil( my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -355,6 +361,8 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, if (my_chemistry->dust_species_track == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { dust_sil(i, j, k) = dust_sil(i, j, k) * factor; + dust_oliv(i, j, k) = dust_oliv(i, j, k) * factor; + dust_pyro(i, j, k) = dust_pyro(i, j, k) * factor; dust_carb(i, j, k) = dust_carb(i, j, k) * factor; } } diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index ad8293dad..176071cae 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -125,6 +125,30 @@ inline void scale_fields_table(grackle_field_data* my_fields, double factor) { } } } + if (my_fields->dust_density_olivine != nullptr) { + grackle::impl::View dust_oliv( + my_fields->dust_density_olivine, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + dust_oliv(i, j, k) = dust_oliv(i, j, k) * factor; + } + } + } + } + if (my_fields->dust_density_pyroxene != nullptr) { + grackle::impl::View dust_pyro( + my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + for (int k = grid_start[2]; k <= grid_end[2]; k++) { + for (int j = grid_start[1]; j <= grid_end[1]; j++) { + for (int i = grid_start[0]; i <= grid_end[0]; i++) { + dust_pyro(i, j, k) = dust_pyro(i, j, k) * factor; + } + } + } + } if (my_fields->dust_density_carbonaceous != nullptr) { grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 86d516bd4..54dfe71a4 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -733,9 +733,11 @@ int solve_rate_cool( std::vector destruction_dM(my_fields->grid_dimension[0]); // Phase B/C: species-specific growth & destruction outputs (used when // dust_species_track == 1; Phase D wires them into dust_update()) - std::vector growth_dM_silicate(my_fields->grid_dimension[0]); + std::vector growth_dM_olivine(my_fields->grid_dimension[0]); + std::vector growth_dM_pyroxene(my_fields->grid_dimension[0]); std::vector growth_dM_carbon(my_fields->grid_dimension[0]); - std::vector destruction_dM_silicate(my_fields->grid_dimension[0]); + std::vector destruction_dM_olivine(my_fields->grid_dimension[0]); + std::vector destruction_dM_pyroxene(my_fields->grid_dimension[0]); std::vector destruction_dM_carbon(my_fields->grid_dimension[0]); // iteration masks @@ -930,20 +932,24 @@ int solve_rate_cool( // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ if (my_chemistry->dust_species_track == 1) { - // Calculate and apply the silicate + carbonaceous rates. + // Calculate and apply the olivine + pyroxene + carbonaceous rates. grackle::impl::dust_growth_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), - growth_dM_silicate.data(), growth_dM_carbon.data()); + growth_dM_olivine.data(), growth_dM_pyroxene.data(), + growth_dM_carbon.data()); grackle::impl::dust_destruction_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), - destruction_dM_silicate.data(), destruction_dM_carbon.data()); + destruction_dM_olivine.data(), destruction_dM_pyroxene.data(), + destruction_dM_carbon.data()); grackle::impl::dust_update_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM_silicate.data(), growth_dM_carbon.data(), - destruction_dM_silicate.data(), destruction_dM_carbon.data(), + growth_dM_olivine.data(), growth_dM_pyroxene.data(), + growth_dM_carbon.data(), + destruction_dM_olivine.data(), destruction_dM_pyroxene.data(), + destruction_dM_carbon.data(), false); } else { grackle::impl::dust_growth( diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index a14ca7191..a80003e1e 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -339,11 +339,13 @@ typedef struct double dust_condensation_eff; double sne_metal_yield; - /* Two-species dust tracking (silicate + carbonaceous). + /* Species-resolved dust tracking. Requires dust_model=1. 0) off — bulk dust_density (default) - 1) on — evolves dust_density_silicate, dust_density_carbonaceous and - 5-element gas tracking (C, O, Mg, Si, Fe). + 1) on — evolves dust_density_olivine, dust_density_pyroxene, + dust_density_carbonaceous and 5-element gas tracking + (C, O, Mg, Si, Fe). dust_density_silicate is maintained as + olivine + pyroxene for compatibility. REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 */ int dust_species_track; @@ -361,13 +363,22 @@ typedef struct double dust_sputter_tauref_silicate; double dust_sputter_tauref_carbon; - /* Silicate stoichiometric mass fractions (50/50 olivine MgFeSiO4 + - pyroxene MgSiO3 by mass). Sum = 1.000. - REF: Draine 2003 ARA&A 41, 241; Dwek 1998 ApJ 501, 643 */ - double dust_silicate_f_Mg; - double dust_silicate_f_Fe; - double dust_silicate_f_Si; - double dust_silicate_f_O; + /* Initial/fallback mass fraction of silicate dust in olivine. The live + olivine/pyroxene ratio evolves once the split fields are active. */ + double dust_silicate_olivine_fraction; + + /* Olivine stoichiometric mass fractions for MgFeSiO4. Sum = 1.000. */ + double dust_olivine_f_Mg; + double dust_olivine_f_Fe; + double dust_olivine_f_Si; + double dust_olivine_f_O; + + /* Pyroxene stoichiometric mass fractions for MgSiO3. Sum = 1.000. + Fe is zero so Fe depletion does not halt pyroxene growth. */ + double dust_pyroxene_f_Mg; + double dust_pyroxene_f_Fe; + double dust_pyroxene_f_Si; + double dust_pyroxene_f_O; } chemistry_data; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index 9e911ad6f..c1ea45d35 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -88,9 +88,13 @@ typedef struct gr_float *dust_density; // dust_species_track = 1 - // Two-species dust: bulk dust_density = silicate + carbonaceous. - // REF: Hirashita 2015 MNRAS 447, 2937; McKinnon+2018 MNRAS 478, 2851 + // Three-species dust: bulk dust_density = olivine + pyroxene + + // carbonaceous. dust_density_silicate is kept as the derived + // olivine + pyroxene sum for compatibility. + // REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 gr_float *dust_density_silicate; + gr_float *dust_density_olivine; + gr_float *dust_density_pyroxene; gr_float *dust_density_carbonaceous; // primordial_chemistry = 1 diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index 4cda46e1e..bfd34b7c7 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -267,7 +267,9 @@ def _required_density_fields(my_chemistry): if my_chemistry.dust_chemistry == 1: my_fields.append("dust_density") if my_chemistry.dust_species_track == 1: - # Two-species dust (silicate + carbonaceous) with 5-element gas tracking. + # Species-resolved dust (olivine + pyroxene + carbonaceous) with + # 5-element gas tracking. dust_density_silicate is a compatibility + # sum of olivine + pyroxene. # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 # Bulk metal_density and dust_density are required invariants for this # path, even when cooling itself is disabled. @@ -282,6 +284,8 @@ def _required_density_fields(my_chemistry): "metal_density_silicon", "metal_density_iron", "dust_density_silicate", + "dust_density_olivine", + "dust_density_pyroxene", "dust_density_carbonaceous", ]) if my_chemistry.metal_chemistry > 0: diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 94dca3e92..225cd60a2 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -116,6 +116,8 @@ cdef extern from "grackle.h": gr_float *metal_density_iron; gr_float *dust_density; gr_float *dust_density_silicate; + gr_float *dust_density_olivine; + gr_float *dust_density_pyroxene; gr_float *dust_density_carbonaceous; gr_float *e_density; gr_float *HI_density; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index 1c5b2072e..0457d1cb9 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -662,6 +662,8 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.metal_density_iron = get_field(fc, "metal_density_iron") my_fields.dust_density = get_field(fc, "dust_density") my_fields.dust_density_silicate = get_field(fc, "dust_density_silicate") + my_fields.dust_density_olivine = get_field(fc, "dust_density_olivine") + my_fields.dust_density_pyroxene = get_field(fc, "dust_density_pyroxene") my_fields.dust_density_carbonaceous = get_field(fc, "dust_density_carbonaceous") my_fields.e_density = get_field(fc, "e_density") diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 9e5ab08a0..4d37494ba 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -31,11 +31,13 @@ "Fe": "metal_density_iron", } -# Default silicate / carbonaceous mass split for IC seeding. +# Default olivine / pyroxene / carbonaceous mass split for IC seeding. # REF: Draine 2003 ARA&A 41, 241; Zubko, Dwek & Arendt 2004 ApJS 152, 211 — # canonical MW diffuse-ISM split is ~0.6-0.7 silicate, ~0.3-0.4 carbonaceous. _DUST_SPECIES_FRACTIONS = { "silicate": 0.65, + "olivine": 0.50, + "pyroxene": 0.50, "carbonaceous": 0.35, } @@ -61,8 +63,9 @@ def seed_dust_species_metal_elements(fc): Fill metal_density_carbon/oxygen/magnesium/silicon/iron from fc['metal_density'] using solar mass fractions. Required when dust_species_track==1: dust_update_species() reads these as the - gas-phase reservoir for silicate accretion (Mg/Fe/Si/O via Choban+2022 - key-reactant scheme) and feeds shock destruction back into them. + gas-phase reservoirs for olivine and pyroxene accretion + (Mg/Fe/Si/O via Choban+2022 key-reactant scheme) and feeds shock + destruction back into them. """ fractions = solar_metal_mass_fractions(_DUST_SPECIES_ELEMENT_FIELDS.keys()) for el, field in _DUST_SPECIES_ELEMENT_FIELDS.items(): @@ -71,12 +74,21 @@ def seed_dust_species_metal_elements(fc): def seed_dust_species_dust(fc): """ - Split fc['dust_density'] into silicate / carbonaceous reservoirs using - canonical MW diffuse-ISM mass fractions (Draine 2003). + Split fc['dust_density'] into olivine / pyroxene / carbonaceous + reservoirs using canonical MW diffuse-ISM mass fractions (Draine 2003). """ - fc["dust_density_silicate"][:] = ( + silicate = ( _DUST_SPECIES_FRACTIONS["silicate"] * fc["dust_density"] ) + fc["dust_density_olivine"][:] = ( + _DUST_SPECIES_FRACTIONS["olivine"] * silicate + ) + fc["dust_density_pyroxene"][:] = ( + _DUST_SPECIES_FRACTIONS["pyroxene"] * silicate + ) + fc["dust_density_silicate"][:] = ( + fc["dust_density_olivine"] + fc["dust_density_pyroxene"] + ) fc["dust_density_carbonaceous"][:] = ( _DUST_SPECIES_FRACTIONS["carbonaceous"] * fc["dust_density"] ) From 47541566699d32ad94d4ef40cb4e066b7c5658d9 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Sun, 10 May 2026 23:50:35 +0100 Subject: [PATCH 66/71] Update to species and timescale calculation --- src/clib/dust/dust_growth_and_destruction.cpp | 585 ++++++++++-------- src/clib/dust/dust_growth_and_destruction.hpp | 60 +- src/clib/field_data_misc_fdatamembers.def | 6 +- src/clib/grackle_chemistry_data_fields.def | 71 ++- src/clib/initialize_chemistry_data.cpp | 28 + src/clib/make_consistent.cpp | 82 +-- src/clib/scale_fields.cpp | 12 +- src/clib/scale_fields.hpp | 16 +- src/clib/solve_rate_cool.cpp | 18 +- src/include/grackle_chemistry_data.h | 65 +- src/include/grackle_types.h | 12 +- src/python/gracklepy/fluid_container.py | 12 +- src/python/gracklepy/grackle_defs.pxd | 4 +- src/python/gracklepy/grackle_wrapper.pyx | 4 +- src/python/gracklepy/utilities/convenience.py | 28 +- 15 files changed, 568 insertions(+), 435 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index f0cd07e1c..fb6917c6e 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -8,13 +8,37 @@ namespace { const double sec_per_year = 3.155e7; +const double sec_per_Myr_local = 1.0e6 * sec_per_year; const double tiny_value = 1.0e-20; const double huge_value = 1.0e+20; const double t_ref = 20; -const double species_t_ref = 50; -const double species_nH_ref = 1.0e3; +const double colibre_growth_t_ref = 10.0; +const double colibre_growth_nH_ref = 10.0; +const double colibre_sputter_t_ref = 2.0e6; + +const double atomic_C = 12.01; +const double atomic_O = 16.00; +const double atomic_Mg = 24.31; +const double atomic_Si = 28.09; +const double atomic_Fe = 55.85; + +double limited_expm1(double x) { + if (!std::isfinite(x) || x <= 0.0) return 0.0; + return std::expm1(std::min(x, 50.0)); +} + +double e_fold_growth_rate(double rho_dust, double dt, double tau) { + if (rho_dust <= 0.0 || dt <= 0.0 || tau <= 0.0) return 0.0; + return rho_dust * limited_expm1(dt / tau) / dt; +} + +double e_fold_loss_rate(double rho_dust, double dt, double inv_tau) { + if (rho_dust <= 0.0 || dt <= 0.0 || inv_tau <= 0.0) return 0.0; + double x = std::min(dt * inv_tau, 50.0); + return rho_dust * (-std::expm1(-x)) / dt; +} // Solar metal mass fractions for the tracked dust-forming elements, relative // to total solar metals. These match gracklepy.utilities.convenience, which @@ -105,40 +129,31 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, } // ========================================== -// DUST GROWTH (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) +// DUST GROWTH (SPECIES-SPECIFIC: Mg-silicate + Fe-silicate + carbonaceous) // ========================================== -// Parallel accretion rates onto pre-existing dust seeds, gated by -// dust_species_track == 1. Carbonaceous growth is rate-limited by gas-phase -// carbon. Olivine growth is rate-limited by the least-available reactant in -// {Mg, Si, Fe, O}; pyroxene growth is limited by {Mg, Si, O} because its Fe -// stoichiometric coefficient is zero. -// -// The species tau_ref values follow Hirashita 2011 MNRAS 416, 1340 section -// 2.6: they are normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, -// a = 0.1 micron, and solar abundance of the relevant key species. This branch -// rescales the paper's S_0.3 factor through dust_growth_sticking_coeff and -// a_0.1 through dust_grainsize / 0.1. -// It therefore computes the density factor from local hydrogen number density, -// not from the bulk SIMBA -// dust_growth_densref parameter. Since this path tracks five silicate -// elements, we apply the same key-species logic separately to olivine and -// pyroxene. This preserves tau_ref for a solar mixture while slowing each -// channel only when a reactant required by that channel is depleted. +// COLIBRE-style accretion (Trayford+2026 section 3.3). Carbonaceous growth is +// bottlenecked by gas-phase C. Silicate growth is computed once for the total +// silicate reservoir using the maximum depletion factor over O, Si, and Mg+Fe, +// then split between Mg2SiO4 and Fe2SiO4 endmembers according to the local +// gas-phase Mg/Fe number abundance. The dense-gas rate uses a clumped +// hydrogen density n_H' = C n_H; thermal sputtering below deliberately uses the +// unclumped n_H. This species-tracking branch applies growth as an exponential +// e-folding update, returning an equivalent per-time rate for dust_update(). void grackle::impl::dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* growth_dM_olivine, double* growth_dM_pyroxene, + double* growth_dM_mg_silicate, double* growth_dM_fe_silicate, double* growth_dM_carbon) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, my_fields->grid_dimension[0], + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], @@ -199,11 +214,11 @@ void grackle::impl::dust_growth_species( my_fields->grid_dimension[1], my_fields->grid_dimension[2]); double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); - // tau_ref values are stored in Gyr (Hirashita 2011 section 2.6). - double tau_ref_sil = my_chemistry->dust_growth_tauref_silicate * 1e9 * - sec_per_year / internalu.tbase1; - double tau_ref_carb = my_chemistry->dust_growth_tauref_carbon * 1e9 * - sec_per_year / internalu.tbase1; + // tau_ref values are stored in Myr (COLIBRE Table 2). + double tau_ref_sil = my_chemistry->dust_growth_tauref_silicate * + sec_per_Myr_local / internalu.tbase1; + double tau_ref_carb = my_chemistry->dust_growth_tauref_carbon * + sec_per_Myr_local / internalu.tbase1; double sticking_factor = (my_chemistry->dust_growth_sticking_coeff > 0.0) ? 0.3 / my_chemistry->dust_growth_sticking_coeff @@ -213,40 +228,50 @@ void grackle::impl::dust_growth_species( ? my_chemistry->dust_grainsize / 0.1 : huge_value; - double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; - double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; - double f_ol_Si = my_chemistry->dust_olivine_f_Si; - double f_ol_O = my_chemistry->dust_olivine_f_O; - double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; - double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; - double f_py_Si = my_chemistry->dust_pyroxene_f_Si; - double f_py_O = my_chemistry->dust_pyroxene_f_O; - double solar_nonmetal = std::max(1.0 - my_chemistry->SolarMetalFractionByMass, tiny_value); double solar_H = std::max(my_chemistry->HydrogenFractionByMass * solar_nonmetal, tiny_value); - double solar_C = my_chemistry->SolarMetalFractionByMass * - solar_frac_C / solar_H; - double solar_O = my_chemistry->SolarMetalFractionByMass * - solar_frac_O / solar_H; - double solar_Mg = my_chemistry->SolarMetalFractionByMass * - solar_frac_Mg / solar_H; - double solar_Si = my_chemistry->SolarMetalFractionByMass * - solar_frac_Si / solar_H; - double solar_Fe = my_chemistry->SolarMetalFractionByMass * - solar_frac_Fe / solar_H; + double solar_mass_C = my_chemistry->SolarMetalFractionByMass * + solar_frac_C / solar_H; + double solar_mass_O = my_chemistry->SolarMetalFractionByMass * + solar_frac_O / solar_H; + double solar_mass_Mg = my_chemistry->SolarMetalFractionByMass * + solar_frac_Mg / solar_H; + double solar_mass_Si = my_chemistry->SolarMetalFractionByMass * + solar_frac_Si / solar_H; + double solar_mass_Fe = my_chemistry->SolarMetalFractionByMass * + solar_frac_Fe / solar_H; + double solar_eps_C = solar_mass_C / atomic_C; + double solar_eps_O = solar_mass_O / atomic_O; + double solar_eps_Mg = solar_mass_Mg / atomic_Mg; + double solar_eps_Si = solar_mass_Si / atomic_Si; + double solar_eps_Fe = solar_mass_Fe / atomic_Fe; + double solar_eps_MgFe = solar_eps_Mg + solar_eps_Fe; + + auto clumping_factor = [&](double nH) -> double { + double cmax = std::max(my_chemistry->dust_growth_clumping_factor_max, 1.0); + double nmin = std::max(my_chemistry->dust_growth_clumping_nH_min, + tiny_value); + double nmax = std::max(my_chemistry->dust_growth_clumping_nH_max, + nmin * (1.0 + 1.0e-12)); + if (nH <= nmin || cmax <= 1.0) return 1.0; + if (nH >= nmax) return cmax; + double x = (std::log10(nH) - std::log10(nmin)) / + (std::log10(nmax) - std::log10(nmin)); + return std::pow(cmax, std::clamp(x, 0.0, 1.0)); + }; // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - growth_dM_olivine[i] = 0.0; - growth_dM_pyroxene[i] = 0.0; + growth_dM_mg_silicate[i] = 0.0; + growth_dM_fe_silicate[i] = 0.0; growth_dM_carbon[i] = 0.0; if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); - double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); + double rho_dust_mg_sil_i = dust_mg_sil(i, idx_range.j, idx_range.k); + double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); double rho_metal_total = metal(i, idx_range.j, idx_range.k); @@ -285,73 +310,67 @@ void grackle::impl::dust_growth_species( continue; } - double accr_struct = (species_nH_ref / nH) * - std::pow(species_t_ref / T, 0.5); + double nH_eff = nH * clumping_factor(nH); + double accr_struct = (colibre_growth_nH_ref / nH_eff) * + std::pow(colibre_growth_t_ref / T, 0.5) * + grain_size_factor * sticking_factor; + double inv_rho_H = 1.0 / rho_H_nuclei; + double eps_C = rho_C * inv_rho_H / atomic_C; + double eps_O = rho_O * inv_rho_H / atomic_O; + double eps_Mg = rho_Mg * inv_rho_H / atomic_Mg; + double eps_Si = rho_Si * inv_rho_H / atomic_Si; + double eps_Fe = rho_Fe * inv_rho_H / atomic_Fe; + double eps_MgFe = eps_Mg + eps_Fe; + + auto abundance_ratio = [](double solar_eps, double local_eps) { + if (solar_eps <= 0.0) return 0.0; + if (local_eps <= 0.0) return huge_value; + return solar_eps / local_eps; + }; // ---------- Carbonaceous: rate-limited by gas-phase C ---------- if (rho_C >= metal_gate_threshold * rho_gas && - rho_dust_carb_i > 0.0 && solar_C > 0.0 && dt > 0.0) { - double rho_C_total = rho_C + rho_dust_carb_i; - double z_C_total_eff = std::max(rho_C_total / rho_H_nuclei, - tiny_value); + rho_dust_carb_i > 0.0 && solar_eps_C > 0.0 && eps_C > 0.0 && + dt > 0.0) { double tau_accr_carb = tau_ref_carb * accr_struct * - (solar_C / z_C_total_eff) * - grain_size_factor * - sticking_factor; + abundance_ratio(solar_eps_C, eps_C); tau_accr_carb = std::min(std::max(tau_accr_carb, tiny_value), huge_value); - double frac_avail = rho_C / (rho_dust_carb_i + rho_C); - frac_avail = std::clamp(frac_avail, 0.0, 1.0); - double rate = frac_avail * (rho_dust_carb_i / tau_accr_carb); + double rate = e_fold_growth_rate(rho_dust_carb_i, dt, + tau_accr_carb); growth_dM_carbon[i] = std::min(rate, rho_C / dt); } - // ---------- Silicates: Choban+2022 key-reactant by species ---------- - // For each required element X, the maximum dust mass that could be - // assembled from X is rho_X / f_X. The bottleneck element sets the - // rate, and the solar normalization must use that same element. - auto silicate_growth_rate = [&](double rho_dust, double f_Mg, - double f_Fe, double f_Si, - double f_O) -> double { - if (rho_dust <= 0.0 || dt <= 0.0) return 0.0; - - double rho_pool = huge_value; - double solar_pool = 0.0; - auto consider_pool = [&](double rho_X, double f_X, double solar_X) { - if (f_X <= 0.0) return; - double mass_from_X = rho_X / f_X; - if (mass_from_X < rho_pool) { - rho_pool = mass_from_X; - solar_pool = solar_X / f_X; - } - }; - consider_pool(rho_Mg, f_Mg, solar_Mg); - consider_pool(rho_Fe, f_Fe, solar_Fe); - consider_pool(rho_Si, f_Si, solar_Si); - consider_pool(rho_O, f_O, solar_O); - - if (rho_pool < metal_gate_threshold * rho_gas || solar_pool <= 0.0) { - return 0.0; - } - - double rho_total_pool = rho_pool + rho_dust; - double z_pool_total_eff = std::max(rho_total_pool / rho_H_nuclei, - tiny_value); + // ---------- Silicate: COLIBRE Mg+Fe composite bottleneck ---------- + double rho_dust_sil_i = rho_dust_mg_sil_i + rho_dust_fe_sil_i; + if (rho_dust_sil_i > 0.0 && dt > 0.0 && + eps_O > 0.0 && eps_Si > 0.0 && eps_MgFe > 0.0) { + double sil_ratio = std::max({ + abundance_ratio(solar_eps_O, eps_O), + abundance_ratio(solar_eps_Si, eps_Si), + abundance_ratio(solar_eps_MgFe, eps_MgFe)}); double tau_accr = tau_ref_sil * accr_struct * - (solar_pool / z_pool_total_eff) * - grain_size_factor * - sticking_factor; + sil_ratio; tau_accr = std::min(std::max(tau_accr, tiny_value), huge_value); - double frac_avail = rho_pool / (rho_dust + rho_pool); - frac_avail = std::clamp(frac_avail, 0.0, 1.0); - double rate = frac_avail * (rho_dust / tau_accr); - return std::min(rate, rho_pool / dt); - }; - - growth_dM_olivine[i] = silicate_growth_rate( - rho_dust_oliv_i, f_ol_Mg, f_ol_Fe, f_ol_Si, f_ol_O); - growth_dM_pyroxene[i] = silicate_growth_rate( - rho_dust_pyro_i, f_py_Mg, f_py_Fe, f_py_Si, f_py_O); + double sil_rate = e_fold_growth_rate(rho_dust_sil_i, dt, + tau_accr); + + // Split total silicate accretion by endmember molecule abundance. + // Mass fractions include the different Mg2SiO4 / Fe2SiO4 molecule + // weights, so equal Mg and Fe number abundance reproduces the + // COLIBRE equal-molecule seed split. + double mg_weight = eps_Mg * ( + 2.0 * atomic_Mg + atomic_Si + 4.0 * atomic_O); + double fe_weight = eps_Fe * ( + 2.0 * atomic_Fe + atomic_Si + 4.0 * atomic_O); + double endmember_weight = mg_weight + fe_weight; + if (endmember_weight > 0.0) { + double mg_frac = mg_weight / endmember_weight; + mg_frac = std::clamp(mg_frac, 0.0, 1.0); + growth_dM_mg_silicate[i] = sil_rate * mg_frac; + growth_dM_fe_silicate[i] = sil_rate * (1.0 - mg_frac); + } + } } } } @@ -462,40 +481,78 @@ void grackle::impl::dust_destruction( } // ========================================== -// DUST DESTRUCTION (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) +// DUST DESTRUCTION (SPECIES-SPECIFIC: Mg-silicate + Fe-silicate + carbonaceous) // ========================================== // SN-shock destruction + thermal sputtering applied independently to each // dust species, gated by dust_species_track == 1. Carbonaceous (graphite) is -// the shock-vulnerability baseline; olivine and pyroxene use the same -// silicate destruction coefficients. Slavin+2015 Table 2 gives gas-cleared -// masses of 990 Msun for silicates and 600 Msun for carbonaceous grains in -// their standard SNR model, so silicates are destroyed about 1.65x faster +// the shock-vulnerability baseline; both silicate endmembers use the same +// silicate shock coefficient. Slavin+2015 Table 2 gives gas-cleared masses of +// 990 Msun for silicates and 600 Msun for carbonaceous grains in their +// standard SNR model, so silicates are destroyed about 1.65x faster // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740]. -// Thermal sputtering uses species-specific tau_ref values -// [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; -// REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] -// with the same Draine & Salpeter 1979 / Tielens+1994 scaling form -// (a/0.1) * (1e-27/rho_gas) * ((2e6/T)^2.5 + 1) used by the bulk path. +// Thermal sputtering follows the species-independent COLIBRE/Tsai-Mathews +// timescale tau_sp = 0.85 Myr (a/0.1) (n_H/cm^-3)^-1 +// [1 + (T/2e6 K)^-2.5]. Species destruction is applied as exponential decay, +// returning an equivalent per-time rate for dust_update(). // Phase D wires the species outputs into dust_update(). void grackle::impl::dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* destruction_dM_olivine, double* destruction_dM_pyroxene, + double* destruction_dM_mg_silicate, double* destruction_dM_fe_silicate, double* destruction_dM_carbon) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, my_fields->grid_dimension[0], + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View metal( + my_fields->metal_density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + bool use_H_fields = (my_chemistry->primordial_chemistry > 0); + bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); + bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); + bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); + grackle::impl::View HI( + use_H_fields ? my_fields->HI_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HII( + use_H_fields ? my_fields->HII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HM( + use_H2_fields ? my_fields->HM_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2I( + use_H2_fields ? my_fields->H2I_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2II( + use_H2_fields ? my_fields->H2II_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HDI( + use_HD_fields ? my_fields->HDI_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HDII( + use_HeH_fields ? my_fields->HDII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View HeHII( + use_HeH_fields ? my_fields->HeHII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( use_sne ? my_fields->sne_rate : my_fields->density, @@ -521,28 +578,29 @@ void grackle::impl::dust_destruction_species( const double shock_factor_carbon = 1.0; const double shock_factor_silicate = 1.65; - // Species-specific thermal sputtering tau_ref (params stored in years) - double tau_sput_ref_sil = my_chemistry->dust_sputter_tauref_silicate * - sec_per_year / internalu.tbase1; - double tau_sput_ref_carb = my_chemistry->dust_sputter_tauref_carbon * - sec_per_year / internalu.tbase1; + // COLIBRE/Tsai-Mathews thermal sputtering tau_ref (stored in Myr). + double tau_sput_ref = my_chemistry->dust_sputter_tauref * + sec_per_Myr_local / internalu.tbase1; // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - destruction_dM_olivine[i] = 0.0; - destruction_dM_pyroxene[i] = 0.0; + destruction_dM_mg_silicate[i] = 0.0; + destruction_dM_fe_silicate[i] = 0.0; destruction_dM_carbon[i] = 0.0; if (itmask[i] != MASK_FALSE) { double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); - double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); + double rho_dust_mg_sil_i = dust_mg_sil(i, idx_range.j, idx_range.k); + double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); + double rho_metal_total = metal(i, idx_range.j, idx_range.k); - bool oliv_active = (rho_dust_oliv_i >= dust_gate_threshold * rho_gas); - bool pyro_active = (rho_dust_pyro_i >= dust_gate_threshold * rho_gas); + bool mg_sil_active = + (rho_dust_mg_sil_i >= dust_gate_threshold * rho_gas); + bool fe_sil_active = + (rho_dust_fe_sil_i >= dust_gate_threshold * rho_gas); bool carb_active = (rho_dust_carb_i >= dust_gate_threshold * rho_gas); - if (!oliv_active && !pyro_active && !carb_active) continue; + if (!mg_sil_active && !fe_sil_active && !carb_active) continue; double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; if (rho_gas <= 0.0) continue; @@ -551,15 +609,39 @@ void grackle::impl::dust_destruction_species( double dt = dt_value[i]; if (dt <= 0.0) continue; - // Common (species-independent) sputtering structural factor - double sput_struct = (my_chemistry->dust_grainsize / 0.1) * - (1.0e-27 / (dens_proper * rho_gas)) * - (std::pow((2.0e6 / temp), 2.5) + 1.0); + double rho_nonmetal = rho_gas - rho_metal_total; + if (rho_nonmetal <= 0.0) continue; + double rho_H_nuclei = my_chemistry->HydrogenFractionByMass * + rho_nonmetal; + if (use_H_fields) { + rho_H_nuclei = HI(i, idx_range.j, idx_range.k) + + HII(i, idx_range.j, idx_range.k); + if (use_H2_fields) { + rho_H_nuclei += HM(i, idx_range.j, idx_range.k) + + H2I(i, idx_range.j, idx_range.k) + + H2II(i, idx_range.j, idx_range.k); + } + if (use_HD_fields) { + rho_H_nuclei += HDI(i, idx_range.j, idx_range.k) / 3.0; + } + if (use_HeH_fields) { + rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + + HeHII(i, idx_range.j, idx_range.k) / 5.0; + } + } + double nH = rho_H_nuclei * dens_proper / mh; + if (rho_H_nuclei <= 0.0 || nH <= 0.0) continue; - // Helper computing destruction rate for one species - auto compute_dM = [&](double rho_dust, double shock_factor, - double tau_sput_ref) -> double { - double dM_shock = 0.0; + // Common (species-independent) sputtering structural factor. + double sput_struct = (my_chemistry->dust_grainsize / 0.1) * + (1.0 / nH) * + (1.0 + std::pow(temp / colibre_sputter_t_ref, + -2.5)); + + // Helper computing the equivalent per-time destruction rate for one + // species. Its dt product is an exponential e-folding mass loss. + auto compute_dM = [&](double rho_dust, double shock_factor) -> double { + double inv_tau_loss = 0.0; double tau_dest = 1e20; if (use_tau_dest) { @@ -568,25 +650,29 @@ void grackle::impl::dust_destruction_species( // Apply species multiplier on top of user-supplied tau_dest: // higher shock_factor -> shorter tau -> faster destruction. tau_dest = tau_dest / shock_factor; - dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); + if (tau_dest > 0.0 && std::isfinite(tau_dest)) { + inv_tau_loss += 1.0 / tau_dest; + } } else if (use_sne) { if (sne_this > 0.0) { - tau_dest = rho_gas / - (Ms100 * shock_factor * sne_this * - my_chemistry->dust_destruction_eff) * dt; - dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); + // sne_rate is a rate per code time, so add the physical inverse + // shock-destruction timescale here. The exponential update below + // supplies the dt dependence. + double inv_tau_shock = + Ms100 * shock_factor * sne_this * + my_chemistry->dust_destruction_eff / (rho_gas*dt); + if (inv_tau_shock > 0.0 && std::isfinite(inv_tau_shock)) { + inv_tau_loss += inv_tau_shock; + } } } - if (temp >= std::pow(10, 5)) { - double tau_sput = tau_sput_ref * sput_struct; - if (dM_shock < rho_dust / dt) { - dM_shock = dM_shock + rho_dust / tau_sput * 3.0; - dM_shock = std::min(dM_shock, rho_dust / dt); - } + double tau_sput = tau_sput_ref * sput_struct; + if (tau_sput > 0.0 && std::isfinite(tau_sput)) { + inv_tau_loss += 1.0 / tau_sput; } - double dM = -dM_shock; + double dM = -e_fold_loss_rate(rho_dust, dt, inv_tau_loss); if (std::isnan(dM)) { std::cout << "dM (species) calculated as NaN, " << dM << std::endl; dM = 0.0; @@ -596,29 +682,29 @@ void grackle::impl::dust_destruction_species( if (carb_active) { destruction_dM_carbon[i] = compute_dM( - rho_dust_carb_i, shock_factor_carbon, tau_sput_ref_carb); + rho_dust_carb_i, shock_factor_carbon); } - if (oliv_active) { - destruction_dM_olivine[i] = compute_dM( - rho_dust_oliv_i, shock_factor_silicate, tau_sput_ref_sil); + if (mg_sil_active) { + destruction_dM_mg_silicate[i] = compute_dM( + rho_dust_mg_sil_i, shock_factor_silicate); } - if (pyro_active) { - destruction_dM_pyroxene[i] = compute_dM( - rho_dust_pyro_i, shock_factor_silicate, tau_sput_ref_sil); + if (fe_sil_active) { + destruction_dM_fe_silicate[i] = compute_dM( + rho_dust_fe_sil_i, shock_factor_silicate); } } } } // ========================================== -// DUST UPDATE (SPECIES-SPECIFIC: olivine + pyroxene + carbonaceous) +// DUST UPDATE (SPECIES-SPECIFIC: Mg-silicate + Fe-silicate + carbonaceous) // ========================================== // Per-channel mass exchange between dust species and their corresponding // gas-phase reactant pools. Active when dust_species_track == 1. // // carbon channel: rho_dust_carbonaceous <-> rho_metal_carbon -// olivine channel: rho_dust_olivine <-> {Mg, Fe, Si, O} as MgFeSiO4 -// pyroxene channel: rho_dust_pyroxene <-> {Mg, Si, O} as MgSiO3 +// Mg-sil channel: rho_dust_mg_silicate <-> {Mg, Si, O} as Mg2SiO4 +// Fe-sil channel: rho_dust_fe_silicate <-> {Fe, Si, O} as Fe2SiO4 // // Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] // shortfall iteration: growth is capped by the limiting reactant, destruction @@ -627,10 +713,10 @@ void grackle::impl::dust_destruction_species( void grackle::impl::dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - const double* dt_value, const double* growth_dM_olivine, - const double* growth_dM_pyroxene, const double* growth_dM_carbon, - const double* destruction_dM_olivine, - const double* destruction_dM_pyroxene, + const double* dt_value, const double* growth_dM_mg_silicate, + const double* growth_dM_fe_silicate, const double* growth_dM_carbon, + const double* destruction_dM_mg_silicate, + const double* destruction_dM_fe_silicate, const double* destruction_dM_carbon, bool dryrun) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], @@ -641,11 +727,11 @@ void grackle::impl::dust_update_species( grackle::impl::View dust_sil( my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, my_fields->grid_dimension[0], + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], @@ -669,21 +755,21 @@ void grackle::impl::dust_update_species( my_fields->metal_density_iron, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - const double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; - const double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; - const double f_ol_Si = my_chemistry->dust_olivine_f_Si; - const double f_ol_O = my_chemistry->dust_olivine_f_O; - const double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; - const double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; - const double f_py_Si = my_chemistry->dust_pyroxene_f_Si; - const double f_py_O = my_chemistry->dust_pyroxene_f_O; + const double f_mg_sil_Mg = my_chemistry->dust_mg_silicate_f_Mg; + const double f_mg_sil_Fe = my_chemistry->dust_mg_silicate_f_Fe; + const double f_mg_sil_Si = my_chemistry->dust_mg_silicate_f_Si; + const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; + const double f_fe_sil_Mg = my_chemistry->dust_fe_silicate_f_Mg; + const double f_fe_sil_Fe = my_chemistry->dust_fe_silicate_f_Fe; + const double f_fe_sil_Si = my_chemistry->dust_fe_silicate_f_Si; + const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { if (itmask[i] == MASK_FALSE) continue; double rho_gas = d(i, idx_range.j, idx_range.k); - double rho_dust_oliv_i = dust_oliv(i, idx_range.j, idx_range.k); - double rho_dust_pyro_i = dust_pyro(i, idx_range.j, idx_range.k); + double rho_dust_mg_sil_i = dust_mg_sil(i, idx_range.j, idx_range.k); + double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); double rho_metal_total = metal(i, idx_range.j, idx_range.k); double rho_C = mC(i, idx_range.j, idx_range.k); @@ -695,114 +781,129 @@ void grackle::impl::dust_update_species( if (dt <= 0.0) continue; double rho_tracked_before = rho_C + rho_O + rho_Mg + rho_Si + rho_Fe; + // Untracked gas-phase metals (Al, S, Ne, Na, ...). If host code has seeded + // per-element fields independently and they exceed rho_metal_total, + // rho_metal_other would go negative and propagate into rho_metal_new on + // the rebuild below — clamp at zero to prevent silent mass leakage. double rho_metal_other = rho_metal_total - rho_tracked_before; + if (rho_metal_other < 0.0) rho_metal_other = 0.0; // dM > 0 means net growth (gas reactants -> dust); // dM < 0 means net destruction (dust -> gas reactants). double dM_carb = (growth_dM_carbon[i] + destruction_dM_carbon[i]) * dt; - double dM_oliv = (growth_dM_olivine[i] + - destruction_dM_olivine[i]) * dt; - double dM_pyro = (growth_dM_pyroxene[i] + - destruction_dM_pyroxene[i]) * dt; + double dM_mg_sil = (growth_dM_mg_silicate[i] + + destruction_dM_mg_silicate[i]) * dt; + double dM_fe_sil = (growth_dM_fe_silicate[i] + + destruction_dM_fe_silicate[i]) * dt; // ---------- Per-channel pre-cap ---------- // Carbon channel: bounded by rho_C (growth) or rho_dust_carb (destruction). + // Leave a 10% margin on growth so the gas-C reservoir is not driven to + // exactly zero in a single subcycle — make_consistent's gas-phase C + // correction (correctCg = Cg/totalCg) would otherwise zero CI/CII/CO/... + // and destabilize the metal-chemistry implicit solve that follows. if (dM_carb > 0.0) { - dM_carb = std::min(dM_carb, rho_C); + dM_carb = std::min(dM_carb, 0.9 * rho_C); } else { dM_carb = std::max(dM_carb, -rho_dust_carb_i); } - // Olivine and pyroxene channels share Mg/Si/O but not Fe. First cap - // destruction by each species' own dust mass. - if (dM_oliv < 0.0) { - dM_oliv = std::max(dM_oliv, -rho_dust_oliv_i); + // Mg- and Fe-silicate channels share Si/O, but Mg and Fe are independent. + // First cap destruction by each species' own dust mass. + if (dM_mg_sil < 0.0) { + dM_mg_sil = std::max(dM_mg_sil, -rho_dust_mg_sil_i); } - if (dM_pyro < 0.0) { - dM_pyro = std::max(dM_pyro, -rho_dust_pyro_i); + if (dM_fe_sil < 0.0) { + dM_fe_sil = std::max(dM_fe_sil, -rho_dust_fe_sil_i); } - // Then cap positive growth jointly against the shared gas reservoirs. - // This preserves pyroxene growth when Fe alone limits olivine, while still - // preventing double-use of Mg/Si/O if both channels grow together. - double dM_oliv_grow = std::max(dM_oliv, 0.0); - double dM_pyro_grow = std::max(dM_pyro, 0.0); + // Then cap positive growth jointly against the gas reservoirs. This + // preserves Mg-silicate growth when Fe alone limits Fe-silicate, and vice + // versa, while still preventing double-use of shared Si/O. The 0.9 + // safety margin (same rationale as the carbon channel) leaves headroom + // in each gas reservoir so make_consistent's per-element corrections + // don't divide by ~zero in the next subcycle. + const double sil_grow_safety = 0.9; + double dM_mg_sil_grow = std::max(dM_mg_sil, 0.0); + double dM_fe_sil_grow = std::max(dM_fe_sil, 0.0); for (int cap_iter = 0; cap_iter < 4; cap_iter++) { double scale = 1.0; int limiter = -1; // 0=Mg, 1=Fe, 2=Si, 3=O - auto consider_element = [&](double rho_X, double c_ol, - double c_py, int element_id) { - double need = dM_oliv_grow * c_ol + dM_pyro_grow * c_py; - if (need > rho_X && need > 0.0) { - double trial = std::max(0.0, rho_X / need); + auto consider_element = [&](double rho_X, double c_mg_sil, + double c_fe_sil, int element_id) { + double need = dM_mg_sil_grow * c_mg_sil + + dM_fe_sil_grow * c_fe_sil; + double budget = sil_grow_safety * rho_X; + if (need > budget && need > 0.0) { + double trial = std::max(0.0, budget / need); if (trial < scale) { scale = trial; limiter = element_id; } } }; - consider_element(rho_Mg, f_ol_Mg, f_py_Mg, 0); - consider_element(rho_Fe, f_ol_Fe, f_py_Fe, 1); - consider_element(rho_Si, f_ol_Si, f_py_Si, 2); - consider_element(rho_O, f_ol_O, f_py_O, 3); + consider_element(rho_Mg, f_mg_sil_Mg, f_fe_sil_Mg, 0); + consider_element(rho_Fe, f_mg_sil_Fe, f_fe_sil_Fe, 1); + consider_element(rho_Si, f_mg_sil_Si, f_fe_sil_Si, 2); + consider_element(rho_O, f_mg_sil_O, f_fe_sil_O, 3); if (limiter < 0 || scale >= 1.0) break; if (limiter == 0) { - if (f_ol_Mg > 0.0) dM_oliv_grow *= scale; - if (f_py_Mg > 0.0) dM_pyro_grow *= scale; + if (f_mg_sil_Mg > 0.0) dM_mg_sil_grow *= scale; + if (f_fe_sil_Mg > 0.0) dM_fe_sil_grow *= scale; } else if (limiter == 1) { - if (f_ol_Fe > 0.0) dM_oliv_grow *= scale; - if (f_py_Fe > 0.0) dM_pyro_grow *= scale; + if (f_mg_sil_Fe > 0.0) dM_mg_sil_grow *= scale; + if (f_fe_sil_Fe > 0.0) dM_fe_sil_grow *= scale; } else if (limiter == 2) { - if (f_ol_Si > 0.0) dM_oliv_grow *= scale; - if (f_py_Si > 0.0) dM_pyro_grow *= scale; + if (f_mg_sil_Si > 0.0) dM_mg_sil_grow *= scale; + if (f_fe_sil_Si > 0.0) dM_fe_sil_grow *= scale; } else { - if (f_ol_O > 0.0) dM_oliv_grow *= scale; - if (f_py_O > 0.0) dM_pyro_grow *= scale; + if (f_mg_sil_O > 0.0) dM_mg_sil_grow *= scale; + if (f_fe_sil_O > 0.0) dM_fe_sil_grow *= scale; } } - if (dM_oliv > 0.0) { - dM_oliv = dM_oliv_grow; + if (dM_mg_sil > 0.0) { + dM_mg_sil = dM_mg_sil_grow; } - if (dM_pyro > 0.0) { - dM_pyro = dM_pyro_grow; + if (dM_fe_sil > 0.0) { + dM_fe_sil = dM_fe_sil_grow; } - if (dM_oliv < 0.0) { - dM_oliv = std::max(dM_oliv, -rho_dust_oliv_i); + if (dM_mg_sil < 0.0) { + dM_mg_sil = std::max(dM_mg_sil, -rho_dust_mg_sil_i); } else { - dM_oliv = std::max(dM_oliv, 0.0); + dM_mg_sil = std::max(dM_mg_sil, 0.0); } - if (dM_pyro < 0.0) { - dM_pyro = std::max(dM_pyro, -rho_dust_pyro_i); + if (dM_fe_sil < 0.0) { + dM_fe_sil = std::max(dM_fe_sil, -rho_dust_fe_sil_i); } else { - dM_pyro = std::max(dM_pyro, 0.0); + dM_fe_sil = std::max(dM_fe_sil, 0.0); } // ---------- Apply ---------- rho_dust_carb_i += dM_carb; rho_C -= dM_carb; - rho_dust_oliv_i += dM_oliv; - rho_dust_pyro_i += dM_pyro; - rho_Mg -= dM_oliv * f_ol_Mg + dM_pyro * f_py_Mg; - rho_Fe -= dM_oliv * f_ol_Fe + dM_pyro * f_py_Fe; - rho_Si -= dM_oliv * f_ol_Si + dM_pyro * f_py_Si; - rho_O -= dM_oliv * f_ol_O + dM_pyro * f_py_O; + rho_dust_mg_sil_i += dM_mg_sil; + rho_dust_fe_sil_i += dM_fe_sil; + rho_Mg -= dM_mg_sil * f_mg_sil_Mg + dM_fe_sil * f_fe_sil_Mg; + rho_Fe -= dM_mg_sil * f_mg_sil_Fe + dM_fe_sil * f_fe_sil_Fe; + rho_Si -= dM_mg_sil * f_mg_sil_Si + dM_fe_sil * f_fe_sil_Si; + rho_O -= dM_mg_sil * f_mg_sil_O + dM_fe_sil * f_fe_sil_O; // Floors / NaN guard rho_dust_carb_i = std::max(0.0, rho_dust_carb_i); - rho_dust_oliv_i = std::max(0.0, rho_dust_oliv_i); - rho_dust_pyro_i = std::max(0.0, rho_dust_pyro_i); + rho_dust_mg_sil_i = std::max(0.0, rho_dust_mg_sil_i); + rho_dust_fe_sil_i = std::max(0.0, rho_dust_fe_sil_i); rho_C = std::max(0.0, rho_C); rho_O = std::max(0.0, rho_O); rho_Mg = std::max(0.0, rho_Mg); rho_Si = std::max(0.0, rho_Si); rho_Fe = std::max(0.0, rho_Fe); - // Bulk dust = olivine + pyroxene + carbonaceous. The compatibility - // silicate field is olivine + pyroxene. - double rho_dust_sil_i = rho_dust_oliv_i + rho_dust_pyro_i; + // Bulk dust = Mg-silicate + Fe-silicate + carbonaceous. The compatibility + // silicate field is the sum of both silicate endmembers. + double rho_dust_sil_i = rho_dust_mg_sil_i + rho_dust_fe_sil_i; double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; // metal_density_other (= total - C - O - Mg - Si - Fe) is unchanged on @@ -821,16 +922,16 @@ void grackle::impl::dust_update_species( std::isnan(rho_gas)) { std::cout << "dust_update_species: NaN at cell " << i << " dM_carb=" << dM_carb - << " dM_oliv=" << dM_oliv - << " dM_pyro=" << dM_pyro << std::endl; + << " dM_mg_sil=" << dM_mg_sil + << " dM_fe_sil=" << dM_fe_sil << std::endl; continue; } if (!dryrun) { dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_new; dust_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_sil_i; - dust_oliv(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_oliv_i; - dust_pyro(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_pyro_i; + dust_mg_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_mg_sil_i; + dust_fe_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_fe_sil_i; dust_carb(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_carb_i; metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_new; mC(i, idx_range.j, idx_range.k) = (gr_float)rho_C; diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 6f1649b7f..0092c1d7e 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -20,24 +20,22 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, ); // Species-specific accretion onto three pre-existing dust populations -// (olivine + pyroxene + carbonaceous). Active when dust_species_track == 1. -// - carbonaceous: rate-limited by gas-phase carbon -// - olivine: rate-limited by min over {Mg, Fe, Si, O} of (rho_X / f_X) -// - pyroxene: rate-limited by min over {Mg, Si, O} of (rho_X / f_X) -// following the Choban+2022 MNRAS 514, 4506 §2.2 key-reactant approach -// Per-species tau_accr uses the Hirashita 2011 section 2.6 normalization: -// n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, and solar key-species -// abundance, with S rescaled by dust_growth_sticking_coeff and a rescaled by -// dust_grainsize / 0.1. This is independent of the bulk SIMBA -// dust_growth_densref. No bulk dM, no partitioning — Phase D wires the species -// outputs into dust_update(). +// (Mg-silicate + Fe-silicate + carbonaceous). Active when +// dust_species_track == 1. +// - carbonaceous: bottlenecked by gas-phase carbon +// - total silicate: bottlenecked by O, Si, and Mg+Fe, then split between +// Mg2SiO4 and Fe2SiO4 endmembers according to local Mg/Fe abundance +// Uses the COLIBRE dense-gas accretion normalization with clumped +// n_H' = C n_H, T_ref = 10 K, n_H_ref = 10 cm^-3, S_ref = 0.3, a_ref = 0.1 +// micron, and tau_ref values stored in Myr. The returned rates are equivalent +// rates whose product with dt gives the exponential e-folding mass update. void dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* growth_dM_olivine, // output: olivine accretion rate - double* growth_dM_pyroxene, // output: pyroxene accretion rate - double* growth_dM_carbon // output: carbonaceous accretion rate + double* growth_dM_mg_silicate, // output: Mg-silicate accretion rate + double* growth_dM_fe_silicate, // output: Fe-silicate accretion rate + double* growth_dM_carbon // output: carbonaceous accretion rate ); // Calculates dust destruction rates from SNe shocks and thermal sputtering. @@ -51,23 +49,23 @@ void dust_destruction( // Species-specific destruction (SN shocks + thermal sputtering) onto the three // dust populations. Active when dust_species_track == 1. -// - shock yield: graphite is baseline (factor 1.0); olivine and pyroxene -// both follow the +// - shock yield: graphite is baseline (factor 1.0); both silicate endmembers +// follow the // Slavin+2015 standard SNR gas-cleared mass ratio 990/600 = 1.65 // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740] -// - thermal sputtering: species-specific tau_ref -// [REF (silicate): Tsai & Mathews 1995 ApJ 448, 84; -// REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435] -// with Draine & Salpeter 1979 / Tielens+1994 scaling form +// - thermal sputtering: common COLIBRE/Tsai-Mathews tau_ref for all species +// with (a/0.1) (n_H/cm^-3)^-1 [1 + (T/2e6 K)^-2.5] scaling +// Destruction uses exponential decay; returned rates are equivalent rates whose +// product with dt gives the e-folding mass loss. // No bulk dM, no partitioning — Phase D wires the species outputs into // dust_update(). void dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* destruction_dM_olivine, // output: olivine destruction rate - double* destruction_dM_pyroxene, // output: pyroxene destruction rate - double* destruction_dM_carbon // output: carbonaceous destruction rate + double* destruction_dM_mg_silicate, // output: Mg-silicate destruction rate + double* destruction_dM_fe_silicate, // output: Fe-silicate destruction rate + double* destruction_dM_carbon // output: carbonaceous destruction rate ); // Update the density fields using calculated mass changes. @@ -83,8 +81,8 @@ void dust_update( // (dust_species_track==1). // Per-channel mass exchange: // - carbon channel: rho_dust_carbonaceous <-> metal_density_carbon -// - olivine channel: rho_dust_olivine <-> {Mg, Fe, Si, O} as MgFeSiO4 -// - pyroxene channel: rho_dust_pyroxene <-> {Mg, Si, O} as MgSiO3 +// - Mg-sil channel: rho_dust_mg_silicate <-> {Mg, Si, O} as Mg2SiO4 +// - Fe-sil channel: rho_dust_fe_silicate <-> {Fe, Si, O} as Fe2SiO4 // Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] // shortfall mask. No SN injection here — Phase D drops the in-Grackle // dust_creation pathway from the species branch; host code seeds dust species @@ -93,12 +91,12 @@ void dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - const double* growth_dM_olivine, // input: olivine accretion rate - const double* growth_dM_pyroxene, // input: pyroxene accretion rate - const double* growth_dM_carbon, // input: carbonaceous accretion rate - const double* destruction_dM_olivine, // input: olivine destruction rate (<=0) - const double* destruction_dM_pyroxene, // input: pyroxene destruction rate (<=0) - const double* destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) + const double* growth_dM_mg_silicate, // input: Mg-silicate accretion rate + const double* growth_dM_fe_silicate, // input: Fe-silicate accretion rate + const double* growth_dM_carbon, // input: carbonaceous accretion rate + const double* destruction_dM_mg_silicate, // input: Mg-silicate destruction rate (<=0) + const double* destruction_dM_fe_silicate, // input: Fe-silicate destruction rate (<=0) + const double* destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) bool dryrun); } // namespace grackle::impl diff --git a/src/clib/field_data_misc_fdatamembers.def b/src/clib/field_data_misc_fdatamembers.def index a5104efe5..057097d0a 100644 --- a/src/clib/field_data_misc_fdatamembers.def +++ b/src/clib/field_data_misc_fdatamembers.def @@ -40,10 +40,10 @@ ENTRY(metal_density_iron) // use_dust_density_field = 1 ENTRY(dust_density) - // dust_species_track = 1 (olivine + pyroxene + carbonaceous dust) + // dust_species_track = 1 (Mg-silicate + Fe-silicate + carbonaceous dust) ENTRY(dust_density_silicate) -ENTRY(dust_density_olivine) -ENTRY(dust_density_pyroxene) +ENTRY(dust_density_mg_silicate) +ENTRY(dust_density_fe_silicate) ENTRY(dust_density_carbonaceous) // use_volumetric_heating_rate = 1 diff --git a/src/clib/grackle_chemistry_data_fields.def b/src/clib/grackle_chemistry_data_fields.def index 22917a75f..245bb9d92 100644 --- a/src/clib/grackle_chemistry_data_fields.def +++ b/src/clib/grackle_chemistry_data_fields.def @@ -362,43 +362,42 @@ ENTRY(sne_metal_yield, DOUBLE, 3.0) /* Species-resolved dust tracking. Requires dust_model=1. 0) off (default — bulk dust_density) - 1) on — evolves dust_density_olivine, dust_density_pyroxene, + 1) on — evolves dust_density_mg_silicate, dust_density_fe_silicate, dust_density_carbonaceous and 5-element gas tracking (C, O, Mg, Si, Fe). dust_density_silicate is maintained as - olivine + pyroxene for compatibility. - REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937; - McKinnon+2018 MNRAS 478, 2851 */ + Mg-silicate + Fe-silicate for compatibility. + REF: Trayford+2026 MNRAS 545, staf2040 */ ENTRY(dust_species_track, INT, 0) -/* Species-specific growth (accretion) reference timescales [Gyr]. - Normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, and - solar key-species abundance for the carbon / limiting silicate reactant - pools. dust_growth_species rescales these values by - (dust_grainsize / 0.1) * (0.3 / dust_growth_sticking_coeff). - REF: Hirashita 2011 MNRAS 416, 1340 section 2.6; Asano+2013 EP&S 65, 213 */ -ENTRY(dust_growth_tauref_silicate, DOUBLE, 6.3e-2) -ENTRY(dust_growth_tauref_carbon, DOUBLE, 5.59e-2) - -/* Species-specific thermal sputtering reference timescales [yr]. - Same scaling as Draine & Salpeter 1979 ApJ 231, 77; Tielens+1994 ApJ 431, 321: - tau_sp = tau_ref * (a/0.1) * (1e-27/rho_gas) * ((2e6/T)^2.5 + 1). - REF (silicate): Tsai & Mathews 1995 ApJ 448, 84 - REF (carbon, ~2x silicate): Nozawa+2006 ApJ 648, 435 */ -ENTRY(dust_sputter_tauref_silicate, DOUBLE, 1.7e8) -ENTRY(dust_sputter_tauref_carbon, DOUBLE, 3.4e8) - -/* Initial/fallback mass fraction of silicate dust in olivine. The live - olivine/pyroxene ratio evolves once the split fields are active. */ -ENTRY(dust_silicate_olivine_fraction, DOUBLE, 0.5) - -/* Olivine stoichiometric mass fractions for MgFeSiO4. Sum = 1.000. */ -ENTRY(dust_olivine_f_Mg, DOUBLE, 0.141) -ENTRY(dust_olivine_f_Fe, DOUBLE, 0.324) -ENTRY(dust_olivine_f_Si, DOUBLE, 0.163) -ENTRY(dust_olivine_f_O, DOUBLE, 0.372) - -/* Pyroxene stoichiometric mass fractions for MgSiO3. Sum = 1.000. */ -ENTRY(dust_pyroxene_f_Mg, DOUBLE, 0.242) -ENTRY(dust_pyroxene_f_Fe, DOUBLE, 0.0) -ENTRY(dust_pyroxene_f_Si, DOUBLE, 0.280) -ENTRY(dust_pyroxene_f_O, DOUBLE, 0.478) +/* COLIBRE-style growth reference timescales [Myr]. + Normalized at n_H' = 10 cm^-3, T = 10 K, S = 0.3, a = 0.1 micron, + and solar bottleneck abundance. REF: Trayford+2026 Table 2, eq. 4. */ +ENTRY(dust_growth_tauref_silicate, DOUBLE, 99.3) +ENTRY(dust_growth_tauref_carbon, DOUBLE, 180.0) + +/* COLIBRE-style unresolved-density boost for grain growth. + C rises from 1 to 100 between n_H = 0.1 and 100 cm^-3, clipped outside. */ +ENTRY(dust_growth_clumping_factor_max, DOUBLE, 100.0) +ENTRY(dust_growth_clumping_nH_min, DOUBLE, 1.0e-1) +ENTRY(dust_growth_clumping_nH_max, DOUBLE, 1.0e2) + +/* COLIBRE / Tsai-Mathews common thermal sputtering reference time [Myr]. + tau_sp = tau_ref * (a/0.1) * (n_H/1 cm^-3)^-1 + * [1 + (T/2e6 K)^-2.5]. */ +ENTRY(dust_sputter_tauref, DOUBLE, 0.85) + +/* Initial/fallback mass fraction of silicate dust in Mg-silicate. COLIBRE + seeds equal numbers of Mg2SiO4 and Fe2SiO4 molecules, not equal masses. */ +ENTRY(dust_silicate_mg_fraction, DOUBLE, 0.408428) + +/* Mg-rich silicate endmember stoichiometric mass fractions for Mg2SiO4. */ +ENTRY(dust_mg_silicate_f_Mg, DOUBLE, 0.345521) +ENTRY(dust_mg_silicate_f_Fe, DOUBLE, 0.0) +ENTRY(dust_mg_silicate_f_Si, DOUBLE, 0.199630) +ENTRY(dust_mg_silicate_f_O, DOUBLE, 0.454849) + +/* Fe-rich silicate endmember stoichiometric mass fractions for Fe2SiO4. */ +ENTRY(dust_fe_silicate_f_Mg, DOUBLE, 0.0) +ENTRY(dust_fe_silicate_f_Fe, DOUBLE, 0.548111) +ENTRY(dust_fe_silicate_f_Si, DOUBLE, 0.137825) +ENTRY(dust_fe_silicate_f_O, DOUBLE, 0.314064) diff --git a/src/clib/initialize_chemistry_data.cpp b/src/clib/initialize_chemistry_data.cpp index 4ddcae002..e72fda622 100644 --- a/src/clib/initialize_chemistry_data.cpp +++ b/src/clib/initialize_chemistry_data.cpp @@ -232,6 +232,34 @@ static int local_initialize_chemistry_data_( return GR_FAIL; } + // dust_species_track=1 is the new species path (Mg-silicate + Fe-silicate + // + carbonaceous) and is mutually exclusive with the legacy grain_growth / + // dust_sublimation correction loop in make_consistent — that block scales + // legacy dust fields (MgSiO3, AC, Mg2SiO4, Fe3O4, SiO2D, MgO, FeS, Al2O3, + // SiM, FeM) against per-element ratios that the Phase F hijack has just + // overwritten with values derived from the new species fields. Running + // both paths simultaneously silently corrupts both reservoirs. + if (my_chemistry->dust_species_track == 1 && + my_chemistry->grain_growth == 1) { + fprintf(stderr, + "ERROR: dust_species_track = 1 is mutually exclusive with " + "grain_growth = 1 (set grain_growth = 0).\n"); + return GR_FAIL; + } + if (my_chemistry->dust_species_track == 1 && + my_chemistry->dust_sublimation == 1) { + fprintf(stderr, + "ERROR: dust_species_track = 1 is mutually exclusive with " + "dust_sublimation = 1 (set dust_sublimation = 0).\n"); + return GR_FAIL; + } + if (my_chemistry->dust_species_track == 1 && + my_chemistry->dust_model != 1) { + fprintf(stderr, + "ERROR: dust_species_track = 1 requires dust_model = 1.\n"); + return GR_FAIL; + } + // Default photo-electric heating to off if unset. if (my_chemistry->photoelectric_heating < 0) { my_chemistry->photoelectric_heating = 0; diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 8c29bc15f..73626bcd0 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -242,7 +242,7 @@ void make_consistent( // from our tracked metal_density_X + dust species fields, overriding the // SN-yield-derived computation below. Views are constructed once here. grackle::impl::View mC_view, mO_view, mMg_view, - mSi_view, mFe_view, dust_sil_view, dust_oliv_view, dust_pyro_view, + mSi_view, mFe_view, dust_sil_view, dust_mg_sil_view, dust_fe_sil_view, dust_carb_view; if (my_chemistry->dust_species_track == 1) { mC_view = grackle::impl::View( @@ -269,12 +269,12 @@ void make_consistent( const_cast(my_fields->dust_density_silicate), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - dust_oliv_view = grackle::impl::View( - const_cast(my_fields->dust_density_olivine), + dust_mg_sil_view = grackle::impl::View( + const_cast(my_fields->dust_density_mg_silicate), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - dust_pyro_view = grackle::impl::View( - const_cast(my_fields->dust_density_pyroxene), + dust_fe_sil_view = grackle::impl::View( + const_cast(my_fields->dust_density_fe_silicate), my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); dust_carb_view = grackle::impl::View( @@ -431,27 +431,27 @@ void make_consistent( // Phase F hijack: replace SN-yield-derived per-element arrays with // values pulled directly from our tracked fields. Carbonaceous dust - // is pure C; olivine and pyroxene contribute Mg/Fe/Si/O at their own + // is pure C; Mg- and Fe-silicates contribute Mg/Fe/Si/O at their own // stoichiometric mass fractions. Al and S are not part of this // architecture, so their per-element arrays are zeroed. if (my_chemistry->dust_species_track == 1) { - const double f_ol = std::clamp( - my_chemistry->dust_silicate_olivine_fraction, 0.0, 1.0); - const double f_ol_Mg = my_chemistry->dust_olivine_f_Mg; - const double f_ol_Fe = my_chemistry->dust_olivine_f_Fe; - const double f_ol_Si = my_chemistry->dust_olivine_f_Si; - const double f_ol_O = my_chemistry->dust_olivine_f_O; - const double f_py_Mg = my_chemistry->dust_pyroxene_f_Mg; - const double f_py_Fe = my_chemistry->dust_pyroxene_f_Fe; - const double f_py_Si = my_chemistry->dust_pyroxene_f_Si; - const double f_py_O = my_chemistry->dust_pyroxene_f_O; + const double f_mg_sil = std::clamp( + my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); + const double f_mg_sil_Mg = my_chemistry->dust_mg_silicate_f_Mg; + const double f_mg_sil_Fe = my_chemistry->dust_mg_silicate_f_Fe; + const double f_mg_sil_Si = my_chemistry->dust_mg_silicate_f_Si; + const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; + const double f_fe_sil_Mg = my_chemistry->dust_fe_silicate_f_Mg; + const double f_fe_sil_Fe = my_chemistry->dust_fe_silicate_f_Fe; + const double f_fe_sil_Si = my_chemistry->dust_fe_silicate_f_Si; + const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - double oliv = dust_oliv_view(i, j, k); - double pyro = dust_pyro_view(i, j, k); + double mg_sil = dust_mg_sil_view(i, j, k); + double fe_sil = dust_fe_sil_view(i, j, k); const double sil_compat = dust_sil_view(i, j, k); - if (oliv + pyro <= 0.0 && sil_compat > 0.0) { - oliv = f_ol * sil_compat; - pyro = (1.0 - f_ol) * sil_compat; + if (mg_sil + fe_sil <= 0.0 && sil_compat > 0.0) { + mg_sil = f_mg_sil * sil_compat; + fe_sil = (1.0 - f_mg_sil) * sil_compat; } const double carb = dust_carb_view(i, j, k); Cg[i] = mC_view(i, j, k); @@ -460,10 +460,10 @@ void make_consistent( Sig[i] = mSi_view(i, j, k); Feg[i] = mFe_view(i, j, k); Cd[i] = carb; - Od[i] = oliv * f_ol_O + pyro * f_py_O; - Mgd[i] = oliv * f_ol_Mg + pyro * f_py_Mg; - Sid[i] = oliv * f_ol_Si + pyro * f_py_Si; - Fed[i] = oliv * f_ol_Fe + pyro * f_py_Fe; + Od[i] = mg_sil * f_mg_sil_O + fe_sil * f_fe_sil_O; + Mgd[i] = mg_sil * f_mg_sil_Mg + fe_sil * f_fe_sil_Mg; + Sid[i] = mg_sil * f_mg_sil_Si + fe_sil * f_fe_sil_Si; + Fed[i] = mg_sil * f_mg_sil_Fe + fe_sil * f_fe_sil_Fe; Ct[i] = Cg[i] + Cd[i]; Ot[i] = Og[i] + Od[i]; Mgt[i] = Mgg[i] + Mgd[i]; @@ -1075,15 +1075,15 @@ void make_consistent( } } - // Phase E invariant: bulk dust_density = olivine + pyroxene + carbonaceous, - // with dust_density_silicate maintained as olivine + pyroxene. + // Phase E invariant: bulk dust_density = Mg-silicate + Fe-silicate + // + carbonaceous, with dust_density_silicate maintained as the silicate sum. // dust_update_species() maintains this per-cell, but external mutations // (host injection, inject_pathway writes) can break it before make_consistent. // Re-derive here so downstream consumers (calc_tdust_3d.cpp, cooling tables) // see a consistent bulk field. if (my_chemistry->dust_species_track == 1) { - const double f_ol = std::clamp( - my_chemistry->dust_silicate_olivine_fraction, 0.0, 1.0); + const double f_mg_sil = std::clamp( + my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); grackle::impl::View dust_bulk( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -1091,12 +1091,12 @@ void make_consistent( my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( @@ -1106,16 +1106,16 @@ void make_consistent( for (k = my_fields->grid_start[2]; k <= my_fields->grid_end[2]; k++) { for (j = my_fields->grid_start[1]; j <= my_fields->grid_end[1]; j++) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { - double oliv = dust_oliv(i, j, k); - double pyro = dust_pyro(i, j, k); + double mg_sil = dust_mg_sil(i, j, k); + double fe_sil = dust_fe_sil(i, j, k); const double sil_compat = dust_sil(i, j, k); - if (oliv + pyro <= 0.0 && sil_compat > 0.0) { - oliv = f_ol * sil_compat; - pyro = (1.0 - f_ol) * sil_compat; - dust_oliv(i, j, k) = (gr_float)oliv; - dust_pyro(i, j, k) = (gr_float)pyro; + if (mg_sil + fe_sil <= 0.0 && sil_compat > 0.0) { + mg_sil = f_mg_sil * sil_compat; + fe_sil = (1.0 - f_mg_sil) * sil_compat; + dust_mg_sil(i, j, k) = (gr_float)mg_sil; + dust_fe_sil(i, j, k) = (gr_float)fe_sil; } - const double sil = oliv + pyro; + const double sil = mg_sil + fe_sil; dust_sil(i, j, k) = (gr_float)sil; dust_bulk(i, j, k) = (gr_float)(sil + dust_carb(i, j, k)); } diff --git a/src/clib/scale_fields.cpp b/src/clib/scale_fields.cpp index 9e807be40..fc7014b48 100644 --- a/src/clib/scale_fields.cpp +++ b/src/clib/scale_fields.cpp @@ -127,11 +127,11 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, grackle::impl::View dust_sil( my_fields->dust_density_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, my_fields->grid_dimension[0], + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], @@ -361,8 +361,8 @@ void scale_fields(int imetal, gr_float factor, chemistry_data* my_chemistry, if (my_chemistry->dust_species_track == 1) { for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { dust_sil(i, j, k) = dust_sil(i, j, k) * factor; - dust_oliv(i, j, k) = dust_oliv(i, j, k) * factor; - dust_pyro(i, j, k) = dust_pyro(i, j, k) * factor; + dust_mg_sil(i, j, k) = dust_mg_sil(i, j, k) * factor; + dust_fe_sil(i, j, k) = dust_fe_sil(i, j, k) * factor; dust_carb(i, j, k) = dust_carb(i, j, k) * factor; } } diff --git a/src/clib/scale_fields.hpp b/src/clib/scale_fields.hpp index 176071cae..bfc492898 100644 --- a/src/clib/scale_fields.hpp +++ b/src/clib/scale_fields.hpp @@ -125,26 +125,26 @@ inline void scale_fields_table(grackle_field_data* my_fields, double factor) { } } } - if (my_fields->dust_density_olivine != nullptr) { - grackle::impl::View dust_oliv( - my_fields->dust_density_olivine, my_fields->grid_dimension[0], + if (my_fields->dust_density_mg_silicate != nullptr) { + grackle::impl::View dust_mg_sil( + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); for (int k = grid_start[2]; k <= grid_end[2]; k++) { for (int j = grid_start[1]; j <= grid_end[1]; j++) { for (int i = grid_start[0]; i <= grid_end[0]; i++) { - dust_oliv(i, j, k) = dust_oliv(i, j, k) * factor; + dust_mg_sil(i, j, k) = dust_mg_sil(i, j, k) * factor; } } } } - if (my_fields->dust_density_pyroxene != nullptr) { - grackle::impl::View dust_pyro( - my_fields->dust_density_pyroxene, my_fields->grid_dimension[0], + if (my_fields->dust_density_fe_silicate != nullptr) { + grackle::impl::View dust_fe_sil( + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); for (int k = grid_start[2]; k <= grid_end[2]; k++) { for (int j = grid_start[1]; j <= grid_end[1]; j++) { for (int i = grid_start[0]; i <= grid_end[0]; i++) { - dust_pyro(i, j, k) = dust_pyro(i, j, k) * factor; + dust_fe_sil(i, j, k) = dust_fe_sil(i, j, k) * factor; } } } diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 54dfe71a4..308d7e11d 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -733,11 +733,11 @@ int solve_rate_cool( std::vector destruction_dM(my_fields->grid_dimension[0]); // Phase B/C: species-specific growth & destruction outputs (used when // dust_species_track == 1; Phase D wires them into dust_update()) - std::vector growth_dM_olivine(my_fields->grid_dimension[0]); - std::vector growth_dM_pyroxene(my_fields->grid_dimension[0]); + std::vector growth_dM_mg_silicate(my_fields->grid_dimension[0]); + std::vector growth_dM_fe_silicate(my_fields->grid_dimension[0]); std::vector growth_dM_carbon(my_fields->grid_dimension[0]); - std::vector destruction_dM_olivine(my_fields->grid_dimension[0]); - std::vector destruction_dM_pyroxene(my_fields->grid_dimension[0]); + std::vector destruction_dM_mg_silicate(my_fields->grid_dimension[0]); + std::vector destruction_dM_fe_silicate(my_fields->grid_dimension[0]); std::vector destruction_dM_carbon(my_fields->grid_dimension[0]); // iteration masks @@ -932,23 +932,23 @@ int solve_rate_cool( // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ if (my_chemistry->dust_species_track == 1) { - // Calculate and apply the olivine + pyroxene + carbonaceous rates. + // Calculate and apply the Mg-silicate + Fe-silicate + carbonaceous rates. grackle::impl::dust_growth_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), - growth_dM_olivine.data(), growth_dM_pyroxene.data(), + growth_dM_mg_silicate.data(), growth_dM_fe_silicate.data(), growth_dM_carbon.data()); grackle::impl::dust_destruction_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), - destruction_dM_olivine.data(), destruction_dM_pyroxene.data(), + destruction_dM_mg_silicate.data(), destruction_dM_fe_silicate.data(), destruction_dM_carbon.data()); grackle::impl::dust_update_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), - growth_dM_olivine.data(), growth_dM_pyroxene.data(), + growth_dM_mg_silicate.data(), growth_dM_fe_silicate.data(), growth_dM_carbon.data(), - destruction_dM_olivine.data(), destruction_dM_pyroxene.data(), + destruction_dM_mg_silicate.data(), destruction_dM_fe_silicate.data(), destruction_dM_carbon.data(), false); } else { diff --git a/src/include/grackle_chemistry_data.h b/src/include/grackle_chemistry_data.h index a80003e1e..13c4b6d0a 100644 --- a/src/include/grackle_chemistry_data.h +++ b/src/include/grackle_chemistry_data.h @@ -342,43 +342,48 @@ typedef struct /* Species-resolved dust tracking. Requires dust_model=1. 0) off — bulk dust_density (default) - 1) on — evolves dust_density_olivine, dust_density_pyroxene, + 1) on — evolves dust_density_mg_silicate, dust_density_fe_silicate, dust_density_carbonaceous and 5-element gas tracking (C, O, Mg, Si, Fe). dust_density_silicate is maintained as - olivine + pyroxene for compatibility. - REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 */ + Mg-silicate + Fe-silicate for compatibility. + REF: Trayford+2026 MNRAS 545, staf2040 */ int dust_species_track; - /* Species-specific growth (accretion) reference timescales [Gyr]. - Normalized at n_H = 1e3 cm^-3, T = 50 K, S = 0.3, a = 0.1 micron, - and solar key-species abundance. Rescaled by dust_grainsize and - dust_growth_sticking_coeff. - REF: Hirashita 2011 MNRAS 416, 1340 section 2.6; Asano+2013 EP&S 65, 213 */ + /* COLIBRE-style species growth reference timescales [Myr]. + Normalized at n_H' = 10 cm^-3, T = 10 K, S = 0.3, a = 0.1 micron, + and solar bottleneck abundance. Rescaled by dust_grainsize and + dust_growth_sticking_coeff. REF: Trayford+2026 MNRAS 545, staf2040. */ double dust_growth_tauref_silicate; double dust_growth_tauref_carbon; - /* Species-specific thermal sputtering reference timescales [yr]. - REF (silicate): Tsai & Mathews 1995 ApJ 448, 84 - REF (carbon): Nozawa+2006 ApJ 648, 435 */ - double dust_sputter_tauref_silicate; - double dust_sputter_tauref_carbon; - - /* Initial/fallback mass fraction of silicate dust in olivine. The live - olivine/pyroxene ratio evolves once the split fields are active. */ - double dust_silicate_olivine_fraction; - - /* Olivine stoichiometric mass fractions for MgFeSiO4. Sum = 1.000. */ - double dust_olivine_f_Mg; - double dust_olivine_f_Fe; - double dust_olivine_f_Si; - double dust_olivine_f_O; - - /* Pyroxene stoichiometric mass fractions for MgSiO3. Sum = 1.000. - Fe is zero so Fe depletion does not halt pyroxene growth. */ - double dust_pyroxene_f_Mg; - double dust_pyroxene_f_Fe; - double dust_pyroxene_f_Si; - double dust_pyroxene_f_O; + /* COLIBRE-style unresolved-density boost for accretion: C rises from 1 to + dust_growth_clumping_factor_max between dust_growth_clumping_nH_min and + dust_growth_clumping_nH_max, using log-linear interpolation in n_H. */ + double dust_growth_clumping_factor_max; + double dust_growth_clumping_nH_min; + double dust_growth_clumping_nH_max; + + /* COLIBRE / Tsai-Mathews thermal sputtering reference time [Myr]. + tau_sp = tau_ref * (a/0.1) * (n_H/1 cm^-3)^-1 + * [1 + (T/2e6 K)^-2.5]. */ + double dust_sputter_tauref; + + /* Initial/fallback mass fraction of silicate dust in Mg-rich silicate. + COLIBRE seeds equal numbers of Mg2SiO4 and Fe2SiO4 molecules, which + corresponds to this mass fraction rather than an equal-mass split. */ + double dust_silicate_mg_fraction; + + /* Mg-rich silicate endmember stoichiometric mass fractions for Mg2SiO4. */ + double dust_mg_silicate_f_Mg; + double dust_mg_silicate_f_Fe; + double dust_mg_silicate_f_Si; + double dust_mg_silicate_f_O; + + /* Fe-rich silicate endmember stoichiometric mass fractions for Fe2SiO4. */ + double dust_fe_silicate_f_Mg; + double dust_fe_silicate_f_Fe; + double dust_fe_silicate_f_Si; + double dust_fe_silicate_f_O; } chemistry_data; diff --git a/src/include/grackle_types.h b/src/include/grackle_types.h index c1ea45d35..fc95a488f 100644 --- a/src/include/grackle_types.h +++ b/src/include/grackle_types.h @@ -79,7 +79,7 @@ typedef struct // dust_species_track = 1 // 5-element gas tracking adds Mg, Si, Fe alongside C, O above. - // Subsets of metal_density. REF: Choban+2022 MNRAS 514, 4506 + // Subsets of metal_density. gr_float *metal_density_magnesium; gr_float *metal_density_silicon; gr_float *metal_density_iron; @@ -88,13 +88,13 @@ typedef struct gr_float *dust_density; // dust_species_track = 1 - // Three-species dust: bulk dust_density = olivine + pyroxene + + // Three-species dust: bulk dust_density = Mg-silicate + Fe-silicate + // carbonaceous. dust_density_silicate is kept as the derived - // olivine + pyroxene sum for compatibility. - // REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 + // Mg-silicate + Fe-silicate sum for compatibility. + // REF: Trayford+2026 MNRAS 545, staf2040. gr_float *dust_density_silicate; - gr_float *dust_density_olivine; - gr_float *dust_density_pyroxene; + gr_float *dust_density_mg_silicate; + gr_float *dust_density_fe_silicate; gr_float *dust_density_carbonaceous; // primordial_chemistry = 1 diff --git a/src/python/gracklepy/fluid_container.py b/src/python/gracklepy/fluid_container.py index bfd34b7c7..56893215d 100644 --- a/src/python/gracklepy/fluid_container.py +++ b/src/python/gracklepy/fluid_container.py @@ -267,10 +267,10 @@ def _required_density_fields(my_chemistry): if my_chemistry.dust_chemistry == 1: my_fields.append("dust_density") if my_chemistry.dust_species_track == 1: - # Species-resolved dust (olivine + pyroxene + carbonaceous) with - # 5-element gas tracking. dust_density_silicate is a compatibility - # sum of olivine + pyroxene. - # REF: Choban+2022 MNRAS 514, 4506; Hirashita 2015 MNRAS 447, 2937 + # Species-resolved dust (Mg-silicate + Fe-silicate + carbonaceous) + # with 5-element gas tracking. dust_density_silicate is a compatibility + # sum of Mg-silicate + Fe-silicate. + # REF: Trayford+2026 MNRAS 545, staf2040. # Bulk metal_density and dust_density are required invariants for this # path, even when cooling itself is disabled. if "metal_density" not in my_fields: @@ -284,8 +284,8 @@ def _required_density_fields(my_chemistry): "metal_density_silicon", "metal_density_iron", "dust_density_silicate", - "dust_density_olivine", - "dust_density_pyroxene", + "dust_density_mg_silicate", + "dust_density_fe_silicate", "dust_density_carbonaceous", ]) if my_chemistry.metal_chemistry > 0: diff --git a/src/python/gracklepy/grackle_defs.pxd b/src/python/gracklepy/grackle_defs.pxd index 225cd60a2..5fa65edde 100644 --- a/src/python/gracklepy/grackle_defs.pxd +++ b/src/python/gracklepy/grackle_defs.pxd @@ -116,8 +116,8 @@ cdef extern from "grackle.h": gr_float *metal_density_iron; gr_float *dust_density; gr_float *dust_density_silicate; - gr_float *dust_density_olivine; - gr_float *dust_density_pyroxene; + gr_float *dust_density_mg_silicate; + gr_float *dust_density_fe_silicate; gr_float *dust_density_carbonaceous; gr_float *e_density; gr_float *HI_density; diff --git a/src/python/gracklepy/grackle_wrapper.pyx b/src/python/gracklepy/grackle_wrapper.pyx index 0457d1cb9..64e858b94 100644 --- a/src/python/gracklepy/grackle_wrapper.pyx +++ b/src/python/gracklepy/grackle_wrapper.pyx @@ -662,8 +662,8 @@ cdef c_field_data setup_field_data(object fc, int[::1] buf, my_fields.metal_density_iron = get_field(fc, "metal_density_iron") my_fields.dust_density = get_field(fc, "dust_density") my_fields.dust_density_silicate = get_field(fc, "dust_density_silicate") - my_fields.dust_density_olivine = get_field(fc, "dust_density_olivine") - my_fields.dust_density_pyroxene = get_field(fc, "dust_density_pyroxene") + my_fields.dust_density_mg_silicate = get_field(fc, "dust_density_mg_silicate") + my_fields.dust_density_fe_silicate = get_field(fc, "dust_density_fe_silicate") my_fields.dust_density_carbonaceous = get_field(fc, "dust_density_carbonaceous") my_fields.e_density = get_field(fc, "e_density") diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 4d37494ba..24652ef4b 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -31,13 +31,15 @@ "Fe": "metal_density_iron", } -# Default olivine / pyroxene / carbonaceous mass split for IC seeding. +# Default Mg-silicate / Fe-silicate / carbonaceous mass split for IC seeding. # REF: Draine 2003 ARA&A 41, 241; Zubko, Dwek & Arendt 2004 ApJS 152, 211 — # canonical MW diffuse-ISM split is ~0.6-0.7 silicate, ~0.3-0.4 carbonaceous. +# The Mg/Fe split follows COLIBRE's equal-number Mg2SiO4/Fe2SiO4 seed, which +# is not an equal-mass split. _DUST_SPECIES_FRACTIONS = { "silicate": 0.65, - "olivine": 0.50, - "pyroxene": 0.50, + "mg_silicate": 0.408428, + "fe_silicate": 0.591572, "carbonaceous": 0.35, } @@ -63,9 +65,8 @@ def seed_dust_species_metal_elements(fc): Fill metal_density_carbon/oxygen/magnesium/silicon/iron from fc['metal_density'] using solar mass fractions. Required when dust_species_track==1: dust_update_species() reads these as the - gas-phase reservoirs for olivine and pyroxene accretion - (Mg/Fe/Si/O via Choban+2022 key-reactant scheme) and feeds shock - destruction back into them. + gas-phase reservoirs for COLIBRE-style carbonaceous and silicate + accretion and feeds destruction back into them. """ fractions = solar_metal_mass_fractions(_DUST_SPECIES_ELEMENT_FIELDS.keys()) for el, field in _DUST_SPECIES_ELEMENT_FIELDS.items(): @@ -74,20 +75,21 @@ def seed_dust_species_metal_elements(fc): def seed_dust_species_dust(fc): """ - Split fc['dust_density'] into olivine / pyroxene / carbonaceous - reservoirs using canonical MW diffuse-ISM mass fractions (Draine 2003). + Split fc['dust_density'] into Mg-silicate / Fe-silicate / carbonaceous + reservoirs using canonical MW diffuse-ISM mass fractions (Draine 2003) + and the COLIBRE equal-molecule Mg2SiO4/Fe2SiO4 seed split. """ silicate = ( _DUST_SPECIES_FRACTIONS["silicate"] * fc["dust_density"] ) - fc["dust_density_olivine"][:] = ( - _DUST_SPECIES_FRACTIONS["olivine"] * silicate + fc["dust_density_mg_silicate"][:] = ( + _DUST_SPECIES_FRACTIONS["mg_silicate"] * silicate ) - fc["dust_density_pyroxene"][:] = ( - _DUST_SPECIES_FRACTIONS["pyroxene"] * silicate + fc["dust_density_fe_silicate"][:] = ( + _DUST_SPECIES_FRACTIONS["fe_silicate"] * silicate ) fc["dust_density_silicate"][:] = ( - fc["dust_density_olivine"] + fc["dust_density_pyroxene"] + fc["dust_density_mg_silicate"] + fc["dust_density_fe_silicate"] ) fc["dust_density_carbonaceous"][:] = ( _DUST_SPECIES_FRACTIONS["carbonaceous"] * fc["dust_density"] From 2a982e712d085535a6a4dc3cd749f9329f59c7f7 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Tue, 12 May 2026 03:19:06 +0100 Subject: [PATCH 67/71] Fixing nH density --- src/clib/dust/dust_growth_and_destruction.cpp | 78 +++++++++++++++++++ src/python/gracklepy/utilities/convenience.py | 69 ++++++++++++---- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index fb6917c6e..5b784b749 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -165,6 +165,7 @@ void grackle::impl::dust_growth_species( bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); + bool use_metal_H_fields = (my_chemistry->metal_chemistry == 1); grackle::impl::View HI( use_H_fields ? my_fields->HI_density : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], @@ -197,6 +198,34 @@ void grackle::impl::dust_growth_species( use_HeH_fields ? my_fields->HeHII_density : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View OH( + use_metal_H_fields ? my_fields->OH_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2O( + use_metal_H_fields ? my_fields->H2O_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View CH( + use_metal_H_fields ? my_fields->CH_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View CH2( + use_metal_H_fields ? my_fields->CH2_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View OHII( + use_metal_H_fields ? my_fields->OHII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2OII( + use_metal_H_fields ? my_fields->H2OII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H3OII( + use_metal_H_fields ? my_fields->H3OII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); grackle::impl::View mC( my_fields->metal_density_carbon, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -304,6 +333,16 @@ void grackle::impl::dust_growth_species( rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + HeHII(i, idx_range.j, idx_range.k) / 5.0; } + if (use_metal_H_fields) { + rho_H_nuclei += + OH(i, idx_range.j, idx_range.k) / 17.0 + + H2O(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + + CH(i, idx_range.j, idx_range.k) / 13.0 + + CH2(i, idx_range.j, idx_range.k) * (2.0 / 14.0) + + OHII(i, idx_range.j, idx_range.k) / 17.0 + + H2OII(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + + H3OII(i, idx_range.j, idx_range.k) * (3.0 / 19.0); + } } double nH = rho_H_nuclei * dens_proper / mh; if (rho_gas <= 0.0 || rho_H_nuclei <= 0.0 || nH <= 0.0) { @@ -521,6 +560,7 @@ void grackle::impl::dust_destruction_species( bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); + bool use_metal_H_fields = (my_chemistry->metal_chemistry == 1); grackle::impl::View HI( use_H_fields ? my_fields->HI_density : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], @@ -553,6 +593,34 @@ void grackle::impl::dust_destruction_species( use_HeH_fields ? my_fields->HeHII_density : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + grackle::impl::View OH( + use_metal_H_fields ? my_fields->OH_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2O( + use_metal_H_fields ? my_fields->H2O_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View CH( + use_metal_H_fields ? my_fields->CH_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View CH2( + use_metal_H_fields ? my_fields->CH2_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View OHII( + use_metal_H_fields ? my_fields->OHII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H2OII( + use_metal_H_fields ? my_fields->H2OII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); + grackle::impl::View H3OII( + use_metal_H_fields ? my_fields->H3OII_density : my_fields->density, + my_fields->grid_dimension[0], my_fields->grid_dimension[1], + my_fields->grid_dimension[2]); bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( use_sne ? my_fields->sne_rate : my_fields->density, @@ -628,6 +696,16 @@ void grackle::impl::dust_destruction_species( rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + HeHII(i, idx_range.j, idx_range.k) / 5.0; } + if (use_metal_H_fields) { + rho_H_nuclei += + OH(i, idx_range.j, idx_range.k) / 17.0 + + H2O(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + + CH(i, idx_range.j, idx_range.k) / 13.0 + + CH2(i, idx_range.j, idx_range.k) * (2.0 / 14.0) + + OHII(i, idx_range.j, idx_range.k) / 17.0 + + H2OII(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + + H3OII(i, idx_range.j, idx_range.k) * (3.0 / 19.0); + } } double nH = rho_H_nuclei * dens_proper / mh; if (rho_H_nuclei <= 0.0 || nH <= 0.0) continue; diff --git a/src/python/gracklepy/utilities/convenience.py b/src/python/gracklepy/utilities/convenience.py index 24652ef4b..06caf2ff0 100644 --- a/src/python/gracklepy/utilities/convenience.py +++ b/src/python/gracklepy/utilities/convenience.py @@ -60,19 +60,6 @@ def solar_metal_mass_fractions(elements=("C", "O", "Mg", "Si", "Fe")): return {el: solar_abundance[el] * atomic_mass[el] / total for el in elements} -def seed_dust_species_metal_elements(fc): - """ - Fill metal_density_carbon/oxygen/magnesium/silicon/iron from - fc['metal_density'] using solar mass fractions. Required when - dust_species_track==1: dust_update_species() reads these as the - gas-phase reservoirs for COLIBRE-style carbonaceous and silicate - accretion and feeds destruction back into them. - """ - fractions = solar_metal_mass_fractions(_DUST_SPECIES_ELEMENT_FIELDS.keys()) - for el, field in _DUST_SPECIES_ELEMENT_FIELDS.items(): - fc[field][:] = fractions[el] * fc["metal_density"] - - def seed_dust_species_dust(fc): """ Split fc['dust_density'] into Mg-silicate / Fe-silicate / carbonaceous @@ -95,6 +82,57 @@ def seed_dust_species_dust(fc): _DUST_SPECIES_FRACTIONS["carbonaceous"] * fc["dust_density"] ) + +def seed_dust_species_metal_elements(fc): + """ + Seed the gas-phase per-element reservoirs so that for each tracked + element X in {C, O, Mg, Si, Fe} the total elemental budget is + conserved: + + gas_X + dust_X = solar_X_share * metal_density_total + + where ``metal_density_total`` is the input fc['metal_density'] + (the Z-scaled solar metal share) before any subtraction. Must run + AFTER seed_dust_species_dust(): reads the dust species fields and + the chemistry_data stoichiometric f_X attributes to compute dust_X. + + Also reduces fc['metal_density'] to its gas-phase-only value + (subtracting total dust mass) so the C++ make_consistent invariant + ``metal_density = sum(tracked gas X) + untracked metals`` holds. + + Why: prior implementation set gas_X = solar_X_share * metal_density + while dust was seeded independently. Total elemental budget exceeded + solar by (1 + DGR/metal_mass_fraction) ≈ 1.77x at MW conditions, + putting IC DTM at ~0.55 instead of the physically motivated ~0.15. + """ + chem = fc.chemistry_data + fractions = solar_metal_mass_fractions(_DUST_SPECIES_ELEMENT_FIELDS.keys()) + + metal_density_total = np.asarray(fc["metal_density"]).copy() + dust_mg_sil = np.asarray(fc["dust_density_mg_silicate"]) + dust_fe_sil = np.asarray(fc["dust_density_fe_silicate"]) + dust_carb = np.asarray(fc["dust_density_carbonaceous"]) + + dust_per_element = { + "C": dust_carb, + "O": (chem.dust_mg_silicate_f_O * dust_mg_sil + + chem.dust_fe_silicate_f_O * dust_fe_sil), + "Mg": chem.dust_mg_silicate_f_Mg * dust_mg_sil, + "Si": (chem.dust_mg_silicate_f_Si * dust_mg_sil + + chem.dust_fe_silicate_f_Si * dust_fe_sil), + "Fe": chem.dust_fe_silicate_f_Fe * dust_fe_sil, + } + + total_dust = np.zeros_like(metal_density_total) + for el, field in _DUST_SPECIES_ELEMENT_FIELDS.items(): + total_X = fractions[el] * metal_density_total + fc[field][:] = np.maximum(total_X - dust_per_element[el], 0.0) + total_dust = total_dust + dust_per_element[el] + + fc["metal_density"][:] = np.maximum( + metal_density_total - total_dust, 0.0 + ) + def check_convergence(fc1, fc2, fields=None, tol=0.01): "Check for fields to be different by less than tol." @@ -283,8 +321,11 @@ def setup_fluid_container(my_chemistry, fc[field][:] = state_vals.get(field, tiny_density) if my_chemistry.dust_species_track == 1: - seed_dust_species_metal_elements(fc) + # Order matters: seed dust species first so that + # seed_dust_species_metal_elements can subtract their mass from + # the per-element gas-phase budgets (elemental conservation). seed_dust_species_dust(fc) + seed_dust_species_metal_elements(fc) fc.calculate_mean_molecular_weight() fc["internal_energy"] = temperature / \ From 5d3f310f342cf722bf4e537ca818dc43cd77c618 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 11 Jun 2026 16:17:17 -0400 Subject: [PATCH 68/71] Clean up dead codes --- src/clib/dust/dust_growth_and_destruction.cpp | 585 +++++++----------- src/clib/dust/dust_growth_and_destruction.hpp | 39 +- src/clib/solve_rate_cool.cpp | 9 +- 3 files changed, 246 insertions(+), 387 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index 5b784b749..99ae421f7 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include "dust_growth_and_destruction.hpp" @@ -13,10 +12,15 @@ const double sec_per_Myr_local = 1.0e6 * sec_per_year; const double tiny_value = 1.0e-20; const double huge_value = 1.0e+20; -const double t_ref = 20; -const double colibre_growth_t_ref = 10.0; -const double colibre_growth_nH_ref = 10.0; -const double colibre_sputter_t_ref = 2.0e6; +// Reference temperature for the bulk (dust_model=1, non-species) accretion +// timescale. +const double bulk_growth_t_ref = 20.0; + +// COLIBRE accretion / sputtering reference values +// [REF: Trayford+2026 MNRAS 545, staf2040, section 3.3]. +const double colibre_growth_t_ref = 10.0; // K +const double colibre_growth_nH_ref = 10.0; // cm^-3 +const double colibre_sputter_t_ref = 2.0e6; // K const double atomic_C = 12.01; const double atomic_O = 16.00; @@ -29,6 +33,8 @@ double limited_expm1(double x) { return std::expm1(std::min(x, 50.0)); } +// Equivalent per-time rates whose product with dt reproduces one exponential +// e-folding update: rate * dt = rho_dust * (exp(+-dt/tau) - 1). double e_fold_growth_rate(double rho_dust, double dt, double tau) { if (rho_dust <= 0.0 || dt <= 0.0 || tau <= 0.0) return 0.0; return rho_dust * limited_expm1(dt / tau) / dt; @@ -49,15 +55,91 @@ const double solar_frac_Mg = 0.045644817372018066; const double solar_frac_Si = 0.052744600629574714; const double solar_frac_Fe = 0.08523143041944482; -// Gate thresholds for dust evolution: skip cells where both dust and metals -// are negligible fractions of the baryon density. The metal threshold matches -// the 1e-9 ratio used in make_consistent (make_consistent.cpp ~line 530) to -// distinguish metal-poor from metal-rich cells. The dust threshold uses the -// same ratio for consistency — below 1 ppb of baryon density, any dust -// evolution would just integrate numerical noise. +// Gate thresholds for dust evolution: skip cells where dust or metals are a +// negligible fraction of the baryon density. The 1e-9 ratio matches the +// metal-poor threshold used in make_consistent; below it, dust evolution +// would just integrate numerical noise. const double dust_gate_threshold = 1.0e-9; const double metal_gate_threshold = 1.0e-9; +// Solar-to-local abundance ratio entering the COLIBRE accretion timescale. +// Degenerate inputs yield an effectively infinite timescale (no growth). +double abundance_ratio(double solar_eps, double local_eps) { + if (solar_eps <= 0.0 || local_eps <= 0.0) return huge_value; + return solar_eps / local_eps; +} + +// Hydrogen-nuclei mass density census. Counts H from whichever species fields +// are evolved so that the n_H entering the dust rates stays consistent with +// the active network (primordial_chemistry / metal_chemistry); falls back to +// HydrogenFractionByMass * (rho_gas - rho_metal) when no primordial species +// are evolved. Returns 0 for pathological cells (metals >= total density). +struct HNucleiCensus { + bool use_H, use_H2, use_HD, use_HeH, use_metal_H; + double hfrac; + grackle::impl::View d, metal; + grackle::impl::View HI, HII, HM, H2I, H2II, HDI, HDII, HeHII; + grackle::impl::View OH, H2O, CH, CH2, OHII, H2OII, H3OII; + + HNucleiCensus(const chemistry_data* my_chemistry, + const grackle_field_data* my_fields) + : use_H(my_chemistry->primordial_chemistry > 0), + use_H2(my_chemistry->primordial_chemistry > 1), + use_HD(my_chemistry->primordial_chemistry > 2), + use_HeH(my_chemistry->primordial_chemistry > 3), + use_metal_H(my_chemistry->metal_chemistry == 1), + hfrac(my_chemistry->HydrogenFractionByMass), + d(view_(my_fields, my_fields->density, true)), + metal(view_(my_fields, my_fields->metal_density, true)), + HI(view_(my_fields, my_fields->HI_density, use_H)), + HII(view_(my_fields, my_fields->HII_density, use_H)), + HM(view_(my_fields, my_fields->HM_density, use_H2)), + H2I(view_(my_fields, my_fields->H2I_density, use_H2)), + H2II(view_(my_fields, my_fields->H2II_density, use_H2)), + HDI(view_(my_fields, my_fields->HDI_density, use_HD)), + HDII(view_(my_fields, my_fields->HDII_density, use_HeH)), + HeHII(view_(my_fields, my_fields->HeHII_density, use_HeH)), + OH(view_(my_fields, my_fields->OH_density, use_metal_H)), + H2O(view_(my_fields, my_fields->H2O_density, use_metal_H)), + CH(view_(my_fields, my_fields->CH_density, use_metal_H)), + CH2(view_(my_fields, my_fields->CH2_density, use_metal_H)), + OHII(view_(my_fields, my_fields->OHII_density, use_metal_H)), + H2OII(view_(my_fields, my_fields->H2OII_density, use_metal_H)), + H3OII(view_(my_fields, my_fields->H3OII_density, use_metal_H)) {} + + double rho_H(int i, int j, int k) const { + double rho_nonmetal = d(i, j, k) - metal(i, j, k); + if (rho_nonmetal <= 0.0) return 0.0; + if (!use_H) return hfrac * rho_nonmetal; + + double rho = HI(i, j, k) + HII(i, j, k); + if (use_H2) { + rho += HM(i, j, k) + H2I(i, j, k) + H2II(i, j, k); + } + if (use_HD) { + rho += HDI(i, j, k) / 3.0; + } + if (use_HeH) { + rho += HDII(i, j, k) / 3.0 + HeHII(i, j, k) / 5.0; + } + if (use_metal_H) { + rho += OH(i, j, k) / 17.0 + H2O(i, j, k) * (2.0 / 18.0) + + CH(i, j, k) / 13.0 + CH2(i, j, k) * (2.0 / 14.0) + + OHII(i, j, k) / 17.0 + H2OII(i, j, k) * (2.0 / 18.0) + + H3OII(i, j, k) * (3.0 / 19.0); + } + return rho; + } + + private: + static grackle::impl::View view_( + const grackle_field_data* my_fields, gr_float* ptr, bool active) { + return grackle::impl::View( + active ? ptr : my_fields->density, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + } +}; + } // namespace // ========================================== @@ -83,47 +165,40 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, double tau_ref = my_chemistry->dust_growth_tauref * 1e9 * sec_per_year / internalu.tbase1; - // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero growth_dM[i] = 0.0; if (itmask[i] != MASK_FALSE) { double rho_dust = dust(i, idx_range.j, idx_range.k); // metal_density holds total gas-phase metals (C, O, and all others) double rho_metal = metal(i, idx_range.j, idx_range.k); - double rho_d = d(i, idx_range.j, idx_range.k); + double rho_gas = d(i, idx_range.j, idx_range.k); // No metals to accrete onto grains — skip growth - if (rho_metal < metal_gate_threshold * rho_d) { + if (rho_metal < metal_gate_threshold * rho_gas) { continue; } double temp = t_gas[i]; double dt = dt_value[i]; + // tau_accr = tau_ref * (densref / rho_metal,phys) * sqrt(T_ref / T) + // * Z_sun: the density and metallicity scalings combine into + // the single rho_metal term since rho_metal = Z * rho_gas. double tau_accr0 = tau_ref * (my_chemistry->dust_growth_densref / dens_proper) * - std::pow(t_ref / temp, 0.5); + std::pow(bulk_growth_t_ref / temp, 0.5); double rho_metal_eff = std::max(rho_metal, tiny_value); double tau_accr = tau_accr0 * (my_chemistry->SolarMetalFractionByMass / rho_metal_eff); - tau_accr = std::min(tau_accr, huge_value); - tau_accr = std::max(tau_accr, tiny_value); - double frac_metal_available = 0.0; - if (rho_metal <= 0.0) { - frac_metal_available = 0.0; - } else if (rho_dust > 0.0 && rho_metal < 1e-12 * rho_dust) { - frac_metal_available = rho_metal / rho_dust; - } else { - frac_metal_available = rho_metal / (rho_dust + rho_metal); - } - frac_metal_available = std::clamp(frac_metal_available, 0.0, 1.0); - double growth_rate = frac_metal_available * (rho_dust / tau_accr); - double dM = std::min(growth_rate, rho_metal / dt); + tau_accr = std::clamp(tau_accr, tiny_value, huge_value); - // Store the calculated mass change in the output array - growth_dM[i] = dM; + // Fraction of the combined dust+metal reservoir still in the gas + // phase; throttles growth as metals deplete. + double frac_metal_available = + std::clamp(rho_metal / (rho_dust + rho_metal), 0.0, 1.0); + double growth_rate = frac_metal_available * (rho_dust / tau_accr); + growth_dM[i] = std::min(growth_rate, rho_metal / dt); } } } @@ -131,14 +206,15 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, // ========================================== // DUST GROWTH (SPECIES-SPECIFIC: Mg-silicate + Fe-silicate + carbonaceous) // ========================================== -// COLIBRE-style accretion (Trayford+2026 section 3.3). Carbonaceous growth is -// bottlenecked by gas-phase C. Silicate growth is computed once for the total -// silicate reservoir using the maximum depletion factor over O, Si, and Mg+Fe, -// then split between Mg2SiO4 and Fe2SiO4 endmembers according to the local -// gas-phase Mg/Fe number abundance. The dense-gas rate uses a clumped -// hydrogen density n_H' = C n_H; thermal sputtering below deliberately uses the -// unclumped n_H. This species-tracking branch applies growth as an exponential -// e-folding update, returning an equivalent per-time rate for dust_update(). +// COLIBRE-style accretion [REF: Trayford+2026 MNRAS 545, staf2040, section +// 3.3]. Carbonaceous growth is bottlenecked by gas-phase C. Silicate growth +// is computed once for the total silicate reservoir using the maximum +// depletion factor over O, Si, and Mg+Fe, then split between Mg2SiO4 and +// Fe2SiO4 endmembers according to the local gas-phase Mg/Fe number abundance. +// The dense-gas rate uses a clumped hydrogen density n_H' = C n_H; thermal +// sputtering in dust_destruction_species deliberately uses the unclumped n_H. +// Growth is applied as an exponential e-folding update, returned as an +// equivalent per-time rate for dust_update_species(). void grackle::impl::dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, @@ -158,74 +234,6 @@ void grackle::impl::dust_growth_species( grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( - my_fields->metal_density, my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool use_H_fields = (my_chemistry->primordial_chemistry > 0); - bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); - bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); - bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); - bool use_metal_H_fields = (my_chemistry->metal_chemistry == 1); - grackle::impl::View HI( - use_H_fields ? my_fields->HI_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HII( - use_H_fields ? my_fields->HII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HM( - use_H2_fields ? my_fields->HM_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2I( - use_H2_fields ? my_fields->H2I_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2II( - use_H2_fields ? my_fields->H2II_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HDI( - use_HD_fields ? my_fields->HDI_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HDII( - use_HeH_fields ? my_fields->HDII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HeHII( - use_HeH_fields ? my_fields->HeHII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View OH( - use_metal_H_fields ? my_fields->OH_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2O( - use_metal_H_fields ? my_fields->H2O_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View CH( - use_metal_H_fields ? my_fields->CH_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View CH2( - use_metal_H_fields ? my_fields->CH2_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View OHII( - use_metal_H_fields ? my_fields->OHII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2OII( - use_metal_H_fields ? my_fields->H2OII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H3OII( - use_metal_H_fields ? my_fields->H3OII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); grackle::impl::View mC( my_fields->metal_density_carbon, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -241,6 +249,7 @@ void grackle::impl::dust_growth_species( grackle::impl::View mFe( my_fields->metal_density_iron, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + HNucleiCensus h_census(my_chemistry, my_fields); double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); // tau_ref values are stored in Myr (COLIBRE Table 2). @@ -248,6 +257,8 @@ void grackle::impl::dust_growth_species( sec_per_Myr_local / internalu.tbase1; double tau_ref_carb = my_chemistry->dust_growth_tauref_carbon * sec_per_Myr_local / internalu.tbase1; + // COLIBRE reference values S_ref = 0.3, a_ref = 0.1 micron; non-positive + // parameters disable growth via an effectively infinite timescale. double sticking_factor = (my_chemistry->dust_growth_sticking_coeff > 0.0) ? 0.3 / my_chemistry->dust_growth_sticking_coeff @@ -257,6 +268,8 @@ void grackle::impl::dust_growth_species( ? my_chemistry->dust_grainsize / 0.1 : huge_value; + // Solar abundances (per H mass, then per H nucleus) of the tracked + // elements, used as the depletion reference in the tau scalings. double solar_nonmetal = std::max(1.0 - my_chemistry->SolarMetalFractionByMass, tiny_value); double solar_H = std::max(my_chemistry->HydrogenFractionByMass * @@ -278,6 +291,8 @@ void grackle::impl::dust_growth_species( double solar_eps_Fe = solar_mass_Fe / atomic_Fe; double solar_eps_MgFe = solar_eps_Mg + solar_eps_Fe; + // Sub-grid clumping boost: C rises log-linearly in n_H from 1 at nH_min to + // factor_max at nH_max. auto clumping_factor = [&](double nH) -> double { double cmax = std::max(my_chemistry->dust_growth_clumping_factor_max, 1.0); double nmin = std::max(my_chemistry->dust_growth_clumping_nH_min, @@ -291,7 +306,6 @@ void grackle::impl::dust_growth_species( return std::pow(cmax, std::clamp(x, 0.0, 1.0)); }; - // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { growth_dM_mg_silicate[i] = 0.0; growth_dM_fe_silicate[i] = 0.0; @@ -302,7 +316,6 @@ void grackle::impl::dust_growth_species( double rho_dust_mg_sil_i = dust_mg_sil(i, idx_range.j, idx_range.k); double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); - double rho_metal_total = metal(i, idx_range.j, idx_range.k); double rho_C = mC(i, idx_range.j, idx_range.k); double rho_O = mO(i, idx_range.j, idx_range.k); @@ -312,40 +325,13 @@ void grackle::impl::dust_growth_species( double T = std::max(t_gas[i], tiny_value); double dt = dt_value[i]; - double rho_nonmetal = rho_gas - rho_metal_total; - if (rho_nonmetal <= 0.0) { + + double rho_H_nuclei = h_census.rho_H(i, idx_range.j, idx_range.k); + if (rho_gas <= 0.0 || rho_H_nuclei <= 0.0) { continue; } - double rho_H_nuclei = my_chemistry->HydrogenFractionByMass * - rho_nonmetal; - if (use_H_fields) { - rho_H_nuclei = HI(i, idx_range.j, idx_range.k) + - HII(i, idx_range.j, idx_range.k); - if (use_H2_fields) { - rho_H_nuclei += HM(i, idx_range.j, idx_range.k) + - H2I(i, idx_range.j, idx_range.k) + - H2II(i, idx_range.j, idx_range.k); - } - if (use_HD_fields) { - rho_H_nuclei += HDI(i, idx_range.j, idx_range.k) / 3.0; - } - if (use_HeH_fields) { - rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + - HeHII(i, idx_range.j, idx_range.k) / 5.0; - } - if (use_metal_H_fields) { - rho_H_nuclei += - OH(i, idx_range.j, idx_range.k) / 17.0 + - H2O(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + - CH(i, idx_range.j, idx_range.k) / 13.0 + - CH2(i, idx_range.j, idx_range.k) * (2.0 / 14.0) + - OHII(i, idx_range.j, idx_range.k) / 17.0 + - H2OII(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + - H3OII(i, idx_range.j, idx_range.k) * (3.0 / 19.0); - } - } double nH = rho_H_nuclei * dens_proper / mh; - if (rho_gas <= 0.0 || rho_H_nuclei <= 0.0 || nH <= 0.0) { + if (nH <= 0.0) { continue; } @@ -361,20 +347,12 @@ void grackle::impl::dust_growth_species( double eps_Fe = rho_Fe * inv_rho_H / atomic_Fe; double eps_MgFe = eps_Mg + eps_Fe; - auto abundance_ratio = [](double solar_eps, double local_eps) { - if (solar_eps <= 0.0) return 0.0; - if (local_eps <= 0.0) return huge_value; - return solar_eps / local_eps; - }; - // ---------- Carbonaceous: rate-limited by gas-phase C ---------- if (rho_C >= metal_gate_threshold * rho_gas && - rho_dust_carb_i > 0.0 && solar_eps_C > 0.0 && eps_C > 0.0 && - dt > 0.0) { + rho_dust_carb_i > 0.0 && dt > 0.0) { double tau_accr_carb = tau_ref_carb * accr_struct * abundance_ratio(solar_eps_C, eps_C); - tau_accr_carb = std::min(std::max(tau_accr_carb, tiny_value), - huge_value); + tau_accr_carb = std::clamp(tau_accr_carb, tiny_value, huge_value); double rate = e_fold_growth_rate(rho_dust_carb_i, dt, tau_accr_carb); growth_dM_carbon[i] = std::min(rate, rho_C / dt); @@ -388,11 +366,9 @@ void grackle::impl::dust_growth_species( abundance_ratio(solar_eps_O, eps_O), abundance_ratio(solar_eps_Si, eps_Si), abundance_ratio(solar_eps_MgFe, eps_MgFe)}); - double tau_accr = tau_ref_sil * accr_struct * - sil_ratio; - tau_accr = std::min(std::max(tau_accr, tiny_value), huge_value); - double sil_rate = e_fold_growth_rate(rho_dust_sil_i, dt, - tau_accr); + double tau_accr = tau_ref_sil * accr_struct * sil_ratio; + tau_accr = std::clamp(tau_accr, tiny_value, huge_value); + double sil_rate = e_fold_growth_rate(rho_dust_sil_i, dt, tau_accr); // Split total silicate accretion by endmember molecule abundance. // Mass fractions include the different Mg2SiO4 / Fe2SiO4 molecule @@ -404,8 +380,7 @@ void grackle::impl::dust_growth_species( 2.0 * atomic_Fe + atomic_Si + 4.0 * atomic_O); double endmember_weight = mg_weight + fe_weight; if (endmember_weight > 0.0) { - double mg_frac = mg_weight / endmember_weight; - mg_frac = std::clamp(mg_frac, 0.0, 1.0); + double mg_frac = std::clamp(mg_weight / endmember_weight, 0.0, 1.0); growth_dM_mg_silicate[i] = sil_rate * mg_frac; growth_dM_fe_silicate[i] = sil_rate * (1.0 - mg_frac); } @@ -415,21 +390,19 @@ void grackle::impl::dust_growth_species( } // ========================================== -// 2. DUST DESTRUCTION (SNe + SPUTTERING) +// DUST DESTRUCTION (SNe + SPUTTERING) // ========================================== void grackle::impl::dust_destruction( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - const double* dt_value, const double* t_gas, double* destruction_dM) { + const double* dt_value, double dt_full, const double* t_gas, + double* destruction_dM) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( - my_fields->metal_density, my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( use_sne ? my_fields->sne_rate : my_fields->density, @@ -443,14 +416,15 @@ void grackle::impl::dust_destruction( double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); + // Gas mass shocked to >= 100 km/s per SN, M_s(100) = 6800 Msun + // (v_s/100 km/s)^-2 [REF: McKee 1989; used as in Li, Narayanan & Dave + // 2019], converted to code density * code volume. double Ms100 = 6800.0 * my_chemistry->sne_coeff * (100.0 / my_chemistry->sne_shockspeed) * (100.0 / my_chemistry->sne_shockspeed) * SolarMass / (internalu.urho * std::pow(internalu.uxyz, 3)); - // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { - // Initialize to zero destruction_dM[i] = 0.0; if (itmask[i] != MASK_FALSE) { @@ -465,56 +439,45 @@ void grackle::impl::dust_destruction( double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; double temp = t_gas[i]; double dt = dt_value[i]; - double tau_dest = 0; - - double dM = 0; double dM_shock = 0.0; if (use_tau_dest) { // user-provided destruction timescale - tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); + double tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); if (tau_dest <= 0) { - tau_dest = 1e20; + tau_dest = huge_value; } dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); - } else if (use_sne) { - // destruction by SN shocks - if (sne_this <= 0) { - tau_dest = 1e20; - // dM_shock = 0.0; - } else { - tau_dest = rho_gas / - (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * - dt; - dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); - } + } else if (use_sne && sne_this > 0) { + // SN shock destruction: sne_rate holds the number of SNe per code + // volume delivered over the external timestep dt_full, so + // Ms100 * sne_this / dt_full is the shocked-gas mass rate and + // tau_dest the time to shock all gas in the cell. + double tau_dest = + rho_gas / + (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * dt_full; + dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); } - if (temp >= std::pow(10, 5)) { - // destruction by thermal sputtering + // Thermal sputtering in hot gas [REF: Tsai & Mathews 1995 ApJ 448, + // 84]: tau_sp = 0.17 Gyr (a/0.1 um) (rho/1e-27 g cm^-3)^-1 + // [(2e6 K / T)^2.5 + 1]. The factor 3 converts the grain-radius + // e-folding time to a mass-loss time (m ~ a^3). + if (temp >= 1.0e5 && dM_shock < rho_dust / dt) { double tau_sput = 1.7e8 * sec_per_year / internalu.tbase1 * (my_chemistry->dust_grainsize / 0.1) * (1.0e-27 / (dens_proper * rho_gas)) * (std::pow((2.0e6 / temp), 2.5) + 1.0); - - if (dM_shock >= rho_dust / dt) { - if (dM_shock > rho_dust / dt) { - std::cout << "WARNING: dM_shock > M_dust SNe shock destruction, " - << sne_this << ", " << tau_dest << std::endl; - } - } else { - dM_shock = dM_shock + rho_dust / tau_sput * 3.0; - dM_shock = std::min(dM_shock, rho_dust / dt); - } + dM_shock = std::min(dM_shock + 3.0 * rho_dust / tau_sput, + rho_dust / dt); } - // dM = - rho_dust * dM_shock; - dM = -dM_shock; - if (std::isnan(dM)) { - std::cout << "dM calculated as NaN, " << dM << std::endl; + + if (std::isnan(dM_shock)) { + std::cout << "dust_destruction: dM calculated as NaN" << std::endl; + dM_shock = 0.0; } - // Store the calculated mass change in the output array - destruction_dM[i] = dM; + destruction_dM[i] = -dM_shock; } } } @@ -530,14 +493,14 @@ void grackle::impl::dust_destruction( // standard SNR model, so silicates are destroyed about 1.65x faster // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740]. // Thermal sputtering follows the species-independent COLIBRE/Tsai-Mathews -// timescale tau_sp = 0.85 Myr (a/0.1) (n_H/cm^-3)^-1 -// [1 + (T/2e6 K)^-2.5]. Species destruction is applied as exponential decay, -// returning an equivalent per-time rate for dust_update(). -// Phase D wires the species outputs into dust_update(). +// timescale tau_sp = tau_ref (a/0.1) (n_H/cm^-3)^-1 [1 + (T/2e6 K)^-2.5]. +// Destruction is applied as exponential decay, returned as an equivalent +// per-time rate for dust_update_species(). void grackle::impl::dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, const double* dt_value, const double* t_gas, + const gr_mask_type* itmask, const double* dt_value, double dt_full, + const double* t_gas, double* destruction_dM_mg_silicate, double* destruction_dM_fe_silicate, double* destruction_dM_carbon) { @@ -553,74 +516,6 @@ void grackle::impl::dust_destruction_species( grackle::impl::View dust_carb( my_fields->dust_density_carbonaceous, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - grackle::impl::View metal( - my_fields->metal_density, my_fields->grid_dimension[0], - my_fields->grid_dimension[1], my_fields->grid_dimension[2]); - bool use_H_fields = (my_chemistry->primordial_chemistry > 0); - bool use_H2_fields = (my_chemistry->primordial_chemistry > 1); - bool use_HD_fields = (my_chemistry->primordial_chemistry > 2); - bool use_HeH_fields = (my_chemistry->primordial_chemistry > 3); - bool use_metal_H_fields = (my_chemistry->metal_chemistry == 1); - grackle::impl::View HI( - use_H_fields ? my_fields->HI_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HII( - use_H_fields ? my_fields->HII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HM( - use_H2_fields ? my_fields->HM_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2I( - use_H2_fields ? my_fields->H2I_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2II( - use_H2_fields ? my_fields->H2II_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HDI( - use_HD_fields ? my_fields->HDI_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HDII( - use_HeH_fields ? my_fields->HDII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View HeHII( - use_HeH_fields ? my_fields->HeHII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View OH( - use_metal_H_fields ? my_fields->OH_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2O( - use_metal_H_fields ? my_fields->H2O_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View CH( - use_metal_H_fields ? my_fields->CH_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View CH2( - use_metal_H_fields ? my_fields->CH2_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View OHII( - use_metal_H_fields ? my_fields->OHII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H2OII( - use_metal_H_fields ? my_fields->H2OII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); - grackle::impl::View H3OII( - use_metal_H_fields ? my_fields->H3OII_density : my_fields->density, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); bool use_sne = (my_chemistry->use_sne_field > 0); grackle::impl::View sne( use_sne ? my_fields->sne_rate : my_fields->density, @@ -631,18 +526,20 @@ void grackle::impl::dust_destruction_species( use_tau_dest ? my_fields->tau_dest : my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); + HNucleiCensus h_census(my_chemistry, my_fields); double dens_proper = internalu.urho * std::pow(internalu.a_value, 3); - // Base shock-yield mass coefficient (Li+2019 framework). + // Gas mass shocked per SN, M_s(100) = 6800 Msun (v_s/100 km/s)^-2 + // [REF: McKee 1989; used as in Li, Narayanan & Dave 2019]. double Ms100 = 6800.0 * my_chemistry->sne_coeff * (100.0 / my_chemistry->sne_shockspeed) * (100.0 / my_chemistry->sne_shockspeed) * SolarMass / (internalu.urho * std::pow(internalu.uxyz, 3)); // Species-specific shock-vulnerability multipliers. Graphite is the - // baseline (1.0); silicate follows the standard Slavin+2015 SNR - // gas-cleared mass ratio, 990/600 = 1.65. + // baseline (1.0); silicate follows the Slavin+2015 SNR gas-cleared mass + // ratio, 990/600 = 1.65. const double shock_factor_carbon = 1.0; const double shock_factor_silicate = 1.65; @@ -650,7 +547,6 @@ void grackle::impl::dust_destruction_species( double tau_sput_ref = my_chemistry->dust_sputter_tauref * sec_per_Myr_local / internalu.tbase1; - // --- MAIN LOOP --- for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { destruction_dM_mg_silicate[i] = 0.0; destruction_dM_fe_silicate[i] = 0.0; @@ -661,7 +557,6 @@ void grackle::impl::dust_destruction_species( double rho_dust_mg_sil_i = dust_mg_sil(i, idx_range.j, idx_range.k); double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); - double rho_metal_total = metal(i, idx_range.j, idx_range.k); bool mg_sil_active = (rho_dust_mg_sil_i >= dust_gate_threshold * rho_gas); @@ -669,46 +564,17 @@ void grackle::impl::dust_destruction_species( (rho_dust_fe_sil_i >= dust_gate_threshold * rho_gas); bool carb_active = (rho_dust_carb_i >= dust_gate_threshold * rho_gas); if (!mg_sil_active && !fe_sil_active && !carb_active) continue; - - double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; if (rho_gas <= 0.0) continue; + double sne_this = use_sne ? sne(i, idx_range.j, idx_range.k) : 0.0; double temp = std::max(t_gas[i], tiny_value); double dt = dt_value[i]; if (dt <= 0.0) continue; - double rho_nonmetal = rho_gas - rho_metal_total; - if (rho_nonmetal <= 0.0) continue; - double rho_H_nuclei = my_chemistry->HydrogenFractionByMass * - rho_nonmetal; - if (use_H_fields) { - rho_H_nuclei = HI(i, idx_range.j, idx_range.k) + - HII(i, idx_range.j, idx_range.k); - if (use_H2_fields) { - rho_H_nuclei += HM(i, idx_range.j, idx_range.k) + - H2I(i, idx_range.j, idx_range.k) + - H2II(i, idx_range.j, idx_range.k); - } - if (use_HD_fields) { - rho_H_nuclei += HDI(i, idx_range.j, idx_range.k) / 3.0; - } - if (use_HeH_fields) { - rho_H_nuclei += HDII(i, idx_range.j, idx_range.k) / 3.0 + - HeHII(i, idx_range.j, idx_range.k) / 5.0; - } - if (use_metal_H_fields) { - rho_H_nuclei += - OH(i, idx_range.j, idx_range.k) / 17.0 + - H2O(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + - CH(i, idx_range.j, idx_range.k) / 13.0 + - CH2(i, idx_range.j, idx_range.k) * (2.0 / 14.0) + - OHII(i, idx_range.j, idx_range.k) / 17.0 + - H2OII(i, idx_range.j, idx_range.k) * (2.0 / 18.0) + - H3OII(i, idx_range.j, idx_range.k) * (3.0 / 19.0); - } - } + double rho_H_nuclei = h_census.rho_H(i, idx_range.j, idx_range.k); + if (rho_H_nuclei <= 0.0) continue; double nH = rho_H_nuclei * dens_proper / mh; - if (rho_H_nuclei <= 0.0 || nH <= 0.0) continue; + if (nH <= 0.0) continue; // Common (species-independent) sputtering structural factor. double sput_struct = (my_chemistry->dust_grainsize / 0.1) * @@ -716,32 +582,31 @@ void grackle::impl::dust_destruction_species( (1.0 + std::pow(temp / colibre_sputter_t_ref, -2.5)); - // Helper computing the equivalent per-time destruction rate for one - // species. Its dt product is an exponential e-folding mass loss. + // Equivalent per-time destruction rate for one species; its product + // with dt is an exponential e-folding mass loss. auto compute_dM = [&](double rho_dust, double shock_factor) -> double { double inv_tau_loss = 0.0; - double tau_dest = 1e20; if (use_tau_dest) { - tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); - if (tau_dest <= 0) tau_dest = 1e20; + double tau_dest = tau_dest_field(i, idx_range.j, idx_range.k); + if (tau_dest <= 0) tau_dest = huge_value; // Apply species multiplier on top of user-supplied tau_dest: // higher shock_factor -> shorter tau -> faster destruction. tau_dest = tau_dest / shock_factor; if (tau_dest > 0.0 && std::isfinite(tau_dest)) { inv_tau_loss += 1.0 / tau_dest; } - } else if (use_sne) { - if (sne_this > 0.0) { - // sne_rate is a rate per code time, so add the physical inverse - // shock-destruction timescale here. The exponential update below - // supplies the dt dependence. - double inv_tau_shock = - Ms100 * shock_factor * sne_this * - my_chemistry->dust_destruction_eff / (rho_gas*dt); - if (inv_tau_shock > 0.0 && std::isfinite(inv_tau_shock)) { - inv_tau_loss += inv_tau_shock; - } + } else if (use_sne && sne_this > 0.0) { + // sne_rate holds the number of SNe per code volume delivered over + // the external timestep dt_full; Ms100 * sne_this is then the + // shocked-gas mass per volume in dt_full, and dividing by + // (rho_gas * dt_full) gives the inverse shock-destruction + // timescale. The exponential update supplies the dt dependence. + double inv_tau_shock = + Ms100 * shock_factor * sne_this * + my_chemistry->dust_destruction_eff / (rho_gas * dt_full); + if (inv_tau_shock > 0.0 && std::isfinite(inv_tau_shock)) { + inv_tau_loss += inv_tau_shock; } } @@ -784,10 +649,9 @@ void grackle::impl::dust_destruction_species( // Mg-sil channel: rho_dust_mg_silicate <-> {Mg, Si, O} as Mg2SiO4 // Fe-sil channel: rho_dust_fe_silicate <-> {Fe, Si, O} as Fe2SiO4 // -// Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] -// shortfall iteration: growth is capped by the limiting reactant, destruction -// is capped by available dust mass. No SN injection on this path — host code -// seeds the species directly. +// Growth is capped by the limiting gas-phase reactant, destruction by the +// available dust mass. No SN injection on this path — host code seeds the +// species directly. void grackle::impl::dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, @@ -863,8 +727,8 @@ void grackle::impl::dust_update_species( // per-element fields independently and they exceed rho_metal_total, // rho_metal_other would go negative and propagate into rho_metal_new on // the rebuild below — clamp at zero to prevent silent mass leakage. - double rho_metal_other = rho_metal_total - rho_tracked_before; - if (rho_metal_other < 0.0) rho_metal_other = 0.0; + double rho_metal_other = + std::max(rho_metal_total - rho_tracked_before, 0.0); // dM > 0 means net growth (gas reactants -> dust); // dM < 0 means net destruction (dust -> gas reactants). @@ -874,12 +738,13 @@ void grackle::impl::dust_update_species( double dM_fe_sil = (growth_dM_fe_silicate[i] + destruction_dM_fe_silicate[i]) * dt; - // ---------- Per-channel pre-cap ---------- - // Carbon channel: bounded by rho_C (growth) or rho_dust_carb (destruction). - // Leave a 10% margin on growth so the gas-C reservoir is not driven to - // exactly zero in a single subcycle — make_consistent's gas-phase C - // correction (correctCg = Cg/totalCg) would otherwise zero CI/CII/CO/... - // and destabilize the metal-chemistry implicit solve that follows. + // ---------- Per-channel caps ---------- + // Carbon channel: bounded by rho_C (growth) or rho_dust_carb + // (destruction). Leave a 10% margin on growth so the gas-C reservoir is + // not driven to exactly zero in a single subcycle — make_consistent's + // gas-phase C correction (correctCg = Cg/totalCg) would otherwise zero + // CI/CII/CO/... and destabilize the metal-chemistry implicit solve that + // follows. if (dM_carb > 0.0) { dM_carb = std::min(dM_carb, 0.9 * rho_C); } else { @@ -887,7 +752,7 @@ void grackle::impl::dust_update_species( } // Mg- and Fe-silicate channels share Si/O, but Mg and Fe are independent. - // First cap destruction by each species' own dust mass. + // Cap destruction by each species' own dust mass. if (dM_mg_sil < 0.0) { dM_mg_sil = std::max(dM_mg_sil, -rho_dust_mg_sil_i); } @@ -895,12 +760,13 @@ void grackle::impl::dust_update_species( dM_fe_sil = std::max(dM_fe_sil, -rho_dust_fe_sil_i); } - // Then cap positive growth jointly against the gas reservoirs. This - // preserves Mg-silicate growth when Fe alone limits Fe-silicate, and vice - // versa, while still preventing double-use of shared Si/O. The 0.9 - // safety margin (same rationale as the carbon channel) leaves headroom - // in each gas reservoir so make_consistent's per-element corrections - // don't divide by ~zero in the next subcycle. + // Cap positive growth jointly against the gas reservoirs: each pass + // rescales only the channels that consume the most-limiting element, so + // Mg-silicate growth survives when Fe alone limits Fe-silicate (and vice + // versa) while shared Si/O is never double-used. The 0.9 safety margin + // (same rationale as the carbon channel) leaves headroom in each gas + // reservoir so make_consistent's per-element corrections don't divide by + // ~zero in the next subcycle. const double sil_grow_safety = 0.9; double dM_mg_sil_grow = std::max(dM_mg_sil, 0.0); double dM_fe_sil_grow = std::max(dM_fe_sil, 0.0); @@ -947,17 +813,6 @@ void grackle::impl::dust_update_species( dM_fe_sil = dM_fe_sil_grow; } - if (dM_mg_sil < 0.0) { - dM_mg_sil = std::max(dM_mg_sil, -rho_dust_mg_sil_i); - } else { - dM_mg_sil = std::max(dM_mg_sil, 0.0); - } - if (dM_fe_sil < 0.0) { - dM_fe_sil = std::max(dM_fe_sil, -rho_dust_fe_sil_i); - } else { - dM_fe_sil = std::max(dM_fe_sil, 0.0); - } - // ---------- Apply ---------- rho_dust_carb_i += dM_carb; rho_C -= dM_carb; @@ -984,13 +839,12 @@ void grackle::impl::dust_update_species( double rho_dust_sil_i = rho_dust_mg_sil_i + rho_dust_fe_sil_i; double rho_dust_new = rho_dust_sil_i + rho_dust_carb_i; - // metal_density_other (= total - C - O - Mg - Si - Fe) is unchanged on - // the dust loop. Rebuilding total metals from the updated tracked fields - // preserves consistency even if user-edited silicate fractions do not sum - // to exactly one. + // Rebuild total metals from the updated tracked fields plus the unchanged + // untracked remainder; this stays consistent even if user-edited silicate + // fractions do not sum to exactly one. double rho_tracked_after = rho_C + rho_O + rho_Mg + rho_Si + rho_Fe; - double rho_metal_new = rho_metal_other + rho_tracked_after; - rho_metal_new = std::max(0.0, rho_metal_new); + double rho_metal_new = + std::max(rho_metal_other + rho_tracked_after, 0.0); double delta_metal_total = rho_metal_new - rho_metal_total; // Gas density tracks metals as a subset (dust is not part of `density`). @@ -1022,6 +876,9 @@ void grackle::impl::dust_update_species( } } +// ========================================== +// DUST UPDATE (BULK) +// ========================================== void grackle::impl::dust_update(chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, @@ -1046,9 +903,11 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, double rho_metal_total = metal(i, idx_range.j, idx_range.k); double dt = dt_value[i]; - // dM_exchange > 0: net growth (metal -> dust); < 0: destruction (dust -> metal) + // dM_exchange > 0: net growth (metal -> dust); < 0: destruction + // (dust -> metal). Cap destruction by available dust and growth by 90% + // of gas-phase metals. double dM_exchange = (growth_dM[i] + destruction_dM[i]) * dt; - dM_exchange = std::max(-1.0 * rho_dust, dM_exchange); + dM_exchange = std::max(-rho_dust, dM_exchange); dM_exchange = std::min(0.9 * rho_metal_total, dM_exchange); rho_dust += dM_exchange; diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index 0092c1d7e..2a316ba0c 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -11,7 +11,7 @@ namespace grackle::impl { // Calculates dust growth rates (accretion) onto grain surfaces. -// Stores the mass change dM for each cell in growth_dM array. +// Stores the mass change rate for each cell in growth_dM. void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, @@ -27,8 +27,10 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, // Mg2SiO4 and Fe2SiO4 endmembers according to local Mg/Fe abundance // Uses the COLIBRE dense-gas accretion normalization with clumped // n_H' = C n_H, T_ref = 10 K, n_H_ref = 10 cm^-3, S_ref = 0.3, a_ref = 0.1 -// micron, and tau_ref values stored in Myr. The returned rates are equivalent -// rates whose product with dt gives the exponential e-folding mass update. +// micron, and tau_ref values stored in Myr +// [REF: Trayford+2026 MNRAS 545, staf2040]. +// The returned rates are equivalent rates whose product with dt gives the +// exponential e-folding mass update. void dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, @@ -39,30 +41,31 @@ void dust_growth_species( ); // Calculates dust destruction rates from SNe shocks and thermal sputtering. -// Stores the mass change dM for each cell in destruction_dM array. +// dt_full is the full external timestep (sne_rate is normalized per external +// step, not per subcycle). Stores the mass change rate for each cell in +// destruction_dM. void dust_destruction( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - const double* dt_value, const double* t_gas, + const double* dt_value, double dt_full, const double* t_gas, double* destruction_dM // output: mass change rate for each cell ); -// Species-specific destruction (SN shocks + thermal sputtering) onto the three +// Species-specific destruction (SN shocks + thermal sputtering) of the three // dust populations. Active when dust_species_track == 1. // - shock yield: graphite is baseline (factor 1.0); both silicate endmembers -// follow the -// Slavin+2015 standard SNR gas-cleared mass ratio 990/600 = 1.65 +// follow the Slavin+2015 standard SNR gas-cleared mass ratio +// 990/600 = 1.65 // [REF: Slavin, Dwek, Jones 2015 ApJ 803, 7; Jones+1996 ApJ 469, 740] // - thermal sputtering: common COLIBRE/Tsai-Mathews tau_ref for all species // with (a/0.1) (n_H/cm^-3)^-1 [1 + (T/2e6 K)^-2.5] scaling -// Destruction uses exponential decay; returned rates are equivalent rates whose -// product with dt gives the e-folding mass loss. -// No bulk dM, no partitioning — Phase D wires the species outputs into -// dust_update(). +// [REF: Tsai & Mathews 1995 ApJ 448, 84] +// Destruction uses exponential decay; the returned rates are equivalent rates +// whose product with dt gives the e-folding mass loss. void dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, - const double* dt_value, const double* t_gas, + const double* dt_value, double dt_full, const double* t_gas, double* destruction_dM_mg_silicate, // output: Mg-silicate destruction rate double* destruction_dM_fe_silicate, // output: Fe-silicate destruction rate double* destruction_dM_carbon // output: carbonaceous destruction rate @@ -78,15 +81,13 @@ void dust_update( bool dryrun); // Species-specific field update for the split-silicate path -// (dust_species_track==1). -// Per-channel mass exchange: +// (dust_species_track == 1). Per-channel mass exchange: // - carbon channel: rho_dust_carbonaceous <-> metal_density_carbon // - Mg-sil channel: rho_dust_mg_silicate <-> {Mg, Si, O} as Mg2SiO4 // - Fe-sil channel: rho_dust_fe_silicate <-> {Fe, Si, O} as Fe2SiO4 -// Per-channel pre-cap in absolute mass units replaces the legacy 3-way active[] -// shortfall mask. No SN injection here — Phase D drops the in-Grackle -// dust_creation pathway from the species branch; host code seeds dust species -// directly (or via inject_pathway machinery in make_consistent). +// Growth is capped by the limiting gas-phase reactant, destruction by the +// available dust mass. No SN injection here — host code seeds dust species +// directly (or via the inject_pathway machinery in make_consistent). void dust_update_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, diff --git a/src/clib/solve_rate_cool.cpp b/src/clib/solve_rate_cool.cpp index 308d7e11d..b93daef32 100644 --- a/src/clib/solve_rate_cool.cpp +++ b/src/clib/solve_rate_cool.cpp @@ -731,8 +731,7 @@ int solve_rate_cool( // Arrays to store dust growth and destruction mass changes std::vector growth_dM(my_fields->grid_dimension[0]); std::vector destruction_dM(my_fields->grid_dimension[0]); - // Phase B/C: species-specific growth & destruction outputs (used when - // dust_species_track == 1; Phase D wires them into dust_update()) + // species-specific growth & destruction outputs (dust_species_track == 1) std::vector growth_dM_mg_silicate(my_fields->grid_dimension[0]); std::vector growth_dM_fe_silicate(my_fields->grid_dimension[0]); std::vector growth_dM_carbon(my_fields->grid_dimension[0]); @@ -932,7 +931,7 @@ int solve_rate_cool( // will be restructured once that integration lands. if (my_chemistry->dust_model == 1){ if (my_chemistry->dust_species_track == 1) { - // Calculate and apply the Mg-silicate + Fe-silicate + carbonaceous rates. + // Compute and apply Mg-silicate + Fe-silicate + carbonaceous rates grackle::impl::dust_growth_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), tgas.data(), @@ -940,7 +939,7 @@ int solve_rate_cool( growth_dM_carbon.data()); grackle::impl::dust_destruction_species( my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), + dtit.data(), dt, tgas.data(), destruction_dM_mg_silicate.data(), destruction_dM_fe_silicate.data(), destruction_dM_carbon.data()); grackle::impl::dust_update_species( @@ -957,7 +956,7 @@ int solve_rate_cool( dtit.data(), tgas.data(), growth_dM.data()); grackle::impl::dust_destruction( my_chemistry, my_fields, internalu, idx_range, itmask.data(), - dtit.data(), tgas.data(), destruction_dM.data()); + dtit.data(), dt, tgas.data(), destruction_dM.data()); grackle::impl::dust_update( my_chemistry, my_fields, internalu, idx_range, itmask.data(), dtit.data(), growth_dM.data(), destruction_dM.data(), false); From 7b2457a0ac7b7a9ee9833416e2b573593a7f1fa0 Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 11 Jun 2026 16:51:50 -0400 Subject: [PATCH 69/71] Sync Fortran interface structs with C definitions Add missing dust/metal species fields and dust model parameters. --- src/include/grackle_fortran_interface.def | 42 ++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/include/grackle_fortran_interface.def b/src/include/grackle_fortran_interface.def index 7b541d1c3..391b3c7b3 100644 --- a/src/include/grackle_fortran_interface.def +++ b/src/include/grackle_fortran_interface.def @@ -49,7 +49,16 @@ c This is the fortran definition of grackle_field_data TYPE(C_PTR) :: y_velocity TYPE(C_PTR) :: z_velocity TYPE(C_PTR) :: metal_density + TYPE(C_PTR) :: metal_density_carbon + TYPE(C_PTR) :: metal_density_oxygen + TYPE(C_PTR) :: metal_density_magnesium + TYPE(C_PTR) :: metal_density_silicon + TYPE(C_PTR) :: metal_density_iron TYPE(C_PTR) :: dust_density + TYPE(C_PTR) :: dust_density_silicate + TYPE(C_PTR) :: dust_density_mg_silicate + TYPE(C_PTR) :: dust_density_fe_silicate + TYPE(C_PTR) :: dust_density_carbonaceous TYPE(C_PTR) :: e_density TYPE(C_PTR) :: HI_density TYPE(C_PTR) :: HII_density @@ -133,6 +142,8 @@ c This is the fortran definition of grackle_field_data TYPE(C_PTR) :: ref_org_dust_temperature TYPE(C_PTR) :: vol_org_dust_temperature TYPE(C_PTR) :: H2O_ice_dust_temperature + TYPE(C_PTR) :: sne_rate + TYPE(C_PTR) :: tau_dest END TYPE c This is the fortran definition of grackle_chemistry_data @@ -216,7 +227,36 @@ c This is the fortran definition of grackle_chemistry_data INTEGER(C_INT) :: uniform_grain_isrf_heating_rate INTEGER(C_INT) :: max_iterations INTEGER(C_INT) :: exit_after_iterations_exceeded -cc INTEGER(C_INT) :: omp_nthreads // not supported in fortran + INTEGER(C_INT) :: omp_nthreads + INTEGER(C_INT) :: solver_method + INTEGER(C_INT) :: dust_model + INTEGER(C_INT) :: use_sne_field + INTEGER(C_INT) :: use_tau_dest_field + REAL(C_DOUBLE) :: dust_destruction_eff + REAL(C_DOUBLE) :: sne_coeff + REAL(C_DOUBLE) :: sne_shockspeed + REAL(C_DOUBLE) :: dust_grainsize + REAL(C_DOUBLE) :: dust_growth_densref + REAL(C_DOUBLE) :: dust_growth_tauref + REAL(C_DOUBLE) :: dust_growth_sticking_coeff + REAL(C_DOUBLE) :: dust_condensation_eff + REAL(C_DOUBLE) :: sne_metal_yield + INTEGER(C_INT) :: dust_species_track + REAL(C_DOUBLE) :: dust_growth_tauref_silicate + REAL(C_DOUBLE) :: dust_growth_tauref_carbon + REAL(C_DOUBLE) :: dust_growth_clumping_factor_max + REAL(C_DOUBLE) :: dust_growth_clumping_nH_min + REAL(C_DOUBLE) :: dust_growth_clumping_nH_max + REAL(C_DOUBLE) :: dust_sputter_tauref + REAL(C_DOUBLE) :: dust_silicate_mg_fraction + REAL(C_DOUBLE) :: dust_mg_silicate_f_Mg + REAL(C_DOUBLE) :: dust_mg_silicate_f_Fe + REAL(C_DOUBLE) :: dust_mg_silicate_f_Si + REAL(C_DOUBLE) :: dust_mg_silicate_f_O + REAL(C_DOUBLE) :: dust_fe_silicate_f_Mg + REAL(C_DOUBLE) :: dust_fe_silicate_f_Fe + REAL(C_DOUBLE) :: dust_fe_silicate_f_Si + REAL(C_DOUBLE) :: dust_fe_silicate_f_O END TYPE c The following define the fortran interfaces to the C routines From 4f6df20a51aa15b43ed7d7c6c810a6ee11509f5b Mon Sep 17 00:00:00 2001 From: aqua-hl Date: Thu, 11 Jun 2026 19:04:31 -0400 Subject: [PATCH 70/71] Remove stale calc_tdust_1d_g.lo from Make.config.objects The source moved to dust/; the duplicate top-level entry broke the classic make build in CI. --- src/clib/Make.config.objects | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clib/Make.config.objects b/src/clib/Make.config.objects index adf4a76cb..5a3c9eb6c 100644 --- a/src/clib/Make.config.objects +++ b/src/clib/Make.config.objects @@ -50,7 +50,6 @@ OBJS_CONFIG_LIB = \ solve_rate_cool.lo \ support/status_reporting.lo \ update_UVbackground_rates.lo \ - calc_tdust_1d_g.lo \ dust/calc_tdust_3d.lo \ calc_grain_size_increment_1d.lo \ rate_functions.lo \ From 40f75f26f980809a5930ce1b6470f3ae48a6af2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:13:00 +0000 Subject: [PATCH 71/71] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/clib/dust/dust_growth_and_destruction.cpp | 216 ++++++++---------- src/clib/dust/dust_growth_and_destruction.hpp | 19 +- src/clib/make_consistent.cpp | 60 ++--- src/clib/tabulated/calc_temp1d_cloudy.cpp | 4 +- 4 files changed, 143 insertions(+), 156 deletions(-) diff --git a/src/clib/dust/dust_growth_and_destruction.cpp b/src/clib/dust/dust_growth_and_destruction.cpp index 99ae421f7..adafea51c 100644 --- a/src/clib/dust/dust_growth_and_destruction.cpp +++ b/src/clib/dust/dust_growth_and_destruction.cpp @@ -22,8 +22,8 @@ const double colibre_growth_t_ref = 10.0; // K const double colibre_growth_nH_ref = 10.0; // cm^-3 const double colibre_sputter_t_ref = 2.0e6; // K -const double atomic_C = 12.01; -const double atomic_O = 16.00; +const double atomic_C = 12.01; +const double atomic_O = 16.00; const double atomic_Mg = 24.31; const double atomic_Si = 28.09; const double atomic_Fe = 55.85; @@ -49,8 +49,8 @@ double e_fold_loss_rate(double rho_dust, double dt, double inv_tau) { // Solar metal mass fractions for the tracked dust-forming elements, relative // to total solar metals. These match gracklepy.utilities.convenience, which // seeds the dust_species_track gas reservoirs from metal_density. -const double solar_frac_C = 0.15925782394660776; -const double solar_frac_O = 0.4242932765702842; +const double solar_frac_C = 0.15925782394660776; +const double solar_frac_O = 0.4242932765702842; const double solar_frac_Mg = 0.045644817372018066; const double solar_frac_Si = 0.052744600629574714; const double solar_frac_Fe = 0.08523143041944482; @@ -131,7 +131,7 @@ struct HNucleiCensus { return rho; } - private: +private: static grackle::impl::View view_( const grackle_field_data* my_fields, gr_float* ptr, bool active) { return grackle::impl::View( @@ -217,11 +217,9 @@ void grackle::impl::dust_growth(chemistry_data* my_chemistry, // equivalent per-time rate for dust_update_species(). void grackle::impl::dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, const double* dt_value, const double* t_gas, - double* growth_dM_mg_silicate, double* growth_dM_fe_silicate, - double* growth_dM_carbon) { - + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* growth_dM_mg_silicate, + double* growth_dM_fe_silicate, double* growth_dM_carbon) { grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -259,31 +257,29 @@ void grackle::impl::dust_growth_species( sec_per_Myr_local / internalu.tbase1; // COLIBRE reference values S_ref = 0.3, a_ref = 0.1 micron; non-positive // parameters disable growth via an effectively infinite timescale. - double sticking_factor = - (my_chemistry->dust_growth_sticking_coeff > 0.0) - ? 0.3 / my_chemistry->dust_growth_sticking_coeff - : huge_value; - double grain_size_factor = - (my_chemistry->dust_grainsize > 0.0) - ? my_chemistry->dust_grainsize / 0.1 - : huge_value; + double sticking_factor = (my_chemistry->dust_growth_sticking_coeff > 0.0) + ? 0.3 / my_chemistry->dust_growth_sticking_coeff + : huge_value; + double grain_size_factor = (my_chemistry->dust_grainsize > 0.0) + ? my_chemistry->dust_grainsize / 0.1 + : huge_value; // Solar abundances (per H mass, then per H nucleus) of the tracked // elements, used as the depletion reference in the tau scalings. double solar_nonmetal = std::max(1.0 - my_chemistry->SolarMetalFractionByMass, tiny_value); - double solar_H = std::max(my_chemistry->HydrogenFractionByMass * - solar_nonmetal, tiny_value); - double solar_mass_C = my_chemistry->SolarMetalFractionByMass * - solar_frac_C / solar_H; - double solar_mass_O = my_chemistry->SolarMetalFractionByMass * - solar_frac_O / solar_H; - double solar_mass_Mg = my_chemistry->SolarMetalFractionByMass * - solar_frac_Mg / solar_H; - double solar_mass_Si = my_chemistry->SolarMetalFractionByMass * - solar_frac_Si / solar_H; - double solar_mass_Fe = my_chemistry->SolarMetalFractionByMass * - solar_frac_Fe / solar_H; + double solar_H = std::max( + my_chemistry->HydrogenFractionByMass * solar_nonmetal, tiny_value); + double solar_mass_C = + my_chemistry->SolarMetalFractionByMass * solar_frac_C / solar_H; + double solar_mass_O = + my_chemistry->SolarMetalFractionByMass * solar_frac_O / solar_H; + double solar_mass_Mg = + my_chemistry->SolarMetalFractionByMass * solar_frac_Mg / solar_H; + double solar_mass_Si = + my_chemistry->SolarMetalFractionByMass * solar_frac_Si / solar_H; + double solar_mass_Fe = + my_chemistry->SolarMetalFractionByMass * solar_frac_Fe / solar_H; double solar_eps_C = solar_mass_C / atomic_C; double solar_eps_O = solar_mass_O / atomic_O; double solar_eps_Mg = solar_mass_Mg / atomic_Mg; @@ -295,8 +291,8 @@ void grackle::impl::dust_growth_species( // factor_max at nH_max. auto clumping_factor = [&](double nH) -> double { double cmax = std::max(my_chemistry->dust_growth_clumping_factor_max, 1.0); - double nmin = std::max(my_chemistry->dust_growth_clumping_nH_min, - tiny_value); + double nmin = + std::max(my_chemistry->dust_growth_clumping_nH_min, tiny_value); double nmax = std::max(my_chemistry->dust_growth_clumping_nH_max, nmin * (1.0 + 1.0e-12)); if (nH <= nmin || cmax <= 1.0) return 1.0; @@ -317,13 +313,13 @@ void grackle::impl::dust_growth_species( double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); - double rho_C = mC(i, idx_range.j, idx_range.k); - double rho_O = mO(i, idx_range.j, idx_range.k); + double rho_C = mC(i, idx_range.j, idx_range.k); + double rho_O = mO(i, idx_range.j, idx_range.k); double rho_Mg = mMg(i, idx_range.j, idx_range.k); double rho_Si = mSi(i, idx_range.j, idx_range.k); double rho_Fe = mFe(i, idx_range.j, idx_range.k); - double T = std::max(t_gas[i], tiny_value); + double T = std::max(t_gas[i], tiny_value); double dt = dt_value[i]; double rho_H_nuclei = h_census.rho_H(i, idx_range.j, idx_range.k); @@ -340,32 +336,31 @@ void grackle::impl::dust_growth_species( std::pow(colibre_growth_t_ref / T, 0.5) * grain_size_factor * sticking_factor; double inv_rho_H = 1.0 / rho_H_nuclei; - double eps_C = rho_C * inv_rho_H / atomic_C; - double eps_O = rho_O * inv_rho_H / atomic_O; + double eps_C = rho_C * inv_rho_H / atomic_C; + double eps_O = rho_O * inv_rho_H / atomic_O; double eps_Mg = rho_Mg * inv_rho_H / atomic_Mg; double eps_Si = rho_Si * inv_rho_H / atomic_Si; double eps_Fe = rho_Fe * inv_rho_H / atomic_Fe; double eps_MgFe = eps_Mg + eps_Fe; // ---------- Carbonaceous: rate-limited by gas-phase C ---------- - if (rho_C >= metal_gate_threshold * rho_gas && - rho_dust_carb_i > 0.0 && dt > 0.0) { - double tau_accr_carb = tau_ref_carb * accr_struct * - abundance_ratio(solar_eps_C, eps_C); + if (rho_C >= metal_gate_threshold * rho_gas && rho_dust_carb_i > 0.0 && + dt > 0.0) { + double tau_accr_carb = + tau_ref_carb * accr_struct * abundance_ratio(solar_eps_C, eps_C); tau_accr_carb = std::clamp(tau_accr_carb, tiny_value, huge_value); - double rate = e_fold_growth_rate(rho_dust_carb_i, dt, - tau_accr_carb); + double rate = e_fold_growth_rate(rho_dust_carb_i, dt, tau_accr_carb); growth_dM_carbon[i] = std::min(rate, rho_C / dt); } // ---------- Silicate: COLIBRE Mg+Fe composite bottleneck ---------- double rho_dust_sil_i = rho_dust_mg_sil_i + rho_dust_fe_sil_i; - if (rho_dust_sil_i > 0.0 && dt > 0.0 && - eps_O > 0.0 && eps_Si > 0.0 && eps_MgFe > 0.0) { - double sil_ratio = std::max({ - abundance_ratio(solar_eps_O, eps_O), - abundance_ratio(solar_eps_Si, eps_Si), - abundance_ratio(solar_eps_MgFe, eps_MgFe)}); + if (rho_dust_sil_i > 0.0 && dt > 0.0 && eps_O > 0.0 && eps_Si > 0.0 && + eps_MgFe > 0.0) { + double sil_ratio = + std::max({abundance_ratio(solar_eps_O, eps_O), + abundance_ratio(solar_eps_Si, eps_Si), + abundance_ratio(solar_eps_MgFe, eps_MgFe)}); double tau_accr = tau_ref_sil * accr_struct * sil_ratio; tau_accr = std::clamp(tau_accr, tiny_value, huge_value); double sil_rate = e_fold_growth_rate(rho_dust_sil_i, dt, tau_accr); @@ -374,10 +369,10 @@ void grackle::impl::dust_growth_species( // Mass fractions include the different Mg2SiO4 / Fe2SiO4 molecule // weights, so equal Mg and Fe number abundance reproduces the // COLIBRE equal-molecule seed split. - double mg_weight = eps_Mg * ( - 2.0 * atomic_Mg + atomic_Si + 4.0 * atomic_O); - double fe_weight = eps_Fe * ( - 2.0 * atomic_Fe + atomic_Si + 4.0 * atomic_O); + double mg_weight = + eps_Mg * (2.0 * atomic_Mg + atomic_Si + 4.0 * atomic_O); + double fe_weight = + eps_Fe * (2.0 * atomic_Fe + atomic_Si + 4.0 * atomic_O); double endmember_weight = mg_weight + fe_weight; if (endmember_weight > 0.0) { double mg_frac = std::clamp(mg_weight / endmember_weight, 0.0, 1.0); @@ -454,8 +449,8 @@ void grackle::impl::dust_destruction( // Ms100 * sne_this / dt_full is the shocked-gas mass rate and // tau_dest the time to shock all gas in the cell. double tau_dest = - rho_gas / - (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * dt_full; + rho_gas / (Ms100 * sne_this * my_chemistry->dust_destruction_eff) * + dt_full; dM_shock = std::min(rho_dust / tau_dest, rho_dust / dt); } @@ -468,8 +463,8 @@ void grackle::impl::dust_destruction( (my_chemistry->dust_grainsize / 0.1) * (1.0e-27 / (dens_proper * rho_gas)) * (std::pow((2.0e6 / temp), 2.5) + 1.0); - dM_shock = std::min(dM_shock + 3.0 * rho_dust / tau_sput, - rho_dust / dt); + dM_shock = + std::min(dM_shock + 3.0 * rho_dust / tau_sput, rho_dust / dt); } if (std::isnan(dM_shock)) { @@ -498,12 +493,10 @@ void grackle::impl::dust_destruction( // per-time rate for dust_update_species(). void grackle::impl::dust_destruction_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, const double* dt_value, double dt_full, - const double* t_gas, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, double dt_full, const double* t_gas, double* destruction_dM_mg_silicate, double* destruction_dM_fe_silicate, double* destruction_dM_carbon) { - grackle::impl::View d( my_fields->density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); @@ -540,12 +533,12 @@ void grackle::impl::dust_destruction_species( // Species-specific shock-vulnerability multipliers. Graphite is the // baseline (1.0); silicate follows the Slavin+2015 SNR gas-cleared mass // ratio, 990/600 = 1.65. - const double shock_factor_carbon = 1.0; + const double shock_factor_carbon = 1.0; const double shock_factor_silicate = 1.65; // COLIBRE/Tsai-Mathews thermal sputtering tau_ref (stored in Myr). - double tau_sput_ref = my_chemistry->dust_sputter_tauref * - sec_per_Myr_local / internalu.tbase1; + double tau_sput_ref = + my_chemistry->dust_sputter_tauref * sec_per_Myr_local / internalu.tbase1; for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { destruction_dM_mg_silicate[i] = 0.0; @@ -558,10 +551,8 @@ void grackle::impl::dust_destruction_species( double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); - bool mg_sil_active = - (rho_dust_mg_sil_i >= dust_gate_threshold * rho_gas); - bool fe_sil_active = - (rho_dust_fe_sil_i >= dust_gate_threshold * rho_gas); + bool mg_sil_active = (rho_dust_mg_sil_i >= dust_gate_threshold * rho_gas); + bool fe_sil_active = (rho_dust_fe_sil_i >= dust_gate_threshold * rho_gas); bool carb_active = (rho_dust_carb_i >= dust_gate_threshold * rho_gas); if (!mg_sil_active && !fe_sil_active && !carb_active) continue; if (rho_gas <= 0.0) continue; @@ -577,10 +568,8 @@ void grackle::impl::dust_destruction_species( if (nH <= 0.0) continue; // Common (species-independent) sputtering structural factor. - double sput_struct = (my_chemistry->dust_grainsize / 0.1) * - (1.0 / nH) * - (1.0 + std::pow(temp / colibre_sputter_t_ref, - -2.5)); + double sput_struct = (my_chemistry->dust_grainsize / 0.1) * (1.0 / nH) * + (1.0 + std::pow(temp / colibre_sputter_t_ref, -2.5)); // Equivalent per-time destruction rate for one species; its product // with dt is an exponential e-folding mass loss. @@ -602,9 +591,9 @@ void grackle::impl::dust_destruction_species( // shocked-gas mass per volume in dt_full, and dividing by // (rho_gas * dt_full) gives the inverse shock-destruction // timescale. The exponential update supplies the dt dependence. - double inv_tau_shock = - Ms100 * shock_factor * sne_this * - my_chemistry->dust_destruction_eff / (rho_gas * dt_full); + double inv_tau_shock = Ms100 * shock_factor * sne_this * + my_chemistry->dust_destruction_eff / + (rho_gas * dt_full); if (inv_tau_shock > 0.0 && std::isfinite(inv_tau_shock)) { inv_tau_loss += inv_tau_shock; } @@ -624,16 +613,16 @@ void grackle::impl::dust_destruction_species( }; if (carb_active) { - destruction_dM_carbon[i] = compute_dM( - rho_dust_carb_i, shock_factor_carbon); + destruction_dM_carbon[i] = + compute_dM(rho_dust_carb_i, shock_factor_carbon); } if (mg_sil_active) { - destruction_dM_mg_silicate[i] = compute_dM( - rho_dust_mg_sil_i, shock_factor_silicate); + destruction_dM_mg_silicate[i] = + compute_dM(rho_dust_mg_sil_i, shock_factor_silicate); } if (fe_sil_active) { - destruction_dM_fe_silicate[i] = compute_dM( - rho_dust_fe_sil_i, shock_factor_silicate); + destruction_dM_fe_silicate[i] = + compute_dM(rho_dust_fe_sil_i, shock_factor_silicate); } } } @@ -700,11 +689,11 @@ void grackle::impl::dust_update_species( const double f_mg_sil_Mg = my_chemistry->dust_mg_silicate_f_Mg; const double f_mg_sil_Fe = my_chemistry->dust_mg_silicate_f_Fe; const double f_mg_sil_Si = my_chemistry->dust_mg_silicate_f_Si; - const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; + const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; const double f_fe_sil_Mg = my_chemistry->dust_fe_silicate_f_Mg; const double f_fe_sil_Fe = my_chemistry->dust_fe_silicate_f_Fe; const double f_fe_sil_Si = my_chemistry->dust_fe_silicate_f_Si; - const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; + const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; for (int i = idx_range.i_start; i < idx_range.i_stop; i++) { if (itmask[i] == MASK_FALSE) continue; @@ -714,8 +703,8 @@ void grackle::impl::dust_update_species( double rho_dust_fe_sil_i = dust_fe_sil(i, idx_range.j, idx_range.k); double rho_dust_carb_i = dust_carb(i, idx_range.j, idx_range.k); double rho_metal_total = metal(i, idx_range.j, idx_range.k); - double rho_C = mC(i, idx_range.j, idx_range.k); - double rho_O = mO(i, idx_range.j, idx_range.k); + double rho_C = mC(i, idx_range.j, idx_range.k); + double rho_O = mO(i, idx_range.j, idx_range.k); double rho_Mg = mMg(i, idx_range.j, idx_range.k); double rho_Si = mSi(i, idx_range.j, idx_range.k); double rho_Fe = mFe(i, idx_range.j, idx_range.k); @@ -733,10 +722,10 @@ void grackle::impl::dust_update_species( // dM > 0 means net growth (gas reactants -> dust); // dM < 0 means net destruction (dust -> gas reactants). double dM_carb = (growth_dM_carbon[i] + destruction_dM_carbon[i]) * dt; - double dM_mg_sil = (growth_dM_mg_silicate[i] + - destruction_dM_mg_silicate[i]) * dt; - double dM_fe_sil = (growth_dM_fe_silicate[i] + - destruction_dM_fe_silicate[i]) * dt; + double dM_mg_sil = + (growth_dM_mg_silicate[i] + destruction_dM_mg_silicate[i]) * dt; + double dM_fe_sil = + (growth_dM_fe_silicate[i] + destruction_dM_fe_silicate[i]) * dt; // ---------- Per-channel caps ---------- // Carbon channel: bounded by rho_C (growth) or rho_dust_carb @@ -775,8 +764,7 @@ void grackle::impl::dust_update_species( int limiter = -1; // 0=Mg, 1=Fe, 2=Si, 3=O auto consider_element = [&](double rho_X, double c_mg_sil, double c_fe_sil, int element_id) { - double need = dM_mg_sil_grow * c_mg_sil + - dM_fe_sil_grow * c_fe_sil; + double need = dM_mg_sil_grow * c_mg_sil + dM_fe_sil_grow * c_fe_sil; double budget = sil_grow_safety * rho_X; if (need > budget && need > 0.0) { double trial = std::max(0.0, budget / need); @@ -789,7 +777,7 @@ void grackle::impl::dust_update_species( consider_element(rho_Mg, f_mg_sil_Mg, f_fe_sil_Mg, 0); consider_element(rho_Fe, f_mg_sil_Fe, f_fe_sil_Fe, 1); consider_element(rho_Si, f_mg_sil_Si, f_fe_sil_Si, 2); - consider_element(rho_O, f_mg_sil_O, f_fe_sil_O, 3); + consider_element(rho_O, f_mg_sil_O, f_fe_sil_O, 3); if (limiter < 0 || scale >= 1.0) break; if (limiter == 0) { @@ -815,21 +803,21 @@ void grackle::impl::dust_update_species( // ---------- Apply ---------- rho_dust_carb_i += dM_carb; - rho_C -= dM_carb; + rho_C -= dM_carb; rho_dust_mg_sil_i += dM_mg_sil; rho_dust_fe_sil_i += dM_fe_sil; rho_Mg -= dM_mg_sil * f_mg_sil_Mg + dM_fe_sil * f_fe_sil_Mg; rho_Fe -= dM_mg_sil * f_mg_sil_Fe + dM_fe_sil * f_fe_sil_Fe; rho_Si -= dM_mg_sil * f_mg_sil_Si + dM_fe_sil * f_fe_sil_Si; - rho_O -= dM_mg_sil * f_mg_sil_O + dM_fe_sil * f_fe_sil_O; + rho_O -= dM_mg_sil * f_mg_sil_O + dM_fe_sil * f_fe_sil_O; // Floors / NaN guard rho_dust_carb_i = std::max(0.0, rho_dust_carb_i); rho_dust_mg_sil_i = std::max(0.0, rho_dust_mg_sil_i); rho_dust_fe_sil_i = std::max(0.0, rho_dust_fe_sil_i); - rho_C = std::max(0.0, rho_C); - rho_O = std::max(0.0, rho_O); + rho_C = std::max(0.0, rho_C); + rho_O = std::max(0.0, rho_O); rho_Mg = std::max(0.0, rho_Mg); rho_Si = std::max(0.0, rho_Si); rho_Fe = std::max(0.0, rho_Fe); @@ -843,8 +831,7 @@ void grackle::impl::dust_update_species( // untracked remainder; this stays consistent even if user-edited silicate // fractions do not sum to exactly one. double rho_tracked_after = rho_C + rho_O + rho_Mg + rho_Si + rho_Fe; - double rho_metal_new = - std::max(rho_metal_other + rho_tracked_after, 0.0); + double rho_metal_new = std::max(rho_metal_other + rho_tracked_after, 0.0); double delta_metal_total = rho_metal_new - rho_metal_total; // Gas density tracks metals as a subset (dust is not part of `density`). @@ -853,25 +840,24 @@ void grackle::impl::dust_update_species( if (std::isnan(rho_dust_new) || std::isnan(rho_metal_new) || std::isnan(rho_gas)) { std::cout << "dust_update_species: NaN at cell " << i - << " dM_carb=" << dM_carb - << " dM_mg_sil=" << dM_mg_sil + << " dM_carb=" << dM_carb << " dM_mg_sil=" << dM_mg_sil << " dM_fe_sil=" << dM_fe_sil << std::endl; continue; } if (!dryrun) { - dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_new; - dust_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_sil_i; + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_new; + dust_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_sil_i; dust_mg_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_mg_sil_i; dust_fe_sil(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_fe_sil_i; dust_carb(i, idx_range.j, idx_range.k) = (gr_float)rho_dust_carb_i; - metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_new; - mC(i, idx_range.j, idx_range.k) = (gr_float)rho_C; - mO(i, idx_range.j, idx_range.k) = (gr_float)rho_O; - mMg(i, idx_range.j, idx_range.k) = (gr_float)rho_Mg; - mSi(i, idx_range.j, idx_range.k) = (gr_float)rho_Si; - mFe(i, idx_range.j, idx_range.k) = (gr_float)rho_Fe; - d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_new; + mC(i, idx_range.j, idx_range.k) = (gr_float)rho_C; + mO(i, idx_range.j, idx_range.k) = (gr_float)rho_O; + mMg(i, idx_range.j, idx_range.k) = (gr_float)rho_Mg; + mSi(i, idx_range.j, idx_range.k) = (gr_float)rho_Si; + mFe(i, idx_range.j, idx_range.k) = (gr_float)rho_Fe; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; } } } @@ -910,17 +896,17 @@ void grackle::impl::dust_update(chemistry_data* my_chemistry, dM_exchange = std::max(-rho_dust, dM_exchange); dM_exchange = std::min(0.9 * rho_metal_total, dM_exchange); - rho_dust += dM_exchange; - rho_metal_total -= dM_exchange; - rho_gas -= dM_exchange; // gas tracks metals as a subset + rho_dust += dM_exchange; + rho_metal_total -= dM_exchange; + rho_gas -= dM_exchange; // gas tracks metals as a subset - rho_dust = std::max(0.0, rho_dust); + rho_dust = std::max(0.0, rho_dust); rho_metal_total = std::max(0.0, rho_metal_total); if (!dryrun) { - dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; + dust(i, idx_range.j, idx_range.k) = (gr_float)rho_dust; metal(i, idx_range.j, idx_range.k) = (gr_float)rho_metal_total; - d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; + d(i, idx_range.j, idx_range.k) = (gr_float)rho_gas; } } } diff --git a/src/clib/dust/dust_growth_and_destruction.hpp b/src/clib/dust/dust_growth_and_destruction.hpp index ca6e4410a..777c77365 100644 --- a/src/clib/dust/dust_growth_and_destruction.hpp +++ b/src/clib/dust/dust_growth_and_destruction.hpp @@ -33,8 +33,8 @@ void dust_growth(chemistry_data* my_chemistry, grackle_field_data* my_fields, // exponential e-folding mass update. void dust_growth_species( chemistry_data* my_chemistry, grackle_field_data* my_fields, - InternalGrUnits internalu, IndexRange idx_range, - const gr_mask_type* itmask, const double* dt_value, const double* t_gas, + InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, + const double* dt_value, const double* t_gas, double* growth_dM_mg_silicate, // output: Mg-silicate accretion rate double* growth_dM_fe_silicate, // output: Fe-silicate accretion rate double* growth_dM_carbon // output: carbonaceous accretion rate @@ -68,7 +68,7 @@ void dust_destruction_species( const double* dt_value, double dt_full, const double* t_gas, double* destruction_dM_mg_silicate, // output: Mg-silicate destruction rate double* destruction_dM_fe_silicate, // output: Fe-silicate destruction rate - double* destruction_dM_carbon // output: carbonaceous destruction rate + double* destruction_dM_carbon // output: carbonaceous destruction rate ); // Update the density fields using calculated mass changes. @@ -76,8 +76,8 @@ void dust_update( chemistry_data* my_chemistry, grackle_field_data* my_fields, InternalGrUnits internalu, IndexRange idx_range, const gr_mask_type* itmask, const double* dt_value, - const double* growth_dM, // input: mass change from growth - const double* destruction_dM, // input: mass change from destruction + const double* growth_dM, // input: mass change from growth + const double* destruction_dM, // input: mass change from destruction bool dryrun); // Species-specific field update for the split-silicate path @@ -95,9 +95,12 @@ void dust_update_species( const double* growth_dM_mg_silicate, // input: Mg-silicate accretion rate const double* growth_dM_fe_silicate, // input: Fe-silicate accretion rate const double* growth_dM_carbon, // input: carbonaceous accretion rate - const double* destruction_dM_mg_silicate, // input: Mg-silicate destruction rate (<=0) - const double* destruction_dM_fe_silicate, // input: Fe-silicate destruction rate (<=0) - const double* destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) + const double* destruction_dM_mg_silicate, // input: Mg-silicate destruction + // rate (<=0) + const double* destruction_dM_fe_silicate, // input: Fe-silicate destruction + // rate (<=0) + const double* + destruction_dM_carbon, // input: carbonaceous destruction rate (<=0) bool dryrun); } // namespace grackle::impl diff --git a/src/clib/make_consistent.cpp b/src/clib/make_consistent.cpp index 73626bcd0..c5f2fc3cb 100644 --- a/src/clib/make_consistent.cpp +++ b/src/clib/make_consistent.cpp @@ -241,8 +241,8 @@ void make_consistent( // gas/dust/total nuclide arrays (Cg/Cd/Ct etc.) are populated directly // from our tracked metal_density_X + dust species fields, overriding the // SN-yield-derived computation below. Views are constructed once here. - grackle::impl::View mC_view, mO_view, mMg_view, - mSi_view, mFe_view, dust_sil_view, dust_mg_sil_view, dust_fe_sil_view, + grackle::impl::View mC_view, mO_view, mMg_view, mSi_view, + mFe_view, dust_sil_view, dust_mg_sil_view, dust_fe_sil_view, dust_carb_view; if (my_chemistry->dust_species_track == 1) { mC_view = grackle::impl::View( @@ -416,7 +416,6 @@ void make_consistent( Sg[i] = Sg[i] + onlygas_metal_yields.S[iSN] * cur_val; Feg[i] = Feg[i] + onlygas_metal_yields.Fe[iSN] * cur_val; } - } for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { @@ -435,16 +434,16 @@ void make_consistent( // stoichiometric mass fractions. Al and S are not part of this // architecture, so their per-element arrays are zeroed. if (my_chemistry->dust_species_track == 1) { - const double f_mg_sil = std::clamp( - my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); + const double f_mg_sil = + std::clamp(my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); const double f_mg_sil_Mg = my_chemistry->dust_mg_silicate_f_Mg; const double f_mg_sil_Fe = my_chemistry->dust_mg_silicate_f_Fe; const double f_mg_sil_Si = my_chemistry->dust_mg_silicate_f_Si; - const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; + const double f_mg_sil_O = my_chemistry->dust_mg_silicate_f_O; const double f_fe_sil_Mg = my_chemistry->dust_fe_silicate_f_Mg; const double f_fe_sil_Fe = my_chemistry->dust_fe_silicate_f_Fe; const double f_fe_sil_Si = my_chemistry->dust_fe_silicate_f_Si; - const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; + const double f_fe_sil_O = my_chemistry->dust_fe_silicate_f_O; for (i = my_fields->grid_start[0]; i <= my_fields->grid_end[0]; i++) { double mg_sil = dust_mg_sil_view(i, j, k); double fe_sil = dust_fe_sil_view(i, j, k); @@ -454,23 +453,27 @@ void make_consistent( fe_sil = (1.0 - f_mg_sil) * sil_compat; } const double carb = dust_carb_view(i, j, k); - Cg[i] = mC_view(i, j, k); - Og[i] = mO_view(i, j, k); + Cg[i] = mC_view(i, j, k); + Og[i] = mO_view(i, j, k); Mgg[i] = mMg_view(i, j, k); Sig[i] = mSi_view(i, j, k); Feg[i] = mFe_view(i, j, k); - Cd[i] = carb; - Od[i] = mg_sil * f_mg_sil_O + fe_sil * f_fe_sil_O; + Cd[i] = carb; + Od[i] = mg_sil * f_mg_sil_O + fe_sil * f_fe_sil_O; Mgd[i] = mg_sil * f_mg_sil_Mg + fe_sil * f_fe_sil_Mg; Sid[i] = mg_sil * f_mg_sil_Si + fe_sil * f_fe_sil_Si; Fed[i] = mg_sil * f_mg_sil_Fe + fe_sil * f_fe_sil_Fe; - Ct[i] = Cg[i] + Cd[i]; - Ot[i] = Og[i] + Od[i]; + Ct[i] = Cg[i] + Cd[i]; + Ot[i] = Og[i] + Od[i]; Mgt[i] = Mgg[i] + Mgd[i]; Sit[i] = Sig[i] + Sid[i]; Fet[i] = Feg[i] + Fed[i]; - Alt[i] = 0.; Alg[i] = 0.; Ald[i] = 0.; - St[i] = 0.; Sg[i] = 0.; Sd[i] = 0.; + Alt[i] = 0.; + Alg[i] = 0.; + Ald[i] = 0.; + St[i] = 0.; + Sg[i] = 0.; + Sd[i] = 0.; } } @@ -618,7 +621,7 @@ void make_consistent( // ! & min(1.e6_DKIND/(metal(i,j,k)/d(i,j,k)/0.02d-4)**2 // ! & ,1.e6_DKIND)) then if (my_chemistry->dust_species_track || - ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || + ((imetal == 0) && (d(i, j, k) * dom < 1.e8)) || ((imetal == 1) && (((metal(i, j, k) <= 1.e-9 * d(i, j, k)) && (d(i, j, k) * dom < 1.e8)) || ((metal(i, j, k) > 1.e-9 * d(i, j, k)) && @@ -1078,27 +1081,24 @@ void make_consistent( // Phase E invariant: bulk dust_density = Mg-silicate + Fe-silicate // + carbonaceous, with dust_density_silicate maintained as the silicate sum. // dust_update_species() maintains this per-cell, but external mutations - // (host injection, inject_pathway writes) can break it before make_consistent. - // Re-derive here so downstream consumers (calc_tdust_3d.cpp, cooling tables) - // see a consistent bulk field. + // (host injection, inject_pathway writes) can break it before + // make_consistent. Re-derive here so downstream consumers (calc_tdust_3d.cpp, + // cooling tables) see a consistent bulk field. if (my_chemistry->dust_species_track == 1) { - const double f_mg_sil = std::clamp( - my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); + const double f_mg_sil = + std::clamp(my_chemistry->dust_silicate_mg_fraction, 0.0, 1.0); grackle::impl::View dust_bulk( my_fields->dust_density, my_fields->grid_dimension[0], my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_sil( - my_fields->dust_density_silicate, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); + my_fields->dust_density_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_mg_sil( - my_fields->dust_density_mg_silicate, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); + my_fields->dust_density_mg_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_fe_sil( - my_fields->dust_density_fe_silicate, - my_fields->grid_dimension[0], my_fields->grid_dimension[1], - my_fields->grid_dimension[2]); + my_fields->dust_density_fe_silicate, my_fields->grid_dimension[0], + my_fields->grid_dimension[1], my_fields->grid_dimension[2]); grackle::impl::View dust_carb( const_cast(my_fields->dust_density_carbonaceous), my_fields->grid_dimension[0], my_fields->grid_dimension[1], diff --git a/src/clib/tabulated/calc_temp1d_cloudy.cpp b/src/clib/tabulated/calc_temp1d_cloudy.cpp index 7714046f1..f471cb36b 100644 --- a/src/clib/tabulated/calc_temp1d_cloudy.cpp +++ b/src/clib/tabulated/calc_temp1d_cloudy.cpp @@ -141,9 +141,7 @@ void calc_temp1d_cloudy(const double* rhoH, double* tgas, double* mmw, if (imetal == 1) { double total_metal_1d = metal(i, idx_range.j, idx_range.k); munew = d(i, idx_range.j, idx_range.k) / - ((d(i, idx_range.j, idx_range.k) - - total_metal_1d) / - munew + + ((d(i, idx_range.j, idx_range.k) - total_metal_1d) / munew + total_metal_1d / mu_metal); tgas[i] = tgas[i] * munew / muold; }