Skip to content

Commit 35b1d3b

Browse files
authored
Merge pull request #52 from APLA-Toolbox/add-astar
Implement A*
2 parents 0f360fe + 324999e commit 35b1d3b

14 files changed

+216
-168
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ jobs:
2323
- name: Install Python dependencies
2424
run: |
2525
python -m pip install --upgrade pip
26-
python -m pip install julia
27-
python -m pip install pycall
26+
python -m pip install -r requirements.txt
2827
- name: Lint with flake8
2928
run: |
3029
python -m pip install flake8

.github/workflows/tests.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,12 @@ jobs:
4747
version: '1.4.1'
4848
- name: Install Julia dependencies
4949
run: |
50-
julia --color=yes -e 'using Pkg; Pkg.add(Pkg.PackageSpec(path="https://github.com/APLA-Toolbox/PDDL.jl"))'
5150
julia --color=yes -e 'using Pkg; Pkg.add(Pkg.PackageSpec(path="https://github.com/JuliaPy/PyCall.jl"))'
51+
julia --color=yes -e 'using Pkg; Pkg.add(Pkg.PackageSpec(path="https://github.com/APLA-Toolbox/PDDL.jl"))'
5252
- name: Install Python dependencies
5353
run: |
5454
python -m pip install --upgrade pip
55-
python -m pip install julia
56-
python -m pip install pycall
55+
python -m pip install -r requirements.txt
5756
- name: Lint with flake8
5857
run: |
5958
python -m pip install flake8

.mergify.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
pull_request_rules:
22
- name: Assign the main reviewers
33
conditions:
4-
- check-success=tests
54
- check-success=build
65
- check-success=CodeFactor
76
actions:
87
request_reviews:
9-
teams:
10-
- "@APLA-Toolbox/reviewers"
8+
users:
9+
- guilyx
10+
- sampreets3
1111
- name: Automatic merge on approval
1212
conditions:
1313
- "#approved-reviews-by>=1"

main.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import src.automated_planner as parser
1+
from src.automated_planner import AutomatedPlanner
22
import argparse
3-
import logging
43
import os
54

65

@@ -13,22 +12,17 @@ def main():
1312
args_parser.add_argument("problem", type=str, help="PDDL problem file")
1413
args_parser.add_argument("-v", "--verbose", help="Increases the output's verbosity")
1514
args = args_parser.parse_args()
16-
logging.basicConfig(
17-
filename="logs/main.log",
18-
format="%(levelname)s:%(message)s",
19-
filemode="w",
20-
level=logging.INFO,
21-
) # Creates the log file
22-
apla_tbx = parser.AutomatedPlanner(args.domain, args.problem)
23-
logging.info("Starting the tool")
24-
path, time = apla_tbx.depth_first_search(time_it=True)
25-
logging.info(apla_tbx.get_actions_from_path(path))
26-
logging.info("Computation time: %.2f seconds" % time)
27-
logging.info("Tool finished")
28-
# Output the log (to show something in the output screen)
29-
logfile = open("logs/main.log", "r")
30-
print(logfile.read())
31-
logfile.close()
15+
apla_tbx = AutomatedPlanner(args.domain, args.problem)
16+
apla_tbx.logger.info("Starting the planning script")
17+
apla_tbx.logger.debug(
18+
"Available heuristics: " + str(apla_tbx.available_heuristics.keys())
19+
)
20+
21+
path, computation_time = apla_tbx.dijktra_best_first_search(time_it=True)
22+
apla_tbx.logger.debug(apla_tbx.get_actions_from_path(path))
23+
24+
apla_tbx.logger.debug("Computation time: %.2f seconds" % computation_time)
25+
apla_tbx.logger.info("Terminate with grace...")
3226

3327

3428
if __name__ == "__main__":

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
julia==0.5.6
2+
coloredlogs==15.0

src/a_star.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from .heuristics import zero_heuristic
2+
from .node import Node
3+
import logging
4+
import math
5+
from time import time as now
6+
from datetime import datetime as timestamp
7+
8+
9+
class AStarBestFirstSearch:
10+
def __init__(self, automated_planner, heuristic_function):
11+
self.automated_planner = automated_planner
12+
self.init = Node(
13+
self.automated_planner.initial_state,
14+
automated_planner,
15+
is_closed=False,
16+
is_open=True,
17+
heuristic=heuristic_function,
18+
)
19+
self.heuristic_function = heuristic_function
20+
self.open_nodes_n = 1
21+
self.nodes = dict()
22+
self.nodes[self.__hash(self.init)] = self.init
23+
24+
def __hash(self, node):
25+
sep = ", Dict{Symbol,Any}"
26+
string = str(node.state)
27+
return string.split(sep, 1)[0] + ")"
28+
29+
def search(self):
30+
time_start = now()
31+
self.automated_planner.logger.debug(
32+
"Search started at: " + str(timestamp.now())
33+
)
34+
while self.open_nodes_n > 0:
35+
current_key = min(
36+
[n for n in self.nodes if self.nodes[n].is_open],
37+
key=(lambda k: self.nodes[k].f_cost),
38+
)
39+
current_node = self.nodes[current_key]
40+
41+
if self.automated_planner.satisfies(
42+
self.automated_planner.problem.goal, current_node.state
43+
):
44+
computation_time = now() - time_start
45+
self.automated_planner.logger.debug(
46+
"Search finished at: " + str(timestamp.now())
47+
)
48+
return current_node, computation_time
49+
50+
current_node.is_closed = True
51+
current_node.is_open = False
52+
self.open_nodes_n -= 1
53+
54+
actions = self.automated_planner.available_actions(current_node.state)
55+
for act in actions:
56+
child = Node(
57+
state=self.automated_planner.transition(current_node.state, act),
58+
automated_planner=self.automated_planner,
59+
parent_action=act,
60+
parent=current_node,
61+
heuristic=self.heuristic_function,
62+
is_closed=False,
63+
is_open=True,
64+
)
65+
child_hash = self.__hash(child)
66+
if child_hash in self.nodes:
67+
if self.nodes[child_hash].is_closed:
68+
continue
69+
if not self.nodes[child_hash].is_open:
70+
self.nodes[child_hash] = child
71+
self.open_nodes_n += 1
72+
else:
73+
if child.g_cost < self.nodes[child_hash].g_cost:
74+
self.nodes[child_hash] = child
75+
self.open_nodes_n += 1
76+
77+
else:
78+
self.nodes[child_hash] = child
79+
self.open_nodes_n += 1
80+
computation_time = now() - time_start
81+
self.automated_planner.logger.warning("!!! No path found !!!")
82+
return None, computation_time

src/astar.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/automated_planner.py

Lines changed: 34 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,58 @@
1-
from .modules import loading_bar_handler
21
from .bfs import BreadthFirstSearch
32
from .dfs import DepthFirstSearch
43
from .dijkstra import DijkstraBestFirstSearch
5-
import logging
6-
7-
UI = False
8-
9-
if UI:
10-
loading_bar_handler(False)
4+
from .a_star import AStarBestFirstSearch
5+
from .heuristics import goal_count_heuristic, zero_heuristic
6+
import coloredlogs, logging
117
import julia
128

13-
_ = julia.Julia(compiled_modules=False)
14-
15-
if UI:
16-
loading_bar_handler(True)
17-
9+
_ = julia.Julia(compiled_modules=False, debug=False)
1810
from julia import PDDL
1911
from time import time as now
2012

13+
logging.getLogger("julia").setLevel(logging.WARNING)
14+
2115

2216
class AutomatedPlanner:
23-
def __init__(self, domain_path, problem_path):
17+
def __init__(self, domain_path, problem_path, log_level="DEBUG"):
18+
# Planning Tool
2419
self.pddl = PDDL
2520
self.domain = self.pddl.load_domain(domain_path)
2621
self.problem = self.pddl.load_problem(problem_path)
2722
self.initial_state = self.pddl.initialize(self.problem)
2823
self.goals = self.__flatten_goal()
29-
30-
"""
31-
Transition from one state to the next using an action
32-
"""
24+
self.available_heuristics = dict()
25+
self.available_heuristics["goal_count"] = goal_count_heuristic
26+
self.available_heuristics["zero"] = zero_heuristic
27+
28+
# Logging
29+
logging.basicConfig(
30+
filename="logs/main.log",
31+
format="%(levelname)s:%(message)s",
32+
filemode="w",
33+
level=log_level,
34+
) # Creates the log file
35+
self.logger = logging.getLogger("automated_planning")
36+
coloredlogs.install(level=log_level)
3337

3438
def transition(self, state, action):
3539
return self.pddl.transition(self.domain, state, action, check=False)
3640

37-
"""
38-
Returns all available actions from the given state
39-
"""
40-
4141
def available_actions(self, state):
4242
return self.pddl.available(state, self.domain)
4343

44-
"""
45-
Check if a vector of terms is satisfied by the given state
46-
"""
47-
4844
def satisfies(self, asserted_state, state):
4945
return self.pddl.satisfy(asserted_state, state, self.domain)[0]
5046

51-
"""
52-
Check if the term is satisfied by the state
53-
To do: compare if it's faster to compute the check on a vector of terms in julia or python
54-
"""
55-
5647
def state_has_term(self, state, term):
5748
if self.pddl.has_term_in_state(self.domain, state, term):
5849
return True
5950
else:
6051
return False
6152

62-
"""
63-
Flatten the goal to a vector of terms
64-
To do: check if we can iterate over the jl vector
65-
"""
66-
6753
def __flatten_goal(self):
6854
return self.pddl.flatten_goal(self.problem)
6955

70-
"""
71-
Retrieves the linked list path
72-
"""
73-
7456
def __retrace_path(self, node):
7557
if not node:
7658
return []
@@ -81,13 +63,9 @@ def __retrace_path(self, node):
8163
path.reverse()
8264
return path
8365

84-
"""
85-
Returns all the actions operated to reach the goal
86-
"""
87-
8866
def get_actions_from_path(self, path):
8967
if not path:
90-
logging.warning("Path is empty, can't operate...")
68+
self.logger.warning("Path is empty, can't operate...")
9169
return []
9270
actions = []
9371
for node in path:
@@ -99,64 +77,45 @@ def get_actions_from_path(self, path):
9977
else:
10078
return (actions, cost)
10179

102-
"""
103-
Returns all the states that should be opened from start to goal
104-
"""
105-
10680
def get_state_def_from_path(self, path):
10781
if not path:
108-
logging.warning("Path is empty, can't operate...")
82+
self.logger.warning("Path is empty, can't operate...")
10983
return []
11084
trimmed_path = []
11185
for node in path:
11286
trimmed_path.append(node.state)
11387
return trimmed_path
11488

115-
"""
116-
Runs the BFS algorithm on the loaded domain/problem
117-
"""
118-
11989
def breadth_first_search(self, time_it=False):
120-
if time_it:
121-
start_time = now()
12290
bfs = BreadthFirstSearch(self)
123-
last_node = bfs.search()
124-
if time_it:
125-
total_time = now() - start_time
91+
last_node, total_time = bfs.search()
12692
path = self.__retrace_path(last_node)
12793
if time_it:
12894
return path, total_time
12995
else:
13096
return path, None
13197

132-
"""
133-
Runs the DFS algorithm on the domain/problem
134-
"""
135-
13698
def depth_first_search(self, time_it=False):
137-
if time_it:
138-
start_time = now()
13999
dfs = DepthFirstSearch(self)
140-
last_node = dfs.search()
141-
if time_it:
142-
total_time = now() - start_time
100+
last_node, total_time = dfs.search()
143101
path = self.__retrace_path(last_node)
144102
if time_it:
145103
return path, total_time
146104
else:
147105
return path, None
148106

149-
"""
150-
Runs the Dijkstra algorithm on the domain/problem
151-
"""
152-
153107
def dijktra_best_first_search(self, time_it=False):
154-
if time_it:
155-
start_time = now()
156108
dijkstra = DijkstraBestFirstSearch(self)
157-
last_node = dijkstra.search()
109+
last_node, total_time = dijkstra.search()
110+
path = self.__retrace_path(last_node)
158111
if time_it:
159-
total_time = now() - start_time
112+
return path, total_time
113+
else:
114+
return path, None
115+
116+
def astar_best_first_search(self, time_it=False, heuristic=goal_count_heuristic):
117+
astar = AStarBestFirstSearch(self, heuristic)
118+
last_node, total_time = astar.search()
160119
path = self.__retrace_path(last_node)
161120
if time_it:
162121
return path, total_time

0 commit comments

Comments
 (0)