bem_restart_records.f90 Source File


This file depends on

sourcefile~~bem_restart_records.f90~~EfferentGraph sourcefile~bem_restart_records.f90 bem_restart_records.f90 sourcefile~bem_kinds.f90 bem_kinds.f90 sourcefile~bem_restart_records.f90->sourcefile~bem_kinds.f90 sourcefile~bem_matching_plane_summary.f90 bem_matching_plane_summary.f90 sourcefile~bem_restart_records.f90->sourcefile~bem_matching_plane_summary.f90 sourcefile~bem_restart.f90 bem_restart.f90 sourcefile~bem_restart_records.f90->sourcefile~bem_restart.f90 sourcefile~bem_matching_plane_summary.f90->sourcefile~bem_kinds.f90 sourcefile~bem_string_utils.f90 bem_string_utils.f90 sourcefile~bem_matching_plane_summary.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_types.f90 bem_types.f90 sourcefile~bem_matching_plane_summary.f90->sourcefile~bem_types.f90 sourcefile~bem_restart.f90->sourcefile~bem_kinds.f90 sourcefile~bem_app_config_types.f90 bem_app_config_types.f90 sourcefile~bem_restart.f90->sourcefile~bem_app_config_types.f90 sourcefile~bem_charge_ledger.f90 bem_charge_ledger.f90 sourcefile~bem_restart.f90->sourcefile~bem_charge_ledger.f90 sourcefile~bem_checkpoint_contract.f90 bem_checkpoint_contract.f90 sourcefile~bem_restart.f90->sourcefile~bem_checkpoint_contract.f90 sourcefile~bem_mpi.f90 bem_mpi.F90 sourcefile~bem_restart.f90->sourcefile~bem_mpi.f90 sourcefile~bem_restart.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_restart.f90->sourcefile~bem_types.f90 sourcefile~bem_app_config_types.f90->sourcefile~bem_kinds.f90 sourcefile~bem_app_config_types.f90->sourcefile~bem_types.f90 sourcefile~bem_external_boundary_contract.f90 bem_external_boundary_contract.f90 sourcefile~bem_app_config_types.f90->sourcefile~bem_external_boundary_contract.f90 sourcefile~bem_filesystem.f90 bem_filesystem.f90 sourcefile~bem_app_config_types.f90->sourcefile~bem_filesystem.f90 sourcefile~bem_physics_config_types.f90 bem_physics_config_types.f90 sourcefile~bem_app_config_types.f90->sourcefile~bem_physics_config_types.f90 sourcefile~bem_charge_ledger.f90->sourcefile~bem_kinds.f90 sourcefile~bem_checkpoint_contract.f90->sourcefile~bem_kinds.f90 sourcefile~bem_checkpoint_contract.f90->sourcefile~bem_filesystem.f90 sourcefile~bem_mpi.f90->sourcefile~bem_kinds.f90 sourcefile~bem_types.f90->sourcefile~bem_kinds.f90 sourcefile~bem_outflow_spectrum.f90 bem_outflow_spectrum.f90 sourcefile~bem_types.f90->sourcefile~bem_outflow_spectrum.f90 sourcefile~bem_external_boundary_contract.f90->sourcefile~bem_kinds.f90 sourcefile~bem_external_boundary_contract.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_outflow_spectrum.f90->sourcefile~bem_kinds.f90 sourcefile~bem_physics_config_types.f90->sourcefile~bem_kinds.f90 sourcefile~bem_physics_config_types.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_physics_config_types.f90->sourcefile~bem_types.f90

Source Code

!> チェックポイントの統計・電荷・台帳の読み込みと形式検証。
submodule(bem_restart) bem_restart_records
  use bem_kinds, only: dp, i64
  use bem_matching_plane_summary, only: matching_plane_summary_key_count, read_matching_plane_summary_key, &
                                        require_matching_plane_summary_keys, read_matching_summary_real
  implicit none
contains

  !> Read one complete numeric CSV record before list-directed conversion. Null fields, slash termination,
  !! repetition syntax and cross-record reads must never reuse a previous checkpoint value.
  module procedure read_checkpoint_csv_record
  integer :: length, first, last, column, comma

  read (unit, '(a)', advance='no', size=length, iostat=ios) line
  if (ios == iostat_end .and. length == 0) return
  if (ios /= iostat_eor) then
    ios = 1
    return
  end if
  ios = 1
  first = 1
  do column = 1, columns
    comma = index(line(first:length), ',')
    if (column < columns) then
      if (comma == 0) return
      last = first + comma - 2
    else
      if (comma /= 0) return
      last = length
    end if
    if (.not. is_decimal_real_token(trim(adjustl(line(first:last))))) return
    first = last + 2
  end do
  ios = 0
  end procedure read_checkpoint_csv_record

  !> `summary.txt` を読み込み、必須キーの存在と要素数整合を検証する。
  !! 欠落キーやメッシュ要素数不一致は再開不能として停止する。
  !! @param[in] path `summary.txt` のファイルパス。
  !! @param[in] expected_nelem 現在メッシュの要素数(整合性検証に使用)。
  !! @param[out] stats 復元した統計値。
  module procedure load_summary_file

  integer :: u, ios, pos
  integer(i32) :: mesh_nelem, saved_world_size, summary_schema_version
  ! The outer photoelectron spectrum is one long line of bin fluxes.
  character(len=8192) :: line
  character(len=64) :: key
  character(len=8192) :: value
  logical :: found_mesh, found_processed, found_absorbed, found_escaped
  logical :: found_batches, found_rel, found_world_size
  logical :: found_matching(matching_plane_summary_key_count)

  stats = sim_stats()
  mesh_nelem = -1_i32
  saved_world_size = 1_i32
  summary_schema_version = -1_i32
  found_mesh = .false.
  found_processed = .false.
  found_absorbed = .false.
  found_escaped = .false.
  found_batches = .false.
  found_rel = .false.
  found_world_size = .false.
  found_matching = .false.

  open (newunit=u, file=trim(path), status='old', action='read', iostat=ios)
  if (ios /= 0) error stop 'Failed to open summary.txt for resume.'

  do
    read (u, '(A)', iostat=ios) line
    if (ios /= 0) exit
    line = trim(adjustl(line))
    if (len_trim(line) == 0) cycle
    pos = index(line, '=')
    if (pos <= 0) cycle

    key = trim(adjustl(line(:pos - 1)))
    value = trim(adjustl(line(pos + 1:)))

    select case (trim(key))
    case ('checkpoint_schema_version')
      read (value, *) summary_schema_version
    case ('mesh_nelem')
      read (value, *) mesh_nelem
      found_mesh = .true.
    case ('mpi_world_size')
      read (value, *) saved_world_size
      found_world_size = .true.
    case ('processed_particles')
      read (value, *) stats%processed_particles
      found_processed = .true.
    case ('absorbed')
      read (value, *) stats%absorbed
      found_absorbed = .true.
    case ('escaped')
      read (value, *) stats%escaped
      found_escaped = .true.
    case ('batches')
      read (value, *) stats%batches
      found_batches = .true.
    case ('escaped_boundary')
      read (value, *) stats%escaped_boundary
    case ('survived_max_step')
      read (value, *) stats%survived_max_step
    case ('multiple_box_events_retry_attempted')
      read (value, *) stats%multiple_box_events_retry_attempted
    case ('multiple_box_events_retry_resolved')
      read (value, *) stats%multiple_box_events_retry_resolved
    case ('adaptive_substep_halved_steps')
      read (value, *) stats%adaptive_substep_halved_steps
    case ('adaptive_substeps')
      read (value, *) stats%adaptive_substeps
    case ('adaptive_substep_depth_limited_steps')
      read (value, *) stats%adaptive_substep_depth_limited_steps
    case ('adaptive_grown_steps')
      read (value, *) stats%adaptive_grown_steps
    case ('particle_pool_claims')
      read (value, *) stats%particle_pool_claims
    case ('particle_pool_claim_seconds')
      read (value, *) stats%particle_pool_claim_seconds
    case ('multiple_box_events_soft_discarded')
      read (value, *) stats%multiple_box_events_soft_discarded
    case ('multiple_box_events_soft_discarded_abs_charge_C')
      read (value, *) stats%multiple_box_events_soft_discarded_abs_charge
    case ('last_rel_change')
      read (value, *) stats%last_rel_change
      found_rel = .true.
    case ('simulated_time_s')
      read (value, *) stats%simulated_time
    case ('adaptive_nonzero_mode_rejected_trials')
      read (value, *) stats%adaptive_nonzero_mode%rejected_trials
    case ('adaptive_nonzero_mode_last_batch_duration_s')
      read (value, *) stats%adaptive_nonzero_mode%last_batch_duration
    case ('adaptive_nonzero_mode_last_potential_step_V')
      read (value, *) stats%adaptive_nonzero_mode%last_potential_step
    case ('adaptive_nonzero_mode_omp_threads')
      read (value, *) stats%adaptive_nonzero_mode%omp_threads
    case ('secondary_emission_hits')
      read (value, *) stats%secondary_emission_hits
    case ('secondary_emission_count')
      read (value, *) stats%secondary_emission_count
    case ('secondary_generation_capped_count')
      read (value, *) stats%secondary_generation_capped_count
    case ('secondary_emitted_charge_C')
      call read_matching_summary_real(value, key, stats%secondary_emitted_charge_c)
    case ('secondary_yield_sum')
      call read_matching_summary_real(value, key, stats%secondary_yield_sum)
    case default
      call read_matching_plane_summary_key(key, value, stats%matching_plane, found_matching)
    end select
  end do
  close (u)

  if (.not. (found_mesh .and. found_processed .and. found_absorbed .and. &
             found_escaped .and. found_batches .and. found_rel)) then
    error stop 'Resume checkpoint summary is missing required keys.'
  end if
  if (mesh_nelem /= expected_nelem) then
    error stop 'Resume checkpoint mesh element count does not match current mesh.'
  end if
  if (summary_schema_version >= 9_i32) call require_matching_plane_summary_keys(found_matching)
  call validate_summary_stats(stats)
  if (present(expected_world_size)) then
    if (.not. found_world_size .and. expected_world_size > 1_i32) then
      error stop 'Resume checkpoint summary is missing mpi_world_size.'
    end if
    if (max(1_i32, expected_world_size) /= saved_world_size) then
      error stop 'Resume checkpoint mpi_world_size does not match current MPI world size.'
    end if
  end if
  end procedure load_summary_file

  !> `charges.csv` を読み込み、各要素の電荷をメッシュへ復元する。
  !! 行重複や要素数不足を検出した場合は停止する。
  !! @param[in] path `charges.csv` のファイルパス。
  !! @param[inout] mesh 要素電荷 `q_elem` を復元値で上書きするメッシュ。
  module procedure load_charge_file

  integer :: u, ios
  integer(i32) :: elem_idx, n_loaded
  real(dp) :: charge
  character(len=512) :: header, record
  logical, allocatable :: seen(:)

  if (.not. allocated(mesh%q_elem)) error stop 'Mesh charges are not allocated.'

  allocate (seen(mesh%nelem))
  seen = .false.
  mesh%q_elem = 0.0d0
  n_loaded = 0_i32

  open (newunit=u, file=trim(path), status='old', action='read', iostat=ios)
  if (ios /= 0) error stop 'Failed to open charges.csv for resume.'

  read (u, '(A)', iostat=ios) header
  if (ios /= 0) error stop 'Failed to read charges.csv header.'

  do
    call read_checkpoint_csv_record(u, record, 2, ios)
    if (ios < 0) exit
    if (ios == 0) read (record, *, iostat=ios) elem_idx, charge
    if (ios /= 0) error stop 'Failed to parse charges.csv during resume.'
    if (elem_idx < 1_i32 .or. elem_idx > mesh%nelem) then
      error stop 'Resume checkpoint charges.csv has an invalid element index.'
    end if
    if (seen(elem_idx)) then
      error stop 'Resume checkpoint charges.csv contains duplicate element rows.'
    end if
    if (.not. ieee_is_finite(charge)) then
      error stop 'Resume checkpoint charges.csv contains non-finite charge values.'
    end if
    seen(elem_idx) = .true.
    mesh%q_elem(elem_idx) = charge
    n_loaded = n_loaded + 1_i32
  end do
  close (u)

  if (n_loaded /= mesh%nelem) then
    error stop 'Resume checkpoint charges.csv does not match the current mesh.'
  end if
  end procedure load_charge_file

  !> summary stock と species 別 CSV flux/count から累積 charge ledger を復元する。
  module procedure load_charge_ledger_checkpoint
  integer :: u, ios, pos, columns
  integer(i32) :: nspecies, batch_count, row_batch, species_idx, loaded
  integer(i64) :: count_values(5)
  real(dp) :: charge_values(18), fixed_charge_values(13), legacy_charge_values(8), stock_values(6)
  character(len=512) :: line
  character(len=2048) :: header
  character(len=2048) :: record
  character(len=96) :: key
  character(len=256) :: value
  logical :: found_nspecies, found_batch, found_stocks(6), has_fixed_current_columns, has_current_budget_columns
  logical, allocatable :: seen(:)

  nspecies = 0_i32
  batch_count = 0_i32
  found_nspecies = .false.
  found_batch = .false.
  found_stocks = .false.
  stock_values = 0.0_dp
  open (newunit=u, file=trim(summary_path), status='old', action='read', iostat=ios)
  if (ios /= 0) error stop 'Failed to open summary.txt for charge ledger resume.'
  do
    read (u, '(A)', iostat=ios) line
    if (ios /= 0) exit
    pos = index(line, '=')
    if (pos <= 0) cycle
    key = trim(adjustl(line(:pos - 1)))
    value = trim(adjustl(line(pos + 1:)))
    select case (trim(key))
    case ('charge_ledger_nspecies')
      read (value, *, iostat=ios) nspecies
      found_nspecies = ios == 0
    case ('charge_ledger_batch_count')
      read (value, *, iostat=ios) batch_count
      found_batch = ios == 0
    case ('charge_ledger_surface_charge_before_C')
      read (value, *, iostat=ios) stock_values(1)
      found_stocks(1) = ios == 0
    case ('charge_ledger_surface_charge_after_C')
      read (value, *, iostat=ios) stock_values(2)
      found_stocks(2) = ios == 0
    case ('charge_ledger_local_flight_charge_before_C')
      read (value, *, iostat=ios) stock_values(3)
      found_stocks(3) = ios == 0
    case ('charge_ledger_local_flight_charge_after_C')
      read (value, *, iostat=ios) stock_values(4)
      found_stocks(4) = ios == 0
    case ('charge_ledger_unresolved_stock_before_C')
      read (value, *, iostat=ios) stock_values(5)
      found_stocks(5) = ios == 0
    case ('charge_ledger_unresolved_stock_after_C')
      read (value, *, iostat=ios) stock_values(6)
      found_stocks(6) = ios == 0
    end select
  end do
  close (u)
  if (.not. found_nspecies .or. .not. found_batch .or. .not. all(found_stocks)) then
    error stop 'Resume checkpoint charge ledger summary is incomplete.'
  end if
  if (nspecies < 1_i32 .or. batch_count < 0_i32) then
    error stop 'Resume checkpoint charge ledger dimensions are invalid.'
  end if
  if (any(.not. ieee_is_finite(stock_values))) then
    error stop 'Resume checkpoint charge ledger stocks must be finite.'
  end if

  call ledger%init(nspecies)
  ledger%batch_count = batch_count
  ledger%surface_charge_before = stock_values(1)
  ledger%surface_charge_after = stock_values(2)
  ledger%local_flight_charge_before = stock_values(3)
  ledger%local_flight_charge_after = stock_values(4)
  ledger%unresolved_stock_before = stock_values(5)
  ledger%unresolved_stock_after = stock_values(6)
  allocate (seen(nspecies))
  seen = .false.
  loaded = 0_i32
  open (newunit=u, file=trim(ledger_path), status='old', action='read', iostat=ios)
  if (ios /= 0) error stop 'Failed to open charge_ledger.csv for resume.'
  read (u, '(A)', iostat=ios) header
  if (ios /= 0) error stop 'Failed to read charge_ledger.csv header.'
  has_fixed_current_columns = index(header, 'fixed_current_correction_C') > 0
  has_current_budget_columns = index(header, 'fixed_escape_correction_C') > 0
  columns = 15
  if (has_fixed_current_columns) columns = 20
  if (has_current_budget_columns) columns = 25
  do
    call read_checkpoint_csv_record(u, record, columns, ios)
    if (ios < 0) exit
    if (ios > 0) error stop 'Failed to parse charge_ledger.csv during resume.'
    charge_values = [ &
                    0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 1.0_dp, 0.0_dp, &
                    0.0_dp, 1.0_dp, 0.0_dp, 1.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp &
                    ]
    if (has_current_budget_columns) then
      read (record, *, iostat=ios) row_batch, species_idx, charge_values, count_values
    else if (has_fixed_current_columns) then
      read (record, *, iostat=ios) row_batch, species_idx, fixed_charge_values, count_values
      if (ios == 0) then
        charge_values(1:13) = fixed_charge_values
        charge_values(14) = fixed_charge_values(9)
        charge_values(15) = fixed_charge_values(11)
      end if
    else
      read (record, *, iostat=ios) row_batch, species_idx, legacy_charge_values, count_values
      if (ios == 0) charge_values(1:8) = legacy_charge_values
    end if
    if (ios /= 0) error stop 'Failed to parse charge_ledger.csv during resume.'
    if (row_batch /= batch_count) error stop 'Resume charge ledger batch count mismatch.'
    if (species_idx < 1_i32 .or. species_idx > nspecies) error stop 'Resume charge ledger species index is invalid.'
    if (seen(species_idx)) error stop 'Resume charge ledger contains duplicate species rows.'
    if (any(.not. ieee_is_finite(charge_values))) error stop 'Resume charge ledger charges must be finite.'
    if (charge_values(14) /= charge_values(9) .or. charge_values(15) /= charge_values(11) .or. &
        charge_values(17) /= charge_values(16)) then
      error stop 'Resume charge ledger applied-charge aliases must match their target charges.'
    end if
    if (any(count_values < 0_i64)) error stop 'Resume charge ledger counts must be nonnegative.'
    seen(species_idx) = .true.
    ledger%injected_from_remote(species_idx) = charge_values(1)
    ledger%emitted_from_surface(species_idx) = charge_values(2)
    ledger%absorbed_on_surface(species_idx) = charge_values(3)
    ledger%escaped_to_infinity(species_idx) = charge_values(4)
    ledger%discarded_unresolved(species_idx) = charge_values(5)
    ledger%neutral_return_correction(species_idx) = charge_values(6)
    ledger%neutral_return_weight_scale(species_idx) = charge_values(7)
    ledger%neutral_return_unresolved_fraction(species_idx) = charge_values(8)
    ! Applied-charge CSV columns are backward-compatible aliases.  Only the
    ! target charge is retained in memory because the closure applies it exactly.
    ledger%fixed_absorbed_target_charge(species_idx) = charge_values(9)
    ledger%fixed_absorbed_weight_scale(species_idx) = charge_values(10)
    ledger%fixed_emission_target_charge(species_idx) = charge_values(11)
    ledger%fixed_emission_weight_scale(species_idx) = charge_values(12)
    ledger%fixed_current_correction(species_idx) = charge_values(13)
    ledger%fixed_escape_target_charge(species_idx) = charge_values(16)
    ledger%fixed_escape_correction(species_idx) = charge_values(18)
    ledger%injected_count(species_idx) = count_values(1)
    ledger%emitted_count(species_idx) = count_values(2)
    ledger%absorbed_count(species_idx) = count_values(3)
    ledger%escaped_count(species_idx) = count_values(4)
    ledger%discarded_unresolved_count(species_idx) = count_values(5)
    loaded = loaded + 1_i32
  end do
  close (u)
  if (loaded /= nspecies) error stop 'Resume charge ledger species rows are incomplete.'
  end procedure load_charge_ledger_checkpoint

  !> summary.txt から復元した統計値が壊れていないことを検証する。
  subroutine validate_summary_stats(stats)
    type(sim_stats), intent(in) :: stats
    real(dp) :: matching_budget_scale

    if (stats%processed_particles < 0_i64) error stop 'Resume checkpoint processed_particles must be >= 0.'
    if (stats%absorbed < 0_i64) error stop 'Resume checkpoint absorbed must be >= 0.'
    if (stats%escaped < 0_i64) error stop 'Resume checkpoint escaped must be >= 0.'
    if (stats%escaped_boundary < 0_i64) error stop 'Resume checkpoint escaped_boundary must be >= 0.'
    if (stats%survived_max_step < 0_i64) error stop 'Resume checkpoint survived_max_step must be >= 0.'
    if (stats%multiple_box_events_retry_attempted < 0_i64 .or. &
        stats%multiple_box_events_retry_resolved < 0_i64 .or. &
        stats%multiple_box_events_retry_resolved > stats%multiple_box_events_retry_attempted) then
      error stop 'Resume checkpoint multiple_box_events retry counters are inconsistent.'
    end if
    if (stats%multiple_box_events_soft_discarded < 0_i64) then
      error stop 'Resume checkpoint multiple_box_events_soft_discarded must be >= 0.'
    end if
    if (stats%adaptive_grown_steps < 0_i64) error stop 'Resume checkpoint adaptive_grown_steps must be >= 0.'
    if (stats%adaptive_substep_halved_steps < 0_i64 .or. stats%adaptive_substeps < 0_i64 .or. &
        stats%adaptive_substep_depth_limited_steps < 0_i64 .or. &
        stats%adaptive_substep_halved_steps > stats%adaptive_substeps .or. &
        stats%adaptive_substep_depth_limited_steps > stats%adaptive_substep_halved_steps) then
      error stop 'Resume checkpoint adaptive_substep counters are inconsistent.'
    end if
    if (.not. ieee_is_finite(stats%multiple_box_events_soft_discarded_abs_charge) .or. &
        stats%multiple_box_events_soft_discarded_abs_charge < 0.0_dp) then
      error stop 'Resume checkpoint multiple_box_events_soft_discarded_abs_charge_C must be finite and >= 0.'
    end if
    if (stats%batches < 0_i32) error stop 'Resume checkpoint batches must be >= 0.'
    if (.not. ieee_is_finite(stats%last_rel_change) .or. stats%last_rel_change < 0.0d0) then
      error stop 'Resume checkpoint last_rel_change must be finite and >= 0.'
    end if
    if (.not. ieee_is_finite(stats%simulated_time) .or. stats%simulated_time < 0.0_dp) then
      error stop 'Resume checkpoint simulated_time_s must be finite and >= 0.'
    end if
    if (stats%adaptive_nonzero_mode%rejected_trials < 0_i64) then
      error stop 'Resume checkpoint adaptive rejected-trial count must be >= 0.'
    end if
    if (.not. ieee_is_finite(stats%adaptive_nonzero_mode%last_batch_duration) .or. &
        stats%adaptive_nonzero_mode%last_batch_duration < 0.0_dp) then
      error stop 'Resume checkpoint adaptive last batch duration must be finite and >= 0.'
    end if
    if (.not. ieee_is_finite(stats%adaptive_nonzero_mode%last_potential_step) .or. &
        stats%adaptive_nonzero_mode%last_potential_step < 0.0_dp) then
      error stop 'Resume checkpoint adaptive last potential step must be finite and >= 0.'
    end if
    if (stats%adaptive_nonzero_mode%omp_threads < 0_i32) then
      error stop 'Resume checkpoint adaptive OpenMP thread count must be >= 0.'
    end if
    if (stats%secondary_emission_hits < 0_i64 .or. stats%secondary_emission_count < 0_i64 .or. &
        stats%secondary_generation_capped_count < 0_i64 .or. &
        .not. ieee_is_finite(stats%secondary_yield_sum) .or. stats%secondary_yield_sum < 0.0_dp .or. &
        .not. ieee_is_finite(stats%secondary_emitted_charge_c) .or. stats%secondary_emitted_charge_c > 0.0_dp) then
      error stop 'Resume checkpoint secondary-emission diagnostics are invalid.'
    end if
    if (stats%matching_plane%state_valid) then
      if (.not. all(ieee_is_finite([ &
                                   stats%matching_plane%displacement_c_m2, stats%matching_plane%phi_v, &
                                   stats%matching_plane%response, stats%matching_plane%feedback, &
                                   stats%matching_plane%photoelectron_return_flux_m2_s, &
                                   stats%matching_plane%photoelectron_escape_flux_m2_s, stats%matching_plane%residual, &
                                   stats%matching_plane%secondary_share, stats%matching_plane%secondary_budget_residual_a_m2 &
                                   ]))) then
        error stop 'Resume checkpoint matching-plane state must be finite.'
      end if
      if (any(stats%matching_plane%feedback < 0.0_dp) .or. &
          any(stats%matching_plane%response(2:3) < 0.0_dp) .or. &
          stats%matching_plane%photoelectron_return_flux_m2_s < 0.0_dp .or. &
          stats%matching_plane%photoelectron_escape_flux_m2_s < 0.0_dp .or. &
          stats%matching_plane%iterations <= 0_i32 .or. stats%matching_plane%residual < 0.0_dp .or. &
          stats%matching_plane%secondary_share < 0.0_dp .or. stats%matching_plane%secondary_share > 1.0_dp) then
        error stop 'Resume checkpoint matching-plane state is outside its physical range.'
      end if
      matching_budget_scale = max( &
                              1.0_dp, stats%matching_plane%feedback(1), &
                              stats%matching_plane%photoelectron_return_flux_m2_s, &
                              stats%matching_plane%photoelectron_escape_flux_m2_s &
                              )
      if (abs( &
          stats%matching_plane%feedback(1) - &
          stats%matching_plane%photoelectron_return_flux_m2_s - &
          stats%matching_plane%photoelectron_escape_flux_m2_s &
          ) > sqrt(epsilon(1.0_dp))*matching_budget_scale) then
        error stop 'Resume checkpoint matching-plane photoelectron budget is inconsistent.'
      end if
      if (.not. all(ieee_is_finite(stats%matching_plane%photoelectron_spectrum_m2_s)) .or. &
          any(stats%matching_plane%photoelectron_spectrum_m2_s < 0.0_dp)) then
        error stop 'Resume checkpoint matching-plane photoelectron spectrum must be finite and >= 0.'
      end if
      if (any(stats%matching_plane%photoelectron_spectrum_m2_s > 0.0_dp) .and. &
          abs(sum(stats%matching_plane%photoelectron_spectrum_m2_s) - stats%matching_plane%feedback(1)) > &
          sqrt(epsilon(1.0_dp))*matching_budget_scale) then
        error stop 'Resume checkpoint matching-plane photoelectron spectrum does not match its outward flux.'
      end if
    end if
  end subroutine validate_summary_stats

end submodule bem_restart_records