bem_sheath_model_core.f90 Source File


This file depends on

sourcefile~~bem_sheath_model_core.f90~~EfferentGraph sourcefile~bem_sheath_model_core.f90 bem_sheath_model_core.f90 sourcefile~bem_constants.f90 bem_constants.f90 sourcefile~bem_sheath_model_core.f90->sourcefile~bem_constants.f90 sourcefile~bem_injection.f90 bem_injection.f90 sourcefile~bem_sheath_model_core.f90->sourcefile~bem_injection.f90 sourcefile~bem_kinds.f90 bem_kinds.f90 sourcefile~bem_sheath_model_core.f90->sourcefile~bem_kinds.f90 sourcefile~bem_string_utils.f90 bem_string_utils.f90 sourcefile~bem_sheath_model_core.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_constants.f90->sourcefile~bem_kinds.f90 sourcefile~bem_injection.f90->sourcefile~bem_constants.f90 sourcefile~bem_injection.f90->sourcefile~bem_kinds.f90 sourcefile~bem_injection.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_boundary.f90 bem_boundary.f90 sourcefile~bem_injection.f90->sourcefile~bem_boundary.f90 sourcefile~bem_collision.f90 bem_collision.f90 sourcefile~bem_injection.f90->sourcefile~bem_collision.f90 sourcefile~bem_particles.f90 bem_particles.f90 sourcefile~bem_injection.f90->sourcefile~bem_particles.f90 sourcefile~bem_types.f90 bem_types.f90 sourcefile~bem_injection.f90->sourcefile~bem_types.f90 sourcefile~bem_boundary.f90->sourcefile~bem_kinds.f90 sourcefile~bem_boundary.f90->sourcefile~bem_types.f90 sourcefile~bem_collision.f90->sourcefile~bem_kinds.f90 sourcefile~bem_collision.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_collision.f90->sourcefile~bem_types.f90 sourcefile~bem_particles.f90->sourcefile~bem_kinds.f90 sourcefile~bem_particles.f90->sourcefile~bem_types.f90 sourcefile~bem_types.f90->sourcefile~bem_kinds.f90

Files dependent on this one

sourcefile~~bem_sheath_model_core.f90~~AfferentGraph sourcefile~bem_sheath_model_core.f90 bem_sheath_model_core.f90 sourcefile~bem_sheath_runtime.f90 bem_sheath_runtime.f90 sourcefile~bem_sheath_runtime.f90->sourcefile~bem_sheath_model_core.f90 sourcefile~bem_sheath_injection_model.f90 bem_sheath_injection_model.f90 sourcefile~bem_sheath_injection_model.f90->sourcefile~bem_sheath_runtime.f90 sourcefile~bem_app_config_runtime.f90 bem_app_config_runtime.f90 sourcefile~bem_app_config_runtime.f90->sourcefile~bem_sheath_injection_model.f90 sourcefile~bem_app_config.f90 bem_app_config.f90 sourcefile~bem_app_config.f90->sourcefile~bem_app_config_runtime.f90 sourcefile~bem_simulator.f90 bem_simulator.f90 sourcefile~bem_simulator.f90->sourcefile~bem_app_config.f90 sourcefile~main.f90 main.f90 sourcefile~main.f90->sourcefile~bem_app_config.f90 sourcefile~main.f90->sourcefile~bem_simulator.f90 sourcefile~bem_simulator_io.f90 bem_simulator_io.f90 sourcefile~bem_simulator_io.f90->sourcefile~bem_simulator.f90 sourcefile~bem_simulator_loop.f90 bem_simulator_loop.f90 sourcefile~bem_simulator_loop.f90->sourcefile~bem_simulator.f90 sourcefile~bem_simulator_stats.f90 bem_simulator_stats.f90 sourcefile~bem_simulator_stats.f90->sourcefile~bem_simulator.f90

Source Code

!> Zhao 系シース数値モデルの core 実装。
module bem_sheath_model_core
  use bem_kinds, only: dp
  use bem_constants, only: k_boltzmann, pi, eps0, qe
  use bem_injection, only: compute_inflow_flux_from_drifting_maxwellian
  use bem_string_utils, only: lower_ascii
  use, intrinsic :: ieee_arithmetic, only: ieee_is_finite
  implicit none

  real(dp), parameter :: nonlinear_tol = 1.0d-5
  integer, parameter :: nonlinear_max_iter = 60
  integer, parameter :: nonlinear_max_backtrack = 20
  real(dp), parameter :: zhao_profile_phi_tol_hat = 1.0d-3
  real(dp), parameter :: zhao_type_a_phi_m_eps_hat = 1.0d-5
  integer, parameter :: zhao_profile_grid = 8000

  type :: sheath_model_species
    real(dp) :: q_particle = 0.0d0
    real(dp) :: m_particle = 0.0d0
    real(dp) :: number_density_m3 = 0.0d0
    real(dp) :: temperature_k = 0.0d0
    real(dp) :: drift_velocity(3) = 0.0d0
  end type sheath_model_species

  abstract interface
    subroutine nonlinear_residual(x, f)
      import :: dp
      real(dp), intent(in) :: x(:)
      real(dp), intent(out) :: f(:)
    end subroutine nonlinear_residual
  end interface

  type :: zhao_params_type
    real(dp) :: alpha_rad = 0.0d0
    real(dp) :: n_swi_inf_m3 = 0.0d0
    real(dp) :: n_phe_ref_m3 = 0.0d0
    real(dp) :: n_phe0_m3 = 0.0d0
    real(dp) :: t_swe_ev = 0.0d0
    real(dp) :: t_phe_ev = 0.0d0
    real(dp) :: v_d_electron_mps = 0.0d0
    real(dp) :: v_d_ion_mps = 0.0d0
    real(dp) :: m_i_kg = 0.0d0
    real(dp) :: m_e_kg = 0.0d0
    real(dp) :: v_swe_th_mps = 0.0d0
    real(dp) :: v_phe_th_mps = 0.0d0
    real(dp) :: cs_mps = 0.0d0
    real(dp) :: mach = 0.0d0
    real(dp) :: u = 0.0d0
    real(dp) :: tau = 0.0d0
    real(dp) :: lambda_d_phe_ref_m = 0.0d0
  end type zhao_params_type

  type :: zhao_local_state_type
    character(len=1) :: branch = ' '
    character(len=16) :: side = 'monotonic'
    real(dp) :: z_m_m = 0.0d0
    real(dp) :: phi_hat = 0.0d0
    real(dp) :: phi_v = 0.0d0
    real(dp) :: n_swi_m3 = 0.0d0
    real(dp) :: n_swe_f_m3 = 0.0d0
    real(dp) :: n_swe_r_m3 = 0.0d0
    real(dp) :: n_phe_f_m3 = 0.0d0
    real(dp) :: n_phe_c_m3 = 0.0d0
    real(dp) :: electron_source_density_m3 = 0.0d0
    real(dp) :: vcut_swe_mps = 0.0d0
    real(dp) :: vcut_phe_mps = 0.0d0
    real(dp) :: v_i_mps = 0.0d0
    logical :: swe_reflected_active = .false.
    logical :: phe_captured_active = .false.
  end type zhao_local_state_type

  public :: sheath_model_species
  public :: zhao_params_type
  public :: zhao_local_state_type
  public :: solve_no_photo_floating_potential
  public :: build_zhao_params
  public :: solve_zhao_unknowns
  public :: sample_zhao_state_at_z
  public :: resolve_species_drift_speed
  public :: zhao_electron_vmin_normal
  public :: zhao_photo_vmin_normal
  public :: zhao_photo_emit_current_density

contains

  subroutine solve_no_photo_floating_potential(spec_e, spec_i, inward_normal, phi0_v)
    type(sheath_model_species), intent(in) :: spec_e, spec_i
    real(dp), intent(in) :: inward_normal(3)
    real(dp), intent(out) :: phi0_v

    real(dp) :: n_e_inf_m3, n_i_inf_m3, gamma_i, f_low, f_high, f_mid, phi_low, phi_high, phi_mid
    integer :: iter

    n_e_inf_m3 = spec_e%number_density_m3
    n_i_inf_m3 = spec_i%number_density_m3
    gamma_i = compute_inflow_flux_from_drifting_maxwellian( &
              n_i_inf_m3, spec_i%temperature_k, spec_i%m_particle, spec_i%drift_velocity, inward_normal &
              )
    if (gamma_i <= 0.0d0) error stop 'floating_no_photo requires a positive ion inflow flux.'

    phi_low = -128.0d0*max(1.0d0, temperature_ev_from_species(spec_e))
    phi_high = 0.0d0
    f_low = no_photo_current_balance(phi_low, n_e_inf_m3, spec_e, inward_normal, gamma_i)
    f_high = no_photo_current_balance(phi_high, n_e_inf_m3, spec_e, inward_normal, gamma_i)
    if (f_high < 0.0d0) then
      error stop 'floating_no_photo could not bracket a negative sheath potential.'
    end if
    do while (f_low > 0.0d0)
      phi_low = 2.0d0*phi_low
      f_low = no_photo_current_balance(phi_low, n_e_inf_m3, spec_e, inward_normal, gamma_i)
      if (phi_low < -1.0d6) error stop 'floating_no_photo bracket search failed.'
    end do

    do iter = 1, 80
      phi_mid = 0.5d0*(phi_low + phi_high)
      f_mid = no_photo_current_balance(phi_mid, n_e_inf_m3, spec_e, inward_normal, gamma_i)
      if (abs(f_mid) <= 1.0d-12*max(1.0d0, gamma_i)) exit
      if (f_mid > 0.0d0) then
        phi_high = phi_mid
      else
        phi_low = phi_mid
      end if
    end do
    phi0_v = 0.5d0*(phi_low + phi_high)
  end subroutine solve_no_photo_floating_potential

  real(dp) function no_photo_current_balance(phi0_v, n_e_inf_m3, spec_e, inward_normal, gamma_i) result(balance)
    real(dp), intent(in) :: phi0_v
    real(dp), intent(in) :: n_e_inf_m3
    type(sheath_model_species), intent(in) :: spec_e
    real(dp), intent(in) :: inward_normal(3)
    real(dp), intent(in) :: gamma_i
    real(dp) :: vmin_normal

    vmin_normal = sqrt(max(0.0d0, 2.0d0*qe*max(0.0d0, -phi0_v))/abs(spec_e%m_particle))
    balance = compute_inflow_flux_from_drifting_maxwellian( &
              n_e_inf_m3, spec_e%temperature_k, spec_e%m_particle, spec_e%drift_velocity, inward_normal, &
              vmin_normal=vmin_normal &
              ) - gamma_i
  end function no_photo_current_balance

  subroutine sample_zhao_state_at_z(p, branch, phi0_v, phi_m_v, n_swe_inf_m3, z_m, state)
    type(zhao_params_type), intent(in) :: p
    character(len=1), intent(in) :: branch
    real(dp), intent(in) :: phi0_v, phi_m_v, n_swe_inf_m3, z_m
    type(zhao_local_state_type), intent(out) :: state

    real(dp) :: phi0_hat, phi_m_hat, phi_hat, z_m_hat
    character(len=16) :: side

    phi0_hat = phi0_v/p%t_phe_ev
    phi_m_hat = phi_m_v/p%t_phe_ev
    if (z_m <= 0.0d0) then
      phi_hat = phi0_hat
      if (branch == 'A') then
        side = 'lower'
      else
        side = 'monotonic'
      end if
      z_m_hat = 0.0d0
    else
      select case (branch)
      case ('A')
        call sample_type_a_phi_hat_at_z(p, phi0_hat, phi_m_hat, n_swe_inf_m3/p%n_phe_ref_m3, z_m, phi_hat, side, z_m_hat)
      case ('B', 'C')
        call sample_monotonic_phi_hat_at_z( &
          p, branch, phi0_hat, phi_m_hat, n_swe_inf_m3/p%n_phe_ref_m3, z_m, phi_hat, side, z_m_hat &
          )
      case default
        error stop 'Unknown Zhao branch in local state reconstruction.'
      end select
    end if

    call evaluate_zhao_state_from_phi_hat( &
      p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_m3, z_m_hat*p%lambda_d_phe_ref_m, state &
      )
  end subroutine sample_zhao_state_at_z

  subroutine sample_type_a_phi_hat_at_z(p, phi0_hat, phi_m_hat, n_swe_inf_hat, z_m, phi_hat, side, z_m_hat)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: phi0_hat, phi_m_hat, n_swe_inf_hat, z_m
    real(dp), intent(out) :: phi_hat, z_m_hat
    character(len=16), intent(out) :: side

    integer :: i, ngrid_lower, ngrid_upper
    real(dp) :: z_hat_target, phi_m_eps_hat, phi_end_hat, x
    real(dp), allocatable :: phi_lower_asc(:), s_lower_asc(:), phi_upper_asc(:), s_upper_asc(:)
    real(dp), allocatable :: z_lower_desc(:), phi_lower_desc(:), z_upper_asc(:)

    z_hat_target = z_m/p%lambda_d_phe_ref_m
    phi_m_eps_hat = min(zhao_type_a_phi_m_eps_hat, 5.0d-2*max(1.0d-8, abs(phi_m_hat)))
    phi_m_eps_hat = max(phi_m_eps_hat, 1.0d-8)
    phi_end_hat = -abs(zhao_profile_phi_tol_hat)
    if (phi_end_hat <= phi_m_hat) phi_end_hat = 0.5d0*phi_m_hat

    ngrid_upper = max(2000, zhao_profile_grid)
    ngrid_lower = max(ngrid_upper/2, 1500)
    allocate (phi_lower_asc(ngrid_lower), s_lower_asc(ngrid_lower))
    allocate (phi_upper_asc(ngrid_upper), s_upper_asc(ngrid_upper), z_upper_asc(ngrid_upper))
    allocate (z_lower_desc(ngrid_lower), phi_lower_desc(ngrid_lower))

    do i = 1, ngrid_lower
      x = real(i - 1, dp)/real(ngrid_lower - 1, dp)
      phi_lower_asc(i) = (phi_m_hat + phi_m_eps_hat) + (phi0_hat - (phi_m_hat + phi_m_eps_hat))*x*x
    end do
    call build_type_a_branch_from_minimum(p, phi_lower_asc, phi0_hat, n_swe_inf_hat, phi_m_hat, 'lower', s_lower_asc)
    z_m_hat = s_lower_asc(ngrid_lower)
    do i = 1, ngrid_lower
      phi_lower_desc(i) = phi_lower_asc(ngrid_lower - i + 1)
      z_lower_desc(i) = z_m_hat - s_lower_asc(ngrid_lower - i + 1)
    end do

    do i = 1, ngrid_upper
      x = real(i - 1, dp)/real(ngrid_upper - 1, dp)
      phi_upper_asc(i) = (phi_m_hat + phi_m_eps_hat) + (phi_end_hat - (phi_m_hat + phi_m_eps_hat))*(2.0d0*x - x*x)
    end do
    call build_type_a_branch_from_minimum(p, phi_upper_asc, phi0_hat, n_swe_inf_hat, phi_m_hat, 'upper', s_upper_asc)
    z_upper_asc = z_m_hat + s_upper_asc

    if (z_hat_target <= z_m_hat) then
      side = 'lower'
      phi_hat = interpolate_profile_value(z_lower_desc, phi_lower_desc, z_hat_target)
    else if (z_hat_target <= z_upper_asc(ngrid_upper)) then
      side = 'upper'
      phi_hat = interpolate_profile_value(z_upper_asc, phi_upper_asc, z_hat_target)
    else
      side = 'upper'
      phi_hat = 0.0d0
    end if
  end subroutine sample_type_a_phi_hat_at_z

  subroutine sample_monotonic_phi_hat_at_z(p, branch, phi0_hat, phi_m_hat, n_swe_inf_hat, z_m, phi_hat, side, z_m_hat)
    type(zhao_params_type), intent(in) :: p
    character(len=1), intent(in) :: branch
    real(dp), intent(in) :: phi0_hat, phi_m_hat, n_swe_inf_hat, z_m
    real(dp), intent(out) :: phi_hat, z_m_hat
    character(len=16), intent(out) :: side

    integer :: i, ngrid
    real(dp) :: z_hat_target, x, phi_end_hat, dphi, integral
    real(dp), allocatable :: phi_nodes(:), rho_nodes(:), e2_nodes(:), z_nodes(:), cumulative(:)

    z_hat_target = z_m/p%lambda_d_phe_ref_m
    z_m_hat = 0.0d0
    side = 'monotonic'
    ngrid = max(2000, zhao_profile_grid)
    allocate (phi_nodes(ngrid), rho_nodes(ngrid), e2_nodes(ngrid), z_nodes(ngrid))

    select case (branch)
    case ('B')
      phi_end_hat = abs(zhao_profile_phi_tol_hat)
      do i = 1, ngrid
        x = real(i - 1, dp)/real(ngrid - 1, dp)
        phi_nodes(i) = phi0_hat + (phi_end_hat - phi0_hat)*(2.0d0*x - x*x)
      end do
      do i = 1, ngrid
        call evaluate_zhao_rho_hat(p, branch, side, phi_nodes(i), phi0_hat, phi_m_hat, n_swe_inf_hat, rho_nodes(i))
      end do
      e2_nodes(ngrid) = 0.0d0
      integral = 0.0d0
      do i = ngrid - 1, 1, -1
        dphi = phi_nodes(i + 1) - phi_nodes(i)
        integral = integral + 0.5d0*(rho_nodes(i) + rho_nodes(i + 1))*dphi
        e2_nodes(i) = max(2.0d0*integral, 0.0d0)
      end do
    case ('C')
      phi_end_hat = -abs(zhao_profile_phi_tol_hat)
      allocate (cumulative(ngrid))
      do i = 1, ngrid
        x = real(i - 1, dp)/real(ngrid - 1, dp)
        phi_nodes(i) = phi0_hat + (phi_end_hat - phi0_hat)*(2.0d0*x - x*x)
      end do
      do i = 1, ngrid
        call evaluate_zhao_rho_hat(p, branch, side, phi_nodes(i), phi0_hat, phi_m_hat, n_swe_inf_hat, rho_nodes(i))
      end do
      cumulative(1) = 0.0d0
      do i = 2, ngrid
        dphi = phi_nodes(i) - phi_nodes(i - 1)
        cumulative(i) = cumulative(i - 1) + 0.5d0*(rho_nodes(i - 1) + rho_nodes(i))*dphi
      end do
      do i = 1, ngrid
        e2_nodes(i) = max(2.0d0*(cumulative(ngrid) - cumulative(i)), 0.0d0)
      end do
      deallocate (cumulative)
    case default
      error stop 'Unknown monotonic Zhao branch.'
    end select

    z_nodes(1) = 0.0d0
    do i = 2, ngrid
      dphi = abs(phi_nodes(i) - phi_nodes(i - 1))
      z_nodes(i) = z_nodes(i - 1) + dphi/sqrt(max(0.5d0*(e2_nodes(i - 1) + e2_nodes(i)), 1.0d-14))
    end do

    if (z_hat_target <= z_nodes(ngrid)) then
      phi_hat = interpolate_profile_value(z_nodes, phi_nodes, z_hat_target)
    else
      phi_hat = 0.0d0
    end if
  end subroutine sample_monotonic_phi_hat_at_z

  subroutine build_type_a_branch_from_minimum(p, phi_nodes_asc, phi0_hat, n_swe_inf_hat, phi_m_hat, side, s_nodes)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: phi_nodes_asc(:), phi0_hat, n_swe_inf_hat, phi_m_hat
    character(len=*), intent(in) :: side
    real(dp), intent(out) :: s_nodes(:)

    integer :: i
    real(dp) :: rho_m_hat, dphi0, integral_node, e2_mid, rho_m_neg, dphi
    real(dp), allocatable :: rho_nodes(:)

    if (size(phi_nodes_asc) /= size(s_nodes)) error stop 'Type-A profile work array size mismatch.'
    allocate (rho_nodes(size(phi_nodes_asc)))
    do i = 1, size(phi_nodes_asc)
      call evaluate_zhao_rho_hat(p, 'A', side, phi_nodes_asc(i), phi0_hat, phi_m_hat, n_swe_inf_hat, rho_nodes(i))
    end do
    call evaluate_zhao_rho_hat(p, 'A', side, phi_m_hat, phi0_hat, phi_m_hat, n_swe_inf_hat, rho_m_hat)

    rho_m_neg = max(-rho_m_hat, 1.0d-14)
    dphi0 = phi_nodes_asc(1) - phi_m_hat
    integral_node = 0.5d0*(rho_m_hat + rho_nodes(1))*dphi0
    s_nodes(1) = sqrt(max(0.0d0, 2.0d0*dphi0/rho_m_neg))

    do i = 2, size(phi_nodes_asc)
      dphi = phi_nodes_asc(i) - phi_nodes_asc(i - 1)
      integral_node = integral_node + 0.5d0*(rho_nodes(i - 1) + rho_nodes(i))*dphi
      e2_mid = max(-2.0d0*(integral_node - 0.25d0*(rho_nodes(i - 1) + rho_nodes(i))*dphi), 1.0d-14)
      s_nodes(i) = s_nodes(i - 1) + dphi/sqrt(e2_mid)
    end do
  end subroutine build_type_a_branch_from_minimum

  subroutine evaluate_zhao_state_from_phi_hat(p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_m3, z_m_m, state)
    type(zhao_params_type), intent(in) :: p
    character(len=1), intent(in) :: branch
    character(len=*), intent(in) :: side
    real(dp), intent(in) :: phi_hat, phi0_hat, phi_m_hat, n_swe_inf_m3, z_m_m
    type(zhao_local_state_type), intent(out) :: state

    real(dp) :: n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat
    real(dp) :: arg_ion, a_swe, a_phe

    call evaluate_zhao_density_hat( &
      p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_m3/p%n_phe_ref_m3, &
      n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat &
      )

    arg_ion = 1.0d0 - 2.0d0*phi_hat/(p%tau*p%mach*p%mach)
    if (arg_ion <= 0.0d0) error stop 'Zhao local ion energy argument became non-positive.'

    select case (branch)
    case ('A')
      a_swe = sqrt(max(0.0d0, (phi_hat - phi_m_hat)/p%tau))
      a_phe = sqrt(max(0.0d0, phi_hat - phi_m_hat))
      state%swe_reflected_active = trim(side) == 'upper'
      state%phe_captured_active = trim(side) == 'lower'
    case ('B')
      a_swe = 0.0d0
      a_phe = sqrt(max(0.0d0, phi_hat))
      state%swe_reflected_active = .false.
      state%phe_captured_active = .true.
    case ('C')
      a_swe = sqrt(max(0.0d0, (phi_hat - phi0_hat)/p%tau))
      a_phe = sqrt(max(0.0d0, phi_hat - phi0_hat))
      state%swe_reflected_active = .true.
      state%phe_captured_active = .false.
    case default
      error stop 'Unknown Zhao branch in local state evaluation.'
    end select

    state%branch = branch
    state%side = side
    state%z_m_m = z_m_m
    state%phi_hat = phi_hat
    state%phi_v = phi_hat*p%t_phe_ev
    state%n_swi_m3 = n_swi_hat*p%n_phe_ref_m3
    state%n_swe_f_m3 = n_swe_f_hat*p%n_phe_ref_m3
    state%n_swe_r_m3 = n_swe_r_hat*p%n_phe_ref_m3
    state%n_phe_f_m3 = n_phe_f_hat*p%n_phe_ref_m3
    state%n_phe_c_m3 = n_phe_c_hat*p%n_phe_ref_m3
    state%electron_source_density_m3 = n_swe_inf_m3*exp(phi_hat/p%tau)
    state%vcut_swe_mps = a_swe*p%v_swe_th_mps
    state%vcut_phe_mps = a_phe*p%v_phe_th_mps
    state%v_i_mps = p%v_d_ion_mps*sqrt(arg_ion)
  end subroutine evaluate_zhao_state_from_phi_hat

  subroutine evaluate_zhao_rho_hat(p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_hat, rho_hat)
    type(zhao_params_type), intent(in) :: p
    character(len=1), intent(in) :: branch
    character(len=*), intent(in) :: side
    real(dp), intent(in) :: phi_hat, phi0_hat, phi_m_hat, n_swe_inf_hat
    real(dp), intent(out) :: rho_hat

    real(dp) :: n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat

    call evaluate_zhao_density_hat( &
      p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_hat, &
      n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat &
      )
    rho_hat = n_swi_hat - n_swe_f_hat - n_swe_r_hat - n_phe_f_hat - n_phe_c_hat
  end subroutine evaluate_zhao_rho_hat

  subroutine evaluate_zhao_density_hat( &
    p, branch, side, phi_hat, phi0_hat, phi_m_hat, n_swe_inf_hat, &
    n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat &
    )
    type(zhao_params_type), intent(in) :: p
    character(len=1), intent(in) :: branch
    character(len=*), intent(in) :: side
    real(dp), intent(in) :: phi_hat, phi0_hat, phi_m_hat, n_swe_inf_hat
    real(dp), intent(out) :: n_swi_hat, n_swe_f_hat, n_swe_r_hat, n_phe_f_hat, n_phe_c_hat

    real(dp) :: arg_ion, s_swe, s_phe, sin_alpha

    sin_alpha = p%n_phe0_m3/p%n_phe_ref_m3
    arg_ion = 1.0d0 - 2.0d0*phi_hat/(p%tau*p%mach*p%mach)
    if (arg_ion <= 0.0d0) error stop 'Zhao ion density argument became non-positive.'
    n_swi_hat = (p%n_swi_inf_m3/p%n_phe_ref_m3)*arg_ion**(-0.5d0)

    select case (branch)
    case ('A')
      s_swe = sqrt(max(0.0d0, (phi_hat - phi_m_hat)/p%tau))
      s_phe = sqrt(max(0.0d0, phi_hat - phi_m_hat))
      n_swe_f_hat = 0.5d0*n_swe_inf_hat*exp(phi_hat/p%tau)*(1.0d0 - erf(s_swe - p%u))
      n_phe_f_hat = 0.5d0*sin_alpha*exp(phi_hat - phi0_hat)*(1.0d0 - erf(s_phe))
      if (trim(side) == 'lower') then
        n_swe_r_hat = 0.0d0
        n_phe_c_hat = sin_alpha*exp(phi_hat - phi0_hat)*erf(s_phe)
      else if (trim(side) == 'upper') then
        n_swe_r_hat = n_swe_inf_hat*exp(phi_hat/p%tau)*(erf(s_swe - p%u) + erf(p%u))
        n_phe_c_hat = 0.0d0
      else
        error stop 'Unknown Type-A Zhao side.'
      end if
    case ('B')
      s_phe = sqrt(max(0.0d0, phi_hat))
      n_swe_f_hat = 0.5d0*n_swe_inf_hat*exp(phi_hat/p%tau)*(1.0d0 + erf(p%u))
      n_swe_r_hat = 0.0d0
      n_phe_f_hat = 0.5d0*sin_alpha*exp(phi_hat - phi0_hat)*(1.0d0 - erf(s_phe))
      n_phe_c_hat = sin_alpha*exp(phi_hat - phi0_hat)*erf(s_phe)
    case ('C')
      s_swe = sqrt(max(0.0d0, (phi_hat - phi0_hat)/p%tau))
      s_phe = sqrt(max(0.0d0, phi_hat - phi0_hat))
      n_swe_f_hat = 0.5d0*n_swe_inf_hat*exp(phi_hat/p%tau)*(1.0d0 - erf(s_swe - p%u))
      n_swe_r_hat = n_swe_inf_hat*exp(phi_hat/p%tau)*(erf(s_swe - p%u) + erf(p%u))
      n_phe_f_hat = 0.5d0*sin_alpha*exp(phi_hat - phi0_hat)*erfc(s_phe)
      n_phe_c_hat = 0.0d0
    case default
      error stop 'Unknown Zhao branch in density evaluation.'
    end select
  end subroutine evaluate_zhao_density_hat

  real(dp) function interpolate_profile_value(x_nodes, y_nodes, x_query) result(y_query)
    real(dp), intent(in) :: x_nodes(:), y_nodes(:), x_query

    integer :: i
    real(dp) :: t

    if (size(x_nodes) /= size(y_nodes)) error stop 'Interpolation node size mismatch.'
    if (x_query <= x_nodes(1)) then
      y_query = y_nodes(1)
      return
    end if
    do i = 2, size(x_nodes)
      if (x_query <= x_nodes(i)) then
        t = (x_query - x_nodes(i - 1))/max(x_nodes(i) - x_nodes(i - 1), tiny(1.0d0))
        y_query = (1.0d0 - t)*y_nodes(i - 1) + t*y_nodes(i)
        return
      end if
    end do
    y_query = y_nodes(size(y_nodes))
  end function interpolate_profile_value

  subroutine build_zhao_params( &
    alpha_deg, n_swi_inf_m3, n_phe_ref_m3, t_swe_ev, t_phe_ev, v_d_electron_mps, v_d_ion_mps, m_i_kg, m_e_kg, p &
    )
    real(dp), intent(in) :: alpha_deg, n_swi_inf_m3, n_phe_ref_m3, t_swe_ev, t_phe_ev
    real(dp), intent(in) :: v_d_electron_mps, v_d_ion_mps, m_i_kg, m_e_kg
    type(zhao_params_type), intent(out) :: p

    if (t_swe_ev <= 0.0d0) error stop 'Zhao sheath requires electron temperature > 0.'
    if (t_phe_ev <= 0.0d0) error stop 'Zhao sheath requires photoelectron temperature > 0.'
    if (n_swi_inf_m3 <= 0.0d0) error stop 'Zhao sheath requires ion density > 0.'
    if (n_phe_ref_m3 <= 0.0d0) error stop 'Zhao sheath requires sheath_photoelectron_ref_density_cm3 > 0.'
    if (v_d_ion_mps <= 0.0d0) error stop 'Zhao sheath requires positive ion drift.'
    if (m_i_kg <= 0.0d0 .or. m_e_kg <= 0.0d0) error stop 'Zhao sheath requires positive particle masses.'

    p%alpha_rad = alpha_deg*pi/180.0d0
    p%n_swi_inf_m3 = n_swi_inf_m3
    p%n_phe_ref_m3 = n_phe_ref_m3
    p%n_phe0_m3 = n_phe_ref_m3*sin(p%alpha_rad)
    p%t_swe_ev = t_swe_ev
    p%t_phe_ev = t_phe_ev
    p%v_d_electron_mps = v_d_electron_mps
    p%v_d_ion_mps = v_d_ion_mps
    p%m_i_kg = m_i_kg
    p%m_e_kg = m_e_kg
    p%v_swe_th_mps = sqrt(2.0d0*qe*p%t_swe_ev/p%m_e_kg)
    p%v_phe_th_mps = sqrt(2.0d0*qe*p%t_phe_ev/p%m_e_kg)
    p%cs_mps = sqrt(qe*p%t_swe_ev/p%m_i_kg)
    p%mach = p%v_d_ion_mps/p%cs_mps
    p%u = p%v_d_electron_mps/p%v_swe_th_mps
    p%tau = p%t_swe_ev/p%t_phe_ev
    p%lambda_d_phe_ref_m = sqrt(eps0*qe*p%t_phe_ev/(p%n_phe_ref_m3*qe*qe))

    if (.not. ieee_is_finite(p%mach) .or. p%mach <= 0.0d0) then
      error stop 'Zhao sheath produced an invalid Mach number.'
    end if
  end subroutine build_zhao_params

  subroutine solve_zhao_unknowns(model, p, phi0_v, phi_m_v, n_swe_inf_m3, branch)
    character(len=*), intent(in) :: model
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: phi0_v, phi_m_v, n_swe_inf_m3
    character(len=1), intent(out) :: branch

    real(dp) :: x3(3), x2(2)
    logical :: success
    character(len=1), dimension(3) :: order
    integer :: i

    select case (trim(model))
    case ('zhao_a')
      call solve_zhao_branch_a(p, phi0_v, phi_m_v, n_swe_inf_m3)
      branch = 'A'
      return
    case ('zhao_b')
      call solve_zhao_branch_b(p, phi0_v, n_swe_inf_m3)
      phi_m_v = phi0_v
      branch = 'B'
      return
    case ('zhao_c')
      call solve_zhao_branch_c(p, phi0_v, n_swe_inf_m3)
      phi_m_v = phi0_v
      branch = 'C'
      return
    case ('zhao_auto')
      if (p%alpha_rad*180.0d0/pi < 20.0d0) then
        order = ['C', 'A', 'B']
      else
        order = ['A', 'B', 'C']
      end if
    case default
      error stop 'Unknown Zhao sheath model.'
    end select

    do i = 1, size(order)
      select case (order(i))
      case ('A')
        call try_solve_zhao_branch_a(p, x3, success)
        if (success) then
          phi0_v = x3(1)
          phi_m_v = x3(2)
          n_swe_inf_m3 = x3(3)
          branch = 'A'
          return
        end if
      case ('B')
        call try_solve_zhao_branch_b(p, x2, success)
        if (success) then
          phi0_v = x2(1)
          phi_m_v = x2(1)
          n_swe_inf_m3 = x2(2)
          branch = 'B'
          return
        end if
      case ('C')
        call try_solve_zhao_branch_c(p, x2, success)
        if (success) then
          phi0_v = x2(1)
          phi_m_v = x2(1)
          n_swe_inf_m3 = x2(2)
          branch = 'C'
          return
        end if
      end select
    end do

    error stop 'Zhao sheath auto branch selection failed.'
  end subroutine solve_zhao_unknowns

  subroutine solve_zhao_branch_a(p, phi0_v, phi_m_v, n_swe_inf_m3)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: phi0_v, phi_m_v, n_swe_inf_m3
    real(dp) :: x(3)
    logical :: success

    call try_solve_zhao_branch_a(p, x, success)
    if (.not. success) error stop 'Zhao Type-A root solve failed.'
    phi0_v = x(1)
    phi_m_v = x(2)
    n_swe_inf_m3 = x(3)
  end subroutine solve_zhao_branch_a

  subroutine solve_zhao_branch_b(p, phi0_v, n_swe_inf_m3)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: phi0_v, n_swe_inf_m3
    real(dp) :: x(2)
    logical :: success

    call try_solve_zhao_branch_b(p, x, success)
    if (.not. success) error stop 'Zhao Type-B root solve failed.'
    phi0_v = x(1)
    n_swe_inf_m3 = x(2)
  end subroutine solve_zhao_branch_b

  subroutine solve_zhao_branch_c(p, phi0_v, n_swe_inf_m3)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: phi0_v, n_swe_inf_m3
    real(dp) :: x(2)
    logical :: success

    call try_solve_zhao_branch_c(p, x, success)
    if (.not. success) error stop 'Zhao Type-C root solve failed.'
    phi0_v = x(1)
    n_swe_inf_m3 = x(2)
  end subroutine solve_zhao_branch_c

  subroutine try_solve_zhao_branch_a(p, x, success)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: x(3)
    logical, intent(out) :: success

    real(dp) :: guesses(3, 3)

    guesses(:, 1) = [3.6d0, -0.5d0, 8.2d6]
    guesses(:, 2) = [2.8d0, -0.3d0, 8.0d6]
    guesses(:, 3) = [4.5d0, -0.8d0, 8.4d6]
    call solve_nonlinear_system(3, guesses, residual_a, x, success)

  contains

    subroutine residual_a(xa, fa)
      real(dp), intent(in) :: xa(:)
      real(dp), intent(out) :: fa(:)

      call zhao_residuals_type_a(p, xa, fa)
    end subroutine residual_a

  end subroutine try_solve_zhao_branch_a

  subroutine try_solve_zhao_branch_b(p, x, success)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: x(2)
    logical, intent(out) :: success

    real(dp) :: guesses(2, 3)

    guesses(:, 1) = [1.3d0, 7.0d6]
    guesses(:, 2) = [0.8d0, 6.5d6]
    guesses(:, 3) = [2.0d0, 7.8d6]
    call solve_nonlinear_system(2, guesses, residual_b, x, success)

  contains

    subroutine residual_b(xb, fb)
      real(dp), intent(in) :: xb(:)
      real(dp), intent(out) :: fb(:)

      call zhao_residuals_type_b(p, xb, fb)
    end subroutine residual_b

  end subroutine try_solve_zhao_branch_b

  subroutine try_solve_zhao_branch_c(p, x, success)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(out) :: x(2)
    logical, intent(out) :: success

    real(dp) :: guesses(2, 5)

    guesses(:, 1) = [-0.5d0, 6.0d6]
    guesses(:, 2) = [-2.0d0, 7.0d6]
    guesses(:, 3) = [-5.0d0, 8.0d6]
    guesses(:, 4) = [-10.0d0, 8.2d6]
    guesses(:, 5) = [-15.0d0, 8.5d6]
    call solve_nonlinear_system(2, guesses, residual_c, x, success)

  contains

    subroutine residual_c(xc, fc)
      real(dp), intent(in) :: xc(:)
      real(dp), intent(out) :: fc(:)

      call zhao_residuals_type_c(p, xc, fc)
    end subroutine residual_c

  end subroutine try_solve_zhao_branch_c

  subroutine zhao_residuals_type_a(p, x, f)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: x(:)
    real(dp), intent(out) :: f(:)

    real(dp) :: phi0_v, phi_m_v, n_swe_inf_m3, a_swe, a_phe, ion_term

    phi0_v = x(1)
    phi_m_v = x(2)
    n_swe_inf_m3 = x(3)
    if (phi0_v <= 0.0d0 .or. phi_m_v >= 0.0d0 .or. phi_m_v >= phi0_v .or. n_swe_inf_m3 <= 0.0d0) then
      f = 1.0d6
      return
    end if

    a_swe = sqrt(max(0.0d0, -phi_m_v/p%t_swe_ev)) - p%u
    a_phe = sqrt(max(0.0d0, -phi_m_v/p%t_phe_ev))
    ion_term = p%n_swi_inf_m3*sqrt(2.0d0*pi*p%t_swe_ev/p%t_phe_ev*p%m_e_kg/p%m_i_kg)*p%mach

    f(1) = 0.5d0*n_swe_inf_m3*(1.0d0 + 2.0d0*erf(p%u) + erf(a_swe)) + &
           0.5d0*p%n_phe0_m3*exp(-phi0_v/p%t_phe_ev)*(1.0d0 - erf(a_phe)) - p%n_swi_inf_m3
    f(2) = p%n_phe0_m3*exp((phi_m_v - phi0_v)/p%t_phe_ev) - swe_free_current_term(p, n_swe_inf_m3, a_swe) + ion_term
    f(3) = type_a_e2_sum_at_infinity(p, phi0_v, phi_m_v, n_swe_inf_m3)
  end subroutine zhao_residuals_type_a

  subroutine zhao_residuals_type_b(p, x, f)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: x(:)
    real(dp), intent(out) :: f(:)

    real(dp) :: phi0_v, n_swe_inf_m3, ion_term

    phi0_v = x(1)
    n_swe_inf_m3 = x(2)
    if (phi0_v <= 0.0d0 .or. n_swe_inf_m3 <= 0.0d0) then
      f = 1.0d6
      return
    end if

    ion_term = p%n_swi_inf_m3*sqrt(2.0d0*pi*p%t_swe_ev/p%t_phe_ev*p%m_e_kg/p%m_i_kg)*p%mach
    f(1) = 0.5d0*n_swe_inf_m3*(1.0d0 + erf(p%u)) + 0.5d0*p%n_phe0_m3*exp(-phi0_v/p%t_phe_ev) - p%n_swi_inf_m3
    f(2) = p%n_phe0_m3*exp(-phi0_v/p%t_phe_ev) - swe_free_current_term(p, n_swe_inf_m3, -p%u) + ion_term
  end subroutine zhao_residuals_type_b

  subroutine zhao_residuals_type_c(p, x, f)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: x(:)
    real(dp), intent(out) :: f(:)

    real(dp) :: phi0_v, n_swe_inf_m3, a_swe, a_phe, ion_term

    phi0_v = x(1)
    n_swe_inf_m3 = x(2)
    if (phi0_v >= 0.0d0 .or. n_swe_inf_m3 <= 0.0d0) then
      f = 1.0d6
      return
    end if

    a_swe = sqrt(max(0.0d0, -phi0_v/p%t_swe_ev)) - p%u
    a_phe = sqrt(max(0.0d0, -phi0_v/p%t_phe_ev))
    ion_term = p%n_swi_inf_m3*sqrt(2.0d0*pi*p%t_swe_ev/p%t_phe_ev*p%m_e_kg/p%m_i_kg)*p%mach

    f(1) = 0.5d0*n_swe_inf_m3*(1.0d0 + 2.0d0*erf(p%u) + erf(a_swe)) + &
           0.5d0*p%n_phe0_m3*exp(-phi0_v/p%t_phe_ev)*erfc(a_phe) - p%n_swi_inf_m3
    f(2) = p%n_phe0_m3 - swe_free_current_term(p, n_swe_inf_m3, a_swe) + ion_term
  end subroutine zhao_residuals_type_c

  real(dp) function swe_free_current_term(p, n_swe_inf_m3, a_swe) result(term)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: n_swe_inf_m3, a_swe

    term = n_swe_inf_m3*(sqrt(p%t_swe_ev/p%t_phe_ev)*exp(-(a_swe*a_swe)) + &
                         sqrt(pi)*(p%v_d_electron_mps/p%v_phe_th_mps)*erfc(a_swe))
  end function swe_free_current_term

  real(dp) function type_a_e2_sum_at_infinity(p, phi0_v, phi_m_v, n_swe_inf_m3) result(e2_sum)
    type(zhao_params_type), intent(in) :: p
    real(dp), intent(in) :: phi0_v, phi_m_v, n_swe_inf_m3

    real(dp) :: phi, s_swe, s_phe, e2_swe_f, e2_swe_r, e2_phe_f, e2_swi, arg_phi, arg_m

    if (abs(p%u) <= 1.0d-12) then
      e2_sum = 1.0d30
      return
    end if

    phi = 0.0d0
    s_swe = sqrt(max(0.0d0, (phi - phi_m_v)/p%t_swe_ev))
    s_phe = sqrt(max(0.0d0, (phi - phi_m_v)/p%t_phe_ev))

    e2_swe_f = (p%t_swe_ev/p%t_phe_ev)*(n_swe_inf_m3/p%n_phe_ref_m3)*( &
               exp(phi/p%t_swe_ev)*(1.0d0 - erf(s_swe - p%u)) - &
               exp(phi_m_v/p%t_swe_ev)*(1.0d0 - erf(-p%u)) + &
               (1.0d0/(sqrt(pi)*p%u))*exp(phi_m_v/p%t_swe_ev - p%u*p%u)*(exp(2.0d0*p%u*s_swe) - 1.0d0) &
               )

    e2_swe_r = 2.0d0*(p%t_swe_ev/p%t_phe_ev)*(n_swe_inf_m3/p%n_phe_ref_m3)*( &
               exp(phi/p%t_swe_ev)*(erf(s_swe - p%u) + erf(p%u)) - &
               (1.0d0/(sqrt(pi)*p%u))*exp(phi_m_v/p%t_swe_ev - p%u*p%u)*(exp(2.0d0*p%u*s_swe) - 1.0d0) &
               )

    e2_phe_f = (p%n_phe0_m3/p%n_phe_ref_m3)*( &
               exp((phi - phi0_v)/p%t_phe_ev)*(1.0d0 - erf(s_phe)) - &
               exp((phi_m_v - phi0_v)/p%t_phe_ev)*(1.0d0 - 2.0d0*s_phe/sqrt(pi)) &
               )

    arg_phi = 1.0d0 - 2.0d0*phi/(p%t_swe_ev*p%mach*p%mach)
    arg_m = 1.0d0 - 2.0d0*phi_m_v/(p%t_swe_ev*p%mach*p%mach)
    if (arg_phi <= 0.0d0 .or. arg_m <= 0.0d0) then
      e2_sum = 1.0d30
      return
    end if

    e2_swi = 2.0d0*(p%t_swe_ev/p%t_phe_ev)*(p%n_swi_inf_m3/p%n_phe_ref_m3)*p%mach*p%mach*( &
             sqrt(arg_phi) - sqrt(arg_m) &
             )
    e2_sum = e2_swe_f + e2_swe_r + e2_phe_f + e2_swi
  end function type_a_e2_sum_at_infinity

  subroutine solve_nonlinear_system(n, guesses, residual_fn, x_best, success)
    integer, intent(in) :: n
    real(dp), intent(in) :: guesses(:, :)
    procedure(nonlinear_residual) :: residual_fn
    real(dp), intent(out) :: x_best(n)
    logical, intent(out) :: success

    integer :: guess_idx
    real(dp) :: x_trial(n), best_norm, trial_norm
    logical :: trial_success

    success = .false.
    if (size(guesses, 1) /= n) error stop 'solve_nonlinear_system guess dimension mismatch.'
    x_best = guesses(:, 1)
    best_norm = huge(1.0d0)
    do guess_idx = 1, size(guesses, 2)
      call try_newton_solve(n, guesses(:, guess_idx), residual_fn, x_trial, trial_norm, trial_success)
      if (trial_norm < best_norm) then
        best_norm = trial_norm
        x_best = x_trial
      end if
      if (trial_success .and. trial_norm < nonlinear_tol) then
        success = .true.
        x_best = x_trial
        return
      end if
    end do

    success = best_norm < nonlinear_tol
  end subroutine solve_nonlinear_system

  subroutine try_newton_solve(n, x0, residual_fn, x_out, final_norm, success)
    integer, intent(in) :: n
    real(dp), intent(in) :: x0(n)
    procedure(nonlinear_residual) :: residual_fn
    real(dp), intent(out) :: x_out(n)
    real(dp), intent(out) :: final_norm
    logical, intent(out) :: success

    integer :: iter, backtrack
    real(dp) :: x(n), f(n), jac(n, n), dx(n), x_trial(n), f_trial(n), step_scale, fnorm, trial_norm
    logical :: linear_ok, improved

    x = x0
    call residual_fn(x, f)
    fnorm = residual_norm(f)
    do iter = 1, nonlinear_max_iter
      if (fnorm < nonlinear_tol) exit
      call numerical_jacobian(n, x, f, residual_fn, jac)
      call solve_small_linear_system(n, jac, -f, dx, linear_ok)
      if (.not. linear_ok) exit

      step_scale = 1.0d0
      improved = .false.
      do backtrack = 1, nonlinear_max_backtrack
        x_trial = x + step_scale*dx
        call residual_fn(x_trial, f_trial)
        trial_norm = residual_norm(f_trial)
        if (trial_norm < fnorm) then
          x = x_trial
          f = f_trial
          fnorm = trial_norm
          improved = .true.
          exit
        end if
        step_scale = 0.5d0*step_scale
      end do
      if (.not. improved) exit
    end do

    x_out = x
    final_norm = fnorm
    success = fnorm < nonlinear_tol
  end subroutine try_newton_solve

  subroutine numerical_jacobian(n, x, f0, residual_fn, jac)
    integer, intent(in) :: n
    real(dp), intent(in) :: x(n), f0(n)
    procedure(nonlinear_residual) :: residual_fn
    real(dp), intent(out) :: jac(n, n)

    integer :: j
    real(dp) :: h, xh(n), fh(n)

    do j = 1, n
      h = 1.0d-6*max(1.0d0, abs(x(j)))
      xh = x
      xh(j) = xh(j) + h
      call residual_fn(xh, fh)
      jac(:, j) = (fh - f0)/h
    end do
  end subroutine numerical_jacobian

  subroutine solve_small_linear_system(n, a_in, b_in, x, ok)
    integer, intent(in) :: n
    real(dp), intent(in) :: a_in(n, n), b_in(n)
    real(dp), intent(out) :: x(n)
    logical, intent(out) :: ok

    integer :: i, j, k, pivot_row
    real(dp) :: a(n, n), b(n), factor, pivot_abs, tmp_row(n), tmp_val

    a = a_in
    b = b_in
    ok = .true.

    do k = 1, n
      pivot_row = k
      pivot_abs = abs(a(k, k))
      do i = k + 1, n
        if (abs(a(i, k)) > pivot_abs) then
          pivot_abs = abs(a(i, k))
          pivot_row = i
        end if
      end do
      if (pivot_abs <= 1.0d-18) then
        ok = .false.
        x = 0.0d0
        return
      end if
      if (pivot_row /= k) then
        tmp_row = a(k, :)
        a(k, :) = a(pivot_row, :)
        a(pivot_row, :) = tmp_row
        tmp_val = b(k)
        b(k) = b(pivot_row)
        b(pivot_row) = tmp_val
      end if
      do i = k + 1, n
        factor = a(i, k)/a(k, k)
        a(i, k:n) = a(i, k:n) - factor*a(k, k:n)
        b(i) = b(i) - factor*b(k)
      end do
    end do

    x = 0.0d0
    do i = n, 1, -1
      x(i) = b(i)
      do j = i + 1, n
        x(i) = x(i) - a(i, j)*x(j)
      end do
      x(i) = x(i)/a(i, i)
    end do
  end subroutine solve_small_linear_system

  real(dp) function residual_norm(f) result(norm2)
    real(dp), intent(in) :: f(:)

    if (.not. all(ieee_is_finite(f))) then
      norm2 = huge(1.0d0)
      return
    end if
    norm2 = sqrt(sum(f*f))
  end function residual_norm

  real(dp) function temperature_ev_from_species(spec) result(temp_ev)
    type(sheath_model_species), intent(in) :: spec

    temp_ev = spec%temperature_k*k_boltzmann/qe
  end function temperature_ev_from_species

  real(dp) function resolve_species_drift_speed(spec, drift_mode, inward_normal) result(speed)
    type(sheath_model_species), intent(in) :: spec
    character(len=*), intent(in) :: drift_mode
    real(dp), intent(in) :: inward_normal(3)

    select case (trim(lower_ascii(drift_mode)))
    case ('full')
      speed = sqrt(sum(spec%drift_velocity*spec%drift_velocity))
    case ('normal')
      speed = dot_product(spec%drift_velocity, inward_normal)
    case default
      error stop 'Unknown sheath drift mode.'
    end select
  end function resolve_species_drift_speed

  real(dp) function zhao_electron_vmin_normal(branch, phi0_v, phi_m_v, m_e_kg) result(vmin_normal)
    character(len=1), intent(in) :: branch
    real(dp), intent(in) :: phi0_v, phi_m_v, m_e_kg
    real(dp) :: delta_phi_swe

    select case (branch)
    case ('A')
      delta_phi_swe = -phi_m_v
    case ('B')
      delta_phi_swe = 0.0d0
    case ('C')
      delta_phi_swe = -phi0_v
    case default
      error stop 'Unexpected Zhao sheath branch.'
    end select
    vmin_normal = sqrt(max(0.0d0, 2.0d0*qe*max(0.0d0, delta_phi_swe))/abs(m_e_kg))
  end function zhao_electron_vmin_normal

  real(dp) function zhao_photo_vmin_normal(branch, phi0_v, phi_m_v, m_phe_kg) result(vmin_normal)
    character(len=1), intent(in) :: branch
    real(dp), intent(in) :: phi0_v, phi_m_v, m_phe_kg
    real(dp) :: delta_phi_phe

    select case (branch)
    case ('A')
      delta_phi_phe = phi0_v - phi_m_v
    case ('B')
      delta_phi_phe = phi0_v
    case ('C')
      delta_phi_phe = 0.0d0
    case default
      error stop 'Unexpected Zhao sheath branch.'
    end select
    vmin_normal = sqrt(max(0.0d0, 2.0d0*qe*max(0.0d0, delta_phi_phe))/abs(m_phe_kg))
  end function zhao_photo_vmin_normal

  real(dp) function zhao_photo_emit_current_density(branch, phi0_v, phi_m_v, photo_charge_c, p) result(current_a_m2)
    character(len=1), intent(in) :: branch
    real(dp), intent(in) :: phi0_v, phi_m_v, photo_charge_c
    type(zhao_params_type), intent(in) :: p

    select case (branch)
    case ('A')
      current_a_m2 = abs(photo_charge_c)*p%n_phe0_m3*exp((phi_m_v - phi0_v)/p%t_phe_ev)*p%v_phe_th_mps/(2.0d0*sqrt(pi))
    case ('B')
      current_a_m2 = abs(photo_charge_c)*p%n_phe0_m3*exp(-phi0_v/p%t_phe_ev)*p%v_phe_th_mps/(2.0d0*sqrt(pi))
    case ('C')
      current_a_m2 = abs(photo_charge_c)*p%n_phe0_m3*p%v_phe_th_mps/(2.0d0*sqrt(pi))
    case default
      error stop 'Unexpected Zhao sheath branch.'
    end select
  end function zhao_photo_emit_current_density

end module bem_sheath_model_core