src/configuration.jl

Snapshot from this documentation build. SHA-256: 0bd3d23ba230dc2266da7f803b7e2a4c54b730f678724fe7c835f6a6ab13125a

    1 "Validated TOML settings for a reproducible simulation launched in a fresh Julia process."
    2 struct RunConfig
    3     settings::Dict{String,Any}
    4     source_path::String
    5 end
    6 
    7 const CONFIG_KEYS = Dict(
    8     "run" => Dict("n"=>"MHD_PMF_N", "backend"=>"MHD_BACKEND", "precision"=>"MHD_PRECISION",
    9         "steps"=>"MHD_STEPS", "max_cycles"=>"MHD_MAX_CYCLES", "cfl"=>"MHD_CFL",
   10         "reconstruction"=>"MHD_RECON", "riemann"=>"MHD_RIEMANN",
   11         "print_every"=>"MHD_PRINT_EVERY", "check_every"=>"MHD_CHECK_EVERY",
   12         "checkpoint_every"=>"MHD_CHECKPOINT_EVERY", "checkpoint_final"=>"MHD_CHECKPOINT_FINAL",
   13         "resume"=>"MHD_CHECKPOINT_RESUME", "mhd_solver"=>"MHD_SOLVER"),
   14     "cosmology" => Dict("zstart"=>"MHD_ZSTART", "zend"=>"MHD_ZEND", "h"=>"MHD_H",
   15         "omega_m"=>"MHD_OM", "omega_b"=>"MHD_OB", "omega_lambda"=>"MHD_OL",
   16         "omega_r"=>"MHD_OR", "box_ckpc_h"=>"MHD_TERMINAL_LBOX_CKPC",
   17         "max_dln_a"=>"MHD_MAXEXP"),
   18     "streaming" => Dict("vx_kms"=>"MHD_STREAMING_VX_KMS", "vy_kms"=>"MHD_STREAMING_VY_KMS",
   19         "vz_kms"=>"MHD_STREAMING_VZ_KMS"),
   20     "magnetic" => Dict("invariant_axis"=>"MHD_PMF_INVARIANT_AXIS", "initial_condition"=>"MHD_PMF_INIT", "b0_ng"=>"MHD_PMF_B0_NG",
   21         "brms_code"=>"MHD_PMF_BRMS", "seed"=>"MHD_PMF_SEED", "kcut"=>"MHD_PMF_KCUT",
   22         "kmax"=>"MHD_PMF_KMAX", "kmin"=>"MHD_PMF_KMIN", "mode_lock_n"=>"MHD_PMF_MODE_LOCK_N",
   23         "curl_symbol"=>"MHD_PMF_CURL_SYMBOL", "curl_reference_n"=>"MHD_PMF_CURL_REFERENCE_N",
   24         "normalization"=>"MHD_PMF_NORMALIZATION_KIND", "fixed_mode_power"=>"MHD_PMF_FIXED_MODE_POWER",
   25         "reference_kmax"=>"MHD_PMF_PRESERVE_LOW_KMAX", "reference_kcut"=>"MHD_PMF_PRESERVE_LOW_KCUT",
   26         "minimum_cells_per_wavelength"=>"MHD_PMF_MIN_CELLS_PER_WAVELENGTH",
   27         "mode_realization"=>"MHD_PMF_MODE_REALIZATION", "realization_box"=>"MHD_PMF_REALIZATION_BOX"),
   28     "chemistry" => Dict("mode"=>"MHD_CHEM", "helium"=>"MHD_CHEM_HELIUM",
   29         "coupling"=>"MHD_CHEM_COUPLING", "expansion"=>"MHD_CHEM_EXPANSION",
   30         "smoothing"=>"MHD_CHEM_SMOOTH", "smoothing_sigma_cells"=>"MHD_CHEM_SMOOTH_SIGMA_CELLS",
   31         "dt_fraction"=>"MHD_CHEM_DTFRAC", "hydrogen_storage"=>"MHD_CHEM_H_STORAGE",
   32         "ionized_fast_zmin"=>"MHD_CHEM_IONIZED_FAST_ZMIN", "velocity_unit_cms"=>"MHD_CHEM_VUNIT",
   33         "x_hii_initial"=>"MHD_XHII_INIT", "x_h2_initial"=>"MHD_XH2_INIT",
   34         "nonlocal_table"=>"MHD_CHEM_NONLOCAL_TABLE"),
   35     "physics" => Dict("gravity"=>"MHD_GRAVITY", "gas_gravity"=>"MHD_GRAVITY_GAS_KICK",
   36         "gravity_drag_max_impulse"=>"MHD_GRAVITY_DRAG_MAX_IMPULSE",
   37         "photon_comoving_initially"=>"MHD_RADIATION_PHOTON_COMOVING",
   38         "photon_adiabatic_initially"=>"MHD_RADIATION_PHOTON_ADIABATIC",
   39         "compton_drag"=>"MHD_COMPTON_DRAG", "drag_mode"=>"MHD_DRAG_DT_MODE",
   40         "photon_response"=>"MHD_RADIATION_RESPONSE_MODEL", "photon_max_dln_a"=>"MHD_RADIATION_MAX_DLNA",
   41         "nonlinear_cfl"=>"MHD_RADIATION_NONLINEAR_CFL", "fft"=>"MHD_FFT",
   42         "pressure"=>"MHD_P0", "scaling"=>"MHD_COSMOLOGICAL_MHD_SCALING",
   43         "glm_project_every"=>"MHD_GLM_PROJECT_EVERY", "glm_ch_factor"=>"MHD_GLM_CH_FAC",
   44         "particle_max_displacement_cells"=>"MHD_PARTICLE_MAX_DISP_CELLS"),
   45     "diagnostics" => Dict("redshifts"=>"MHD_PK_ZS", "every"=>"MHD_PK_EVERY",
   46         "adaptive_fraction"=>"MHD_ADAPTIVE_DIAG_REL", "spectrum_bins"=>"MHD_PK_NBINS",
   47         "angle_bins"=>"MHD_PK_NMU", "pdf_bins"=>"MHD_PDF_NBINS",
   48         "energy_ledger_every"=>"MHD_RADIATION_ENERGY_LEDGER_EVERY",
   49         "streaming"=>"MHD_STREAMING_TSV", "frame_redshifts"=>"MHD_DENSITY_FRAME_ZS", "frame_field"=>"MHD_DENSITY_FRAME_FIELD"),
   50 )
   51 
   52 RunConfig(settings::AbstractDict) = validate_config(RunConfig(Dict{String,Any}(settings), ""))
   53 load_config(path::AbstractString) = validate_config(RunConfig(TOML.parsefile(path), abspath(path)))
   54 _setting(c, section, key, default) = get(get(c.settings, section, Dict()), key, default)
   55 _envstr(x::Bool) = x ? "true" : "false"
   56 _envstr(x::AbstractVector) = join(_envstr.(x), ",")
   57 _envstr(x) = string(x)
   58 
   59 """
   60 Largest accepted CFL number per MHD solver.  Measured (CPU, Float64, 16^3 and 32^3 periodic
   61 box, oblique wave plus white noise, `tools/cfl_stability/cfl_stability.jl`): the GLM driver
   62 (`ch = 2 smax`, dt = cfl dx / ch) is stable to cfl = 0.265 and unstable from 0.27, CT
   63 (dt = cfl dx / smax) is stable to 0.33 and unstable from 0.34 (both: the unsplit scheme
   64 without corner-transport terms has the classical 3D limit near 1/3).  The accepted values
   65 keep a margin of about 6 percent (GLM) and 9 percent (CT) below the lowest stable edge measured.
   66 """
   67 const MAX_STABLE_CFL = Dict("glm"=>0.25, "ct"=>0.30)
   68 
   69 _parse_env_cfl(x::Real) = x
   70 _parse_env_cfl(x) = something(tryparse(Float64, strip(string(x))), NaN)
   71 
   72 function validate_config(c::RunConfig)
   73     for (section, values) in c.settings
   74         section in keys(CONFIG_KEYS) || section == "environment" ||
   75             throw(ArgumentError("unknown configuration section: $section"))
   76         values isa AbstractDict || throw(ArgumentError("$section must be a TOML table"))
   77         for (key, value) in values
   78             value isa Union{AbstractString,Number,Bool,AbstractVector} ||
   79                 throw(ArgumentError("unsupported value for $section.$key"))
   80             if section == "environment"
   81                 key in known_environment_keys() || throw(ArgumentError("unknown solver option $key"))
   82             else
   83                 haskey(CONFIG_KEYS[section], key) || throw(ArgumentError("unknown option $section.$key"))
   84             end
   85         end
   86     end
   87     n = _setting(c, "run", "n", 16)
   88     n isa Integer && n >= 8 && iseven(n) || throw(ArgumentError("run.n must be an even integer >=8"))
   89     be = _setting(c, "run", "backend", "cpu")
   90     be in ("cpu", "metal", "cuda") || throw(ArgumentError("backend must be cpu, metal, or cuda"))
   91     precision = _setting(c, "run", "precision", "f32")
   92     precision in ("f32", "f64") || throw(ArgumentError("precision must be f32 or f64"))
   93     be != "cpu" && precision != "f32" && throw(ArgumentError("GPU production uses f32"))
   94     solver = _setting(c, "run", "mhd_solver", "glm")
   95     solver isa AbstractString && lowercase(solver) in ("glm", "ct") ||
   96         throw(ArgumentError("run.mhd_solver must be \"glm\" or \"ct\""))
   97     if lowercase(solver) == "ct"
   98         raw = Dict{String,String}("MHD_SOLVER"=>"ct")
   99         for (section, mapping) in CONFIG_KEYS, (key, name) in mapping
  100             section == "diagnostics" && key == "streaming" && continue
  101             value = _setting(c, section, key, nothing)
  102             value === nothing || (raw[name] = _envstr(value))
  103         end
  104         for (key, value) in get(c.settings, "environment", Dict())
  105             raw[key] = _envstr(value)
  106         end
  107         raw["MHD_SOLVER"] = "ct"
  108         _validate_ct_environment(raw)
  109     end
  110     # [environment] overrides the named options (see docs/configuration.qmd), so validate the
  111     # values the driver will actually see.
  112     env_overrides = get(c.settings, "environment", Dict())
  113     cfl = haskey(env_overrides, "MHD_CFL") ? _parse_env_cfl(env_overrides["MHD_CFL"]) :
  114           _setting(c, "run", "cfl", 0.2)
  115     eff_solver = lowercase(string(get(env_overrides, "MHD_SOLVER", solver)))
  116     cfl_max = get(MAX_STABLE_CFL, eff_solver, MAX_STABLE_CFL["glm"])
  117     cfl isa Real && !(cfl isa Bool) && isfinite(cfl) && 0 < cfl <= cfl_max ||
  118         throw(ArgumentError("CFL must be in (0,$cfl_max] for mhd_solver=\"$eff_solver\" " *
  119                             "(measured stability limit of the unsplit 3D MUSCL-Hancock scheme, " *
  120                             "see docs/validation.qmd#sec-cfl-limit)"))
  121     for key in ("steps", "max_cycles")
  122         value = _setting(c, "run", key, 1)
  123         value isa Integer && value > 0 || throw(ArgumentError("run.$key must be positive"))
  124     end
  125     zs = _setting(c, "cosmology", "zstart", nothing)
  126     ze = _setting(c, "cosmology", "zend", nothing)
  127     if zs !== nothing || ze !== nothing
  128         zs isa Real && ze isa Real && isfinite(zs) && isfinite(ze) && zs > ze >= 0 ||
  129             throw(ArgumentError("cosmology requires finite zstart > zend >= 0"))
  130         L = _setting(c, "cosmology", "box_ckpc_h", nothing)
  131         L isa Real && isfinite(L) && L > 0 || throw(ArgumentError("set a positive box_ckpc_h"))
  132         B = _setting(c, "magnetic", "b0_ng", nothing)
  133         B isa Real && isfinite(B) && B > 0 || throw(ArgumentError("set a positive comoving b0_ng"))
  134     end
  135     for key in ("vx_kms","vy_kms","vz_kms")
  136         value = _setting(c,"streaming",key,0)
  137         value isa Real && !(value isa Bool) && isfinite(value) ||
  138             throw(ArgumentError("streaming.$key must be finite km/s"))
  139     end
  140     invariant = _setting(c,"magnetic","invariant_axis",0)
  141     invariant isa Integer && !(invariant isa Bool) && invariant in 0:3 ||
  142         throw(ArgumentError("magnetic.invariant_axis must be 0, 1, 2, or 3"))
  143     _setting(c,"diagnostics","streaming",false) isa Bool ||
  144         throw(ArgumentError("diagnostics.streaming must be boolean"))
  145     _setting(c,"diagnostics","frame_field","gas") in ("gas","dm","both") ||
  146         throw(ArgumentError("diagnostics.frame_field must be gas, dm, or both"))
  147     frames = _setting(c,"diagnostics","frame_redshifts",Float64[])
  148     frames isa AbstractVector && all(z->z isa Real && isfinite(z) && z>=0,frames) ||
  149         throw(ArgumentError("diagnostics.frame_redshifts must be finite nonnegative redshifts"))
  150     return c
  151 end
  152 
  153 """
  154 Reject option combinations that the constrained-transport solver (`MHD_SOLVER=ct`) cannot
  155 honour, before any state is allocated. `env` maps `MHD_*` names to strings (unset = default).
  156 The production driver repeats these checks on its own environment.
  157 """
  158 function _validate_ct_environment(env::AbstractDict)
  159     lowercase(get(env, "MHD_SOLVER", "glm")) == "ct" || return nothing
  160     drag = lowercase(get(env, "MHD_DRAG_DT_MODE", ""))
  161     if drag in ("terminal", "hybrid")
  162         induction = lowercase(get(env, "MHD_TERMINAL_INDUCTION", "centered"))
  163         pressure = lowercase(get(env, "MHD_TERMINAL_PRESSURE_IMPLICIT", "0")) in
  164                    ("1", "true", "yes", "on")
  165         induction == "ssprk2" && throw(ArgumentError(
  166             "mhd_solver=\"ct\" does not support MHD_TERMINAL_INDUCTION=ssprk2 (cell-centred SSPRK2 " *
  167             "with a GLM-style B projection); use ct"))
  168         pressure && induction != "ct" && throw(ArgumentError(
  169             "mhd_solver=\"ct\" with MHD_TERMINAL_PRESSURE_IMPLICIT=1 requires MHD_TERMINAL_INDUCTION=ct"))
  170     end
  171     lowercase(get(env, "MHD_INTEGRATOR", "ref")) == "cube" && throw(ArgumentError(
  172         "mhd_solver=\"ct\" requires MHD_INTEGRATOR=ref; the cube integrators are GLM-only"))
  173     if haskey(env, "MHD_GLM_PROJECT_EVERY")
  174         n = tryparse(Int, strip(env["MHD_GLM_PROJECT_EVERY"]))
  175         n !== nothing && n > 0 && throw(ArgumentError(
  176             "mhd_solver=\"ct\" does not use GLM B projection: MHD_GLM_PROJECT_EVERY=$n " *
  177             "would overwrite cell-centred B; leave it unset (CT default 0)"))
  178     end
  179     nothing
  180 end
  181 
  182 function known_environment_keys()
  183     source = read(joinpath(@__DIR__, "production", "runner.jl"), String)
  184     Set(m.captures[1] for m in eachmatch(r"\"(MHD_[A-Z0-9_]+)\"", source))
  185 end
  186 
  187 "Resolve every supplied setting to the production driver's public environment interface."
  188 function run_environment(c::RunConfig; output_dir::AbstractString)
  189     validate_config(c)
  190     env = Dict("MHD_BACKEND"=>"cpu", "MHD_PRECISION"=>"f32", "MHD_PMF_N"=>"16",
  191         "MHD_STEPS"=>"4", "MHD_CFL"=>"0.2", "MHD_PK_NMU"=>"1",
  192         "MHD_CYCLE_PRINT_EVERY"=>"100", "MHD_WARMUP"=>"0",
  193         "MHD_CHEM_IONIZED_FAST_ZMIN"=>"0", "MHD_CHECKPOINT_FINAL"=>"false",
  194         "MHD_CHECKPOINT_RESUME"=>"false", "MHD_FFT"=>"auto")
  195     if _setting(c, "cosmology", "zstart", nothing) !== nothing
  196         merge!(env, Dict("MHD_P0"=>"auto", "MHD_COMPTON_DRAG"=>"true",
  197             "MHD_DRAG_DT_MODE"=>"radiation", "MHD_RADIATION_RESPONSE_MODEL"=>"moments_exponential",
  198             "MHD_RADIATION_NONLINEAR_CFL"=>"0.1", "MHD_RADIATION_MAX_DLNA"=>"2.5e-4",
  199             "MHD_CHEM"=>"mixing", "MHD_CHEM_EXPANSION"=>"on", "MHD_CHEM_HELIUM"=>"true",
  200             "MHD_CHEM_H_STORAGE"=>"centered", "MHD_CHEM_SMOOTH"=>"gaussian",
  201             "MHD_CHEM_SMOOTH_SIGMA_CELLS"=>"8", "MHD_CHEM_COUPLING"=>"strang",
  202             "MHD_PARTICLE_MAX_DISP_CELLS"=>"1", "MHD_COSMOLOGICAL_MHD_SCALING"=>"jedamzik"))
  203     end
  204     for (section, mapping) in CONFIG_KEYS
  205         for (key, name) in mapping
  206             value = _setting(c, section, key, nothing)
  207             section == "diagnostics" && key == "streaming" && continue
  208             value === nothing || (env[name] = _envstr(value))
  209         end
  210     end
  211     # The physical box is ckpc/h; this upstream option expects ckpc.
  212     if haskey(env, "MHD_TERMINAL_LBOX_CKPC")
  213         env["MHD_TERMINAL_LBOX_CKPC"] = string(parse(Float64, env["MHD_TERMINAL_LBOX_CKPC"]) /
  214                                                     parse(Float64, get(env, "MHD_H", "0.674")))
  215     end
  216     out = abspath(output_dir)
  217     merge!(env, Dict("MHD_HISTORY_TSV"=>joinpath(out,"history.tsv"),
  218         "MHD_PK_TSV"=>joinpath(out,"spectra.tsv"), "MHD_PDF_TSV"=>joinpath(out,"pdfs.tsv"),
  219         "MHD_OUT_TSV"=>joinpath(out,"performance.tsv"),
  220         "MHD_MODE_HISTORY_TSV"=>joinpath(out,"modes.tsv"),
  221         "MHD_CHECKPOINT_PATH"=>joinpath(out,"checkpoint.jls"),
  222         "MHD_INITIAL_PK"=>"1", "MHD_FINAL_DIAG"=>"1"))
  223     if _setting(c,"diagnostics","streaming",false) || any(
  224         _setting(c,"streaming",key,0) != 0 for key in ("vx_kms","vy_kms","vz_kms"))
  225         env["MHD_STREAMING_TSV"] = joinpath(out,"streaming.tsv")
  226     end
  227     if _setting(c,"diagnostics","frame_redshifts",nothing) !== nothing
  228         env["MHD_DENSITY_FRAME_DIR"] = joinpath(out,"frames")
  229     end
  230     for (key,value) in get(c.settings,"environment",Dict())
  231         env[key] = _envstr(value)
  232     end
  233     haskey(env, "MHD_SOLVER") && (env["MHD_SOLVER"] = lowercase(env["MHD_SOLVER"]))
  234     _validate_ct_environment(env)
  235     env
  236 end