a_star Subroutine

public subroutine a_star(graph, source, target, heuristic, path, err)

A* search for one shortest path from source to target.

heuristic(v) is a precomputed array of lower bounds on the remaining distance from v to target. An array rather than a procedure argument is a deliberate portability choice: passing procedures through an abstract interface is the more general design, but procedure dummies are exactly the corner of the language where the compilers PIC targets differ most, and a realistic heuristic (straight-line distance to the target from stored coordinates) is computed once per search anyway, so generality buys little here.

The search settles each vertex once and stops as soon as target settles, so the heuristic must be consistent (monotone): heuristic(u) - heuristic(v) <= w(u, v) for every edge. Consistency implies admissibility, and with it A* returns a path of exactly the same total cost as dijkstra. With heuristic = 0 - trivially consistent - it degenerates to Dijkstra exactly, expanding vertices in the same order and returning the same tie-broken path. Both are asserted in the test suite.

path is allocated to the vertices from source to target inclusive, so path(1) == source and path(size(path)) == target; a search with source == target yields the single-element path [source]. An unreachable target is not an error: path comes back zero-size. Reports ERROR_VALIDATION for a malformed graph, an out-of-range source or target, a heuristic whose length is not the number of vertices or which holds a negative entry, or a negative edge weight.

Arguments

Type IntentOptional Attributes Name
type(csr_t), intent(in) :: graph

Square weighted adjacency matrix of the graph

integer(kind=default_int), intent(in) :: source

Start vertex, in 1:n

integer(kind=default_int), intent(in) :: target

Goal vertex, in 1:n

real(kind=dp), intent(in) :: heuristic(:)

Admissible lower bound on the distance from each vertex to target

integer(kind=default_int), intent(out), allocatable :: path(:)

Vertices from source to target, or zero-size if unreachable

type(error_t), intent(out), optional :: err

Calls

proc~~a_star~~CallsGraph proc~a_star a_star proc~check_graph check_graph proc~a_star->proc~check_graph proc~check_weights check_weights proc~a_star->proc~check_weights proc~csr_n_rows csr_t%csr_n_rows proc~a_star->proc~csr_n_rows proc~distance_key distance_key proc~a_star->proc~distance_key proc~fail fail proc~a_star->proc~fail proc~heap_destroy heap_t%heap_destroy proc~a_star->proc~heap_destroy proc~heap_init heap_t%heap_init proc~a_star->proc~heap_init proc~heap_is_empty heap_t%heap_is_empty proc~a_star->proc~heap_is_empty proc~heap_pop heap_t%heap_pop proc~a_star->proc~heap_pop proc~heap_push heap_t%heap_push proc~a_star->proc~heap_push proc~trace_back trace_back proc~a_star->proc~trace_back proc~check_graph->proc~csr_n_rows proc~check_graph->proc~fail proc~csr_is_valid csr_t%csr_is_valid proc~check_graph->proc~csr_is_valid proc~csr_n_cols csr_t%csr_n_cols proc~check_graph->proc~csr_n_cols proc~check_weights->proc~fail proc~csr_nnz csr_t%csr_nnz proc~check_weights->proc~csr_nnz proc~error_set error_t%error_set proc~fail->proc~error_set proc~heap_reserve heap_t%heap_reserve proc~heap_init->proc~heap_reserve proc~root_entry root_entry proc~heap_pop->proc~root_entry proc~sift_down sift_down proc~heap_pop->proc~sift_down proc~heap_capacity heap_t%heap_capacity proc~heap_push->proc~heap_capacity proc~heap_push->proc~heap_reserve proc~sift_up sift_up proc~heap_push->proc~sift_up proc~fail~3 fail proc~csr_is_valid->proc~fail~3 proc~heap_reserve->proc~heap_capacity proc~grow_int32 grow_int32 proc~heap_reserve->proc~grow_int32 proc~grow_int64 grow_int64 proc~heap_reserve->proc~grow_int64 proc~root_entry->proc~error_set proc~precedes precedes proc~sift_down->proc~precedes proc~swap_entries swap_entries proc~sift_down->proc~swap_entries proc~sift_up->proc~precedes proc~sift_up->proc~swap_entries proc~fail~3->proc~error_set

Variables

Type Visibility Attributes Name Initial
real(kind=dp), private :: candidate
real(kind=dp), private, allocatable :: cost_so_far(:)
real(kind=dp), private :: cost_u
integer(kind=default_int), private :: k
integer(kind=int64), private :: key
integer(kind=default_int), private :: n
integer(kind=int32), private :: payload
integer(kind=default_int), private, allocatable :: prev(:)
type(heap_t), private :: queue
logical, private :: reached
logical, private, allocatable :: settled(:)
integer(kind=default_int), private :: u
integer(kind=default_int), private :: v

Source Code

   subroutine a_star(graph, source, target, heuristic, path, err)
      !! A* search for one shortest path from `source` to `target`.
      !!
      !! `heuristic(v)` is a **precomputed array** of lower bounds on the
      !! remaining distance from `v` to `target`. An array rather than a
      !! procedure argument is a deliberate portability choice: passing
      !! procedures through an abstract interface is the more general design,
      !! but procedure dummies are exactly the corner of the language where
      !! the compilers PIC targets differ most, and a realistic heuristic
      !! (straight-line distance to the target from stored coordinates) is
      !! computed once per search anyway, so generality buys little here.
      !!
      !! The search settles each vertex once and stops as soon as `target`
      !! settles, so the heuristic must be **consistent** (monotone):
      !! `heuristic(u) - heuristic(v) <= w(u, v)` for every edge. Consistency
      !! implies admissibility, and with it A* returns a path of exactly the
      !! same total cost as `dijkstra`. With `heuristic = 0` - trivially
      !! consistent - it degenerates to Dijkstra exactly, expanding vertices
      !! in the same order and returning the same tie-broken path. Both are
      !! asserted in the test suite.
      !!
      !! `path` is allocated to the vertices from `source` to `target`
      !! inclusive, so `path(1) == source` and `path(size(path)) == target`; a
      !! search with `source == target` yields the single-element path
      !! `[source]`. An unreachable target is **not** an error: `path` comes
      !! back zero-size. Reports `ERROR_VALIDATION` for a malformed graph, an
      !! out-of-range `source` or `target`, a `heuristic` whose length is not
      !! the number of vertices or which holds a negative entry, or a negative
      !! edge weight.
      type(csr_t), intent(in) :: graph
         !! Square weighted adjacency matrix of the graph
      integer(default_int), intent(in) :: source
         !! Start vertex, in `1:n`
      integer(default_int), intent(in) :: target
         !! Goal vertex, in `1:n`
      real(dp), intent(in) :: heuristic(:)
         !! Admissible lower bound on the distance from each vertex to `target`
      integer(default_int), allocatable, intent(out) :: path(:)
         !! Vertices from `source` to `target`, or zero-size if unreachable
      type(error_t), intent(out), optional :: err

      type(heap_t) :: queue
      logical, allocatable :: settled(:)
      real(dp), allocatable :: cost_so_far(:)
      integer(default_int), allocatable :: prev(:)
      integer(default_int) :: n, u, v, k
      integer(int64) :: key
      integer(int32) :: payload
      real(dp) :: cost_u, candidate
      logical :: reached

      allocate (path(0))
      if (.not. check_graph(graph, source, err)) return
      if (.not. check_weights(graph, err)) return

      n = graph%n_rows()
      if (target < 1 .or. target > n) then
         call fail(err, "pic_graph: a_star target vertex is outside 1:n")
         return
      end if
      if (int(size(heuristic), default_int) /= n) then
         call fail(err, "pic_graph: a_star heuristic must have one entry per vertex")
         return
      end if
      do k = 1, n
         if (heuristic(k) < 0.0_dp) then
            call fail(err, "pic_graph: a_star heuristic must be non-negative")
            return
         end if
      end do

      allocate (cost_so_far(n))
      allocate (prev(n))
      allocate (settled(n))
      cost_so_far = GRAPH_INFINITY
      prev = GRAPH_NO_PREDECESSOR
      settled = .false.

      cost_so_far(source) = 0.0_dp
      reached = .false.
      call queue%init(n)
      call queue%push(distance_key(heuristic(source)), int(source, int32))

      do while (.not. queue%is_empty())
         call queue%pop(key, payload)
         u = int(payload, default_int)
         if (settled(u)) cycle
         settled(u) = .true.
         if (u == target) then
            reached = .true.
            exit
         end if
         cost_u = cost_so_far(u)
         do k = graph%row_ptr(u), graph%row_ptr(u + 1) - 1
            v = graph%col_idx(k)
            candidate = cost_u + graph%values(k)
            if (candidate < cost_so_far(v)) then
               cost_so_far(v) = candidate
               prev(v) = u
               call queue%push(distance_key(candidate + heuristic(v)), int(v, int32))
            end if
         end do
      end do

      call queue%destroy()
      if (reached) then
         deallocate (path)
         call trace_back(prev, source, target, path)
      end if
   end subroutine a_star