Advertisement
Guest User

Untitled

a guest
Sep 29th, 2022
91
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.86 KB | Source Code | 0 0
  1. -- Sourced from: https://github.com/tdepke2/OpenComputersProjects/blob/master/tminer/dig_test.lua
  2. -- Demo of A* algorithm for pathfinding in a 3D scalar grid.
  3.  
  4. -- Priority queue using a binary heap.
  5. local dstructs = {}
  6. dstructs.PriorityQueue = {}
  7.  
  8. local function heapify(arr, comp, length, i)
  9.   local bestIndex = i
  10.   local best = arr[i]
  11.  
  12.   if i * 2 <= length and comp(best, arr[i * 2]) then
  13.     bestIndex = i * 2
  14.     best = arr[bestIndex]
  15.   end
  16.   if i * 2 + 1 <= length and comp(best, arr[i * 2 + 1]) then
  17.     bestIndex = i * 2 + 1
  18.     best = arr[bestIndex]
  19.   end
  20.   if bestIndex ~= i then
  21.     arr[bestIndex] = arr[i]
  22.     arr[i] = best
  23.     return heapify(arr, comp, length, bestIndex)
  24.   end
  25. end
  26.  
  27. --- dstructs.PriorityQueue:new([arr: table[, comp: function]]): table
  28. -- Construction from an array takes O(n) time.
  29. -- The comp is less-than by default (max heap).
  30. function dstructs.PriorityQueue:new(arr, comp)
  31.   self.__index = self
  32.   self = setmetatable({}, self)
  33.  
  34.   self.comp = comp or function(a, b)
  35.     return a < b
  36.   end
  37.   if arr == nil then
  38.     self.arr = {}
  39.     self.length = 0
  40.   else
  41.     self.arr = arr
  42.     self.length = #arr
  43.     for i = math.floor(self.length / 2), 1, -1 do
  44.       heapify(self.arr, self.comp, self.length, i)
  45.     end
  46.   end
  47.  
  48.   return self
  49. end
  50.  
  51. --- dstructs.PriorityQueue:empty(): boolean
  52. function dstructs.PriorityQueue:empty()
  53.   return self.length == 0
  54. end
  55.  
  56. --- dstructs.PriorityQueue:size(): number
  57. function dstructs.PriorityQueue:size()
  58.   return self.length
  59. end
  60.  
  61. --- dstructs.PriorityQueue:top(): any
  62. function dstructs.PriorityQueue:top()
  63.   return self.arr[1]
  64. end
  65.  
  66. --- dstructs.PriorityQueue:push(val: any)
  67. -- Uses heapify-up. Time complexity is O(log n)
  68. function dstructs.PriorityQueue:push(val)
  69.   self.length = self.length + 1
  70.   self.arr[self.length] = val
  71.  
  72.   local i = self.length
  73.   local parent = math.floor(i / 2)
  74.   while i > 1 and self.comp(self.arr[parent], self.arr[i]) do
  75.     self.arr[i], self.arr[parent] = self.arr[parent], self.arr[i]
  76.     i = parent
  77.     parent = math.floor(i / 2)
  78.   end
  79. end
  80.  
  81. --- dstructs.PriorityQueue:pop(): any
  82. -- Uses heapify-down. Time complexity is O(log n)
  83. function dstructs.PriorityQueue:pop()
  84.   local topElement = self.arr[1]
  85.   self.arr[1] = self.arr[self.length]
  86.   self.arr[self.length] = nil
  87.   self.length = self.length - 1
  88.  
  89.   heapify(self.arr, self.comp, self.length, 1)
  90.   return topElement
  91. end
  92.  
  93.  
  94. local reprioritizeCounter = 0
  95. local reprioritizeCounterFalse = 0
  96. local reprioritizeCounterAvgIndex = 0
  97.  
  98.  
  99. --- dstructs.PriorityQueue:updateKey(oldVal: any, newVal: any[, compResult: boolean]): boolean
  100. -- Time complexity is O(n)
  101. function dstructs.PriorityQueue:updateKey(oldVal, newVal, compResult)
  102.   reprioritizeCounter = reprioritizeCounter + 1
  103.   compResult = compResult or self.comp(oldVal, newVal)
  104.   local i = 1
  105.   while i <= self.length and self.arr[i] ~= oldVal do
  106.     i = i + 1
  107.   end
  108.   reprioritizeCounterAvgIndex = reprioritizeCounterAvgIndex + i
  109.   if i > self.length then
  110.     --print("updateKey() false")
  111.     reprioritizeCounterFalse = reprioritizeCounterFalse + 1
  112.     return false
  113.   end
  114.  
  115.   self.arr[i] = newVal
  116.   if compResult then
  117.     --print("updateKey() heap-up")
  118.     local parent = math.floor(i / 2)
  119.     while i > 1 and self.comp(self.arr[parent], self.arr[i]) do
  120.       self.arr[i], self.arr[parent] = self.arr[parent], self.arr[i]
  121.       i = parent
  122.       parent = math.floor(i / 2)
  123.     end
  124.   else
  125.     --print("updateKey() heap-down")
  126.     heapify(self.arr, self.comp, self.length, i)
  127.   end
  128.   return true
  129. end
  130.  
  131.  
  132.  
  133.  
  134. local checkNeighborCounter = 0
  135.  
  136. local findPathHelper
  137.  
  138. -- findPath(grid: table, start: table, stop: table[, turnCost: number]): number,
  139. --   table, number
  140. --
  141. -- Computes the lowest-cost optimal path between two points in a 3-dimensional
  142. -- scalar field. The scalar field (grid) is a 1-indexed table sequence of
  143. -- numbers where the indices increase in the positive x, positive z, then
  144. -- positive y direction (same as the geolyzer scan format). The size of the grid
  145. -- is defined by grid.xSize, grid.ySize, and grid.zSize where coordinates go
  146. -- from 0 to grid.<n>Size - 1. The start and stop values are 3D coordinates in
  147. -- the grid, both are tables with x, y, and z in indices 1, 2, and 3
  148. -- respectively. Scalar values in the grid represent the cost to pass through
  149. -- that point, the special value -1 indicates a barrier that cannot be passed
  150. -- through.
  151. --
  152. -- The turnCost is an optional argument that specifies the unit path cost when
  153. -- moving in a new direction. This is used to bias straight paths in the
  154. -- solution when there are multiple optimal paths. By setting this to 1, the
  155. -- bias is disabled (and a path through an area with uniform scalar values may
  156. -- take seemingly random turns). Setting this above 1 can make straight paths
  157. -- take priority over optimal ones, which has the potential to reduce the path
  158. -- length in the solution.
  159. --
  160. -- Returns the computed path cost, table sequence of indices in the grid along
  161. -- the path (in reverse order), and the length of this sequence. If no solution
  162. -- exists (the goal is blocked behind barriers), then math.huge is returned for
  163. -- the path cost.
  164. --
  165. -- This algorithm uses a modified version of A* search to add bias for
  166. -- straight-line paths. See below for references:
  167. -- https://en.wikipedia.org/wiki/A*_search_algorithm
  168. -- https://www.redblobgames.com/pathfinding/a-star/implementation.html
  169. local function findPath(grid, start, stop, turnCost)
  170.   -- The heuristic used here calculates the Manhattan distance between the target and stop node.
  171.   local function pathCostHeuristicSum(pathCost, x, y, z)
  172.     return pathCost + math.abs(x - stop[1]) + math.abs(y - stop[2]) + math.abs(z - stop[3])
  173.   end
  174.  
  175.   return findPathHelper(grid, start, stop, turnCost, pathCostHeuristicSum)
  176. end
  177.  
  178.  
  179. -- findPathWeighted(grid: table, start: table, stop: table[, turnCost: number,
  180. --   suboptimalityBound: number]): number, table, number
  181. --
  182. -- Variation of findPath(). This uses Weighted A* to find a suboptimal path
  183. -- between the start and stop points. The trade-off is reduced exploration of
  184. -- nodes in the grid which therefore increases speed of the calculation. If the
  185. -- suboptimalityBound is provided, it should be a value greater than 1. This
  186. -- value is multiplied with the heuristic result to make it non-admissible (it
  187. -- overestimates the distance to the goal). As the suboptimalityBound approaches
  188. -- infinity, the search turns into a greedy best-first search.
  189. --
  190. -- See here for details and more variations:
  191. -- http://theory.stanford.edu/~amitp/GameProgramming/Variations.html
  192. local function findPathWeighted(grid, start, stop, turnCost, suboptimalityBound)
  193.   suboptimalityBound = suboptimalityBound or 1.5
  194.   local function pathCostHeuristicSum(pathCost, x, y, z)
  195.     return pathCost + (math.abs(x - stop[1]) + math.abs(y - stop[2]) + math.abs(z - stop[3])) * suboptimalityBound
  196.   end
  197.  
  198.   return findPathHelper(grid, start, stop, turnCost, pathCostHeuristicSum)
  199. end
  200.  
  201.  
  202. -- findPathPWXD(grid: table, start: table, stop: table[, turnCost: number,
  203. --   suboptimalityBound: number]): number, table, number
  204. --
  205. -- Another variation of findPath(). This uses Weighted A* (piecewise Convex
  206. -- Downward) to find a suboptimal path between the start and stop points. This
  207. -- often gives better speed over Weighted A* because the scaling of the path
  208. -- cost is applied as we get closer to the goal. If the suboptimalityBound is
  209. -- provided, it should be a value greater than 1.
  210. --
  211. -- See here for demonstration and implementation details:
  212. -- https://www.movingai.com/SAS/SUB/
  213. -- https://webdocs.cs.ualberta.ca/~nathanst/papers/chen2021general.pdf
  214. local function findPathPWXD(grid, start, stop, turnCost, suboptimalityBound)
  215.   suboptimalityBound = suboptimalityBound or 1.5
  216.   local function pathCostHeuristicSum(pathCost, x, y, z)
  217.     local h = math.abs(x - stop[1]) + math.abs(y - stop[2]) + math.abs(z - stop[3])
  218.     if h > pathCost then
  219.       return pathCost + h
  220.     else
  221.       return (pathCost + (2 * suboptimalityBound - 1) * h) / suboptimalityBound
  222.     end
  223.   end
  224.  
  225.   return findPathHelper(grid, start, stop, turnCost, pathCostHeuristicSum)
  226. end
  227.  
  228.  
  229. -- Helper function for findPath() and variations.
  230. findPathHelper = function(grid, start, stop, turnCost, pathCostHeuristicSum)
  231.   turnCost = turnCost or 1.0001
  232.  
  233.   -- Find indices of the start and stop positions in the grid.
  234.   local startNode = (start[2] * grid.zSize + start[3]) * grid.xSize + start[1] + 1
  235.   local stopNode = (stop[2] * grid.zSize + stop[3]) * grid.xSize + stop[1] + 1
  236.  
  237.   -- For node n, cameFrom[n] is the node immediately preceding it on the cheapest path to get to n.
  238.   local cameFrom = {}
  239.  
  240.   -- For node n, pathCosts[n] is the cost of the cheapest path from start to n.
  241.   local pathCosts = setmetatable({[startNode] = 0}, {
  242.     __index = function()
  243.       return math.huge
  244.     end
  245.   })
  246.  
  247.   -- For node n, nodePriorities[n] is the current best guess of the total path cost to get to the goal when going through n.
  248.   local nodePriorities = setmetatable({[startNode] = 0}, {
  249.     __index = function()
  250.       return math.huge
  251.     end
  252.   })
  253.  
  254.   -- Nodes that have been discovered and need to be expanded. The priority queue keeps the node with the smallest estimated total path cost at the front.
  255.   -- A node that is removed from the queue may be added again later, or a node may change priority from within the queue.
  256.   local fringeNodes = dstructs.PriorityQueue:new({startNode}, function(a, b) return nodePriorities[a] > nodePriorities[b] end)
  257.  
  258.   -- Iterates from stopNode to startNode following the trail left in cameFrom.
  259.   -- The solution (which is the reversed path) is then returned.
  260.   local function reconstructPath()
  261.     local pathReversed, pathReversedSize = {}, 0
  262.     local node = stopNode
  263.     while node do
  264.       pathReversedSize = pathReversedSize + 1
  265.       pathReversed[pathReversedSize] = node
  266.       node = cameFrom[node]
  267.     end
  268.     return pathCosts[stopNode], pathReversed, pathReversedSize
  269.   end
  270.  
  271.   -- Checks a node adjacent to current to see if it corresponds to an unexplored node, or if it improves the path cost of an existing one.
  272.   local function checkNeighbor(current, x, y, z, baseMovementCost)
  273.     local neighbor = (y * grid.zSize + z) * grid.xSize + x + 1
  274.     -- Return early if a barrier is found, or the neighbor is in the reverse direction.
  275.     if grid[neighbor] == -1 or neighbor == cameFrom[current] then
  276.       return
  277.     end
  278.     checkNeighborCounter = checkNeighborCounter + 1
  279.     local oldPathCost = pathCosts[neighbor]
  280.     local newPathCost = pathCosts[current] + baseMovementCost + grid[neighbor]
  281.     --io.write("  checkNeighbor(" .. current .. ", " .. x .. ", " .. y .. ", " .. z .. "), newPathCost = " .. newPathCost .. ", oldPathCost = " .. pathCosts[neighbor])
  282.     if newPathCost < oldPathCost then
  283.       --io.write(" (greater)\n")
  284.       --if oldPathCost ~= math.huge then
  285.         --io.write("score was increased for existing node!\n")
  286.       --end
  287.       cameFrom[neighbor] = current
  288.       pathCosts[neighbor] = newPathCost
  289.       nodePriorities[neighbor] = pathCostHeuristicSum(newPathCost, x, y, z)
  290.      
  291.       -- The neighbor is added to fringe if it hasn't been found before, or it has and is not currently in the fringe.
  292.       -- Another way to do this is to always add it (allow duplicates), but this doesn't work so well when we store the priorities outside of the queue (the heap can get invalidated).
  293.       if oldPathCost == math.huge or not fringeNodes:updateKey(neighbor, neighbor, true) then
  294.         fringeNodes:push(neighbor)
  295.       end
  296.     --else
  297.       --io.write(" (less)\n")
  298.     end
  299.   end
  300.  
  301.   while not fringeNodes:empty() do
  302.     -- Get node at the front of queue (smallest total estimated cost) and explore the neighbor nodes if it's not the goal.
  303.     local current = fringeNodes:pop()
  304.     if current == stopNode then
  305.       return reconstructPath()
  306.     end
  307.     --io.write("current = " .. current .. ", (" .. x .. ", " .. y .. ", " .. z .. "), g = " .. pathCosts[current] .. ", f = " .. nodePriorities[current] .. "\n")
  308.    
  309.     -- Prefer straight line paths by adding a small path cost when turning.
  310.     local xMovementCost, yMovementCost, zMovementCost = turnCost, 1, turnCost
  311.     if cameFrom[current] then
  312.       if math.abs(current - cameFrom[current]) == 1 then
  313.         xMovementCost = 1
  314.         yMovementCost = turnCost
  315.       elseif math.abs(current - cameFrom[current]) == grid.xSize then
  316.         yMovementCost = turnCost
  317.         zMovementCost = 1
  318.       end
  319.     end
  320.    
  321.     local divisor = (current - 1) / grid.xSize
  322.     local x = (current - 1) % grid.xSize
  323.     local z = math.floor(divisor) % grid.zSize
  324.     local y = math.floor(divisor / grid.zSize)
  325.    
  326.     if x < grid.xSize - 1 then
  327.       checkNeighbor(current, x + 1, y, z, xMovementCost)
  328.     end
  329.     if x > 0 then
  330.       checkNeighbor(current, x - 1, y, z, xMovementCost)
  331.     end
  332.     if y < grid.ySize - 1 then
  333.       checkNeighbor(current, x, y + 1, z, yMovementCost)
  334.     end
  335.     if y > 0 then
  336.       checkNeighbor(current, x, y - 1, z, yMovementCost)
  337.     end
  338.     if z < grid.zSize - 1 then
  339.       checkNeighbor(current, x, y, z + 1, zMovementCost)
  340.     end
  341.     if z > 0 then
  342.       checkNeighbor(current, x, y, z - 1, zMovementCost)
  343.     end
  344.   end
  345.   return math.huge, {}, 0
  346. end
  347.  
  348. -- Draw the layers of the 3D grid for visualization purposes.
  349. local function printGrid(grid, paths)
  350.   io.write("\ngrid.xSize = ", tostring(grid.xSize), ", grid.ySize = ", tostring(grid.ySize), ", grid.zSize = ", tostring(grid.zSize), "\n\n")
  351.   local i = 1
  352.   for y = 0, grid.ySize - 1 do
  353.     print("layer y = " .. y)
  354.     for z = 0, grid.zSize - 1 do
  355.       for x = 0, grid.xSize - 1 do
  356.         if grid[i] == -1 then
  357.           io.write("██")
  358.         else
  359.           io.write(paths[i] or " ")
  360.           io.write(tostring(grid[i]))
  361.         end
  362.         i = i + 1
  363.       end
  364.       io.write("\n")
  365.     end
  366.     io.write("\n")
  367.   end
  368.   io.write("\n")
  369. end
  370.  
  371.  
  372.  
  373.  
  374. -- Create the 3D grid and fill it with random weights and barriers (the value -1 marks a barrier).
  375. local grid = {}
  376. grid.xSize = 32
  377. grid.ySize = 4
  378. grid.zSize = 16
  379. local paths = {}
  380. math.randomseed(456)
  381. for i = 1, grid.xSize * grid.ySize * grid.zSize do
  382.   local num = math.random()
  383.   grid[i] = num < 0.1 and -1 or math.random(0, 5)
  384. end
  385.  
  386. -- Define start/stop points and other parameters for the search.
  387. local start = {0, 0, 0}
  388. local stop = {grid.xSize-1, grid.ySize-1, grid.zSize-1}
  389. local w = 1.5
  390. print("finding path from (" .. start[1] .. ", " .. start[2] .. ", " .. start[3] .. ") to (" .. stop[1] .. ", " .. stop[2] .. ", " .. stop[3] .. ")")
  391.  
  392. -- Find the path.
  393. local pathCost, pathReversed, pathReversedSize = findPath(grid, start, stop)
  394. --local pathCost, pathReversed, pathReversedSize = findPathWeighted(grid, start, stop, 1.0001, w)
  395. --local pathCost, pathReversed, pathReversedSize = findPathPWXD(grid, start, stop, 1.0001, w)
  396.  
  397. print("pathCost = ", pathCost)
  398. if pathCost ~= math.huge then
  399.   io.write("pathReversed (indices in grid):\n")
  400.   for i, node in ipairs(pathReversed) do
  401.     io.write(node .. "  ")
  402.     assert(grid[node] >= 0 and grid[node] < 10 and not paths[node])
  403.     paths[node] = "#"
  404.   end
  405.   assert(paths[pathReversed[pathReversedSize]] == "#" and paths[pathReversed[1]] == "#")
  406.   paths[pathReversed[pathReversedSize]] = "A"
  407.   paths[pathReversed[1]] = "B"
  408.   io.write("\n")
  409. else
  410.   paths[(start[2] * grid.zSize + start[3]) * grid.xSize + start[1] + 1] = "A"
  411.   paths[(stop[2] * grid.zSize + stop[3]) * grid.xSize + stop[1] + 1] = "B"
  412.   print("no path!\n")
  413. end
  414. -- Debugging info:
  415. --reprioritizeCounterAvgIndex = reprioritizeCounterAvgIndex / reprioritizeCounter
  416. --print("reprioritizeCounter = " .. reprioritizeCounter .. ", reprioritizeCounterFalse = " .. reprioritizeCounterFalse .. ", reprioritizeCounterAvgIndex = " .. reprioritizeCounterAvgIndex)
  417. --print("checkNeighborCounter = " .. checkNeighborCounter .. "\n")
  418.  
  419. printGrid(grid, paths)
  420.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement