bfsearch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/bfsearch.rb +138 -0
  3. data/test/test_bfsearch.rb +97 -0
  4. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 129bb56223f1aa129ca1abefda4b85b23da47c47
4
+ data.tar.gz: a65004653985341496fc91c1cd440657ebcb230f
5
+ SHA512:
6
+ metadata.gz: e0703db610278237f10eae69ff6b073bebc898127209c5e59939ce165f85568b18f2d40e63f21c1e7f97581da731ea04caa3e5d410397124e7f67445abd50b92
7
+ data.tar.gz: b8dda051b7b5834263486fdc87d6b834bd393b5644361796687d0fc5450b43f5cb05a686cbdf20560392400eaeb1a1855bd3f5ae5769caf5bc05de1798753a2e
data/lib/bfsearch.rb ADDED
@@ -0,0 +1,138 @@
1
+ # IDAstar --- A pure Ruby implementation of the IDA* search algorithm
2
+ # Copyright (C) 2016 Eric MSP Veith <eveith+idastar-ruby@veith-m.de>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ ##
18
+ # This class implements the Best-First Search algorithm.
19
+ #
20
+ # The Best-First Search algorithm (BFS) tries to find a valid path from a
21
+ # starting node to a destination node. It will always find a path if one
22
+ # exists.
23
+ #
24
+ # The typical usage is to call BFsearch.find_path.
25
+ module BFsearch
26
+ Entry = Struct.new(:node, :parent, :distance)
27
+
28
+ ##
29
+ # Finds a way from +root_node+ to +destination_node+ and returns the path
30
+ # node by node.
31
+ #
32
+ # This method implements the Best-First Search algorithm. It starts at
33
+ # +root_node+, trying to find a way to the +destination+.
34
+ #
35
+ # The distance between two nodes is measured using a function object passed
36
+ # as the +distance_function+ parameter. It is called as:
37
+ #
38
+ # distance_function.call(node_1, node_2) # => Float
39
+ #
40
+ # Whenever the algorithm advances from the current node, it needs to know of
41
+ # the node's neighbors. For this, the foruth parameter,
42
+ # +neighbors_function+, is required. It is also assumed to be a lambda
43
+ # object or any object that responds to
44
+ # +neighbors_function#call(current_node)+ and returns a list of adjacent
45
+ # nodes.
46
+ #
47
+ # neighbors_function(node) # => Array
48
+ #
49
+ # BFS is an informed search algorithm, i.e., it uses an heuristic to select
50
+ # the probably best next node at any given state. This heuristic is
51
+ # implemented using a Lambda and the third parameter,
52
+ # +heuristic_function(node)+. The distance function must return an
53
+ # optimistic guess of the distance from the given node to the destination
54
+ # node. 'Optimistic' means that the value returned (a float) must be equal
55
+ # to or lower than the actual distance, but never greater. The air-line
56
+ # distance is a valid implementation of the distance function, since it will
57
+ # always return a value equal to or lower than the actual distance due to
58
+ # the triangle inequality.
59
+ #
60
+ # heuristic_function(node) # => Float
61
+ def self.find_path(root_node, destination,
62
+ distance_function, neighbors_function, heuristic_function)
63
+ root_entry = Entry.new(root_node, nil, 0.0)
64
+ open = [root_entry]
65
+ closed = []
66
+
67
+ until open.empty?
68
+ catch :restart do
69
+ current = open.shift
70
+ return construct_path(root_node, current) if current.node == destination
71
+ closed.push current
72
+
73
+ neighbors_function.call(current.node).each do |node|
74
+ open_entry = open.find { |i| i.node == node }
75
+ closed_entry = closed.find { |i| i.node == node }
76
+ entry = open_entry || closed_entry ||
77
+ Entry.new(node, current, nil)
78
+
79
+ distance_from_start = distance(entry, distance_function)
80
+ entry.distance ||= distance_from_start
81
+
82
+ # Greedy BFS:
83
+
84
+ heuristic_neighbor = heuristic_function.call node
85
+ heuristic_current = heuristic_function.call current.node
86
+ if heuristic_neighbor < heuristic_current
87
+ open.unshift current
88
+ open.unshift entry
89
+ throw :restart
90
+ end
91
+
92
+ if !(open_entry || closed_entry)
93
+ open.push entry
94
+ else
95
+ if distance_from_start < entry.distance
96
+ entry.parent = current
97
+ entry.distance = distance_from_start
98
+ open.push entry unless open_entry
99
+ end
100
+ end
101
+ end
102
+
103
+ open.sort_by(&:distance)
104
+ end
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Constructs and returns an array that contains the best path from the
110
+ # root_node to the destination object.
111
+ def self.construct_path(root_node, destination)
112
+ nodes = [destination[:node]]
113
+
114
+ while destination[:node] != root_node
115
+ destination = destination[:parent]
116
+ nodes.unshift destination[:node]
117
+ end
118
+
119
+ nodes
120
+ end
121
+
122
+ ##
123
+ # Retruns the accumulated distance between two nodes along the currently
124
+ # recorded path
125
+ def self.distance(start, distance_function)
126
+ current = start
127
+ distance = 0.0
128
+
129
+ while current.parent
130
+ distance += distance_function.call current.node, current.parent.node
131
+ current = current.parent
132
+ end
133
+
134
+ distance
135
+ end
136
+
137
+ private_class_method :distance, :construct_path
138
+ end
@@ -0,0 +1,97 @@
1
+ require 'minitest/autorun'
2
+ require 'bfsearch'
3
+
4
+ class BFsearchTest < Minitest::Unit::TestCase
5
+ def test_smallest_tree
6
+ tree = {
7
+ name: :a,
8
+ next: [
9
+ {
10
+ name: :b,
11
+ next: [
12
+ {
13
+ name: :goal,
14
+ next: []
15
+ }
16
+ ]
17
+ },
18
+ {
19
+ name: :c,
20
+ next: []
21
+ }
22
+ ]
23
+ }
24
+
25
+ distance = ->(n, m) { 1.0 }
26
+ heuristic = ->(node) { 1.0 }
27
+ neighbors = ->(node) { node[:next] }
28
+
29
+ path = BFsearch.find_path(tree, tree[:next][0][:next][0],
30
+ distance, neighbors, heuristic)
31
+ assert_equal 3, path.length
32
+ assert_equal :goal, path[-1][:name]
33
+ end
34
+
35
+ def test_parallel_paths
36
+ tree = {
37
+ sb: {
38
+ i: :sb,
39
+ h: 222,
40
+ n: { kl: 70, ka: 145 }
41
+ },
42
+ wu: {
43
+ i: :wu,
44
+ h: 0,
45
+ n: {}
46
+ },
47
+ kl: {
48
+ i: :kl,
49
+ h: 158,
50
+ n: { f: 103, lu: 53 }
51
+ },
52
+ hn: {
53
+ i: :hn,
54
+ h: 87,
55
+ n: { wu: 102 }
56
+ },
57
+ ka: {
58
+ i: :ka,
59
+ h: 140,
60
+ n: { hn: 84 }
61
+ },
62
+ f: {
63
+ i: :f,
64
+ h: 96,
65
+ n: { wu: 116 }
66
+ },
67
+ lu: {
68
+ i: :lu,
69
+ h: 108,
70
+ n: { wu: 183 }
71
+ }
72
+ }
73
+
74
+ distance = ->(n, m) { n[:n][m[:i]] || 1000.0 }
75
+ heuristic = ->(node) { node[:h] }
76
+ neighbors = ->(node) { node[:n].keys.map { |k| tree[k] } }
77
+
78
+ path = BFsearch.find_path(tree[:sb], tree[:wu],
79
+ distance, neighbors, heuristic)
80
+ assert_equal 4, path.length
81
+ assert_equal :sb, path[0][:i]
82
+ assert_equal :kl, path[1][:i]
83
+ assert_equal :f, path[2][:i]
84
+ assert_equal :wu, path[3][:i]
85
+ end
86
+
87
+ def test_map
88
+ map = "########" +
89
+ "# X #" +
90
+ "# #" +
91
+ "# ## #" +
92
+ "# # #" +
93
+ "# #" +
94
+ "# S #" +
95
+ "########"
96
+ end
97
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bfsearch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric MSP Veith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A pure Ruby implementation of gBFS
14
+ email: eveith+bfsearch@veith-m.de
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/bfsearch.rb
20
+ - test/test_bfsearch.rb
21
+ homepage: https://github.com/eveith/bfsearch
22
+ licenses:
23
+ - GPLv3
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.2.2
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: The Greedy Best First Search algorithm
45
+ test_files: []