load_toml_config Subroutine

public subroutine load_toml_config(path, cfg)

TOML 文書を toml-f で解釈して設定へ反映する。 現在は sim / mesh / output / [[mesh.templates]] / [[particles.species]] を扱う。

Arguments

Type IntentOptional Attributes Name
character(len=*), intent(in) :: path

読み込むTOMLファイルパス。

type(app_config), intent(inout) :: cfg

読み込み結果で更新するアプリ設定。


Calls

proc~~load_toml_config~~CallsGraph proc~load_toml_config load_toml_config destroy destroy proc~load_toml_config->destroy interface~resolve_batch_duration resolve_batch_duration proc~load_toml_config->interface~resolve_batch_duration interface~resolve_external_e_field resolve_external_e_field proc~load_toml_config->interface~resolve_external_e_field interface~validate_photo_raycast_species validate_photo_raycast_species proc~load_toml_config->interface~validate_photo_raycast_species interface~validate_reservoir_species validate_reservoir_species proc~load_toml_config->interface~validate_reservoir_species proc~apply_toml_document apply_toml_document proc~load_toml_config->proc~apply_toml_document proc~config_uses_conductor_surface_model config_uses_conductor_surface_model proc~load_toml_config->proc~config_uses_conductor_surface_model proc~init_app_config_authoring init_app_config_authoring proc~load_toml_config->proc~init_app_config_authoring proc~lower_ascii lower_ascii proc~load_toml_config->proc~lower_ascii proc~normalize_high_level_config normalize_high_level_config proc~load_toml_config->proc~normalize_high_level_config toml_parse toml_parse proc~load_toml_config->toml_parse proc~apply_toml_document->proc~lower_ascii get_keys get_keys proc~apply_toml_document->get_keys get_value get_value proc~apply_toml_document->get_value proc~apply_mesh_toml_table apply_mesh_toml_table proc~apply_toml_document->proc~apply_mesh_toml_table proc~apply_output_toml_table apply_output_toml_table proc~apply_toml_document->proc~apply_output_toml_table proc~apply_particles_toml_table apply_particles_toml_table proc~apply_toml_document->proc~apply_particles_toml_table proc~apply_sim_toml_table apply_sim_toml_table proc~apply_toml_document->proc~apply_sim_toml_table proc~require_toml_success require_toml_success proc~apply_toml_document->proc~require_toml_success proc~normalize_high_level_config->proc~lower_ascii proc~apply_mesh_toml_table->proc~lower_ascii proc~apply_mesh_toml_table->get_keys proc~get_toml_real get_toml_real proc~apply_mesh_toml_table->proc~get_toml_real proc~get_toml_real3 get_toml_real3 proc~apply_mesh_toml_table->proc~get_toml_real3 proc~get_toml_string get_toml_string proc~apply_mesh_toml_table->proc~get_toml_string proc~read_mesh_groups_table read_mesh_groups_table proc~apply_mesh_toml_table->proc~read_mesh_groups_table proc~read_template_array read_template_array proc~apply_mesh_toml_table->proc~read_template_array proc~apply_output_toml_table->proc~lower_ascii proc~apply_output_toml_table->get_keys proc~get_toml_int get_toml_int proc~apply_output_toml_table->proc~get_toml_int proc~get_toml_logical get_toml_logical proc~apply_output_toml_table->proc~get_toml_logical proc~apply_output_toml_table->proc~get_toml_string proc~apply_particles_toml_table->proc~lower_ascii proc~apply_particles_toml_table->get_keys proc~read_particle_species_array read_particle_species_array proc~apply_particles_toml_table->proc~read_particle_species_array proc~apply_sim_toml_table->proc~lower_ascii proc~apply_sim_toml_table->get_keys proc~get_toml_boundary_mode get_toml_boundary_mode proc~apply_sim_toml_table->proc~get_toml_boundary_mode proc~apply_sim_toml_table->proc~get_toml_int proc~apply_sim_toml_table->proc~get_toml_logical proc~apply_sim_toml_table->proc~get_toml_real proc~apply_sim_toml_table->proc~get_toml_real3 proc~apply_sim_toml_table->proc~get_toml_string proc~get_toml_boundary_mode->proc~lower_ascii proc~get_toml_boundary_mode->proc~get_toml_string proc~get_toml_int->get_value proc~get_toml_int->proc~require_toml_success proc~get_toml_logical->get_value proc~get_toml_logical->proc~require_toml_success proc~get_toml_real->get_value proc~get_toml_real->proc~require_toml_success proc~get_toml_real3->get_value proc~get_toml_real3->proc~require_toml_success toml_len toml_len proc~get_toml_real3->toml_len proc~get_toml_string->get_value proc~get_toml_string->proc~require_toml_success proc~read_mesh_groups_table->get_keys proc~read_mesh_groups_table->get_value proc~read_mesh_groups_table->proc~require_toml_success proc~apply_mesh_group_toml_table apply_mesh_group_toml_table proc~read_mesh_groups_table->proc~apply_mesh_group_toml_table proc~ensure_authoring_group_capacity ensure_authoring_group_capacity proc~read_mesh_groups_table->proc~ensure_authoring_group_capacity proc~read_particle_species_array->get_value proc~read_particle_species_array->proc~require_toml_success proc~apply_particles_species_toml_table apply_particles_species_toml_table proc~read_particle_species_array->proc~apply_particles_species_toml_table proc~ensure_authoring_particle_capacity ensure_authoring_particle_capacity proc~read_particle_species_array->proc~ensure_authoring_particle_capacity proc~ensure_particle_species_capacity ensure_particle_species_capacity proc~read_particle_species_array->proc~ensure_particle_species_capacity proc~species_from_defaults species_from_defaults proc~read_particle_species_array->proc~species_from_defaults proc~read_particle_species_array->toml_len proc~read_template_array->get_value proc~read_template_array->proc~require_toml_success proc~apply_template_toml_table apply_template_toml_table proc~read_template_array->proc~apply_template_toml_table proc~ensure_authoring_template_capacity ensure_authoring_template_capacity proc~read_template_array->proc~ensure_authoring_template_capacity proc~ensure_template_capacity ensure_template_capacity proc~read_template_array->proc~ensure_template_capacity proc~read_template_array->toml_len proc~apply_mesh_group_toml_table->proc~lower_ascii proc~apply_mesh_group_toml_table->get_keys proc~apply_mesh_group_toml_table->proc~get_toml_real proc~apply_mesh_group_toml_table->proc~get_toml_real3 proc~apply_mesh_group_toml_table->proc~get_toml_string proc~apply_particles_species_toml_table->proc~lower_ascii proc~apply_particles_species_toml_table->get_keys proc~apply_particles_species_toml_table->proc~get_toml_int proc~apply_particles_species_toml_table->proc~get_toml_logical proc~apply_particles_species_toml_table->proc~get_toml_real proc~apply_particles_species_toml_table->proc~get_toml_real3 proc~apply_particles_species_toml_table->proc~get_toml_string proc~get_toml_real2 get_toml_real2 proc~apply_particles_species_toml_table->proc~get_toml_real2 proc~apply_template_toml_table->proc~lower_ascii proc~apply_template_toml_table->get_keys proc~apply_template_toml_table->proc~get_toml_int proc~apply_template_toml_table->proc~get_toml_logical proc~apply_template_toml_table->proc~get_toml_real proc~apply_template_toml_table->proc~get_toml_real3 proc~apply_template_toml_table->proc~get_toml_string proc~get_toml_real_scalar_or_array3 get_toml_real_scalar_or_array3 proc~apply_template_toml_table->proc~get_toml_real_scalar_or_array3 proc~get_toml_real2->get_value proc~get_toml_real2->proc~require_toml_success proc~get_toml_real2->toml_len proc~get_toml_real_scalar_or_array3->get_value proc~get_toml_real_scalar_or_array3->proc~require_toml_success proc~get_toml_real_scalar_or_array3->toml_len

Called by

proc~~load_toml_config~~CalledByGraph proc~load_toml_config load_toml_config proc~load_app_config load_app_config proc~load_app_config->proc~load_toml_config proc~load_or_init_run_state load_or_init_run_state proc~load_or_init_run_state->proc~load_app_config program~main main program~main->proc~load_or_init_run_state

Source Code

  subroutine load_toml_config(path, cfg)
    character(len=*), intent(in) :: path
    type(app_config), intent(inout) :: cfg
    integer :: u, ios, i, axis
    integer(i32) :: per_batch_particles
    integer(i32) :: n_periodic_axes
    logical :: has_dynamic_source_species
    type(toml_table), allocatable :: document
    type(toml_error), allocatable :: parse_error
    type(app_config_authoring) :: authoring

    if (.not. allocated(cfg%templates)) then
      allocate (cfg%templates(max_templates))
      cfg%n_templates = 0_i32
    end if
    if (.not. allocated(cfg%particle_species)) then
      allocate (cfg%particle_species(max_particle_species))
      cfg%particle_species = particle_species_spec()
      cfg%n_particle_species = 0_i32
    end if
    call init_app_config_authoring(authoring, size(cfg%templates), size(cfg%particle_species))

    open (newunit=u, file=trim(path), status='old', action='read', iostat=ios)
    if (ios /= 0) error stop 'Could not open TOML file.'
    call toml_parse(document, u, parse_error)
    close (u)
    if (allocated(parse_error)) then
      error stop 'Failed to parse TOML config: '//parse_error%message
    end if
    if (.not. allocated(document)) error stop 'Failed to parse TOML config.'

    call apply_toml_document(cfg, document, authoring)
    call document%destroy
    call normalize_high_level_config(cfg, authoring)

    if (cfg%sim%batch_count <= 0_i32) error stop 'sim.batch_count must be > 0.'
    if (.not. ieee_is_finite(cfg%sim%dt) .or. cfg%sim%dt <= 0.0d0) then
      error stop 'sim.dt must be finite and > 0.'
    end if
    if (cfg%sim%max_step <= 0_i32) error stop 'sim.max_step must be > 0.'
    if (.not. ieee_is_finite(cfg%sim%tol_rel) .or. cfg%sim%tol_rel < 0.0d0) then
      error stop 'sim.tol_rel must be finite and >= 0.'
    end if
    if (.not. ieee_is_finite(cfg%sim%q_floor) .or. cfg%sim%q_floor <= 0.0d0) then
      error stop 'sim.q_floor must be finite and > 0.'
    end if
    if (.not. ieee_is_finite(cfg%sim%softening) .or. cfg%sim%softening < 0.0d0) then
      error stop 'sim.softening must be finite and >= 0.'
    end if
    if (.not. all(ieee_is_finite(cfg%sim%b0))) error stop 'sim.b0 must contain finite values.'
    if (cfg%sim%use_box) then
      if (.not. all(ieee_is_finite(cfg%sim%box_min)) .or. .not. all(ieee_is_finite(cfg%sim%box_max))) then
        error stop 'sim.box_min/box_max must contain finite values.'
      end if
      if (any(cfg%sim%box_max <= cfg%sim%box_min)) then
        error stop 'sim.box_max must be greater than sim.box_min on all axes when sim.use_box=true.'
      end if
    end if
    if (cfg%n_particle_species <= 0_i32) error stop 'At least one [[particles.species]] entry is required.'
    if (len_trim(cfg%output_restart_from) > 0 .and. .not. cfg%resume_output) then
      error stop 'output.restart_from requires output.resume = true.'
    end if

    cfg%mesh_mode = lower_ascii(trim(cfg%mesh_mode))
    select case (trim(cfg%mesh_mode))
    case ('auto', 'obj', 'template')
      continue
    case default
      error stop 'mesh.mode must be "auto", "obj", or "template".'
    end select
    cfg%mesh_surface_model = lower_ascii(trim(cfg%mesh_surface_model))
    select case (trim(cfg%mesh_surface_model))
    case ('insulator', 'conductor', 'dielectric')
      continue
    case default
      error stop 'mesh.surface_model must be "insulator", "conductor", or "dielectric".'
    end select
    if (.not. ieee_is_finite(cfg%mesh_epsilon_r) .or. cfg%mesh_epsilon_r < 1.0d0) then
      error stop 'mesh.epsilon_r must be finite and >= 1.'
    end if
    do i = 1, cfg%n_templates
      cfg%templates(i)%surface_model = lower_ascii(trim(cfg%templates(i)%surface_model))
      select case (trim(cfg%templates(i)%surface_model))
      case ('insulator', 'conductor', 'dielectric')
        continue
      case default
        error stop 'mesh.templates.surface_model must be "insulator", "conductor", or "dielectric".'
      end select
      if (.not. ieee_is_finite(cfg%templates(i)%epsilon_r) .or. cfg%templates(i)%epsilon_r < 1.0d0) then
        error stop 'mesh.templates.epsilon_r must be finite and >= 1.'
      end if
    end do
    cfg%sim%field_solver = lower_ascii(trim(cfg%sim%field_solver))
    select case (trim(cfg%sim%field_solver))
    case ('direct', 'treecode', 'fmm', 'auto')
      continue
    case default
      error stop 'sim.field_solver must be "direct", "treecode", "fmm", or "auto".'
    end select
    cfg%sim%field_normalization = lower_ascii(trim(cfg%sim%field_normalization))
    select case (trim(cfg%sim%field_normalization))
    case ('si', 'box', 'mesh', 'length')
      continue
    case default
      error stop 'sim.field_normalization must be "si", "box", "mesh", or "length".'
    end select
    if (.not. ieee_is_finite(cfg%sim%field_length_scale) .or. cfg%sim%field_length_scale <= 0.0d0) then
      error stop 'sim.field_length_scale must be finite and > 0.'
    end if
    if (trim(cfg%sim%field_normalization) == 'length' .and. cfg%sim%field_length_scale <= 0.0d0) then
      error stop 'sim.field_normalization="length" requires sim.field_length_scale > 0.'
    end if
    cfg%sim%field_bc_mode = lower_ascii(trim(cfg%sim%field_bc_mode))
    select case (trim(cfg%sim%field_bc_mode))
    case ('free', 'periodic2')
      continue
    case default
      error stop 'sim.field_bc_mode must be "free" or "periodic2".'
    end select
    cfg%sim%field_periodic_far_correction = lower_ascii(trim(cfg%sim%field_periodic_far_correction))
    select case (trim(cfg%sim%field_periodic_far_correction))
    case ('auto')
      continue
    case ('none')
      continue
    case ('m2l_root_oracle')
      continue
    case default
      error stop 'sim.field_periodic_far_correction must be "auto", "none", '// &
        'or "m2l_root_oracle".'
    end select
    if (trim(cfg%sim%field_periodic_far_correction) == 'm2l_root_oracle') then
      if (trim(cfg%sim%field_solver) /= 'fmm' .or. trim(cfg%sim%field_bc_mode) /= 'periodic2') then
        error stop 'sim.field_periodic_far_correction requires sim.field_solver="fmm" and sim.field_bc_mode="periodic2".'
      end if
      if (cfg%sim%field_periodic_ewald_layers < 1_i32) then
        error stop 'sim.field_periodic_ewald_layers must be >= 1 when far correction is enabled.'
      end if
    end if
    if (cfg%sim%field_periodic_image_layers < 0_i32) then
      error stop 'sim.field_periodic_image_layers must be >= 0.'
    end if
    if (.not. ieee_is_finite(cfg%sim%field_periodic_ewald_alpha) .or. cfg%sim%field_periodic_ewald_alpha < 0.0d0) then
      error stop 'sim.field_periodic_ewald_alpha must be finite and >= 0.'
    end if
    if (cfg%sim%field_periodic_ewald_layers < 0_i32) then
      error stop 'sim.field_periodic_ewald_layers must be >= 0.'
    end if
    select case (trim(cfg%sim%field_solver))
    case ('direct', 'treecode', 'auto')
      if (trim(cfg%sim%field_bc_mode) /= 'free') then
        error stop 'sim.field_bc_mode must be "free" when sim.field_solver is "direct", "treecode", or "auto".'
      end if
    case ('fmm')
      if (trim(cfg%sim%field_bc_mode) == 'periodic2') then
        if (.not. cfg%sim%use_box) then
          error stop 'sim.field_bc_mode="periodic2" requires sim.use_box=true.'
        end if
        n_periodic_axes = 0_i32
        do axis = 1, 3
          if ((cfg%sim%bc_low(axis) == bc_periodic) .neqv. (cfg%sim%bc_high(axis) == bc_periodic)) then
            error stop 'periodic2 requires bc_low(axis)=bc_high(axis)=periodic for periodic axes.'
          end if
          if (cfg%sim%bc_low(axis) == bc_periodic) then
            n_periodic_axes = n_periodic_axes + 1_i32
            if (cfg%sim%box_max(axis) <= cfg%sim%box_min(axis)) then
              error stop 'periodic2 requires positive box length on periodic axes.'
            end if
          end if
        end do
        if (n_periodic_axes /= 2_i32) then
          error stop 'sim.field_bc_mode="periodic2" requires exactly two periodic axes.'
        end if
      end if
    end select
    if (trim(cfg%sim%field_bc_mode) /= 'free' .and. config_uses_conductor_surface_model(cfg)) then
      error stop 'surface_model="conductor" currently requires sim.field_bc_mode="free".'
    end if
    if (.not. ieee_is_finite(cfg%sim%tree_theta) .or. cfg%sim%tree_theta <= 0.0d0 .or. cfg%sim%tree_theta > 1.0d0) then
      error stop 'sim.tree_theta must be finite and satisfy 0 < theta <= 1.'
    end if
    if (cfg%sim%tree_leaf_max < 1_i32) then
      error stop 'sim.tree_leaf_max must be >= 1.'
    end if
    if (cfg%sim%tree_min_nelem < 1_i32) then
      error stop 'sim.tree_min_nelem must be >= 1.'
    end if
    call resolve_external_e_field(cfg)
    cfg%sim%reservoir_potential_model = lower_ascii(trim(cfg%sim%reservoir_potential_model))
    select case (trim(cfg%sim%reservoir_potential_model))
    case ('none', 'infinity_barrier')
      continue
    case default
      error stop 'sim.reservoir_potential_model must be "none" or "infinity_barrier".'
    end select
    cfg%sim%open_boundary_model = lower_ascii(trim(cfg%sim%open_boundary_model))
    select case (trim(cfg%sim%open_boundary_model))
    case ('escape', 'potential_barrier')
      continue
    case default
      error stop 'sim.open_boundary_model must be "escape" or "potential_barrier".'
    end select
    if (cfg%sim%injection_face_phi_grid_n < 1_i32) then
      error stop 'sim.injection_face_phi_grid_n must be >= 1.'
    end if
    if (cfg%sim%raycast_max_bounce < 1_i32) then
      error stop 'sim.raycast_max_bounce must be >= 1.'
    end if
    if (.not. ieee_is_finite(cfg%sim%phi_infty)) then
      error stop 'sim.phi_infty must be finite.'
    end if
    cfg%sim%sheath_injection_model = lower_ascii(trim(cfg%sim%sheath_injection_model))
    select case (trim(cfg%sim%sheath_injection_model))
    case ('none', 'zhao_auto', 'zhao_a', 'zhao_b', 'zhao_c', 'floating_no_photo')
      continue
    case default
      error stop 'sim.sheath_injection_model must be "none", "zhao_auto", "zhao_a", "zhao_b", "zhao_c", or "floating_no_photo".'
    end select
    cfg%sim%sheath_electron_drift_mode = lower_ascii(trim(cfg%sim%sheath_electron_drift_mode))
    select case (trim(cfg%sim%sheath_electron_drift_mode))
    case ('normal', 'full')
      continue
    case default
      error stop 'sim.sheath_electron_drift_mode must be "normal" or "full".'
    end select
    cfg%sim%sheath_ion_drift_mode = lower_ascii(trim(cfg%sim%sheath_ion_drift_mode))
    select case (trim(cfg%sim%sheath_ion_drift_mode))
    case ('normal', 'full')
      continue
    case default
      error stop 'sim.sheath_ion_drift_mode must be "normal" or "full".'
    end select
    if (.not. ieee_is_finite(cfg%sim%sheath_alpha_deg) .or. cfg%sim%sheath_alpha_deg < 0.0d0 .or. &
        cfg%sim%sheath_alpha_deg > 90.0d0) then
      error stop 'sim.sheath_alpha_deg must be finite and satisfy 0 <= alpha <= 90.'
    end if
    if (index(trim(cfg%sim%sheath_injection_model), 'zhao_') == 1) then
      if (.not. ieee_is_finite(cfg%sim%sheath_photoelectron_ref_density_cm3) .or. &
          cfg%sim%sheath_photoelectron_ref_density_cm3 <= 0.0d0) then
        error stop 'sim.sheath_photoelectron_ref_density_cm3 must be > 0 for Zhao sheath injection.'
      end if
    end if
    if (cfg%sim%has_sheath_reference_coordinate) then
      if (.not. ieee_is_finite(cfg%sim%sheath_reference_coordinate)) then
        error stop 'sim.sheath_reference_coordinate must be finite.'
      end if
    end if
    if (trim(cfg%sim%sheath_injection_model) /= 'none' .and. trim(cfg%sim%reservoir_potential_model) /= 'none') then
      error stop 'sim.sheath_injection_model currently requires sim.reservoir_potential_model="none".'
    end if
    call resolve_batch_duration(cfg)
    per_batch_particles = 0_i32
    has_dynamic_source_species = .false.
    do i = 1, cfg%n_particle_species
      if (.not. cfg%particle_species(i)%enabled) cycle

      cfg%particle_species(i)%source_mode = lower_ascii(trim(cfg%particle_species(i)%source_mode))
      cfg%particle_species(i)%velocity_distribution = lower_ascii(trim(cfg%particle_species(i)%velocity_distribution))
      cfg%particle_species(i)%velocity_grid_pdf_kind = lower_ascii(trim(cfg%particle_species(i)%velocity_grid_pdf_kind))
      cfg%particle_species(i)%velocity_grid_sampling = lower_ascii(trim(cfg%particle_species(i)%velocity_grid_sampling))
      cfg%particle_species(i)%photo_escape_model = lower_ascii(trim(cfg%particle_species(i)%photo_escape_model))
      if (.not. all(ieee_is_finite(cfg%particle_species(i)%pos_low)) .or. &
          .not. all(ieee_is_finite(cfg%particle_species(i)%pos_high))) then
        error stop 'particles.species.pos_low/pos_high must contain finite values.'
      end if
      if (.not. all(ieee_is_finite(cfg%particle_species(i)%drift_velocity))) then
        error stop 'particles.species.drift_velocity must contain finite values.'
      end if
      if (.not. ieee_is_finite(cfg%particle_species(i)%q_particle) .or. &
          abs(cfg%particle_species(i)%q_particle) <= 0.0d0) then
        error stop 'particles.species.q_particle must be finite and non-zero.'
      end if
      if (.not. ieee_is_finite(cfg%particle_species(i)%m_particle) .or. cfg%particle_species(i)%m_particle <= 0.0d0) then
        error stop 'particles.species.m_particle must be finite and > 0.'
      end if
      if (.not. ieee_is_finite(cfg%particle_species(i)%w_particle) .or. cfg%particle_species(i)%w_particle <= 0.0d0) then
        error stop 'particles.species.w_particle must be finite and > 0.'
      end if
      if (cfg%particle_species(i)%has_temperature_ev) then
        if (.not. ieee_is_finite(cfg%particle_species(i)%temperature_ev) .or. &
            cfg%particle_species(i)%temperature_ev < 0.0d0) then
          error stop 'particles.species.temperature_ev must be finite and >= 0.'
        end if
      end if
      if (cfg%particle_species(i)%has_temperature_k) then
        if (.not. ieee_is_finite(cfg%particle_species(i)%temperature_k) .or. &
            cfg%particle_species(i)%temperature_k < 0.0d0) then
          error stop 'particles.species.temperature_k must be finite and >= 0.'
        end if
      end if
      select case (trim(cfg%particle_species(i)%velocity_distribution))
      case ('maxwellian', 'grid')
        continue
      case default
        error stop 'particles.species.velocity_distribution must be "maxwellian" or "grid".'
      end select
      select case (trim(cfg%particle_species(i)%velocity_grid_pdf_kind))
      case ('phase_space', 'flux_weighted')
        continue
      case default
        error stop 'particles.species.velocity_grid_pdf_kind must be "phase_space" or "flux_weighted".'
      end select
      select case (trim(cfg%particle_species(i)%velocity_grid_sampling))
      case ('auto', 'rectilinear', 'discrete')
        continue
      case default
        error stop 'particles.species.velocity_grid_sampling must be "auto", "rectilinear", or "discrete".'
      end select
      select case (trim(cfg%particle_species(i)%source_mode))
      case ('volume_seed')
        if (cfg%particle_species(i)%npcls_per_step < 0_i32) then
          error stop 'particles.species.npcls_per_step must be >= 0.'
        end if
        if (trim(cfg%particle_species(i)%velocity_distribution) /= 'maxwellian' .or. &
            len_trim(cfg%particle_species(i)%velocity_grid_path) > 0 .or. &
            trim(cfg%particle_species(i)%velocity_grid_sampling) /= 'auto' .or. &
            cfg%particle_species(i)%has_particle_flux_m2_s .or. cfg%particle_species(i)%has_current_density_a_m2) then
          error stop 'velocity_distribution="grid" and flux keys are only valid for reservoir_face.'
        end if
        if (cfg%particle_species(i)%has_target_macro_particles_per_batch) then
          error stop 'target_macro_particles_per_batch is only valid for reservoir_face.'
        end if
        if (abs(cfg%particle_species(i)%emit_current_density_a_m2) > 0.0d0 .or. &
            cfg%particle_species(i)%rays_per_batch /= 0_i32 .or. cfg%particle_species(i)%has_ray_direction .or. &
            cfg%particle_species(i)%has_deposit_opposite_charge_on_emit .or. &
            cfg%particle_species(i)%has_photo_escape_model) then
          error stop 'photo_raycast keys are only valid for source_mode="photo_raycast".'
        end if
        per_batch_particles = per_batch_particles + cfg%particle_species(i)%npcls_per_step
      case ('reservoir_face')
        has_dynamic_source_species = .true.
        call validate_reservoir_species(cfg, i)
      case ('photo_raycast')
        has_dynamic_source_species = .true.
        call validate_photo_raycast_species(cfg, i)
      case default
        error stop 'Unknown particles.species.source_mode.'
      end select
    end do

    if (per_batch_particles <= 0_i32 .and. .not. has_dynamic_source_species) then
      error stop 'At least one enabled [[particles.species]] entry must have npcls_per_step > 0.'
    end if
    cfg%n_particles = cfg%sim%batch_count*per_batch_particles
  end subroutine load_toml_config