!> @file config_2d.f90 !> @brief 2D Euler solver configuration: derived type, namelist loader, validation. !! !! Mirrors the 1D `config` module but for the coexisting `euler_2d` solver. !! Carries grid, physics, time-control, scheme-selection, per-edge !! boundary-condition, and 2D initial-condition fields. The time-control and !! scheme fields drive the `euler_2d` solve (flux/reconstruction/time-integration !! selection and stop criteria). module config_2d use precision, only: wp use output_format_list, only: parse_format_list, max_formats use mesh_2d, only: build_mesh_2d_global, mesh_2d_t, MESH2D_MAX_DIM use string_utils, only: to_lower use logger, only: log_warn use path_util, only: resolve_case_path, case_base_dir use option_registry, only: flux_scheme_2d_names, recon_scheme_2d_names, & time_scheme_2d_names, problem_type_2d_names, & bc_2d_names, char_proj_mode_names, & method_names, & is_fds_flux_scheme, & method_fdm, method_fvm, & bc_periodic, problem_2d_sedov, & problem_2d_toro_explosion, problem_2d_riemann2d_config3 use config_engine, only: field_desc_t, field_meta, validate_fields, & cfg_kind_int, cfg_kind_real, cfg_kind_logical, & cfg_kind_choice, cfg_kind_string, choice_msg_bare use, intrinsic :: iso_fortran_env, only: iostat_end, int64 implicit none private public :: config_2d_t, read_config_2d, validate_config_2d public :: build_field_table_2d !> Frozen ABI schema field count, identical order to !! `config_schema_2d.ensure_schema_2d` — append only. integer, parameter, public :: n_config_fields_2d = 38 type :: config_2d_t ! --- grid --- integer :: nx = 100 integer :: ny = 100 real(wp) :: x_left = 0.0_wp real(wp) :: x_right = 1.0_wp real(wp) :: y_left = 0.0_wp real(wp) :: y_right = 1.0_wp character(len=32) :: grid_type = 'uniform' character(len=256) :: grid_file = '' logical :: grid_binary = .false. ! --- physics --- real(wp) :: gam = 1.4_wp ! --- time control --- real(wp) :: dt = 1.0e-4_wp real(wp) :: cfl = 0.5_wp real(wp) :: time_start = 0.0_wp real(wp) :: time_stop = 0.1_wp integer :: max_iter = 0 ! --- schemes --- character(len=64) :: flux_scheme = 'hllc' character(len=64) :: recon_scheme = 'weno5' character(len=64) :: time_scheme = 'rk3' character(len=8) :: char_proj = 'auto' character(len=64) :: method = 'fvm' ! --- initial / boundary conditions --- character(len=64) :: problem_type = 'isentropic_vortex' character(len=32) :: bc_left = 'periodic' character(len=32) :: bc_right = 'periodic' character(len=32) :: bc_bottom = 'periodic' character(len=32) :: bc_top = 'periodic' ! --- free-stream reference (supersonic_inlet BC) --- real(wp) :: rho_inf = 1.0_wp real(wp) :: u_inf = 0.0_wp real(wp) :: v_inf = 0.0_wp real(wp) :: p_inf = 1.0_wp ! --- limiter switches --- logical :: use_positivity_limiter = .true. ! --- output --- character(len=256) :: output_file = 'result_2d.dat' character(len=64) :: output_format = 'dat' integer :: verbosity = 3 integer :: print_freq = 50 character(len=256) :: log_file = 'run.log' ! --- checkpoint / restart --- integer :: checkpoint_freq = 0 !< write a checkpoint every N iters (0 = off) character(len=256) :: checkpoint_file = 'checkpoint' !< base name; file = <base>_<iter>.bin character(len=256) :: restart_file = '' !< restart from this file (empty = fresh start) end type config_2d_t contains !> Read a namelist file into a config_2d_t and validate it. subroutine read_config_2d(filename, cfg, is_ok, message, case_dir) character(len=*), intent(in) :: filename type(config_2d_t), intent(out) :: cfg logical, intent(out) :: is_ok character(len=*), intent(out) :: 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 integer, parameter :: grid_unset = -huge(1) integer :: nx, ny, max_iter, n_cell real(wp) :: x_left, x_right, y_left, y_right, gam real(wp) :: dt, cfl, time_start, time_stop character(len=64) :: flux_scheme, recon_scheme, time_scheme, problem_type character(len=64) :: method character(len=8) :: char_proj character(len=32) :: bc_left, bc_right, bc_bottom, bc_top real(wp) :: rho_inf, u_inf, v_inf, p_inf character(len=256) :: output_file character(len=64) :: output_format integer :: verbosity, print_freq character(len=256) :: log_file logical :: use_positivity_limiter character(len=32) :: grid_type character(len=256) :: grid_file logical :: grid_binary integer :: checkpoint_freq character(len=256) :: checkpoint_file, restart_file integer :: u, ios type(config_2d_t) :: defaults namelist /grid/ n_cell, nx, ny, x_left, x_right, y_left, y_right, & grid_type, grid_file, grid_binary namelist /physics/ gam namelist /time_ctrl/ dt, cfl, time_start, time_stop, max_iter namelist /schemes/ flux_scheme, recon_scheme, time_scheme, char_proj, use_positivity_limiter, method namelist /initial_condition/ problem_type, bc_left, bc_right, bc_bottom, bc_top, & rho_inf, u_inf, v_inf, p_inf namelist /output/ output_file, output_format, verbosity, print_freq, log_file namelist /checkpoint/ checkpoint_freq, checkpoint_file, restart_file ! Seed namelist locals from the type defaults so omitted keys keep them. n_cell = grid_unset ! sentinel: "not supplied" nx = grid_unset; ny = grid_unset ! sentinel: "not supplied by caller" x_left = defaults % x_left; x_right = defaults % x_right y_left = defaults % y_left; y_right = defaults % y_right grid_type = defaults % grid_type grid_file = defaults % grid_file grid_binary = defaults % grid_binary gam = defaults % gam dt = defaults % dt; cfl = defaults % cfl time_start = defaults % time_start; time_stop = defaults % time_stop max_iter = defaults % max_iter flux_scheme = defaults % flux_scheme; recon_scheme = defaults % recon_scheme time_scheme = defaults % time_scheme; char_proj = defaults % char_proj use_positivity_limiter = defaults % use_positivity_limiter method = defaults % method problem_type = defaults % problem_type bc_left = defaults % bc_left; bc_right = defaults % bc_right bc_bottom = defaults % bc_bottom; bc_top = defaults % bc_top rho_inf = defaults % rho_inf; u_inf = defaults % u_inf v_inf = defaults % v_inf; p_inf = defaults % p_inf output_file = defaults % output_file output_format = defaults % output_format verbosity = defaults % verbosity print_freq = defaults % print_freq log_file = defaults % log_file checkpoint_freq = defaults % checkpoint_freq checkpoint_file = defaults % checkpoint_file restart_file = defaults % restart_file is_ok = .false. message = '' open (newunit=u, file=filename, status='old', action='read', iostat=ios) if (ios /= 0) then message = 'config_2d: cannot open "'//trim(filename)//'"' return end if ! Each group is optional; rewind between reads so order does not matter. rewind (u); read (u, nml=grid, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &grid namelist' close (u, iostat=ios) return end if ! n_cell cascade: n_cell is the isotropic resolution; an explicitly supplied ! nx/ny overrides it. A value supplied but invalid (<=0) is KEPT so that ! validate_config_2d rejects it (do not silently replace user input). if (nx == grid_unset) then if (n_cell /= grid_unset) then nx = n_cell else nx = defaults % nx end if end if if (ny == grid_unset) then if (n_cell /= grid_unset) then ny = n_cell else ny = defaults % ny end if end if rewind (u); read (u, nml=physics, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &physics namelist' close (u, iostat=ios) return end if rewind (u); read (u, nml=time_ctrl, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &time_ctrl namelist' close (u, iostat=ios) return end if rewind (u); read (u, nml=schemes, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &schemes namelist' close (u, iostat=ios) return end if rewind (u); read (u, nml=initial_condition, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &initial_condition namelist' close (u, iostat=ios) return end if rewind (u); read (u, nml=output, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &output namelist' close (u, iostat=ios) return end if rewind (u); read (u, nml=checkpoint, iostat=ios) if (ios /= 0 .and. ios /= iostat_end) then message = 'config_2d: parse error in &checkpoint namelist' close (u, iostat=ios) return end if close (u, iostat=ios) cfg % nx = nx; cfg % ny = ny cfg % x_left = x_left; cfg % x_right = x_right cfg % y_left = y_left; cfg % y_right = y_right cfg % grid_type = to_lower(grid_type) ! Resolve a relative grid_file against the case directory (the explicit ! case_dir override when given, else the namelist's own directory), so a ! case folder is movable and grids load regardless of the process cwd. ! Absolute paths are kept as-is; empty stays empty. cfg % grid_file = resolve_case_path(case_base_dir(filename, case_dir), grid_file) cfg % grid_binary = grid_binary cfg % gam = gam cfg % dt = dt; cfg % cfl = cfl cfg % time_start = time_start; cfg % time_stop = time_stop cfg % max_iter = max_iter cfg % flux_scheme = to_lower(flux_scheme) cfg % recon_scheme = to_lower(recon_scheme) cfg % time_scheme = to_lower(time_scheme) cfg % char_proj = to_lower(char_proj) cfg % method = to_lower(method) cfg % use_positivity_limiter = use_positivity_limiter cfg % problem_type = to_lower(problem_type) cfg % bc_left = to_lower(bc_left) cfg % bc_right = to_lower(bc_right) cfg % bc_bottom = to_lower(bc_bottom) cfg % bc_top = to_lower(bc_top) cfg % rho_inf = rho_inf; cfg % u_inf = u_inf cfg % v_inf = v_inf; cfg % p_inf = p_inf cfg % output_file = output_file cfg % output_format = to_lower(output_format) cfg % verbosity = verbosity cfg % print_freq = print_freq cfg % log_file = log_file cfg % checkpoint_freq = checkpoint_freq cfg % checkpoint_file = checkpoint_file cfg % restart_file = restart_file call validate_config_2d(cfg, is_ok, message) end subroutine read_config_2d !> Bind the 38-entry field-descriptor table (frozen `ensure_schema_2d` !! order) to `cfg`. No casefold flags: the 2D reader already stores !! lowercased selectors, and the engine's choice compare casefolds the !! candidate anyway, preserving the direct-call validate_config_2d !! mixed-case tolerance. Help texts are empty (the 2D schema never !! published help). function build_field_table_2d(cfg) result(fields) type(config_2d_t), intent(inout), target :: cfg type(field_desc_t) :: fields(n_config_fields_2d) ! --- grid: nx/ny/extent checks are conditional on grid_type, kept in ! validate_config_2d --- fields(1) = field_meta('nx', 'grid', cfg_kind_int, '') fields(1) % iptr => cfg % nx fields(2) = field_meta('ny', 'grid', cfg_kind_int, '') fields(2) % iptr => cfg % ny fields(3) = field_meta('x_left', 'grid', cfg_kind_real, '') fields(3) % rptr => cfg % x_left fields(4) = field_meta('x_right', 'grid', cfg_kind_real, '') fields(4) % rptr => cfg % x_right fields(5) = field_meta('y_left', 'grid', cfg_kind_real, '') fields(5) % rptr => cfg % y_left fields(6) = field_meta('y_right', 'grid', cfg_kind_real, '') fields(6) % rptr => cfg % y_right fields(7) = field_meta('grid_type', 'grid', cfg_kind_string, '') fields(7) % sptr => cfg % grid_type fields(8) = field_meta('grid_file', 'grid', cfg_kind_string, '') fields(8) % sptr => cfg % grid_file fields(9) = field_meta('grid_binary', 'grid', cfg_kind_logical, '') fields(9) % lptr => cfg % grid_binary fields(10) = field_meta('gam', 'physics', cfg_kind_real, '', & has_min=.true., min_exclusive=.true., min_value=1.0_wp, & msg_min='config_2d: gam must exceed 1') fields(10) % rptr => cfg % gam fields(11) = field_meta('dt', 'time_ctrl', cfg_kind_real, '', & has_min=.true., min_exclusive=.true., min_value=0.0_wp, & msg_min='config_2d: dt must be positive') fields(11) % rptr => cfg % dt fields(12) = field_meta('cfl', 'time_ctrl', cfg_kind_real, '', & has_min=.true., min_value=0.0_wp, & msg_min='config_2d: cfl must be non-negative') fields(12) % rptr => cfg % cfl fields(13) = field_meta('time_start', 'time_ctrl', cfg_kind_real, '') fields(13) % rptr => cfg % time_start fields(14) = field_meta('time_stop', 'time_ctrl', cfg_kind_real, '') fields(14) % rptr => cfg % time_stop ! max_iter has never been validated by validate_config_2d: metadata only. fields(15) = field_meta('max_iter', 'time_ctrl', cfg_kind_int, '') fields(15) % iptr => cfg % max_iter fields(16) = field_meta('flux_scheme', 'schemes', cfg_kind_choice, '', & choices=flux_scheme_2d_names) fields(16) % sptr => cfg % flux_scheme fields(17) = field_meta('recon_scheme', 'schemes', cfg_kind_choice, '', & choices=recon_scheme_2d_names) fields(17) % sptr => cfg % recon_scheme fields(18) = field_meta('time_scheme', 'schemes', cfg_kind_choice, '', & choices=time_scheme_2d_names) fields(18) % sptr => cfg % time_scheme fields(19) = field_meta('char_proj', 'schemes', cfg_kind_choice, '', & choices=char_proj_mode_names) fields(19) % sptr => cfg % char_proj fields(20) = field_meta('method', 'schemes', cfg_kind_choice, '', & choices=method_names) fields(20) % sptr => cfg % method fields(21) = field_meta('use_positivity_limiter', 'schemes', cfg_kind_logical, '') fields(21) % lptr => cfg % use_positivity_limiter fields(22) = field_meta('problem_type', 'initial_condition', cfg_kind_choice, '', & choices=problem_type_2d_names, choice_msg_style=choice_msg_bare) fields(22) % sptr => cfg % problem_type fields(23) = field_meta('bc_left', 'initial_condition', cfg_kind_choice, '', & choices=bc_2d_names, & msg_choice='config_2d: unknown boundary-condition selector') fields(23) % sptr => cfg % bc_left fields(24) = field_meta('bc_right', 'initial_condition', cfg_kind_choice, '', & choices=bc_2d_names, & msg_choice='config_2d: unknown boundary-condition selector') fields(24) % sptr => cfg % bc_right fields(25) = field_meta('bc_bottom', 'initial_condition', cfg_kind_choice, '', & choices=bc_2d_names, & msg_choice='config_2d: unknown boundary-condition selector') fields(25) % sptr => cfg % bc_bottom fields(26) = field_meta('bc_top', 'initial_condition', cfg_kind_choice, '', & choices=bc_2d_names, & msg_choice='config_2d: unknown boundary-condition selector') fields(26) % sptr => cfg % bc_top fields(27) = field_meta('rho_inf', 'initial_condition', cfg_kind_real, '', & has_min=.true., min_exclusive=.true., min_value=0.0_wp, & msg_min='config_2d: rho_inf and p_inf must be positive') fields(27) % rptr => cfg % rho_inf fields(28) = field_meta('u_inf', 'initial_condition', cfg_kind_real, '') fields(28) % rptr => cfg % u_inf fields(29) = field_meta('v_inf', 'initial_condition', cfg_kind_real, '') fields(29) % rptr => cfg % v_inf fields(30) = field_meta('p_inf', 'initial_condition', cfg_kind_real, '', & has_min=.true., min_exclusive=.true., min_value=0.0_wp, & msg_min='config_2d: rho_inf and p_inf must be positive') fields(30) % rptr => cfg % p_inf fields(31) = field_meta('output_file', 'output', cfg_kind_string, '') fields(31) % sptr => cfg % output_file ! output_format's multi-token parse stays a cross-field/conditional check. fields(32) = field_meta('output_format', 'output', cfg_kind_string, '') fields(32) % sptr => cfg % output_format fields(33) = field_meta('verbosity', 'output', cfg_kind_int, '', & has_min=.true., min_value=0.0_wp, has_max=.true., max_value=4.0_wp, & msg_min='config_2d: verbosity must be 0..4', & msg_max='config_2d: verbosity must be 0..4') fields(33) % iptr => cfg % verbosity fields(34) = field_meta('print_freq', 'output', cfg_kind_int, '', & has_min=.true., min_value=1.0_wp, & msg_min='config_2d: print_freq must be >= 1') fields(34) % iptr => cfg % print_freq fields(35) = field_meta('log_file', 'output', cfg_kind_string, '') fields(35) % sptr => cfg % log_file fields(36) = field_meta('checkpoint_freq', 'checkpoint', cfg_kind_int, '', & has_min=.true., min_value=0.0_wp, & msg_min='config_2d: checkpoint_freq must be >= 0 (0 disables checkpointing)') fields(36) % iptr => cfg % checkpoint_freq fields(37) = field_meta('checkpoint_file', 'checkpoint', cfg_kind_string, '') fields(37) % sptr => cfg % checkpoint_file fields(38) = field_meta('restart_file', 'checkpoint', cfg_kind_string, '') fields(38) % sptr => cfg % restart_file end function build_field_table_2d !> Validate ranges and enumerated selectors. subroutine validate_config_2d(cfg, is_valid, message) type(config_2d_t), intent(in) :: cfg logical, intent(out) :: is_valid character(len=*), intent(out) :: message type(config_2d_t), target :: work type(field_desc_t) :: fields(n_config_fields_2d) logical :: ok is_valid = .false. message = '' work = cfg fields = build_field_table_2d(work) call validate_fields(fields, 'config_2d', ok, message) if (.not. ok) return ! --- Cross-field / conditional checks: KEPT VERBATIM, original order --- ! --- Method token + FDM/grid_type coarse check (before any grid-file probe) --- if (to_lower(cfg % method) == method_fdm .and. & trim(cfg % grid_type) /= 'uniform' .and. trim(cfg % grid_type) /= 'plot3d') then message = "config_2d: method='fdm' supports grid_type 'uniform' (Cartesian) or "// & "'plot3d' (curvilinear); grid_type='"//trim(cfg % grid_type)//"' not supported" return end if if (trim(cfg % grid_type) == 'plot3d') then if (len_trim(cfg % grid_file) == 0) then message = 'config_2d: grid_type=plot3d requires grid_file' return end if block type(mesh_2d_t) :: probe integer :: pnx, pny logical :: gok character(len=256) :: gmsg pnx = 0; pny = 0 call build_mesh_2d_global(probe, 'plot3d', cfg % grid_file, cfg % grid_binary, & pnx, pny, 0.0_wp, 1.0_wp, 0.0_wp, 1.0_wp, gok, gmsg) if (.not. gok) then message = 'config_2d: '//trim(gmsg) return end if ! Curvilinear FDM treats the Plot3D POINTS as nodes and needs >= 3 nodes ! per axis for the 3-point one-sided boundary derivatives. The probe build ! reports cell counts (pnx=ni-1, pny=nj-1), so require pnx >= 2, pny >= 2. if (to_lower(cfg % method) == method_fdm .and. (pnx < 2 .or. pny < 2)) then message = 'config_2d: curvilinear FDM (method=fdm, grid_type=plot3d) requires '// & 'at least 3 grid points per axis' return end if end block ! Sedov / Toro / Config-3 build cell coordinates from x_left + (i-0.5)*dx; ! on a curvilinear grid dx == 0, collapsing the whole field to x_left (and ! dividing by zero in the Sedov hot-cell normaliser). Only freestream, ! uniform, and isentropic_vortex are curvilinear-aware. select case (trim(to_lower(cfg % problem_type))) case (problem_2d_sedov, problem_2d_toro_explosion, problem_2d_riemann2d_config3) message = "config_2d: problem_type '"//trim(cfg % problem_type)// & "' needs uniform-grid cell coordinates and is not supported on "// & "grid_type='plot3d' (use 'freestream', 'uniform', or 'isentropic_vortex')" return end select else if (trim(cfg % grid_type) == 'uniform') then if (cfg % nx <= 0 .or. cfg % ny <= 0) then message = 'config_2d: nx and ny must be positive' return end if if (cfg % nx > MESH2D_MAX_DIM .or. cfg % ny > MESH2D_MAX_DIM) then message = 'config_2d: nx and ny must be <= MESH2D_MAX_DIM' return end if if (int(cfg % nx, int64) * int(cfg % ny, int64) > int(MESH2D_MAX_DIM, int64)**2) then message = 'config_2d: nx*ny exceeds the maximum total cell count' return end if if (cfg % x_right <= cfg % x_left .or. cfg % y_right <= cfg % y_left) then message = 'config_2d: require x_right > x_left and y_right > y_left' return end if else message = "config_2d: unknown grid_type '"//trim(cfg % grid_type)// & "' (expected 'uniform' or 'plot3d')" return end if if (cfg % time_stop <= cfg % time_start) then message = 'config_2d: time_stop must exceed time_start' return end if if (to_lower(cfg % char_proj) /= 'auto') & call log_warn('config_2d: char_proj is ignored by the 2D solver') if (to_lower(cfg % method) == method_fvm .and. & .not. is_fds_flux_scheme(to_lower(cfg % flux_scheme))) then message = "config_2d: method='fvm' requires an FDS flux_scheme (hllc/rusanov); "// & "FVS schemes are FDM-only" return end if if ((to_lower(cfg % bc_left) == bc_periodic) .neqv. (to_lower(cfg % bc_right) == bc_periodic)) then message = 'config_2d: x-axis periodicity requires BOTH bc_left and bc_right = periodic' return end if if ((to_lower(cfg % bc_bottom) == bc_periodic) .neqv. (to_lower(cfg % bc_top) == bc_periodic)) then message = 'config_2d: y-axis periodicity requires BOTH bc_bottom and bc_top = periodic' return end if block character(len=8) :: fmt_tokens(max_formats) integer :: n_fmt logical :: fmt_ok character(len=256) :: fmt_msg call parse_format_list(cfg % output_format, & [character(len=8) :: 'dat', 'vtk', 'tec', 'plt'], & fmt_tokens, n_fmt, fmt_ok, fmt_msg) if (.not. fmt_ok) then message = 'config_2d: '//trim(fmt_msg) return end if end block is_valid = .true. end subroutine validate_config_2d end module config_2d