Skip to content

Commit eb09d7a

Browse files
authored
Merge pull request #179 from cogip/168-planner-rework-planner-using-goap-approach
Planner: Rework Planner using GOAP approach
2 parents 1b8e349 + 2c4ea28 commit eb09d7a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1324
-951
lines changed

cogip/cpp/libraries/shared_memory/binding.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ NB_MODULE(shared_memory, m) {
8585
.def_rw("strategy", &shared_properties_t::strategy, "Strategy ID")
8686
.def_rw("start_position", &shared_properties_t::start_position, "Start position ID")
8787
.def_rw("avoidance_strategy", &shared_properties_t::avoidance_strategy, "Avoidance strategy ID")
88+
.def_rw("goap_depth", &shared_properties_t::goap_depth, "Depth of the GOAP search tree, 0 to disable GOAP")
8889
.def("__repr__", [](const shared_properties_t& properties) {
8990
std::ostringstream oss;
9091
oss << properties;

cogip/cpp/libraries/shared_memory/include/shared_memory/shared_properties.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ typedef struct shared_properties_t {
3535
std::uint8_t strategy; ///< Strategy ID
3636
std::uint8_t start_position; ///< Start position ID
3737
std::uint8_t avoidance_strategy; ///< Avoidance strategy ID
38+
std::uint8_t goap_depth; ///< Depth of the GOAP search tree, 0 to disable GOAP
3839
} shared_properties_t;
3940

4041
/// Overloads the stream insertion operator for `shared_properties_t`.
@@ -58,6 +59,7 @@ inline std::ostream& operator<<(std::ostream& os, const shared_properties_t& dat
5859
<< "strategy=" << static_cast<int>(data.strategy) << ", "
5960
<< "start_position=" << static_cast<int>(data.start_position) << ", "
6061
<< "avoidance_strategy=" << static_cast<int>(data.avoidance_strategy) << ", "
62+
<< "goap_depth=" << static_cast<int>(data.goap_depth) << ", "
6163
<< ")";
6264
return os;
6365
}

cogip/tools/planner/__main__.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
#!/usr/bin/env python3
22
import asyncio
33
import logging
4-
import os
54
from pathlib import Path
65
from typing import Annotated, Optional
76

87
import typer
98
from watchfiles import PythonFilter, run_process
109

1110
from . import logger
12-
from .actions import Strategy
11+
from .actions import StrategyEnum
1312
from .avoidance.avoidance import AvoidanceStrategy
1413
from .planner import Planner
15-
from .positions import StartPosition
1614
from .properties import properties_schema
15+
from .start_positions import StartPositionEnum
1716
from .table import TableEnum
1817

1918

@@ -219,23 +218,23 @@ def main_opt(
219218
),
220219
] = TableEnum.Game.name,
221220
strategy: Annotated[
222-
Strategy,
221+
StrategyEnum,
223222
typer.Option(
224223
"-s",
225224
"--strategy",
226225
help="Default strategy on startup",
227226
envvar="PLANNER_STRATEGY",
228227
),
229-
] = Strategy.TestVisitStartingAreas.name,
228+
] = StrategyEnum.TestVisitStartingAreas.name,
230229
start_position: Annotated[
231-
StartPosition,
230+
StartPositionEnum,
232231
typer.Option(
233232
"-p",
234233
"--start-position",
235234
help="Default start position on startup",
236235
envvar="PLANNER_START_POSITION",
237236
),
238-
] = StartPosition.Bottom.name,
237+
] = StartPositionEnum.Bottom.name,
239238
avoidance_strategy: Annotated[
240239
AvoidanceStrategy,
241240
typer.Option(
@@ -245,6 +244,17 @@ def main_opt(
245244
envvar="PLANNER_AVOIDANCE_STRATEGY",
246245
),
247246
] = AvoidanceStrategy.AvoidanceCpp.name,
247+
goap_depth: Annotated[
248+
int,
249+
typer.Option(
250+
"-gd",
251+
"--goap-depth",
252+
min=properties["goap_depth"]["minimum"],
253+
max=properties["goap_depth"]["maximum"],
254+
help=properties["goap_depth"]["description"],
255+
envvar="PLANNER_GOAP_DEPTH",
256+
),
257+
] = properties["goap_depth"]["default"],
248258
reload: Annotated[
249259
bool,
250260
typer.Option(
@@ -267,9 +277,6 @@ def main_opt(
267277
if debug:
268278
logger.setLevel(logging.DEBUG)
269279

270-
# Make sure robot ID is also available as environment variable for context creation
271-
os.environ["ROBOT_ID"] = str(id)
272-
273280
if not server_url:
274281
server_url = f"http://localhost:809{id}"
275282

@@ -298,6 +305,7 @@ def main_opt(
298305
strategy,
299306
start_position,
300307
avoidance_strategy,
308+
goap_depth,
301309
debug,
302310
)
303311

cogip/tools/planner/actions/__init__.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
from cogip.utils.argenum import ArgEnum
77
from .. import logger
8-
from .actions import Actions
8+
from .strategy import Strategy
99

1010

1111
def strip_action_name(name: str) -> str:
12-
if name.endswith("Actions"):
13-
return name[:-7]
12+
if name.endswith("Strategy"):
13+
return name[:-8]
1414
return name
1515

1616

17-
actions_found = []
17+
strategies_found = []
1818

1919
for path in Path(__file__).parent.glob("*.py"):
2020
if path.name == "__init__.py":
@@ -30,19 +30,19 @@ def strip_action_name(name: str) -> str:
3030
logger.error(
3131
f"Import error in 'cogip/tools/planner/actions/{module_path.name}': "
3232
"Modules from the 'cogip.planner.actions' package cannot use relative import "
33-
"to allow dynamic discovery of Actions classes."
33+
"to allow dynamic discovery of Strategy classes."
3434
)
3535
sys.exit(1)
3636

3737
for name, obj in inspect.getmembers(module, inspect.isclass):
38-
if issubclass(obj, Actions) and obj is not Actions:
39-
actions_found.append(obj)
38+
if issubclass(obj, Strategy) and obj is not Strategy:
39+
strategies_found.append(obj)
4040

41-
sorted_actions = sorted(actions_found, key=lambda cls: cls.__name__)
42-
actions_map = {strip_action_name(strategy.__name__): i + 1 for i, strategy in enumerate(sorted_actions)}
41+
sorted_strategies = sorted(strategies_found, key=lambda cls: cls.__name__)
42+
strategies_map = {strip_action_name(strategy.__name__): i + 1 for i, strategy in enumerate(sorted_strategies)}
4343

44-
Strategy = ArgEnum("Strategy", actions_map)
44+
StrategyEnum = ArgEnum("StrategyEnum", strategies_map)
4545

46-
action_classes: dict[Strategy, Actions] = {
47-
strategy: actions_class for strategy, actions_class in zip(Strategy, sorted_actions)
46+
strategy_classes: dict[StrategyEnum, Strategy] = {
47+
strategy: strategies_class for strategy, strategies_class in zip(StrategyEnum, sorted_strategies)
4848
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import asyncio
2+
import math
3+
from collections.abc import Awaitable, Callable
4+
from typing import TYPE_CHECKING, final
5+
6+
from cogip import models
7+
from cogip.tools.planner import logger
8+
from cogip.tools.planner.pose import Pose
9+
10+
if TYPE_CHECKING:
11+
from ..planner import Planner
12+
from .strategy import Strategy
13+
14+
15+
class Action:
16+
"""
17+
This class represents an action of the game.
18+
It contains a list of Pose to reach in order.
19+
A function can be executed before the action starts and after it ends.
20+
"""
21+
22+
logger = logger
23+
24+
def __init__(self, name: str, planner: "Planner", strategy: "Strategy", interruptable: bool = True):
25+
self.name = name
26+
self.planner = planner
27+
self.strategy = strategy
28+
self.interruptable = interruptable
29+
self.poses: list[Pose] = []
30+
self.before_action_func: Callable[[], Awaitable[None]] | None = None
31+
self.after_action_func: Callable[[], Awaitable[None]] | None = None
32+
self.recycled: bool = False
33+
34+
def weight(self) -> float:
35+
"""
36+
Weight of the action.
37+
It can be used to choose the next action to select.
38+
This is the generic implementation.
39+
"""
40+
raise NotImplementedError
41+
42+
@final
43+
async def act_before_action(self):
44+
"""
45+
Function executed before the action starts.
46+
"""
47+
if self.before_action_func:
48+
await self.before_action_func()
49+
50+
@final
51+
async def act_after_action(self):
52+
"""
53+
Function executed after the action ends.
54+
"""
55+
if self.after_action_func:
56+
await self.after_action_func()
57+
58+
# Re-enable all actions after a successful action
59+
for action in self.strategy:
60+
action.recycled = False
61+
62+
async def recycle(self):
63+
"""
64+
Function called if the action is blocked and put back in the actions list
65+
"""
66+
self.recycled = True
67+
68+
@property
69+
def pose_current(self) -> models.Pose:
70+
return self.planner.pose_current
71+
72+
async def evaluate(self):
73+
# Average robot speed in mm/s.
74+
# This is just an empirical value found by testing that gives a good enough
75+
# estimation of the time needed to perform the action.
76+
# This could be improved later by using target speed and acceleration.
77+
average_speed = 100
78+
79+
await self.act_before_action()
80+
81+
# Update countdown
82+
self.planner.game_context.countdown -= asyncio.sleep.total_sleep
83+
asyncio.sleep.reset()
84+
85+
while len(self.poses) and self.planner.game_context.countdown > 0:
86+
pose = self.poses.pop(0)
87+
88+
await pose.act_before_pose()
89+
await pose.act_after_pose()
90+
91+
# Update countdown
92+
distance = math.dist(
93+
(self.planner.pose_current.x, self.planner.pose_current.y),
94+
(pose.x, pose.y),
95+
)
96+
self.planner.game_context.countdown -= asyncio.sleep.total_sleep + distance / average_speed
97+
asyncio.sleep.reset()
98+
99+
# Update pose_current
100+
self.planner.pose_current.x = pose.x
101+
self.planner.pose_current.y = pose.y
102+
self.planner.pose_current.O = pose.O
103+
104+
await self.act_after_action()
105+
106+
# Update countdown
107+
self.planner.game_context.countdown -= asyncio.sleep.total_sleep
108+
asyncio.sleep.reset()
109+
110+
def __str__(self) -> str:
111+
return f"Action[0x{id(self):x}]({self.name})"
112+
113+
def __repr__(self) -> str:
114+
return str(self)

cogip/tools/planner/actions/action_align.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from typing import TYPE_CHECKING
44

55
from cogip import models
6-
from cogip.tools.planner import actuators, logger
7-
from cogip.tools.planner.actions.actions import Action, Actions
6+
from cogip.tools.planner import actuators
7+
from cogip.tools.planner.actions.action import Action
8+
from cogip.tools.planner.actions.strategy import Strategy
89
from cogip.tools.planner.avoidance.avoidance import AvoidanceStrategy
910
from cogip.tools.planner.pose import AdaptedPose, Pose
1011

@@ -21,7 +22,7 @@ class AlignBottomAction(Action):
2122
def __init__(
2223
self,
2324
planner: "Planner",
24-
actions: Actions,
25+
strategy: Strategy,
2526
*,
2627
final_pose: models.Pose = Pose(x=-750, y=-250, O=0),
2728
reset_countdown=False,
@@ -30,11 +31,11 @@ def __init__(
3031
self.final_pose = final_pose
3132
self.reset_countdown = reset_countdown
3233
self.custom_weight = weight
33-
super().__init__("Align Bottom action", planner, actions)
34+
super().__init__("Align Bottom action", planner, strategy)
3435
self.before_action_func = self.init_poses
3536

3637
def set_avoidance(self, new_strategy: AvoidanceStrategy):
37-
logger.info(f"{self.name}: set avoidance to {new_strategy.name}")
38+
self.logger.info(f"{self.name}: set avoidance to {new_strategy.name}")
3839
self.planner.shared_properties.avoidance_strategy = new_strategy.val
3940

4041
async def init_poses(self):
@@ -92,11 +93,11 @@ async def init_poses(self):
9293
self.poses.append(pose)
9394

9495
async def before_align_back(self):
95-
logger.info(f"{self.name}: before_align_back")
96+
self.logger.info(f"{self.name}: before_align_back")
9697
self.set_avoidance(AvoidanceStrategy.Disabled)
9798

9899
async def after_align_back(self):
99-
logger.info(f"{self.name}: after_align_back")
100+
self.logger.info(f"{self.name}: after_align_back")
100101
current_pose = models.Pose(
101102
x=-1000 + self.planner.shared_properties.robot_length / 2,
102103
y=self.start_pose.y,
@@ -106,23 +107,23 @@ async def after_align_back(self):
106107
await asyncio.sleep(1)
107108

108109
async def before_step_forward(self):
109-
logger.info(f"{self.name}: before_step_forward")
110+
self.logger.info(f"{self.name}: before_step_forward")
110111

111112
async def after_step_forward(self):
112-
logger.info(f"{self.name}: after_step_forward")
113+
self.logger.info(f"{self.name}: after_step_forward")
113114

114115
async def before_final_pose(self):
115-
logger.info(f"{self.name}: before_final_pose")
116+
self.logger.info(f"{self.name}: before_final_pose")
116117

117118
async def after_final_pose(self):
118-
logger.info(f"{self.name}: after_final_pose")
119+
self.logger.info(f"{self.name}: after_final_pose")
119120
self.set_avoidance(self.avoidance_backup)
120121
if self.reset_countdown:
121122
now = datetime.now(UTC)
122123
self.planner.countdown_start_timestamp = now
123124
await self.planner.sio_ns.emit(
124125
"start_countdown",
125-
(self.planner.robot_id, self.game_context.game_duration, now.isoformat(), "deepskyblue"),
126+
(self.planner.robot_id, self.planner.game_context.game_duration, now.isoformat(), "deepskyblue"),
126127
)
127128

128129
def weight(self) -> float:
@@ -133,26 +134,24 @@ class AlignBottomForBannerAction(AlignBottomAction):
133134
def __init__(
134135
self,
135136
planner: "Planner",
136-
actions: Actions,
137+
strategy: Strategy,
137138
weight: float = 2000000.0,
138139
):
139140
super().__init__(
140141
planner,
141-
actions,
142+
strategy,
142143
final_pose=models.Pose(x=-1000 + 220, y=-50 - 450 / 2, O=180),
143144
reset_countdown=False,
144145
weight=weight,
145146
)
146147
self.after_action_func = self.after_action
147148

148149
async def after_action(self):
149-
logger.info("AlignBottomForBannerAction: after_action.")
150-
await asyncio.gather(
151-
actuators.arm_left_side(self.planner),
152-
actuators.arm_right_side(self.planner),
153-
actuators.magnet_center_right_in(self.planner),
154-
actuators.magnet_center_left_in(self.planner),
155-
actuators.magnet_side_right_in(self.planner),
156-
actuators.magnet_side_left_in(self.planner),
157-
actuators.lift_125(self.planner),
158-
)
150+
self.logger.info("AlignBottomForBannerAction: after_action.")
151+
await actuators.arm_left_side(self.planner)
152+
await actuators.arm_right_side(self.planner)
153+
await actuators.magnet_center_right_in(self.planner)
154+
await actuators.magnet_center_left_in(self.planner)
155+
await actuators.magnet_side_right_in(self.planner)
156+
await actuators.magnet_side_left_in(self.planner)
157+
await actuators.lift_125(self.planner)

0 commit comments

Comments
 (0)