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