!> @file config.f90 !> @brief Runtime configuration module for the 1D Euler solver. !! !! Reads simulation parameters from a Fortran namelist file (default: !! `input.nml`). A missing or unreadable file is always a fatal error !! (`config: cannot open "..."`); there is no built-in Sod-defaults fallback. !! Callers that want fallback-to-defaults must supply a name that exists. !! !! Namelist groups (all optional; missing groups keep their defaults): !! !! - `&grid` — spatial discretisation !! - `&time_ctrl` — time-stepping parameters !! - `&physics` — gas model (gamma) !! - `&schemes` — algorithm selection strings (incl. MUSCL limiter) !! - `&initial_condition` — Riemann problem left/right primitive states and BC types !! - `&output` — result filename, diagnostic frequency, live snapshots !! - `&checkpoint` — checkpoint write interval and restart path !! !! Example usage in the driver: !! @code !! type(config_t) :: cfg !! call read_config('input.nml', cfg) !! @endcode module config use precision, only: wp use iso_fortran_env, only: iostat_end use mesh_1d, only: read_node_coords use option_registry, only: recon_muscl, problem_from_file, problem_udf, & bc_periodic, bc_subsonic_inlet, bc_subsonic_outlet, & is_valid_limiter, & is_fds_flux_scheme, method_fvm, & join_token_list, & flux_scheme_names, recon_scheme_names, & time_scheme_names, limiter_names, problem_type_names, & boundary_condition_names, char_proj_mode_names, & nrbc_mode_names, hybrid_sensor_names, method_names use output_format_list, only: parse_format_list, max_formats use string_utils, only: lowercase_token use path_util, only: resolve_case_path, case_base_dir use config_engine, only: field_desc_t, field_meta, normalize_fields, validate_fields, & cfg_kind_int, cfg_kind_real, cfg_kind_logical, & cfg_kind_choice, cfg_kind_string, cfg_kind_real3, & choice_msg_one_of implicit none private public :: config_t, read_config, validate_config public :: build_field_table !> Frozen ABI schema field count (1..62, identical order to the old !! config_schema.ensure_schema indices) — append only. integer, parameter, public :: n_config_fields = 62 !> All runtime-configurable simulation parameters with Sod shock tube !! defaults. type, public :: config_t ! -- &grid -- integer :: n_cell = 500 !< Number of grid cells real(wp) :: x_left = 0.0_wp !< Left boundary coordinate [m] real(wp) :: x_right = 1.0_wp !< Right boundary coordinate [m] character(len=32) :: grid_type = 'uniform' !< Grid type: 'uniform' (default) or 'file' (read node coords from grid_file). character(len=256) :: grid_file = '' !< Path to a node-coordinate file (one strictly-increasing coordinate per !! line; '#' comments and blank lines ignored). Required when grid_type='file'. !! n_cell, x_left, x_right are derived from the file when set. ! -- &time_ctrl -- real(wp) :: dt = 1.0e-4_wp !< Time step size [s] (used when cfl = 0) real(wp) :: time_start = 0.0_wp !< Simulation start time [s] real(wp) :: time_stop = 0.15_wp !< Simulation stop time [s] real(wp) :: cfl = 0.0_wp !< CFL number (> 0 enables adaptive dt; 0 = fixed dt) integer :: max_iter = 0 !< Optional iteration cap. When > 0, the time loop exits once !! `ctx % iter` reaches `max_iter`, overriding `time_stop`. !! Default 0 means "ignore" — `time_stop` keeps its usual role. logical :: lapack_solver = .true. !< Use LAPACK dgbsv for the banded solve in backward Euler (.true., default). !! Set to .false. to use the built-in no-pivoting solver instead. !! Only relevant when time_scheme = 'beuler'. ! -- &physics -- real(wp) :: gam = 1.4_wp !< Ratio of specific heats, c_p / c_v ! -- &schemes -- character(len=64) :: method = 'fdm' !< Spatial discretisation method. Parsed from the &schemes namelist. !! 'fdm' (default) — finite-difference method (existing 1D solver path). !! 'fvm' — finite-volume method; requires an FDS/Riemann flux scheme. character(len=64) :: flux_scheme = 'lax_friedrichs' !< Flux scheme. Valid values: !! 'lax_friedrichs' (default), 'steger_warming', 'van_leer', 'ausm_plus', !! 'hll', 'hllc', 'roe' character(len=64) :: recon_scheme = 'weno5' !< Spatial reconstruction variant character(len=64) :: time_scheme = 'rk3' !< Time integration scheme character(len=8) :: char_proj = 'auto' !< Characteristic projection mode. !! 'auto' (default) — enable per-scheme: on for weno5/weno5z/weno_cu6/weno7/ !! weno9/weno11/eno3/mp5/teno5, off otherwise. !! 'yes' — always apply eigensystem decomposition (more accurate for shocks, higher cost). !! 'no' — always skip eigensystem decomposition (faster; safe for scalar/smooth problems). character(len=32) :: limiter = 'minmod' !< TVD limiter for MUSCL reconstruction. !! Valid values: 'minmod' (default), 'superbee', 'mc', 'van_leer', 'koren'. !! Ignored for all non-MUSCL reconstruction schemes. logical :: use_positivity_limiter = .false. !< Enable Zhang-Shu positivity-preserving limiter (default .false.). !! When .true., reconstructed face states on the FDS path (AUSM+, !! HLL, HLLC, Roe) are scaled toward the adjacent cell average to ensure !! density and pressure remain non-negative. !! Ignored when a FVS flux scheme is active. logical :: use_hybrid_recon = .false. !< Enable shock-sensor hybrid reconstruction (default .false.). !! When .true., each face is classified smooth or non-smooth using the !! selected sensor (hybrid_sensor). Smooth faces use the linear !! (optimal-weight) WENO variant — same order as the primary scheme but !! without nonlinear dissipation. Non-smooth faces fall back to the full !! nonlinear primary reconstruction scheme. character(len=32) :: hybrid_sensor = 'jameson' !< Shock sensor used to classify faces when use_hybrid_recon = .true.. !! Valid values: !! 'jameson' — Jameson-Schmidt-Turkel pressure second-derivative !! sensor (JST 1981); physically motivated for !! compressible flows. Threshold ~ 0.1. !! 'density_gradient' — Normalised density jump across the face; cheap !! and robust. Threshold ~ 0.1. !! 'weno_beta' — Ratio max(β)/min(β) of WENO5 smoothness !! indicators; only meaningful for 5-point WENO-family !! schemes; falls back to 'density_gradient' otherwise. !! Threshold ~ 50. real(wp) :: hybrid_sensor_threshold = 0.1_wp !< Sensor value above which the nonlinear WENO scheme is activated. !! Default 0.1 suits the 'jameson' and 'density_gradient' sensors. !! Use ~50 for 'weno_beta'. ! -- &initial_condition -- character(len=64) :: problem_type = 'sod' !< IC problem type. !! Valid values: 'sod', 'shu_osher', 'smooth_wave', 'linear_advection', !! 'woodward_colella', 'lax', 'acoustic_pulse', 'from_file', 'udf'. character(len=256) :: ic_file = '' !< Path to IC data file used when problem_type = 'from_file'. !! File must contain whitespace-separated columns: x rho u p !! (one line per grid point; same format as the solver output file). logical :: ic_interp = .true. !< Allow linear interpolation when the IC file grid differs from the solver grid (default .true.). !! When .false., a grid mismatch causes an error stop. character(len=256) :: ic_udf_src = '' !< Path to a Fortran source file (.f90) containing the user-defined IC subroutine. !! Used when problem_type = 'udf'. The solver compiles this file to a shared !! library at runtime (requires gfortran on PATH) and calls the subroutine to !! fill the initial state. See example/udf_sod.f90 for the required interface. character(len=32) :: bc_left = 'dirichlet' !< Left boundary condition type. !! Valid values: 'dirichlet' (default), 'inflow', 'outflow', 'reflecting', !! 'periodic', 'nonreflecting', 'supersonic_inlet', 'subsonic_inlet', !! 'supersonic_outlet', 'subsonic_outlet', 'neumann', 'neumann_gradient'. character(len=32) :: bc_right = 'dirichlet' !< Right boundary condition type. !! Valid values: 'dirichlet' (default), 'inflow', 'outflow', 'reflecting', !! 'periodic', 'nonreflecting', 'supersonic_inlet', 'subsonic_inlet', !! 'supersonic_outlet', 'subsonic_outlet', 'neumann', 'neumann_gradient'. real(wp) :: p_ref_left = 1.0_wp !< Target far-field pressure for the non-reflecting BC at the left boundary [Pa]. !! Used only when bc_left = 'nonreflecting'. real(wp) :: p_ref_right = 1.0_wp !< Target far-field pressure for the non-reflecting BC at the right boundary [Pa]. !! Used only when bc_right = 'nonreflecting'. real(wp) :: sigma_nrbc = 0.0_wp !< Non-reflecting BC relaxation factor in [0, 1]. !! 0 = fully non-reflecting; 1 = Dirichlet target pressure p_ref. !! See Thompson (1987), Poinsot & Lele (1992). character(len=32) :: nrbc_mode = 'pressure' !< NRBC algorithm variant. !! 'pressure' (default) = isentropic pressure-relaxation (Thompson 1987). !! 'characteristic' = full LODI characteristic targeting (Poinsot & Lele 1992, !! Sec. 3); activates u_ref_*, rho_ref_*, sigma_nrbc_entropy. real(wp) :: u_ref_left = 0.0_wp !< Target far-field velocity for NRBC at the left boundary [m/s]. !! Used only when bc_left = 'nonreflecting' and nrbc_mode = 'characteristic'. real(wp) :: u_ref_right = 0.0_wp !< Target far-field velocity for NRBC at the right boundary [m/s]. !! Used only when bc_right = 'nonreflecting' and nrbc_mode = 'characteristic'. real(wp) :: rho_ref_left = 1.0_wp !< Target far-field density for NRBC at the left boundary [kg/m^3]. !! Used only when bc_left = 'nonreflecting' and nrbc_mode = 'characteristic'. real(wp) :: rho_ref_right = 1.0_wp !< Target far-field density for NRBC at the right boundary [kg/m^3]. !! Used only when bc_right = 'nonreflecting' and nrbc_mode = 'characteristic'. real(wp) :: sigma_nrbc_entropy = 0.0_wp !< Relaxation factor for the entropy (density) characteristic wave, in [0, 1]. !! 0 = fully non-reflecting entropy wave (default); !! 1 = entropy wave driven to the rho_ref target. !! Active only when nrbc_mode = 'characteristic'. !! See Poinsot & Lele (1992), J. Comput. Phys. 101, Sec. 3. ! --- subsonic_inlet --- real(wp) :: p_stag_left = 1.0_wp !< Stagnation (total) pressure for the left subsonic inlet [Pa]. !! Used only when bc_left = 'subsonic_inlet'. !! Derived from static conditions via p0 = p*(1 + (gam-1)/2*Ma^2)^(gam/(gam-1)). real(wp) :: rho_stag_left = 1.0_wp !< Stagnation (total) density for the left subsonic inlet [kg/m^3]. !! Used only when bc_left = 'subsonic_inlet'. !! Derived from static conditions via rho0 = rho*(1 + (gam-1)/2*Ma^2)^(1/(gam-1)). real(wp) :: p_stag_right = 1.0_wp !< Stagnation (total) pressure for the right subsonic inlet [Pa]. !! Used only when bc_right = 'subsonic_inlet'. real(wp) :: rho_stag_right = 1.0_wp !< Stagnation (total) density for the right subsonic inlet [kg/m^3]. !! Used only when bc_right = 'subsonic_inlet'. ! --- subsonic_outlet --- real(wp) :: p_back_left = 1.0_wp !< Back pressure for the left subsonic outlet [Pa]. !! Used only when bc_left = 'subsonic_outlet'. real(wp) :: p_back_right = 1.0_wp !< Back pressure for the right subsonic outlet [Pa]. !! Used only when bc_right = 'subsonic_outlet'. ! --- neumann_gradient --- real(wp) :: neumann_grad_left(3) = 0.0_wp !< Prescribed outward normal gradient dq/dn at the left boundary (conserved vars). !! Used only when bc_left = 'neumann_gradient'. !! Ghost state: q_ghost = q_wall + neumann_grad_left * dx. real(wp) :: neumann_grad_right(3) = 0.0_wp !< Prescribed outward normal gradient dq/dn at the right boundary (conserved vars). !! Used only when bc_right = 'neumann_gradient'. !! Ghost state: q_ghost = q_wall + neumann_grad_right * dx. real(wp) :: rho_left = 1.0_wp !< Left density [kg/m^3] real(wp) :: u_left = 0.0_wp !< Left velocity [m/s] real(wp) :: p_left = 1.0_wp !< Left pressure [Pa] real(wp) :: rho_right = 0.125_wp !< Right density [kg/m^3] real(wp) :: u_right = 0.0_wp !< Right velocity [m/s] real(wp) :: p_right = 0.1_wp !< Right pressure [Pa] real(wp) :: x_diaphragm = 0.5_wp !< Diaphragm location [m] ! -- &output -- character(len=256) :: output_file = 'result.dat' !< Output filename character(len=64) :: output_format = 'dat' !< Space- or comma-separated output format list. Valid tokens: 'dat' (default !! columnar text), 'tec' (Tecplot ASCII), 'plt' (Tecplot binary). Multiple !! formats may be requested, e.g. 'dat plt'; each is written to a filename !! derived from output_file. integer :: print_freq = 50 !< Print residual every N iterations !> Enable detailed wall-clock timing summary output (default .false.). !! When .true., compute_resid() and the driver accumulate fine-grained !! timing and print a performance table after the run. Lightweight !! per-iteration `iter_s` / `elapsed_s` logging is always available. !! Has no effect on results. logical :: do_timing = .false. !> Logger verbosity threshold (default 3 = LOGLVL_INFO). !! 0 = SILENT, 1 = ERROR, 2 = WARN, 3 = INFO, 4 = DEBUG. !! See module `logger` for the LOGLVL_* constants. integer :: verbosity = 3 !> Log file path (default 'run.log'). Empty string disables file logging. character(len=256) :: log_file = 'run.log' !> Write a live solution snapshot every N iterations (0 = disabled). !! The snapshot file is overwritten each time, so a GUI app can watch !! it with inotify/kqueue. Format matches result.dat with a leading !! comment line: "# iter=NNN t=T". integer :: snapshot_freq = 0 !> Path for the live snapshot file (default 'snapshot.dat'). character(len=256) :: snapshot_file = 'snapshot.dat' ! -- &checkpoint -- !> Write a checkpoint every N iterations (0 = disabled). !! Checkpoint files are named '<checkpoint_file>_NNNNNN.bin' and contain !! enough state to resume the run exactly: iter, t, ub, and (if BDF2) !! bdf2_ub_prev. The file 'latest_checkpoint' is also updated to point !! to the most recent checkpoint so restarts need not know the iteration. integer :: checkpoint_freq = 0 !> Base name for checkpoint files (default 'checkpoint'). character(len=256) :: checkpoint_file = 'checkpoint' !> Path to a checkpoint file to resume from (empty = fresh start). !! When set, the IC is skipped and iter/t are loaded from the checkpoint. character(len=256) :: restart_file = '' end type config_t contains !> Read simulation parameters from a namelist file. !! !! Each namelist group in the file is optional. Missing groups keep the !! default values defined in `config_t`. If the file itself cannot be !! opened (e.g. not found), a warning is printed and all defaults are kept. !! !! @param[in] filename Path to the namelist input file. !! @param[out] cfg Populated configuration object. subroutine read_config(filename, cfg, is_ok, message, case_dir) character(len=*), intent(in) :: filename type(config_t), intent(out) :: cfg logical, intent(out), optional :: is_ok character(len=*), intent(out), optional :: message !> Optional case directory to anchor relative file paths against; when !! absent/empty the namelist's own directory is used (see path_util). character(len=*), intent(in), optional :: case_dir ! Fortran namelists require plain scalar/array variable names — derived-type ! component selectors (cfg%n_cell) are not supported in NAMELIST declarations ! by gfortran and would also change the .nml file format. We therefore keep ! one set of local mirror variables: they are initialised from the config_t ! defaults (which cfg already holds via intent(out) default initialisation), ! read from the file, then transferred back into cfg in one block below. integer :: n_cell real(wp) :: x_left, x_right character(len=32) :: grid_type character(len=256) :: grid_file real(wp) :: dt, time_start, time_stop, cfl integer :: max_iter logical :: lapack_solver real(wp) :: gam character(len=64) :: method character(len=64) :: flux_scheme, recon_scheme, time_scheme character(len=8) :: char_proj character(len=32) :: limiter logical :: use_positivity_limiter logical :: use_hybrid_recon character(len=32) :: hybrid_sensor real(wp) :: hybrid_sensor_threshold character(len=64) :: problem_type character(len=256) :: ic_file, ic_udf_src logical :: ic_interp character(len=32) :: bc_left, bc_right real(wp) :: p_ref_left, p_ref_right, sigma_nrbc character(len=32) :: nrbc_mode real(wp) :: u_ref_left, u_ref_right, rho_ref_left, rho_ref_right real(wp) :: sigma_nrbc_entropy real(wp) :: p_stag_left, rho_stag_left, p_stag_right, rho_stag_right real(wp) :: p_back_left, p_back_right real(wp) :: neumann_grad_left(3), neumann_grad_right(3) real(wp) :: rho_left, u_left, p_left real(wp) :: rho_right, u_right, p_right, x_diaphragm character(len=256) :: output_file character(len=64) :: output_format integer :: print_freq logical :: do_timing integer :: verbosity character(len=256) :: log_file integer :: snapshot_freq character(len=256) :: snapshot_file integer :: checkpoint_freq character(len=256) :: checkpoint_file, restart_file namelist /grid/ n_cell, x_left, x_right, grid_type, grid_file namelist /time_ctrl/ dt, time_start, time_stop, cfl, max_iter, lapack_solver namelist /physics/ gam namelist /schemes/ method, flux_scheme, recon_scheme, time_scheme, char_proj, limiter, & use_positivity_limiter, use_hybrid_recon, hybrid_sensor, hybrid_sensor_threshold namelist /initial_condition/ problem_type, ic_file, ic_interp, ic_udf_src, & bc_left, bc_right, p_ref_left, p_ref_right, sigma_nrbc, & nrbc_mode, u_ref_left, u_ref_right, rho_ref_left, rho_ref_right, & sigma_nrbc_entropy, & p_stag_left, rho_stag_left, p_stag_right, rho_stag_right, & p_back_left, p_back_right, & neumann_grad_left, neumann_grad_right, & rho_left, u_left, p_left, rho_right, u_right, p_right, x_diaphragm namelist /output/ output_file, output_format, print_freq, do_timing, verbosity, log_file, & snapshot_freq, snapshot_file namelist /checkpoint/ checkpoint_freq, checkpoint_file, restart_file integer :: u, info logical :: ok character(len=256) :: err if (present(is_ok)) is_ok = .true. if (present(message)) message = '' ! Initialise locals from the config_t defaults so that any namelist group ! absent from the file keeps its documented default value. n_cell = cfg % n_cell x_left = cfg % x_left x_right = cfg % x_right grid_type = cfg % grid_type grid_file = cfg % grid_file dt = cfg % dt time_start = cfg % time_start time_stop = cfg % time_stop cfl = cfg % cfl max_iter = cfg % max_iter lapack_solver = cfg % lapack_solver gam = cfg % gam method = cfg % method flux_scheme = cfg % flux_scheme recon_scheme = cfg % recon_scheme time_scheme = cfg % time_scheme char_proj = cfg % char_proj limiter = cfg % limiter use_positivity_limiter = cfg % use_positivity_limiter use_hybrid_recon = cfg % use_hybrid_recon hybrid_sensor = cfg % hybrid_sensor hybrid_sensor_threshold = cfg % hybrid_sensor_threshold problem_type = cfg % problem_type ic_file = cfg % ic_file ic_udf_src = cfg % ic_udf_src ic_interp = cfg % ic_interp bc_left = cfg % bc_left bc_right = cfg % bc_right p_ref_left = cfg % p_ref_left p_ref_right = cfg % p_ref_right sigma_nrbc = cfg % sigma_nrbc nrbc_mode = cfg % nrbc_mode u_ref_left = cfg % u_ref_left u_ref_right = cfg % u_ref_right rho_ref_left = cfg % rho_ref_left rho_ref_right = cfg % rho_ref_right sigma_nrbc_entropy = cfg % sigma_nrbc_entropy p_stag_left = cfg % p_stag_left rho_stag_left = cfg % rho_stag_left p_stag_right = cfg % p_stag_right rho_stag_right = cfg % rho_stag_right p_back_left = cfg % p_back_left p_back_right = cfg % p_back_right neumann_grad_left = cfg % neumann_grad_left neumann_grad_right = cfg % neumann_grad_right rho_left = cfg % rho_left u_left = cfg % u_left p_left = cfg % p_left rho_right = cfg % rho_right u_right = cfg % u_right p_right = cfg % p_right x_diaphragm = cfg % x_diaphragm output_file = cfg % output_file output_format = cfg % output_format print_freq = cfg % print_freq do_timing = cfg % do_timing verbosity = cfg % verbosity log_file = cfg % log_file snapshot_freq = cfg % snapshot_freq snapshot_file = cfg % snapshot_file checkpoint_freq = cfg % checkpoint_freq checkpoint_file = cfg % checkpoint_file restart_file = cfg % restart_file ! Try to open the input file. A missing or unreadable file is always an ! error: 1D and 2D readers should agree on this contract. Callers that ! want fallback-to-defaults must supply a name that exists. open (newunit=u, file=filename, status='old', action='read', iostat=info) if (info /= 0) then if (present(is_ok)) is_ok = .false. if (present(message)) message = 'config: cannot open "'//trim(filename)//'"' if (.not. present(is_ok) .and. .not. present(message)) & error stop 'config: cannot open "'//trim(filename)//'"' return end if ! Read each group independently (rewind so groups may appear in any order). rewind (u) read (u, nml=grid, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &grid namelist') return end if rewind (u) read (u, nml=time_ctrl, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &time_ctrl namelist') return end if rewind (u) read (u, nml=physics, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &physics namelist') return end if rewind (u) read (u, nml=schemes, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &schemes namelist') return end if rewind (u) read (u, nml=initial_condition, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &initial_condition namelist') return end if rewind (u) read (u, nml=output, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &output namelist') return end if rewind (u) read (u, nml=checkpoint, iostat=info) if (info /= 0 .and. info /= iostat_end) then call fail_read('config: parse error in &checkpoint namelist') return end if close (u, iostat=info) if (info /= 0) then call fail_read('config: file close failed', already_closed=.true.) return end if ! Transfer locals into cfg (single assignment block — no separate populate()). cfg % n_cell = n_cell cfg % x_left = x_left cfg % x_right = x_right cfg % grid_type = grid_type ! Resolve a relative grid_file against the case directory (explicit case_dir ! override when given, else the namelist's own directory) so a case folder is ! movable and the node-coordinate file loads regardless of the process cwd. ! Absolute paths kept as-is; empty stays empty. cfg % grid_file = resolve_case_path(case_base_dir(filename, case_dir), grid_file) cfg % dt = dt cfg % time_start = time_start cfg % time_stop = time_stop cfg % cfl = cfl cfg % max_iter = max_iter cfg % lapack_solver = lapack_solver cfg % gam = gam cfg % method = method cfg % flux_scheme = flux_scheme cfg % recon_scheme = recon_scheme cfg % time_scheme = time_scheme cfg % char_proj = char_proj cfg % limiter = limiter cfg % use_positivity_limiter = use_positivity_limiter cfg % use_hybrid_recon = use_hybrid_recon cfg % hybrid_sensor = hybrid_sensor cfg % hybrid_sensor_threshold = hybrid_sensor_threshold cfg % problem_type = problem_type cfg % ic_file = ic_file cfg % ic_udf_src = ic_udf_src cfg % ic_interp = ic_interp cfg % bc_left = bc_left cfg % bc_right = bc_right cfg % p_ref_left = p_ref_left cfg % p_ref_right = p_ref_right cfg % sigma_nrbc = sigma_nrbc cfg % nrbc_mode = nrbc_mode cfg % u_ref_left = u_ref_left cfg % u_ref_right = u_ref_right cfg % rho_ref_left = rho_ref_left cfg % rho_ref_right = rho_ref_right cfg % sigma_nrbc_entropy = sigma_nrbc_entropy cfg % p_stag_left = p_stag_left cfg % rho_stag_left = rho_stag_left cfg % p_stag_right = p_stag_right cfg % rho_stag_right = rho_stag_right cfg % p_back_left = p_back_left cfg % p_back_right = p_back_right cfg % neumann_grad_left = neumann_grad_left cfg % neumann_grad_right = neumann_grad_right cfg % rho_left = rho_left cfg % u_left = u_left cfg % p_left = p_left cfg % rho_right = rho_right cfg % u_right = u_right cfg % p_right = p_right cfg % x_diaphragm = x_diaphragm cfg % output_file = output_file cfg % output_format = lowercase_token(output_format) cfg % print_freq = print_freq cfg % do_timing = do_timing cfg % verbosity = verbosity cfg % log_file = log_file cfg % snapshot_freq = snapshot_freq cfg % snapshot_file = snapshot_file cfg % checkpoint_freq = checkpoint_freq cfg % checkpoint_file = checkpoint_file cfg % restart_file = restart_file call validate_config(cfg, ok, err) if (present(is_ok)) is_ok = ok if (present(message)) message = trim(err) if (.not. ok .and. .not. present(is_ok) .and. .not. present(message)) error stop trim(err) contains subroutine fail_read(err_msg, already_closed) character(len=*), intent(in) :: err_msg logical, intent(in), optional :: already_closed integer :: close_info logical :: skip_close skip_close = .false. if (present(already_closed)) skip_close = already_closed if (.not. skip_close) close (u, iostat=close_info) if (present(is_ok)) is_ok = .false. if (present(message)) message = trim(err_msg) if (.not. present(is_ok) .and. .not. present(message)) error stop trim(err_msg) end subroutine fail_read end subroutine read_config !> Bind the 62-entry field-descriptor table (ABI schema order) to `cfg`. !! Entry order IS the externally visible schema index — append only. function build_field_table(cfg) result(fields) type(config_t), intent(inout), target :: cfg type(field_desc_t) :: fields(n_config_fields) ! -- grid (checks conditional on grid_type: kept in validate_config) -- fields(1) = field_meta('n_cell', 'grid', cfg_kind_int, 'Number of grid cells', & has_min=.true., min_value=1.0_wp, has_max=.true., max_value=100000.0_wp, & check_in_validate=.false.) fields(1) % iptr => cfg % n_cell fields(2) = field_meta('x_left', 'grid', cfg_kind_real, 'Left boundary coordinate') fields(2) % rptr => cfg % x_left fields(3) = field_meta('x_right', 'grid', cfg_kind_real, 'Right boundary coordinate') fields(3) % rptr => cfg % x_right fields(4) = field_meta('dt', 'time_ctrl', cfg_kind_real, 'Fixed time step', & has_min=.true., min_exclusive=.true., min_value=0.0_wp, & msg_min='config: dt must be positive') fields(4) % rptr => cfg % dt fields(5) = field_meta('time_start', 'time_ctrl', cfg_kind_real, 'Simulation start time') fields(5) % rptr => cfg % time_start fields(6) = field_meta('time_stop', 'time_ctrl', cfg_kind_real, 'Simulation stop time') fields(6) % rptr => cfg % time_stop fields(7) = field_meta('cfl', 'time_ctrl', cfg_kind_real, 'CFL number', & has_min=.true., min_value=0.0_wp, msg_min='config: cfl must be >= 0') fields(7) % rptr => cfg % cfl fields(8) = field_meta('lapack_solver', 'time_ctrl', cfg_kind_logical, & 'Use LAPACK banded solver for backward Euler') fields(8) % lptr => cfg % lapack_solver fields(9) = field_meta('gam', 'physics', cfg_kind_real, 'Ratio of specific heats', & has_min=.true., min_value=1.001_wp, msg_min='config: gam must be >= 1.001') fields(9) % rptr => cfg % gam fields(10) = field_meta('flux_scheme', 'schemes', cfg_kind_choice, 'Numerical flux scheme', & choices=flux_scheme_names, casefold=.true.) fields(10) % sptr => cfg % flux_scheme fields(11) = field_meta('recon_scheme', 'schemes', cfg_kind_choice, 'Spatial reconstruction scheme', & choices=recon_scheme_names, casefold=.true.) fields(11) % sptr => cfg % recon_scheme fields(12) = field_meta('time_scheme', 'schemes', cfg_kind_choice, 'Time integration scheme', & choices=time_scheme_names, casefold=.true.) fields(12) % sptr => cfg % time_scheme fields(13) = field_meta('char_proj', 'schemes', cfg_kind_choice, 'Characteristic projection mode', & choices=char_proj_mode_names, choice_msg_style=choice_msg_one_of, casefold=.true.) fields(13) % sptr => cfg % char_proj fields(14) = field_meta('limiter', 'schemes', cfg_kind_choice, 'MUSCL limiter', & choices=limiter_names, casefold=.true., check_in_validate=.false.) fields(14) % sptr => cfg % limiter fields(15) = field_meta('use_positivity_limiter', 'schemes', cfg_kind_logical, 'Enable positivity limiter') fields(15) % lptr => cfg % use_positivity_limiter fields(16) = field_meta('use_hybrid_recon', 'schemes', cfg_kind_logical, 'Enable hybrid reconstruction') fields(16) % lptr => cfg % use_hybrid_recon fields(17) = field_meta('hybrid_sensor', 'schemes', cfg_kind_choice, 'Hybrid shock sensor', & choices=hybrid_sensor_names, choice_msg_style=choice_msg_one_of, casefold=.true.) fields(17) % sptr => cfg % hybrid_sensor fields(18) = field_meta('hybrid_sensor_threshold', 'schemes', cfg_kind_real, 'Hybrid sensor threshold') fields(18) % rptr => cfg % hybrid_sensor_threshold fields(19) = field_meta('problem_type', 'initial_condition', cfg_kind_choice, & 'Initial condition preset', choices=problem_type_names, casefold=.true.) fields(19) % sptr => cfg % problem_type fields(20) = field_meta('ic_file', 'initial_condition', cfg_kind_string, 'Path to IC data file') fields(20) % sptr => cfg % ic_file fields(21) = field_meta('ic_interp', 'initial_condition', cfg_kind_logical, & 'Interpolate IC file onto solver grid') fields(21) % lptr => cfg % ic_interp fields(22) = field_meta('ic_udf_src', 'initial_condition', cfg_kind_string, 'Path to IC UDF source') fields(22) % sptr => cfg % ic_udf_src fields(23) = field_meta('bc_left', 'initial_condition', cfg_kind_choice, 'Left boundary condition', & choices=boundary_condition_names, casefold=.true.) fields(23) % sptr => cfg % bc_left fields(24) = field_meta('bc_right', 'initial_condition', cfg_kind_choice, 'Right boundary condition', & choices=boundary_condition_names, casefold=.true.) fields(24) % sptr => cfg % bc_right fields(25) = field_meta('p_ref_left', 'initial_condition', cfg_kind_real, 'Left NRBC reference pressure') fields(25) % rptr => cfg % p_ref_left fields(26) = field_meta('p_ref_right', 'initial_condition', cfg_kind_real, 'Right NRBC reference pressure') fields(26) % rptr => cfg % p_ref_right fields(27) = field_meta('sigma_nrbc', 'initial_condition', cfg_kind_real, 'NRBC relaxation factor', & has_min=.true., min_value=0.0_wp, has_max=.true., max_value=1.0_wp, & msg_min='config: sigma_nrbc must be in [0, 1]', & msg_max='config: sigma_nrbc must be in [0, 1]') fields(27) % rptr => cfg % sigma_nrbc fields(28) = field_meta('nrbc_mode', 'initial_condition', cfg_kind_choice, 'NRBC algorithm mode', & choices=nrbc_mode_names, choice_msg_style=choice_msg_one_of, casefold=.true.) fields(28) % sptr => cfg % nrbc_mode fields(29) = field_meta('u_ref_left', 'initial_condition', cfg_kind_real, & 'Left characteristic NRBC reference velocity') fields(29) % rptr => cfg % u_ref_left fields(30) = field_meta('u_ref_right', 'initial_condition', cfg_kind_real, & 'Right characteristic NRBC reference velocity') fields(30) % rptr => cfg % u_ref_right fields(31) = field_meta('rho_ref_left', 'initial_condition', cfg_kind_real, & 'Left characteristic NRBC reference density') fields(31) % rptr => cfg % rho_ref_left fields(32) = field_meta('rho_ref_right', 'initial_condition', cfg_kind_real, & 'Right characteristic NRBC reference density') fields(32) % rptr => cfg % rho_ref_right fields(33) = field_meta('sigma_nrbc_entropy', 'initial_condition', cfg_kind_real, & 'Entropy-wave relaxation factor', & has_min=.true., min_value=0.0_wp, has_max=.true., max_value=1.0_wp, & msg_min='config: sigma_nrbc_entropy must be in [0, 1]', & msg_max='config: sigma_nrbc_entropy must be in [0, 1]') fields(33) % rptr => cfg % sigma_nrbc_entropy ! Stagnation / back-pressure bounds are conditional on the active BC: ! published in the schema, enforced in validate_config's conditional block. fields(34) = field_meta('p_stag_left', 'initial_condition', cfg_kind_real, & 'Left subsonic inlet stagnation pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(34) % rptr => cfg % p_stag_left fields(35) = field_meta('rho_stag_left', 'initial_condition', cfg_kind_real, & 'Left subsonic inlet stagnation density', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(35) % rptr => cfg % rho_stag_left fields(36) = field_meta('p_stag_right', 'initial_condition', cfg_kind_real, & 'Right subsonic inlet stagnation pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(36) % rptr => cfg % p_stag_right fields(37) = field_meta('rho_stag_right', 'initial_condition', cfg_kind_real, & 'Right subsonic inlet stagnation density', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(37) % rptr => cfg % rho_stag_right fields(38) = field_meta('p_back_left', 'initial_condition', cfg_kind_real, & 'Left subsonic outlet back pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(38) % rptr => cfg % p_back_left fields(39) = field_meta('p_back_right', 'initial_condition', cfg_kind_real, & 'Right subsonic outlet back pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(39) % rptr => cfg % p_back_right fields(40) = field_meta('neumann_grad_left', 'initial_condition', cfg_kind_real3, & 'Left Neumann-gradient conserved-variable vector') fields(40) % r3ptr => cfg % neumann_grad_left fields(41) = field_meta('neumann_grad_right', 'initial_condition', cfg_kind_real3, & 'Right Neumann-gradient conserved-variable vector') fields(41) % r3ptr => cfg % neumann_grad_right ! rho/u/p left-right bounds are conditional on problem_type (skipped for ! from_file/udf): published, enforced in validate_config. fields(42) = field_meta('rho_left', 'initial_condition', cfg_kind_real, 'Left density', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(42) % rptr => cfg % rho_left fields(43) = field_meta('u_left', 'initial_condition', cfg_kind_real, 'Left velocity') fields(43) % rptr => cfg % u_left fields(44) = field_meta('p_left', 'initial_condition', cfg_kind_real, 'Left pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(44) % rptr => cfg % p_left fields(45) = field_meta('rho_right', 'initial_condition', cfg_kind_real, 'Right density', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(45) % rptr => cfg % rho_right fields(46) = field_meta('u_right', 'initial_condition', cfg_kind_real, 'Right velocity') fields(46) % rptr => cfg % u_right fields(47) = field_meta('p_right', 'initial_condition', cfg_kind_real, 'Right pressure', & has_min=.true., min_value=0.0_wp, check_in_validate=.false.) fields(47) % rptr => cfg % p_right fields(48) = field_meta('x_diaphragm', 'initial_condition', cfg_kind_real, & 'Riemann problem diaphragm location') fields(48) % rptr => cfg % x_diaphragm fields(49) = field_meta('output_file', 'output', cfg_kind_string, 'Final result file path') fields(49) % sptr => cfg % output_file fields(50) = field_meta('print_freq', 'output', cfg_kind_int, 'Residual print interval', & has_min=.true., min_value=1.0_wp, msg_min='config: print_freq must be positive') fields(50) % iptr => cfg % print_freq fields(51) = field_meta('do_timing', 'output', cfg_kind_logical, 'Enable detailed timing summary output') fields(51) % lptr => cfg % do_timing fields(52) = field_meta('verbosity', 'output', cfg_kind_int, 'Logger verbosity', & has_min=.true., min_value=0.0_wp, has_max=.true., max_value=4.0_wp, & msg_min='config: verbosity must be in [0, 4]', & msg_max='config: verbosity must be in [0, 4]') fields(52) % iptr => cfg % verbosity fields(53) = field_meta('log_file', 'output', cfg_kind_string, 'Log file path') fields(53) % sptr => cfg % log_file fields(54) = field_meta('snapshot_freq', 'output', cfg_kind_int, 'Live snapshot interval', & has_min=.true., min_value=0.0_wp, msg_min='config: snapshot_freq must be >= 0') fields(54) % iptr => cfg % snapshot_freq fields(55) = field_meta('snapshot_file', 'output', cfg_kind_string, 'Live snapshot file path') fields(55) % sptr => cfg % snapshot_file fields(56) = field_meta('checkpoint_freq', 'checkpoint', cfg_kind_int, 'Checkpoint interval', & has_min=.true., min_value=0.0_wp, msg_min='config: checkpoint_freq must be >= 0') fields(56) % iptr => cfg % checkpoint_freq fields(57) = field_meta('checkpoint_file', 'checkpoint', cfg_kind_string, 'Checkpoint base filename') fields(57) % sptr => cfg % checkpoint_file fields(58) = field_meta('restart_file', 'checkpoint', cfg_kind_string, 'Checkpoint file to resume from') fields(58) % sptr => cfg % restart_file fields(59) = field_meta('max_iter', 'time_ctrl', cfg_kind_int, & 'Optional iteration cap; >0 exits the time loop early', & has_min=.true., min_value=0.0_wp, msg_min='config: max_iter must be >= 0') fields(59) % iptr => cfg % max_iter ! grid_type is kind STRING in the published schema (not choice); its ! two-token check keeps its bespoke message in validate_config. fields(60) = field_meta('grid_type', 'grid', cfg_kind_string, & "Grid type: 'uniform' or 'file'", casefold=.true.) fields(60) % sptr => cfg % grid_type fields(61) = field_meta('grid_file', 'grid', cfg_kind_string, & "Path to node-coordinate file when grid_type='file'") fields(61) % sptr => cfg % grid_file fields(62) = field_meta('method', 'schemes', cfg_kind_choice, & 'Spatial discretization method (fdm or fvm)', & choices=method_names, casefold=.true.) fields(62) % sptr => cfg % method end function build_field_table ! --------------------------------------------------------------------------- !> Validate that all namelist parameters are physically and numerically sensible. !! !! `normalize_fields` is called first so that scheme names are compared !! case-insensitively. Three outcomes are possible: !! !! - **Error stop** (default): if validation fails and neither optional !! argument is present, `error stop` is called with a descriptive message. !! - **Return flag + message**: if `is_valid` and/or `message` are present, !! the result is returned in those arguments and execution continues. !! - **Silent pass**: if validation succeeds, `is_valid = .true.` (when !! present) and `message` is set to an empty string (when present). !! !! @param cfg Configuration to validate (normalised in-place). !! @param is_valid `.true.` on success, `.false.` on the first violation found. !! @param message Human-readable description of the first violation, or `''`. subroutine validate_config(cfg, is_valid, message) type(config_t), intent(inout), target :: cfg logical, intent(out), optional :: is_valid character(len=*), intent(out), optional :: message type(field_desc_t) :: fields(n_config_fields) logical :: ok character(len=256) :: err fields = build_field_table(cfg) ! Table-driven replacement for the old normalize_config: lowercases every ! casefold-flagged selector field in place. call normalize_fields(fields) ! Unconditional per-field constraints (ranges + choice lists), table order. call validate_fields(fields, 'config', ok, err) if (ok) then ! --- Cross-field and conditional checks: KEPT VERBATIM from the old ! validate_config, in their original relative order. --- if (ok .and. trim(cfg % problem_type) == problem_from_file .and. len_trim(cfg % ic_file) == 0) then ok = .false. err = 'config: ic_file must be set when problem_type = "from_file"' end if if (ok .and. trim(cfg % problem_type) == problem_udf .and. len_trim(cfg % ic_udf_src) == 0) then ok = .false. err = 'config: ic_udf_src must be set when problem_type = "udf"' end if if (ok .and. ((trim(cfg % bc_left) == bc_periodic) .neqv. & (trim(cfg % bc_right) == bc_periodic))) then ok = .false. err = 'config: bc_left and bc_right must both be periodic or neither' end if if (ok .and. trim(cfg % bc_left) == bc_subsonic_inlet .and. cfg % p_stag_left <= 0.0_wp) then ok = .false. err = 'config: p_stag_left must be positive for subsonic_inlet' end if if (ok .and. trim(cfg % bc_left) == bc_subsonic_inlet .and. cfg % rho_stag_left <= 0.0_wp) then ok = .false. err = 'config: rho_stag_left must be positive for subsonic_inlet' end if if (ok .and. trim(cfg % bc_right) == bc_subsonic_inlet .and. cfg % p_stag_right <= 0.0_wp) then ok = .false. err = 'config: p_stag_right must be positive for subsonic_inlet' end if if (ok .and. trim(cfg % bc_right) == bc_subsonic_inlet .and. cfg % rho_stag_right <= 0.0_wp) then ok = .false. err = 'config: rho_stag_right must be positive for subsonic_inlet' end if if (ok .and. trim(cfg % bc_left) == bc_subsonic_outlet .and. cfg % p_back_left <= 0.0_wp) then ok = .false. err = 'config: p_back_left must be positive for subsonic_outlet' end if if (ok .and. trim(cfg % bc_right) == bc_subsonic_outlet .and. cfg % p_back_right <= 0.0_wp) then ok = .false. err = 'config: p_back_right must be positive for subsonic_outlet' end if if (ok .and. trim(cfg % grid_type) /= 'uniform' .and. trim(cfg % grid_type) /= 'file') then ok = .false. err = 'config: grid_type must be ''uniform'' or ''file''' end if ! FVM now supports non-uniform grids: a grid_type='file' run interprets the ! grid_file nodes as cell FACES (n faces -> n-1 cells) and builds a ! cell-centered non-uniform mesh (see build_mesh_cellcentered_global). Only ! the IC source, not the grid spacing, restricts FVM — see the guard below. ! The file/UDF IC paths sample the nodal mesh (x_node) directly, which the FVM ! path never allocates (it builds only the cell-centered mesh). Reject the ! combination so a valid-looking namelist cannot dereference an unallocated ! array; these problem types stay nodal-only this increment. if (ok .and. trim(cfg % method) == method_fvm .and. & (trim(cfg % problem_type) == problem_from_file .or. & trim(cfg % problem_type) == problem_udf)) then ok = .false. err = 'config: FVM does not support problem_type='//trim(cfg % problem_type)// & ' (from_file/udf are nodal-only this increment)' end if if (ok .and. trim(cfg % grid_type) == 'file') then block real(wp), allocatable :: coords(:) integer :: n logical :: rok character(len=256) :: rmsg if (len_trim(cfg % grid_file) == 0) then ok = .false. err = 'config: grid_type=file requires a non-empty grid_file' else call read_node_coords(cfg % grid_file, coords, n, rok, rmsg) if (.not. rok) then ok = .false. err = rmsg else ! Derive grid dimensions from the file so the config is self-consistent ! immediately after validate_config returns (not only after init_from_config). cfg % n_cell = n - 1 cfg % x_left = coords(1) cfg % x_right = coords(n) end if end if end block else if (ok) then ! Existing uniform-grid checks (only meaningful when not file-driven). if (cfg % n_cell <= 0) then ok = .false. err = 'config: n_cell must be positive' end if ! Schema-published upper bound (config_schema.f90 ensure_schema, entry 1). if (ok .and. cfg % n_cell > 100000) then ok = .false. err = 'config: n_cell must be <= 100000' end if if (ok .and. cfg % x_right <= cfg % x_left) then ok = .false. err = 'config: x_right must be greater than x_left' end if end if if (ok .and. cfg % time_stop <= cfg % time_start) then ok = .false. err = 'config: time_stop must be greater than time_start' end if if (ok .and. (cfg % x_diaphragm < cfg % x_left .or. cfg % x_diaphragm > cfg % x_right)) then ok = .false. err = 'config: x_diaphragm must lie within [x_left, x_right]' end if if (ok .and. trim(cfg % problem_type) /= problem_from_file .and. trim(cfg % problem_type) /= problem_udf) then if (cfg % rho_left <= 0.0_wp .or. cfg % rho_right <= 0.0_wp) then ok = .false. err = 'config: density must be positive' else if (cfg % p_left <= 0.0_wp .or. cfg % p_right <= 0.0_wp) then ok = .false. err = 'config: pressure must be positive' end if end if if (ok) then block character(len=8) :: fmt_tokens(max_formats) integer :: n_fmt logical :: fmt_ok character(len=256) :: fmt_msg call parse_format_list(lowercase_token(cfg % output_format), & [character(len=8) :: 'dat', 'tec', 'plt'], & fmt_tokens, n_fmt, fmt_ok, fmt_msg) if (.not. fmt_ok) then ok = .false. err = 'config: '//trim(fmt_msg) end if end block end if if (ok .and. trim(cfg % method) == method_fvm .and. & .not. is_fds_flux_scheme(cfg % flux_scheme)) then ok = .false. err = 'config: FVM supports FDS/Riemann fluxes only (got '// & trim(cfg % flux_scheme)//')' end if if (ok .and. trim(cfg % recon_scheme) == recon_muscl .and. .not. is_valid_limiter(cfg % limiter)) then ok = .false. err = 'config: unknown limiter "'//trim(cfg % limiter)// & '" for MUSCL; valid: '//trim(join_token_list(limiter_names)) end if end if if (present(is_valid)) is_valid = ok if (present(message)) message = trim(err) if (.not. ok .and. .not. present(is_valid) .and. .not. present(message)) then error stop trim(err) end if end subroutine validate_config end module config