bem_panel_surface_sides.f90 Source File


This file depends on

sourcefile~~bem_panel_surface_sides.f90~~EfferentGraph sourcefile~bem_panel_surface_sides.f90 bem_panel_surface_sides.f90 sourcefile~bem_kinds.f90 bem_kinds.f90 sourcefile~bem_panel_surface_sides.f90->sourcefile~bem_kinds.f90 sourcefile~bem_string_utils.f90 bem_string_utils.f90 sourcefile~bem_panel_surface_sides.f90->sourcefile~bem_string_utils.f90 sourcefile~bem_types.f90 bem_types.f90 sourcefile~bem_panel_surface_sides.f90->sourcefile~bem_types.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_outflow_spectrum.f90->sourcefile~bem_kinds.f90

Files dependent on this one

sourcefile~~bem_panel_surface_sides.f90~~AfferentGraph sourcefile~bem_panel_surface_sides.f90 bem_panel_surface_sides.f90 sourcefile~bem_app_config_mesh_runtime.f90 bem_app_config_mesh_runtime.f90 sourcefile~bem_app_config_mesh_runtime.f90->sourcefile~bem_panel_surface_sides.f90 sourcefile~bem_app_config_runtime.f90 bem_app_config_runtime.f90 sourcefile~bem_app_config_runtime.f90->sourcefile~bem_app_config_mesh_runtime.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_runtime.f90 sourcefile~bem_simulator.f90->sourcefile~bem_app_config.f90 sourcefile~bem_simulator_io.f90 bem_simulator_io.f90 sourcefile~bem_simulator_io.f90->sourcefile~bem_app_config_runtime.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_app_config_runtime.f90 sourcefile~bem_simulator_loop.f90->sourcefile~bem_simulator.f90 sourcefile~bem_zhao_outflow_refresh.f90 bem_zhao_outflow_refresh.f90 sourcefile~bem_simulator_loop.f90->sourcefile~bem_zhao_outflow_refresh.f90 sourcefile~bem_simulator_charge.f90 bem_simulator_charge.f90 sourcefile~bem_simulator_charge.f90->sourcefile~bem_simulator.f90 sourcefile~bem_simulator_particles.f90 bem_simulator_particles.f90 sourcefile~bem_simulator_particles.f90->sourcefile~bem_simulator.f90 sourcefile~bem_simulator_stats.f90 bem_simulator_stats.f90 sourcefile~bem_simulator_stats.f90->sourcefile~bem_simulator.f90 sourcefile~bem_zhao_outflow_refresh.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

Source Code

!> Resolve physical vacuum sides without changing ordered triangle winding.
module bem_panel_surface_sides
  use, intrinsic :: ieee_arithmetic, only: ieee_is_finite
  use bem_kinds, only: dp, i32, i64
  use bem_types, only: mesh_type
  use bem_string_utils, only: lower_ascii
  implicit none
  private

  integer(i32), parameter, public :: panel_surface_side_ok = 0_i32
  integer(i32), parameter, public :: panel_surface_side_open = 1_i32
  integer(i32), parameter, public :: panel_surface_side_inconsistent = 2_i32
  integer(i32), parameter, public :: panel_surface_side_nonmanifold = 3_i32
  integer(i32), parameter, public :: panel_surface_side_invalid_policy = 4_i32

  public :: resolve_panel_surface_sides
  public :: resolve_panel_surface_sides_by_mesh_id

  !> Bucket index of the half-edge start points used by outward_closed edge matching.
  type :: edge_bucket_index_type
    integer(i64), allocatable :: cell_key(:, :)
    integer(i32), allocatable :: table(:)
    integer(i32), allocatable :: cell_of(:)
    integer(i32), allocatable :: cell_start(:), cell_half_edge(:)
    integer(i32), allocatable :: near_start(:), near_cell(:)
    integer(i32) :: ncell = 0_i32
  end type edge_bucket_index_type

contains

  !> Apply one policy to every element, or to the elements of one mesh_id.
  subroutine resolve_panel_surface_sides(mesh, policy, status, message, mesh_id)
    type(mesh_type), intent(inout) :: mesh
    character(len=*), intent(in) :: policy
    integer(i32), intent(out) :: status
    character(len=*), intent(out) :: message
    integer(i32), intent(in), optional :: mesh_id
    integer(i32), allocatable :: elements(:)
    integer(i32) :: i
    real(dp) :: coordinate_scale

    status = panel_surface_side_ok
    message = ''
    if (present(mesh_id)) then
      elements = pack([(i, i=1_i32, mesh%nelem)], mesh%elem_mesh_id == mesh_id)
    else
      elements = [(i, i=1_i32, mesh%nelem)]
    end if
    if (size(elements) == 0) then
      status = panel_surface_side_invalid_policy
      message = 'surface side mesh_id selects no elements'
      return
    end if
    coordinate_scale = 0.0_dp
    if (is_outward_closed(policy)) coordinate_scale = mesh_coordinate_scale(mesh)
    call resolve_element_sides(mesh, policy, elements, coordinate_scale, status, message)
  end subroutine resolve_panel_surface_sides

  !> Apply `policies(k)` to the elements whose mesh_id is k, for k = 1..size(policies), in order.
  !! Elements are grouped by mesh_id once, so each part costs only its own size. On failure the
  !! parts before `failed_mesh_id` stay resolved and the failing part is left unchanged.
  subroutine resolve_panel_surface_sides_by_mesh_id(mesh, policies, status, message, failed_mesh_id)
    type(mesh_type), intent(inout) :: mesh
    character(len=*), intent(in) :: policies(:)
    integer(i32), intent(out) :: status
    character(len=*), intent(out) :: message
    integer(i32), intent(out) :: failed_mesh_id
    integer(i32), allocatable :: part_start(:), part_fill(:), elements(:)
    integer(i32) :: i, k, nparts
    real(dp) :: coordinate_scale

    status = panel_surface_side_ok
    message = ''
    failed_mesh_id = 0_i32
    nparts = int(size(policies), i32)
    allocate (part_start(nparts + 1_i32), part_fill(nparts), elements(mesh%nelem))
    part_start = 0_i32
    do i = 1_i32, mesh%nelem
      k = mesh%elem_mesh_id(i)
      if (k >= 1_i32 .and. k <= nparts) part_start(k + 1_i32) = part_start(k + 1_i32) + 1_i32
    end do
    part_start(1) = 1_i32
    do k = 1_i32, nparts
      part_start(k + 1_i32) = part_start(k + 1_i32) + part_start(k)
    end do
    part_fill = part_start(1:nparts)
    do i = 1_i32, mesh%nelem
      k = mesh%elem_mesh_id(i)
      if (k < 1_i32 .or. k > nparts) cycle
      elements(part_fill(k)) = i
      part_fill(k) = part_fill(k) + 1_i32
    end do

    coordinate_scale = 0.0_dp
    do k = 1_i32, nparts
      if (is_outward_closed(policies(k))) then
        coordinate_scale = mesh_coordinate_scale(mesh)
        exit
      end if
    end do
    do k = 1_i32, nparts
      if (part_start(k + 1_i32) == part_start(k)) then
        status = panel_surface_side_invalid_policy
        message = 'surface side mesh_id selects no elements'
      else
        call resolve_element_sides( &
          mesh, policies(k), elements(part_start(k):part_start(k + 1_i32) - 1_i32), coordinate_scale, status, message &
          )
      end if
      if (status /= panel_surface_side_ok) then
        failed_mesh_id = k
        return
      end if
    end do
  end subroutine resolve_panel_surface_sides_by_mesh_id

  !> Resolve one policy for `elements` (ascending element indices). The mesh is changed only on success.
  !! `coordinate_scale` sets the outward_closed vertex tolerance and must cover the whole mesh.
  subroutine resolve_element_sides(mesh, policy, elements, coordinate_scale, status, message)
    type(mesh_type), intent(inout) :: mesh
    character(len=*), intent(in) :: policy
    integer(i32), intent(in) :: elements(:)
    real(dp), intent(in) :: coordinate_scale
    integer(i32), intent(out) :: status
    character(len=*), intent(out) :: message
    character(len=32) :: normalized
    integer(i32), allocatable :: signs(:), neighbor(:, :), queue(:)
    logical, allocatable :: visited(:)
    integer(i32) :: i, m, edge, face, next_face, head, tail, component_sign, element
    real(dp) :: signed_volume, volume_scale, origin(3), a(3), b(3), c(3)

    status = panel_surface_side_ok
    message = ''
    normalized = lower_ascii(trim(policy))
    m = int(size(elements), i32)
    allocate (signs(m))
    select case (trim(normalized))
    case ('normal_plus')
      signs = 1_i32
    case ('normal_minus')
      signs = -1_i32
    case ('outward_closed')
      allocate (neighbor(3, m), queue(m), visited(m))
      call build_closed_neighbors(mesh, elements, coordinate_scale, neighbor, status, message)
      if (status /= panel_surface_side_ok) return
      visited = .false.
      do i = 1_i32, m
        if (visited(i)) cycle
        head = 1_i32
        tail = 1_i32
        queue(1) = i
        visited(i) = .true.
        ! Signed volume about a vertex of this component, so neither the world origin nor a
        ! 1 m unit enters: each tetrahedron term is a.(b x c)/6 with a, b, c relative to it,
        ! and its rounding error is bounded by a few eps*|a||b||c|/6 (length^3 scale).
        origin = mesh%v0(:, elements(i))
        signed_volume = 0.0_dp
        volume_scale = 0.0_dp
        do while (head <= tail)
          face = queue(head)
          head = head + 1_i32
          element = elements(face)
          a = mesh%v0(:, element) - origin
          b = mesh%v1(:, element) - origin
          c = mesh%v2(:, element) - origin
          signed_volume = signed_volume + dot_product(a, cross_product(b, c))/6.0_dp
          volume_scale = volume_scale + norm2(a)*norm2(b)*norm2(c)/6.0_dp
          do edge = 1, 3
            next_face = neighbor(edge, face)
            if (.not. visited(next_face)) then
              tail = tail + 1_i32
              queue(tail) = next_face
              visited(next_face) = .true.
            end if
          end do
        end do
        if (abs(signed_volume) <= 128.0_dp*epsilon(1.0_dp)*volume_scale) then
          status = panel_surface_side_inconsistent
          message = 'closed component has zero or indeterminate signed volume'
          return
        end if
        component_sign = merge(1_i32, -1_i32, signed_volume > 0.0_dp)
        do face = 1, tail
          signs(queue(face)) = component_sign
        end do
      end do
    case default
      status = panel_surface_side_invalid_policy
      message = 'surface side policy must be normal_plus, normal_minus, or outward_closed'
      return
    end select

    do i = 1_i32, m
      element = elements(i)
      mesh%elem_vacuum_sign(element) = signs(i)
      mesh%vacuum_normals(:, element) = real(signs(i), dp)*mesh%normals(:, element)
    end do
  end subroutine resolve_element_sides

  !> Find, for every edge of `elements`, the one oppositely oriented edge of another element with
  !! the same mesh_id. Two vertices match when every coordinate differs by at most
  !! 128*eps*coordinate_scale. Edges are matched through a hash of start-point buckets four
  !! tolerances wide, so every point within tolerance lies in the same or an adjacent bucket and
  !! the exact pairwise test sees all candidates: an edge with no partner, two partners, or a
  !! same-direction partner is reported for the first such edge in element order, as a full search.
  subroutine build_closed_neighbors(mesh, elements, coordinate_scale, neighbor, status, message)
    type(mesh_type), intent(in) :: mesh
    integer(i32), intent(in) :: elements(:)
    real(dp), intent(in) :: coordinate_scale
    integer(i32), intent(out) :: neighbor(:, :)
    integer(i32), intent(out) :: status
    character(len=*), intent(out) :: message
    type(edge_bucket_index_type) :: buckets
    real(dp) :: a(3), b(3), c(3), d(3), tolerance
    integer(i32), allocatable :: stamp(:)
    integer(i32) :: m, face, edge, half_edge, side, bucket, near_bucket, near_pos, pos
    integer(i32) :: other_half_edge, other_face, other_edge
    integer(i32) :: element, other_element, matches, matched_face
    logical :: same_direction, opposite_direction

    neighbor = 0_i32
    status = panel_surface_side_ok
    message = ''
    m = int(size(elements), i32)
    do face = 1_i32, m
      element = elements(face)
      if (.not. (all(ieee_is_finite(mesh%v0(:, element))) .and. all(ieee_is_finite(mesh%v1(:, element))) .and. &
                 all(ieee_is_finite(mesh%v2(:, element))))) then
        status = panel_surface_side_invalid_policy
        message = 'outward_closed requires finite vertex coordinates'
        return
      end if
    end do
    tolerance = 128.0_dp*epsilon(1.0_dp)*coordinate_scale
    call build_edge_bucket_index(mesh, elements, 4.0_dp*tolerance, buckets)

    allocate (stamp(3_i32*m))
    stamp = 0_i32
    do face = 1_i32, m
      element = elements(face)
      do edge = 1_i32, 3_i32
        half_edge = 3_i32*(face - 1_i32) + edge
        call edge_vertices(mesh, element, edge, a, b)
        matches = 0_i32
        matched_face = 0_i32
        ! A same-direction partner starts near a; an opposite one starts near b, the start of the next edge.
        do side = 1, 2
          if (side == 1) then
            bucket = buckets%cell_of(half_edge)
          else
            bucket = buckets%cell_of(3_i32*(face - 1_i32) + modulo(edge, 3_i32) + 1_i32)
          end if
          do near_pos = buckets%near_start(bucket), buckets%near_start(bucket + 1_i32) - 1_i32
            near_bucket = buckets%near_cell(near_pos)
            do pos = buckets%cell_start(near_bucket), buckets%cell_start(near_bucket + 1_i32) - 1_i32
              other_half_edge = buckets%cell_half_edge(pos)
              if (stamp(other_half_edge) == half_edge) cycle
              stamp(other_half_edge) = half_edge
              other_face = (other_half_edge - 1_i32)/3_i32 + 1_i32
              if (other_face == face) cycle
              other_element = elements(other_face)
              if (mesh%elem_mesh_id(other_element) /= mesh%elem_mesh_id(element)) cycle
              other_edge = other_half_edge - 3_i32*(other_face - 1_i32)
              call edge_vertices(mesh, other_element, other_edge, c, d)
              same_direction = same_point(a, c, tolerance) .and. same_point(b, d, tolerance)
              opposite_direction = same_point(a, d, tolerance) .and. same_point(b, c, tolerance)
              if (same_direction) then
                status = panel_surface_side_inconsistent
                message = 'closed surface contains inconsistent face winding'
                return
              end if
              if (opposite_direction) then
                matches = matches + 1_i32
                matched_face = other_face
              end if
            end do
          end do
        end do
        if (matches == 0_i32) then
          status = panel_surface_side_open
          message = 'outward_closed requires every edge to have a partner'
          return
        else if (matches > 1_i32) then
          status = panel_surface_side_nonmanifold
          message = 'outward_closed requires a two-manifold mesh'
          return
        end if
        neighbor(edge, face) = matched_face
      end do
    end do
  end subroutine build_closed_neighbors

  !> Hash every half-edge start point of `elements` into cubic buckets of edge `bucket_size` and
  !! list, per bucket, its half-edges (3*(face-1)+edge, ascending) and its occupied neighbor
  !! buckets (itself included). A non-finite bucket size, which only an unbounded coordinate
  !! scale produces, puts every point into one bucket.
  subroutine build_edge_bucket_index(mesh, elements, bucket_size, buckets)
    type(mesh_type), intent(in) :: mesh
    integer(i32), intent(in) :: elements(:)
    real(dp), intent(in) :: bucket_size
    type(edge_bucket_index_type), intent(out) :: buckets
    integer(i32), allocatable :: fill(:), found(:)
    integer(i32) :: m, nhalf, half_edge, face, edge, bucket, table_size, slot, pass, nfound, total, dx, dy, dz
    integer(i64) :: key(3)
    real(dp) :: point(3)

    m = int(size(elements), i32)
    nhalf = 3_i32*m
    table_size = 16_i32
    do while (table_size < 2_i32*nhalf)
      table_size = 2_i32*table_size
    end do
    allocate (buckets%cell_key(3, nhalf), buckets%table(0:table_size - 1_i32), buckets%cell_of(nhalf))
    buckets%table = 0_i32
    buckets%ncell = 0_i32
    do face = 1_i32, m
      do edge = 1_i32, 3_i32
        half_edge = 3_i32*(face - 1_i32) + edge
        select case (edge)
        case (1)
          point = mesh%v0(:, elements(face))
        case (2)
          point = mesh%v1(:, elements(face))
        case default
          point = mesh%v2(:, elements(face))
        end select
        if (ieee_is_finite(bucket_size)) then
          key = floor(point/bucket_size, kind=i64)
        else
          key = 0_i64
        end if
        slot = hash_slot(key, table_size)
        do
          bucket = buckets%table(slot)
          if (bucket == 0_i32) then
            buckets%ncell = buckets%ncell + 1_i32
            bucket = buckets%ncell
            buckets%cell_key(:, bucket) = key
            buckets%table(slot) = bucket
            exit
          end if
          if (all(buckets%cell_key(:, bucket) == key)) exit
          slot = iand(slot + 1_i32, table_size - 1_i32)
        end do
        buckets%cell_of(half_edge) = bucket
      end do
    end do

    allocate (buckets%cell_start(buckets%ncell + 1_i32), buckets%cell_half_edge(nhalf), fill(buckets%ncell))
    buckets%cell_start = 0_i32
    do half_edge = 1_i32, nhalf
      bucket = buckets%cell_of(half_edge)
      buckets%cell_start(bucket + 1_i32) = buckets%cell_start(bucket + 1_i32) + 1_i32
    end do
    buckets%cell_start(1) = 1_i32
    do bucket = 1_i32, buckets%ncell
      buckets%cell_start(bucket + 1_i32) = buckets%cell_start(bucket + 1_i32) + buckets%cell_start(bucket)
    end do
    fill = buckets%cell_start(1:buckets%ncell)
    do half_edge = 1_i32, nhalf
      bucket = buckets%cell_of(half_edge)
      buckets%cell_half_edge(fill(bucket)) = half_edge
      fill(bucket) = fill(bucket) + 1_i32
    end do

    ! Two passes over the 27 neighbor keys: count, then fill the per-bucket neighbor list.
    allocate (buckets%near_start(buckets%ncell + 1_i32), found(27))
    buckets%near_start(1) = 1_i32
    total = 0_i32
    do pass = 1, 2
      if (pass == 2) allocate (buckets%near_cell(total))
      do bucket = 1_i32, buckets%ncell
        nfound = 0_i32
        do dz = -1_i32, 1_i32
          do dy = -1_i32, 1_i32
            do dx = -1_i32, 1_i32
              key = buckets%cell_key(:, bucket) + [int(dx, i64), int(dy, i64), int(dz, i64)]
              slot = hash_slot(key, table_size)
              do while (buckets%table(slot) /= 0_i32)
                if (all(buckets%cell_key(:, buckets%table(slot)) == key)) then
                  nfound = nfound + 1_i32
                  found(nfound) = buckets%table(slot)
                  exit
                end if
                slot = iand(slot + 1_i32, table_size - 1_i32)
              end do
            end do
          end do
        end do
        if (pass == 1) then
          buckets%near_start(bucket + 1_i32) = buckets%near_start(bucket) + nfound
        else
          buckets%near_cell(buckets%near_start(bucket):buckets%near_start(bucket + 1_i32) - 1_i32) = found(1:nfound)
        end if
      end do
      total = buckets%near_start(buckets%ncell + 1_i32) - 1_i32
    end do
  end subroutine build_edge_bucket_index

  !> Open-addressing slot of a bucket key; each component is reduced first so no product overflows.
  pure integer(i32) function hash_slot(key, table_size) result(slot)
    integer(i64), intent(in) :: key(3)
    integer(i32), intent(in) :: table_size
    integer(i64), parameter :: prime = 1000003_i64
    integer(i64) :: mixed

    mixed = modulo(key(1), prime)*73856093_i64 + modulo(key(2), prime)*19349663_i64 + &
            modulo(key(3), prime)*83492791_i64
    slot = int(iand(mixed, int(table_size - 1_i32, i64)), i32)
  end function hash_slot

  logical function is_outward_closed(policy)
    character(len=*), intent(in) :: policy
    character(len=32) :: normalized

    normalized = lower_ascii(trim(policy))
    is_outward_closed = trim(normalized) == 'outward_closed'
  end function is_outward_closed

  !> Largest vertex coordinate magnitude of the whole mesh, at least 1; scales the vertex tolerance.
  pure real(dp) function mesh_coordinate_scale(mesh) result(scale)
    type(mesh_type), intent(in) :: mesh

    scale = max(1.0_dp, max(maxval(abs(mesh%v0)), max(maxval(abs(mesh%v1)), maxval(abs(mesh%v2)))))
  end function mesh_coordinate_scale

  pure subroutine edge_vertices(mesh, face, edge, a, b)
    type(mesh_type), intent(in) :: mesh
    integer(i32), intent(in) :: face, edge
    real(dp), intent(out) :: a(3), b(3)

    select case (edge)
    case (1)
      a = mesh%v0(:, face)
      b = mesh%v1(:, face)
    case (2)
      a = mesh%v1(:, face)
      b = mesh%v2(:, face)
    case default
      a = mesh%v2(:, face)
      b = mesh%v0(:, face)
    end select
  end subroutine edge_vertices

  pure logical function same_point(a, b, tolerance) result(same)
    real(dp), intent(in) :: a(3), b(3), tolerance

    same = maxval(abs(a - b)) <= tolerance
  end function same_point

  pure real(dp) function norm2(vector) result(norm)
    real(dp), intent(in) :: vector(3)

    norm = sqrt(sum(vector*vector))
  end function norm2

  pure function cross_product(a, b) result(c)
    real(dp), intent(in) :: a(3), b(3)
    real(dp) :: c(3)

    c = [a(2)*b(3) - a(3)*b(2), a(3)*b(1) - a(1)*b(3), a(1)*b(2) - a(2)*b(1)]
  end function cross_product

end module bem_panel_surface_sides