config.f90 Source File


This file depends on

sourcefile~~config.f90~~EfferentGraph sourcefile~config.f90 config.f90 sourcefile~common.f90~4 common.f90 sourcefile~config.f90->sourcefile~common.f90~4 sourcefile~logger.f90 logger.f90 sourcefile~config.f90->sourcefile~logger.f90 sourcefile~common.f90~4->sourcefile~logger.f90

Files dependent on this one

sourcefile~~config.f90~~AfferentGraph sourcefile~config.f90 config.f90 sourcefile~abl.f90 abl.f90 sourcefile~abl.f90->sourcefile~config.f90 sourcefile~les.f90 les.f90 sourcefile~abl.f90->sourcefile~les.f90 sourcefile~abl.f90~2 abl.f90 sourcefile~abl.f90~2->sourcefile~config.f90 sourcefile~abl.f90~2->sourcefile~abl.f90 sourcefile~base_case.f90 base_case.f90 sourcefile~abl.f90~2->sourcefile~base_case.f90 sourcefile~diagnostics.f90 diagnostics.f90 sourcefile~abl.f90~2->sourcefile~diagnostics.f90 sourcefile~inflow_planes.f90 inflow_planes.f90 sourcefile~abl.f90~2->sourcefile~inflow_planes.f90 sourcefile~turbine_setup.f90 turbine_setup.f90 sourcefile~abl.f90~2->sourcefile~turbine_setup.f90 sourcefile~base_case.f90->sourcefile~config.f90 sourcefile~solver.f90 solver.f90 sourcefile~base_case.f90->sourcefile~solver.f90 sourcefile~io_manager.f90 io_manager.f90 sourcefile~base_case.f90->sourcefile~io_manager.f90 sourcefile~monitoring.f90 monitoring.f90 sourcefile~base_case.f90->sourcefile~monitoring.f90 sourcefile~postprocess.f90 postprocess.f90 sourcefile~base_case.f90->sourcefile~postprocess.f90 sourcefile~channel.f90 channel.f90 sourcefile~channel.f90->sourcefile~config.f90 sourcefile~channel.f90->sourcefile~base_case.f90 sourcefile~channel.f90->sourcefile~solver.f90 sourcefile~checkpoint_manager.f90 checkpoint_manager.f90 sourcefile~checkpoint_manager.f90->sourcefile~config.f90 sourcefile~checkpoint_manager.f90->sourcefile~solver.f90 sourcefile~stats.f90 stats.f90 sourcefile~checkpoint_manager.f90->sourcefile~stats.f90 sourcefile~io_field_utils.f90 io_field_utils.f90 sourcefile~checkpoint_manager.f90->sourcefile~io_field_utils.f90 sourcefile~context.f90 context.f90 sourcefile~context.f90->sourcefile~config.f90 sourcefile~memcheck_device.f90 memcheck_device.f90 sourcefile~context.f90->sourcefile~memcheck_device.f90 sourcefile~cylinder.f90 cylinder.f90 sourcefile~cylinder.f90->sourcefile~config.f90 sourcefile~cylinder.f90->sourcefile~base_case.f90 sourcefile~cylinder.f90->sourcefile~solver.f90 sourcefile~diagnostics.f90->sourcefile~config.f90 sourcefile~diagnostics.f90->sourcefile~les.f90 sourcefile~inflow_planes.f90->sourcefile~config.f90 sourcefile~les.f90->sourcefile~config.f90 sourcefile~memory_estimate.f90 memory_estimate.f90 sourcefile~memory_estimate.f90->sourcefile~config.f90 sourcefile~snapshot_manager.f90 snapshot_manager.f90 sourcefile~snapshot_manager.f90->sourcefile~config.f90 sourcefile~snapshot_manager.f90->sourcefile~solver.f90 sourcefile~snapshot_manager.f90->sourcefile~io_field_utils.f90 sourcefile~solver.f90->sourcefile~config.f90 sourcefile~solver.f90->sourcefile~les.f90 sourcefile~stats.f90->sourcefile~config.f90 sourcefile~stats.f90->sourcefile~solver.f90 sourcefile~turbine_setup.f90->sourcefile~config.f90 sourcefile~turbine_setup.f90->sourcefile~solver.f90 sourcefile~turbine_setup.f90->sourcefile~io_manager.f90 sourcefile~wind_turbine.f90 wind_turbine.f90 sourcefile~wind_turbine.f90->sourcefile~config.f90 sourcefile~wind_turbine.f90->sourcefile~base_case.f90 sourcefile~wind_turbine.f90->sourcefile~solver.f90 sourcefile~wind_turbine.f90->sourcefile~turbine_setup.f90 sourcefile~xcompact.f90 xcompact.f90 sourcefile~xcompact.f90->sourcefile~config.f90 sourcefile~xcompact.f90->sourcefile~base_case.f90 sourcefile~case_factory.f90 case_factory.f90 sourcefile~xcompact.f90->sourcefile~case_factory.f90 sourcefile~build.f90 build.f90 sourcefile~build.f90->sourcefile~abl.f90~2 sourcefile~build.f90->sourcefile~base_case.f90 sourcefile~build.f90->sourcefile~context.f90 sourcefile~build.f90->sourcefile~memory_estimate.f90 sourcefile~build.f90->sourcefile~case_factory.f90 sourcefile~estimate.f90 estimate.f90 sourcefile~build.f90->sourcefile~estimate.f90 sourcefile~build.f90->sourcefile~monitoring.f90 sourcefile~report.f90 report.f90 sourcefile~build.f90->sourcefile~report.f90 sourcefile~case_factory.f90->sourcefile~abl.f90~2 sourcefile~case_factory.f90->sourcefile~base_case.f90 sourcefile~case_factory.f90->sourcefile~channel.f90 sourcefile~case_factory.f90->sourcefile~cylinder.f90 sourcefile~case_factory.f90->sourcefile~wind_turbine.f90 sourcefile~generic.f90 generic.f90 sourcefile~case_factory.f90->sourcefile~generic.f90 sourcefile~tgv.f90 tgv.f90 sourcefile~case_factory.f90->sourcefile~tgv.f90 sourcefile~estimate.f90->sourcefile~context.f90 sourcefile~estimate.f90->sourcefile~memory_estimate.f90 sourcefile~generic.f90->sourcefile~base_case.f90 sourcefile~generic.f90->sourcefile~solver.f90 sourcefile~io_field_utils.f90->sourcefile~solver.f90 sourcefile~io_manager.f90->sourcefile~checkpoint_manager.f90 sourcefile~io_manager.f90->sourcefile~snapshot_manager.f90 sourcefile~io_manager.f90->sourcefile~solver.f90 sourcefile~io_manager.f90->sourcefile~stats.f90 sourcefile~memcheck.f90 memcheck.f90 sourcefile~memcheck.f90->sourcefile~context.f90 sourcefile~memcheck.f90->sourcefile~memory_estimate.f90 sourcefile~memcheck.f90->sourcefile~build.f90 sourcefile~memcheck.f90->sourcefile~estimate.f90 sourcefile~memcheck.f90->sourcefile~report.f90 sourcefile~memcheck_device.f90->sourcefile~base_case.f90 sourcefile~memcheck_device.f90->sourcefile~memory_estimate.f90 sourcefile~monitoring.f90->sourcefile~solver.f90 sourcefile~postprocess.f90->sourcefile~solver.f90 sourcefile~report.f90->sourcefile~context.f90 sourcefile~report.f90->sourcefile~memory_estimate.f90 sourcefile~report.f90->sourcefile~estimate.f90 sourcefile~report.f90->sourcefile~memcheck_device.f90 sourcefile~tgv.f90->sourcefile~base_case.f90 sourcefile~tgv.f90->sourcefile~solver.f90

Source Code

module m_config
  !! Contains all the namelist configurations used in x3d2
  use iso_fortran_env, only: iostat_end
  use m_common
  use m_logger, only: log_warn, abort_run

  implicit none

  integer, parameter :: n_species_max = 99

  !! Maximum number of additional snapshot output fields accepted from the
  !! input file. Some entries, such as ``species``, can expand to multiple
  !! written snapshot fields.
  integer, parameter :: MAX_OUTPUT_FIELDS = 10

  !! Maximum number of actuator-line turbine files in wind_turbine_nml
  integer, parameter :: max_alm_turbines = 100

  type, abstract :: base_config_t
    !! All config types have a method read to initialise their data
  contains
    procedure(read), deferred :: read
  end type base_config_t

  type, extends(base_config_t) :: domain_config_t
    character(len=30) :: flow_case_name
    real(dp) :: L_global(3)
    integer :: dims_global(3), nproc_dir(3)
    character(len=20) :: BC_x(2), BC_y(2), BC_z(2)
    character(len=20) :: stretching(3)
    real(dp) :: beta(3)
  contains
    procedure :: read => read_domain_nml
  end type domain_config_t

  type, extends(base_config_t) :: solver_config_t
    real(dp) :: Re, dt
    logical :: ibm_on
    real(dp), dimension(:), allocatable :: pr_species
    integer :: n_iters, n_output, n_species
    logical :: lowmem_transeq, lowmem_fft
    logical :: spatial_filter          !! explicit low-pass filter on velocity
    real(dp) :: filter_alpha           !! filter parameter, -0.5 < alpha < 0.5
    character(3) :: poisson_solver_type, time_intg
    character(30) :: der1st_scheme, der2nd_scheme, &
                     interpl_scheme, stagder_scheme
    real(dp) :: c_nu, nu0_nu !! hyperviscous (iSVV) der2nd parameters
  contains
    procedure :: read => read_solver_nml
  end type solver_config_t

  type, extends(base_config_t) :: les_config_t
    !! Configuration for explicit sub-grid-scale modelling.
    character(len=20) :: model = 'none'
    real(dp) :: smagorinsky_constant = 0.14_dp
    logical :: wall_damping = .false.
    real(dp) :: wall_damping_n = 3._dp
  contains
    procedure :: read => read_les_nml
  end type les_config_t

  type, extends(base_config_t) :: channel_config_t
    real(dp) :: omega_rot
    real(dp) :: init_noise(3)
    real(dp) :: inlet_noise(3)
    logical :: rotation
    integer :: n_rotate
  contains
    procedure :: read => read_channel_nml
  end type channel_config_t

  type, extends(base_config_t) :: cylinder_config_t
    real(dp) :: init_noise(3)
    real(dp) :: inlet_noise(3)
  contains
    procedure :: read => read_cylinder_nml
  end type cylinder_config_t

  type, extends(base_config_t) :: wind_turbine_config_t
    !! Configuration for the wind-turbine carrier case. Inflow/outflow and
    !! initial noise mirror the cylinder case; the turbine block selects and
    !! drives the turbine forcing model.
    real(dp) :: init_noise(3) = 0._dp
    real(dp) :: inlet_noise(3) = 0._dp  !! perturbation applied at inlet each substep
    real(dp) :: bc_start_u = 1._dp   !! uniform inflow u (Dirichlet at i=1)
    real(dp) :: bc_start_v = 0._dp
    real(dp) :: bc_start_w = 0._dp
    integer :: iturbine = 0          !! 0: none, 1: actuator line, 2: disc
    integer :: iturboutput = 1       !! turbine diagnostics output frequency
    character(len=256) :: adm_coords = '' !! actuator-disc (.ad) coords file
    real(dp) :: rho_air = 1._dp      !! air density used in thrust/power
    real(dp) :: T_relax = -1._dp     !! ADM velocity-filter time; < 0 disables
    !! actuator-line turbine (.turb) files, one per turbine
    character(len=256), allocatable :: alm_turbines(:)
    real(dp) :: eps_factor = 2._dp   !! ALM kernel width in cell sizes
    !> When false, a missing wind_turbine_nml keeps the defaults (no
    !> turbines), for a case that carries turbines optionally (the ABL)
    logical :: required = .true.
  contains
    procedure :: read => read_wind_turbine_nml
  end type wind_turbine_config_t

  type, extends(base_config_t) :: abl_config_t
    real(dp) :: z0 = 0.1_dp            !! aerodynamic roughness length
    real(dp) :: u_star = 0._dp         !! friction velocity
    real(dp) :: delta = 1._dp          !! boundary-layer depth
    real(dp) :: kappa = 0.41_dp        !! von Karman constant
    real(dp) :: dsampling = 3._dp      !! wall-model sampling height, in dy
    real(dp) :: u_geo(3) = 0._dp       !! geostrophic wind UG
    real(dp) :: coriolis_freq = 0._dp  !! Coriolis frequency f
    real(dp) :: init_noise(3) = 0._dp
    !> Seed for the initial noise; 0 draws one from the clock
    integer :: seed = 0
    real(dp) :: u_bulk = 0._dp         !! target bulk velocity (mass_conserve)
    integer :: profile_start_iter = -1 !! first profile sample (-1 = disabled)
    character(len=256) :: profile_file = 'abl_profile.csv'
    logical :: pressure_gradient = .false.
    logical :: coriolis = .false.
    logical :: mass_conserve = .false.
    logical :: damping = .false.
    !> Blend the outlet towards a spanwise-shifted copy of the flow
    !> (Incompact3d ishiftedper); periodic x only
    logical :: shifted_periodic = .false.
  contains
    procedure :: read => read_abl_nml
  end type abl_config_t

  type, extends(base_config_t) :: inflow_config_t
    !! Precursor inflow planes (docs/source/user/input_file.rst)
    logical :: write_planes = .false.  !! sample and write y-z planes
    !> Global x vertex index of the sampling plane; 0 selects the last one
    integer :: plane_x_index = 0
    integer :: ntimesteps = 100        !! planes per file
    integer :: sample_every = 1        !! iterations between samples
    integer :: start_iter = 0          !! first sample at start_iter + sample_every
    character(len=256) :: inflow_prefix = 'inflow'
    !> Successor: impose planes read from inflow_read_prefix at the x inlet
    logical :: read_planes = .false.
    integer :: ninflows = 1            !! files cycled through by a successor
    character(len=256) :: inflow_read_prefix = 'inflow'
  contains
    procedure :: read => read_inflow_nml
  end type inflow_config_t

  type, extends(base_config_t) :: stats_config_t
    integer :: initstat = 0          !! iteration to start accumulating (0 = disabled)
    integer :: istatfreq = 1         !! accumulate every N steps
    integer :: istatout = 0          !! write stats every N steps (0 = disabled)
    character(len=256) :: stats_prefix = "statistics"
  contains
    procedure :: read => read_stats_nml
  end type stats_config_t

  type, extends(base_config_t) :: checkpoint_config_t
    integer :: checkpoint_freq = 0                         !! Frequency of checkpointing (0 = off)
    integer :: snapshot_freq = 0                           !! Frequency of snapshots (0 = off)
    logical :: keep_checkpoint = .true.                    !! If false, only keep latest checkpoint
    character(len=256) :: checkpoint_prefix = "checkpoint"
    character(len=256) :: snapshot_prefix = "snapshot"
    logical :: restart_from_checkpoint = .false.
    character(len=256) :: restart_file = ""
    integer, dimension(3) :: output_stride = [2, 2, 2]     !! Spatial stride for snapshot output
    logical :: snapshot_sp = .false.                       !! if true, snapshot in single precision
    character(len=32) :: output_fields(MAX_OUTPUT_FIELDS) = '' !! additional snapshot output fields
  contains
    procedure :: read => read_checkpoint_nml
  end type checkpoint_config_t

  abstract interface
    subroutine read(self, nml_file, nml_string) !&
      !! Assigns the member variables either from a file or text source.
      !!
      !! nml_file can be an absolute or relative path
      !! nml_string is a character string that contains the namelist.
      !! For example, nml_string="&foobar_nml foo=0, bar='this'/"
      import :: base_config_t

      class(base_config_t) :: self
      character(*), optional, intent(in) :: nml_file
      character(*), optional, intent(in) :: nml_string
    end subroutine read
  end interface

contains

  subroutine read_domain_nml(self, nml_file, nml_string)
    implicit none

    class(domain_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit

    character(len=20) :: flow_case_name
    real(dp), dimension(3) :: L_global
    integer, dimension(3) :: dims_global
    integer, dimension(3) :: nproc_dir
    character(len=20) :: BC_x(2), BC_y(2), BC_z(2)
    character(len=20) :: stretching(3) = ['uniform', 'uniform', 'uniform']
    real(dp), dimension(3) :: beta

    namelist /domain_settings/ flow_case_name, L_global, dims_global, &
      nproc_dir, BC_x, BC_y, BC_z, stretching, beta

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading domain config failed! Provide only a file name &
                     &or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=domain_settings)
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=domain_settings)
    else
      call abort_run('Reading domain config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%flow_case_name = flow_case_name
    self%L_global = L_global
    self%dims_global = dims_global
    self%nproc_dir = nproc_dir
    self%BC_x = BC_x
    self%BC_y = BC_y
    self%BC_z = BC_z
    self%stretching = stretching
    self%beta = beta

  end subroutine read_domain_nml

  subroutine read_solver_nml(self, nml_file, nml_string)
    implicit none

    class(solver_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit

    real(dp) :: Re, dt
    logical :: ibm_on = .false.
    real(dp), dimension(n_species_max) :: pr_species = 1._dp
    integer :: n_iters, n_output, n_species = 0
    !> triggers the low memory implementations
    logical :: lowmem_transeq = .false., lowmem_fft = .false.
    !! Explicit low-pass filter on the velocity, off by default. Needed by
    !! wall-modelled ABL runs, where the 2*dx mode the compact schemes cannot
    !! dissipate otherwise breaks momentum conservation in the convection.
    logical :: spatial_filter = .false.
    !! Incompact3d's value. Smaller alpha is far more dissipative: at 0.40 the
    !! filter damps every resolved wavenumber about ten times harder per
    !! application than at 0.49, which over a long run scrubs out the smallest
    !! resolved eddies. The filter's system is only marginally diagonally
    !! dominant here, so it is solved with Thomas rather than the distributed
    !! algorithm wherever the direction is not decomposed.
    real(dp) :: filter_alpha = 0.49_dp
    character(3) :: time_intg
    character(3) :: poisson_solver_type = 'FFT'
    character(30) :: der1st_scheme = 'compact6', der2nd_scheme = 'compact6', &
                     interpl_scheme = 'classic', stagder_scheme = 'compact6'
    !> hyperviscous (iSVV) der2nd parameters; only used when der2nd_scheme is
    !> 'compact6-hyperviscous' (Incompact3d cnu, nu0nu). Harmless defaults otherwise.
    real(dp) :: c_nu = 0.44_dp, nu0_nu = 0._dp

    namelist /solver_params/ Re, dt, n_iters, n_output, poisson_solver_type, &
      spatial_filter, filter_alpha, &
      n_species, pr_species, lowmem_transeq, lowmem_fft, &
      time_intg, der1st_scheme, der2nd_scheme, interpl_scheme, &
      stagder_scheme, ibm_on, c_nu, nu0_nu

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading solver config failed! Provide only a file name &
                     &or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=solver_params)
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=solver_params)
    else
      call abort_run('Reading solver config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%Re = Re
    self%dt = dt
    self%n_iters = n_iters
    self%n_output = n_output
    self%spatial_filter = spatial_filter
    self%filter_alpha = filter_alpha
    self%ibm_on = ibm_on
    self%n_species = n_species
    if (n_species > 0) self%pr_species = pr_species(1:n_species)
    self%lowmem_transeq = lowmem_transeq
    self%lowmem_fft = lowmem_fft
    self%poisson_solver_type = poisson_solver_type
    self%time_intg = time_intg
    self%der1st_scheme = der1st_scheme
    self%der2nd_scheme = der2nd_scheme
    self%interpl_scheme = interpl_scheme
    self%stagder_scheme = stagder_scheme
    self%c_nu = c_nu
    self%nu0_nu = nu0_nu

  end subroutine read_solver_nml

  subroutine read_les_nml(self, nml_file, nml_string)
    class(les_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit, ierr
    character(len=20) :: model
    real(dp) :: smagorinsky_constant, wall_damping_n
    logical :: wall_damping

    namelist /les_params/ model, smagorinsky_constant, wall_damping, &
      wall_damping_n

    model = self%model
    smagorinsky_constant = self%smagorinsky_constant
    wall_damping = self%wall_damping
    wall_damping_n = self%wall_damping_n

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading LES config failed!  Provide only a file name or &
                     &source.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file, iostat=ierr)
      if (ierr /= 0) &
        call abort_run('Opening LES config file failed.', collective=.true.)
      read (unit, nml=les_params, iostat=ierr)
      close (unit)
      ! Existing input files may omit this optional namelist. End-of-file
      ! therefore selects the defaults, while a malformed block remains fatal.
      if (ierr > 0) &
        call abort_run('Reading LES config failed.', collective=.true.)
    else if (present(nml_string)) then
      read (nml_string, nml=les_params)
    else
      call abort_run('Reading LES config failed! Provide a file name or &
                     &source.', collective=.true.)
    end if

    select case (trim(model))
    case ('none', 'smagorinsky')
    case default
      call abort_run('Unknown LES model. Use "none" or "smagorinsky".', &
                     collective=.true.)
    end select
    if (smagorinsky_constant <= 0._dp) &
      call abort_run('smagorinsky_constant must be positive.', &
                     collective=.true.)
    if (wall_damping_n <= 0._dp) &
      call abort_run('wall_damping_n must be positive.', collective=.true.)

    self%model = trim(model)
    self%smagorinsky_constant = smagorinsky_constant
    self%wall_damping = wall_damping
    self%wall_damping_n = wall_damping_n
  end subroutine read_les_nml

  subroutine read_channel_nml(self, nml_file, nml_string)
    implicit none

    class(channel_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit

    real(dp) :: init_noise(3)
    real(dp) :: inlet_noise(3)
    real(dp) :: omega_rot
    logical :: rotation
    integer :: n_rotate

    namelist /channel_nml/ init_noise, inlet_noise, &
      rotation, omega_rot, n_rotate

    ! Default to no noise if the namelist omits these entries.
    init_noise = 0._dp
    inlet_noise = 0._dp

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading channel config failed! Provide only a file name &
                     &or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=channel_nml)
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=channel_nml)
    else
      call abort_run('Reading channel config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%init_noise = init_noise
    self%inlet_noise = inlet_noise
    self%rotation = rotation
    self%omega_rot = omega_rot
    self%n_rotate = n_rotate

  end subroutine read_channel_nml

  subroutine read_cylinder_nml(self, nml_file, nml_string)
    implicit none

    class(cylinder_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit

    real(dp) :: init_noise(3)
    real(dp) :: inlet_noise(3)

    namelist /cylinder_nml/ init_noise, inlet_noise

    ! Default to no noise if the namelist omits these entries.
    init_noise = 0._dp
    inlet_noise = 0._dp

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading cylinder config failed! Provide only a file &
                     &name or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=cylinder_nml)
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=cylinder_nml)
    else
      call abort_run('Reading cylinder config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%init_noise = init_noise
    self%inlet_noise = inlet_noise

  end subroutine read_cylinder_nml

  subroutine read_wind_turbine_nml(self, nml_file, nml_string)
    implicit none

    class(wind_turbine_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit, ierr
    character(len=256) :: msg

    real(dp) :: init_noise(3) = 0._dp
    real(dp) :: inlet_noise(3) = 0._dp
    real(dp) :: bc_start_u = 1._dp
    real(dp) :: bc_start_v = 0._dp
    real(dp) :: bc_start_w = 0._dp
    integer :: iturbine = 0
    integer :: iturboutput = 1
    character(len=256) :: adm_coords = ''
    real(dp) :: rho_air = 1._dp
    real(dp) :: T_relax = -1._dp
    character(len=256) :: alm_turbines(max_alm_turbines) = ''
    real(dp) :: eps_factor = 2._dp

    namelist /wind_turbine_nml/ init_noise, inlet_noise, bc_start_u, &
      bc_start_v, bc_start_w, iturbine, iturboutput, adm_coords, &
      rho_air, T_relax, alm_turbines, eps_factor

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading wind_turbine config failed! Provide only a file &
                     &name or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=wind_turbine_nml, iostat=ierr, iomsg=msg)
      close (unit)
      ! An end of file means the namelist is absent, unless its group is in
      ! the file and only the closing / is missing
      if (ierr == iostat_end .and. .not. self%required) then
        if (nml_group_in_file(nml_file, 'wind_turbine_nml')) &
          call abort_run('Reading wind_turbine_nml failed: the file ends &
                         &before its closing /.', collective=.true.)
      else if (ierr /= 0) then
        call abort_run('Reading wind_turbine_nml failed: '//trim(msg), &
                       collective=.true.)
      end if
    else if (present(nml_string)) then
      read (nml_string, nml=wind_turbine_nml)
    else
      call abort_run('Reading wind_turbine config failed! Provide at least &
                     &one of the following: file name or source', &
                     collective=.true.)
    end if

    self%init_noise = init_noise
    self%inlet_noise = inlet_noise
    self%bc_start_u = bc_start_u
    self%bc_start_v = bc_start_v
    self%bc_start_w = bc_start_w
    self%iturbine = iturbine
    self%iturboutput = iturboutput
    self%adm_coords = adm_coords
    self%rho_air = rho_air
    self%T_relax = T_relax
    self%alm_turbines = pack(alm_turbines, len_trim(alm_turbines) > 0)
    self%eps_factor = eps_factor

    if (self%iturboutput < 0) &
      call abort_run('wind_turbine_nml: iturboutput must be non-negative.', &
                     collective=.true.)
    if (self%iturbine == 2 .and. len_trim(self%adm_coords) == 0) &
      call abort_run('wind_turbine_nml: iturbine = 2 needs adm_coords.', &
                     collective=.true.)

  end subroutine read_wind_turbine_nml

  subroutine read_abl_nml(self, nml_file, nml_string)
    implicit none

    class(abl_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit

    real(dp) :: z0, u_star, delta, kappa, dsampling
    real(dp) :: u_geo(3), coriolis_freq, init_noise(3), u_bulk
    integer :: profile_start_iter, seed
    character(len=256) :: profile_file
    logical :: pressure_gradient, coriolis, mass_conserve, damping
    logical :: shifted_periodic

    namelist /abl_nml/ z0, u_star, delta, kappa, dsampling, &
      u_geo, coriolis_freq, &
      init_noise, seed, u_bulk, profile_start_iter, profile_file, &
      pressure_gradient, coriolis, mass_conserve, damping, shifted_periodic

    ! Defaults
    z0 = 0.1_dp
    u_star = 0._dp
    delta = 1._dp
    kappa = 0.41_dp
    dsampling = 3._dp
    u_geo = 0._dp
    coriolis_freq = 0._dp
    init_noise = 0._dp
    seed = 0
    u_bulk = 0._dp
    profile_start_iter = -1
    profile_file = 'abl_profile.csv'
    pressure_gradient = .false.
    coriolis = .false.
    mass_conserve = .false.
    damping = .false.
    shifted_periodic = .false.

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading ABL config failed! Provide only a file name or &
                     &source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file)
      read (unit, nml=abl_nml)
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=abl_nml)
    else
      call abort_run('Reading ABL config failed! Provide at least one of the &
                     &following: file name or source', collective=.true.)
    end if

    self%z0 = z0
    self%u_star = u_star
    self%delta = delta
    self%kappa = kappa
    self%dsampling = dsampling
    self%u_geo = u_geo
    self%coriolis_freq = coriolis_freq
    self%init_noise = init_noise
    self%seed = seed
    self%u_bulk = u_bulk
    self%profile_start_iter = profile_start_iter
    self%profile_file = trim(profile_file)
    self%pressure_gradient = pressure_gradient
    self%coriolis = coriolis
    self%mass_conserve = mass_conserve
    self%damping = damping
    self%shifted_periodic = shifted_periodic

    call validate_abl_config(self)

  end subroutine read_abl_nml

  subroutine validate_abl_config(config)
    !! Validate relationships between ABL namelist values in one place.
    type(abl_config_t), intent(in) :: config

    ! The driving mechanism is checked by the case, which may instead be
    ! driven by a precursor inflow.

    if (config%z0 <= 0._dp .or. config%delta <= config%z0 .or. &
        config%kappa <= 0._dp) &
      call abort_run('ABL config error: require delta > z0 > 0 and &
                     &kappa > 0.', collective=.true.)

    if (any(config%init_noise < 0._dp)) &
      call abort_run('ABL config error: init_noise must not be negative.', &
                     collective=.true.)
    if (config%seed < 0) &
      call abort_run('ABL config error: seed must not be negative.', &
                     collective=.true.)

    if (config%profile_start_iter < -1) &
      call abort_run('ABL config error: profile_start_iter must be -1 or &
                     &greater.', collective=.true.)
    if (config%profile_start_iter >= 0 .and. &
        len_trim(config%profile_file) == 0) &
      call abort_run('ABL config error: profile_file must not be empty.', &
                     collective=.true.)
  end subroutine validate_abl_config

  subroutine read_checkpoint_nml(self, nml_file, nml_string)
    implicit none

    class(checkpoint_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit, ierr

    integer :: checkpoint_freq = 0
    integer :: snapshot_freq = 0
    logical :: keep_checkpoint = .false.
    character(len=256) :: checkpoint_prefix = "checkpoint"
    character(len=256) :: snapshot_prefix = "snapshot"
    logical :: restart_from_checkpoint = .false.
    character(len=256) :: restart_file = ""
    integer, dimension(3) :: output_stride = [1, 1, 1]
    logical :: snapshot_sp = .false.
    character(len=32) :: output_fields(MAX_OUTPUT_FIELDS) = ''

    namelist /checkpoint_params/ checkpoint_freq, snapshot_freq, &
      keep_checkpoint, checkpoint_prefix, snapshot_prefix, &
      restart_from_checkpoint, restart_file, output_stride, snapshot_sp, &
      output_fields
    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading checkpoint config failed! Provide only a file &
                     &name or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file, iostat=ierr)
      if (ierr == 0) then
        read (unit, nml=checkpoint_params, iostat=ierr)

        if (ierr /= 0 .and. ierr /= -1) &
          call log_warn('Error in checkpoint_params namelist, using defaults')
      end if
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=checkpoint_params)
    else
      call abort_run('Reading checkpoint config failed! Provide at least one &
                     &of the following: file name or source', &
                     collective=.true.)
    end if

    self%checkpoint_freq = checkpoint_freq
    self%snapshot_freq = snapshot_freq
    self%keep_checkpoint = keep_checkpoint
    self%checkpoint_prefix = checkpoint_prefix
    self%snapshot_prefix = snapshot_prefix
    self%restart_from_checkpoint = restart_from_checkpoint
    self%restart_file = restart_file
    self%output_stride = output_stride
    self%snapshot_sp = snapshot_sp
    self%output_fields = output_fields
  end subroutine read_checkpoint_nml

  subroutine read_inflow_nml(self, nml_file, nml_string)
    !! The namelist is optional: a missing inflow_nml keeps the defaults.
    implicit none

    class(inflow_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit, ierr

    logical :: write_planes, read_planes
    integer :: plane_x_index, ntimesteps, sample_every, start_iter, ninflows
    character(len=256) :: inflow_prefix, inflow_read_prefix

    namelist /inflow_nml/ write_planes, plane_x_index, ntimesteps, &
      sample_every, start_iter, inflow_prefix, read_planes, ninflows, &
      inflow_read_prefix

    write_planes = .false.
    plane_x_index = 0
    ntimesteps = 100
    sample_every = 1
    start_iter = 0
    inflow_prefix = 'inflow'
    read_planes = .false.
    ninflows = 1
    inflow_read_prefix = 'inflow'

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading inflow config failed! Provide only a file name &
                     &or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file, iostat=ierr)
      if (ierr == 0) then
        read (unit, nml=inflow_nml, iostat=ierr)
        ! -1 is end of file: the namelist is absent
        if (ierr /= 0 .and. ierr /= -1) &
          call abort_run('Reading inflow config failed! Error in &
                         &inflow_nml.', collective=.true.)
      end if
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=inflow_nml)
    else
      call abort_run('Reading inflow config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%write_planes = write_planes
    self%plane_x_index = plane_x_index
    self%ntimesteps = ntimesteps
    self%sample_every = sample_every
    self%start_iter = start_iter
    self%inflow_prefix = trim(inflow_prefix)
    self%read_planes = read_planes
    self%ninflows = ninflows
    self%inflow_read_prefix = trim(inflow_read_prefix)

    if (self%ntimesteps < 1) &
      call abort_run('inflow_nml: ntimesteps must be at least 1.', &
                     collective=.true.)
    if (self%sample_every < 1) &
      call abort_run('inflow_nml: sample_every must be at least 1.', &
                     collective=.true.)
    if (self%start_iter < 0) &
      call abort_run('inflow_nml: start_iter must be non-negative.', &
                     collective=.true.)
    if (self%plane_x_index < 0) &
      call abort_run('inflow_nml: plane_x_index must be non-negative.', &
                     collective=.true.)
    if (self%ninflows < 1) &
      call abort_run('inflow_nml: ninflows must be at least 1.', &
                     collective=.true.)
    if (self%write_planes .and. self%read_planes .and. &
        self%inflow_prefix == self%inflow_read_prefix) &
      call abort_run('inflow_nml: inflow_prefix and inflow_read_prefix must &
                     &differ, or the run overwrites the planes it reads.', &
                     collective=.true.)
  end subroutine read_inflow_nml

  subroutine read_stats_nml(self, nml_file, nml_string)
    implicit none

    class(stats_config_t) :: self
    character(*), optional, intent(in) :: nml_file
    character(*), optional, intent(in) :: nml_string

    integer :: unit, ierr

    integer :: initstat = 0
    integer :: istatfreq = 1
    integer :: istatout = 0
    character(len=256) :: stats_prefix = "statistics"

    namelist /stats_params/ initstat, istatfreq, istatout, stats_prefix

    if (present(nml_file) .and. present(nml_string)) then
      call abort_run('Reading stats config failed! Provide only a file name &
                     &or source, not both.', collective=.true.)
    else if (present(nml_file)) then
      open (newunit=unit, file=nml_file, iostat=ierr)
      if (ierr == 0) then
        read (unit, nml=stats_params, iostat=ierr)
        if (ierr /= 0 .and. ierr /= -1) &
          call log_warn('Error in stats_params namelist, using defaults')
      end if
      close (unit)
    else if (present(nml_string)) then
      read (nml_string, nml=stats_params)
    else
      call abort_run('Reading stats config failed! Provide at least one of &
                     &the following: file name or source', collective=.true.)
    end if

    self%initstat = initstat
    self%istatfreq = istatfreq
    self%istatout = istatout
    self%stats_prefix = stats_prefix
  end subroutine read_stats_nml

  pure logical function has_output_field(config, name)
    !! Check whether a field name is present in the output_fields list.
    type(checkpoint_config_t), intent(in) :: config
    character(*), intent(in) :: name

    has_output_field = any(config%output_fields == name)
  end function has_output_field

  pure logical function output_field_active(checkpoint_cfg, name) &
    result(active)
    !! Whether a snapshot output field is actually driven at runtime - a
    !! field only triggers its associated allocations if BOTH it is listed
    !! in output_fields AND snapshotting is enabled. Mirrors the pattern
    !! used for keep_pressure (src/case/base_case.f90:120-122), shared by
    !! run() and x3d2-memcheck.
    type(checkpoint_config_t), intent(in) :: checkpoint_cfg
    character(*), intent(in) :: name

    active = has_output_field(checkpoint_cfg, name) .and. &
             checkpoint_cfg%snapshot_freq > 0
  end function output_field_active

  logical function nml_group_in_file(nml_file, group) result(found)
    !! Whether a line of the file opens the namelist group (&group, in any
    !! case), so an optional namelist that is present but unterminated can be
    !! told apart from an absent one.
    character(*), intent(in) :: nml_file
    character(*), intent(in) :: group

    character(len=1024) :: line
    integer :: unit, ios, i, c, n

    found = .false.
    n = len(group) + 1
    open (newunit=unit, file=nml_file, status='old', action='read', &
          iostat=ios)
    if (ios /= 0) return
    do
      read (unit, '(a)', iostat=ios) line
      if (ios /= 0) exit
      line = adjustl(line)
      do i = 1, n
        c = iachar(line(i:i))
        if (c >= iachar('A') .and. c <= iachar('Z')) &
          line(i:i) = achar(c + 32)
      end do
      ! A blank after the name, so a longer group name does not match
      found = line(1:n) == '&'//group .and. line(n + 1:n + 1) == ' '
      if (found) exit
    end do
    close (unit)
  end function nml_group_in_file

end module m_config