Skip to content

Commit 6a59a42

Browse files
authored
Behavior optimizations (#692)
2 parents cbfb277 + 14d8817 commit 6a59a42

File tree

9 files changed

+78
-29
lines changed

9 files changed

+78
-29
lines changed

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/body_blackboard.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,19 @@ def __init__(self, node: Node, tf_buffer: tf2.BufferInterface):
3434
self.costmap = CostmapCapsule(self.node, self)
3535
self.pathfinding = PathfindingCapsule(self.node, self)
3636
self.team_data = TeamDataCapsule(self.node, self)
37+
38+
self.capsules = [
39+
self.misc,
40+
self.gamestate,
41+
self.animation,
42+
self.kick,
43+
self.world_model,
44+
self.costmap,
45+
self.pathfinding,
46+
self.team_data,
47+
]
48+
49+
def clear_cache(self):
50+
"""Clear the cache of all capsules."""
51+
for capsule in self.capsules:
52+
capsule.clear_cache()

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import wraps
12
from typing import TYPE_CHECKING
23

34
from bitbots_utils.utils import nobeartype
@@ -7,10 +8,29 @@
78
from bitbots_blackboard.body_blackboard import BodyBlackboard
89

910

11+
def cached_capsule_function(method):
12+
"""Decorator to cache the result of a method."""
13+
cache_key = method.__name__
14+
15+
@wraps(method)
16+
def wrapper(self):
17+
if cache_key not in self._cache:
18+
self._cache[cache_key] = method(self)
19+
return self._cache[cache_key]
20+
21+
return wrapper
22+
23+
1024
class AbstractBlackboardCapsule:
1125
"""Abstract class for blackboard capsules."""
1226

1327
@nobeartype
1428
def __init__(self, node: Node, blackboard: "BodyBlackboard"):
1529
self._node = node
1630
self._blackboard = blackboard
31+
32+
self._cache: dict[str, object] = {}
33+
34+
def clear_cache(self):
35+
"""Clear the cache of this capsule."""
36+
self._cache.clear()

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/team_data_capsule.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ros2_numpy import numpify
99
from std_msgs.msg import Float32
1010

11-
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
11+
from bitbots_blackboard.capsules import AbstractBlackboardCapsule, cached_capsule_function
1212
from bitbots_msgs.msg import Strategy, TeamData
1313

1414

@@ -68,16 +68,22 @@ def __init__(self, node, blackboard):
6868
self.data_timeout: float = float(self._node.get_parameter("team_data_timeout").value)
6969
self.ball_max_covariance: float = float(self._node.get_parameter("ball_max_covariance").value)
7070

71+
@cached_capsule_function
72+
def time(self) -> Time:
73+
"""Returns the current time of the node, this is its own function so it can be cached during the decision loop."""
74+
return self._node.get_clock().now()
75+
7176
def is_valid(self, data: TeamData) -> bool:
7277
"""
7378
Checks if a team data message from a given robot is valid.
7479
Meaning it is not too old and the robot is not penalized.
7580
"""
7681
return (
77-
self._node.get_clock().now() - Time.from_msg(data.header.stamp) < Duration(seconds=self.data_timeout)
82+
self.time() - Time.from_msg(data.header.stamp) < Duration(seconds=self.data_timeout)
7883
and data.state != TeamData.STATE_PENALIZED
7984
)
8085

86+
@cached_capsule_function
8187
def is_goalie_handling_ball(self) -> bool:
8288
"""Returns true if the goalie is going to the ball."""
8389
data: TeamData
@@ -90,6 +96,7 @@ def is_goalie_handling_ball(self) -> bool:
9096
return True
9197
return False
9298

99+
@cached_capsule_function
93100
def is_team_mate_kicking(self) -> bool:
94101
"""Returns true if one of the players in the own team is kicking."""
95102
data: TeamData
@@ -190,6 +197,7 @@ def is_not_goalie(team_data: TeamData) -> bool:
190197
# Count valid team data infos (aka robots with valid team data)
191198
return sum(map(self.is_valid, team_data_infos))
192199

200+
@cached_capsule_function
193201
def get_is_goalie_active(self) -> bool:
194202
def is_a_goalie(team_data: TeamData) -> bool:
195203
return team_data.strategy.role == Strategy.ROLE_GOALIE
@@ -221,6 +229,7 @@ def teammate_ball_is_valid(self) -> bool:
221229
"""Returns true if a teammate has seen the ball accurately enough"""
222230
return self.get_teammate_ball() is not None
223231

232+
@cached_capsule_function
224233
def get_teammate_ball(self) -> Optional[PointStamped]:
225234
"""Returns the best ball from all teammates that satisfies a minimum ball precision"""
226235

bitbots_behavior/bitbots_blackboard/bitbots_blackboard/capsules/world_model_capsule.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
TransformStamped,
1111
)
1212
from rclpy.clock import ClockType
13-
from rclpy.duration import Duration
1413
from rclpy.time import Time
1514
from ros2_numpy import msgify, numpify
1615
from std_msgs.msg import Header
1716
from std_srvs.srv import Trigger
1817
from tf2_geometry_msgs import Point, PointStamped
1918
from tf_transformations import euler_from_quaternion
2019

21-
from bitbots_blackboard.capsules import AbstractBlackboardCapsule
20+
from bitbots_blackboard.capsules import AbstractBlackboardCapsule, cached_capsule_function
2221

2322

2423
class WorldModelTFError(Exception): ...
@@ -27,9 +26,6 @@ class WorldModelTFError(Exception): ...
2726
class WorldModelPositionTFError(WorldModelTFError): ...
2827

2928

30-
class WorldModelBallTFError(WorldModelTFError): ...
31-
32-
3329
class WorldModelCapsule(AbstractBlackboardCapsule):
3430
"""Provides information about the world model."""
3531

@@ -78,10 +74,12 @@ def __init__(self, node, blackboard):
7874
### Ball ###
7975
############
8076

77+
@cached_capsule_function
8178
def ball_seen_self(self) -> bool:
8279
"""Returns true we are reasonably sure that we have seen the ball"""
8380
return all(np.diag(self._ball_covariance) < self.ball_max_covariance)
8481

82+
@cached_capsule_function
8583
def ball_has_been_seen(self) -> bool:
8684
"""Returns true if we or a teammate are reasonably sure that we have seen the ball"""
8785
return self.ball_seen_self() or self._blackboard.team_data.teammate_ball_is_valid()
@@ -91,6 +89,7 @@ def get_ball_position_xy(self) -> tuple[float, float]:
9189
ball = self.get_best_ball_point_stamped()
9290
return ball.point.x, ball.point.y
9391

92+
@cached_capsule_function
9493
def get_best_ball_point_stamped(self) -> PointStamped:
9594
"""
9695
Returns the best ball, either its own ball has been in the ball_lost_lost time
@@ -113,21 +112,25 @@ def get_best_ball_point_stamped(self) -> PointStamped:
113112
self.debug_publisher_which_ball.publish(Header(stamp=own_ball.header.stamp, frame_id="own_ball_map"))
114113
return own_ball
115114

115+
@cached_capsule_function
116116
def get_ball_position_uv(self) -> tuple[float, float]:
117117
"""
118118
Returns the ball position relative to the robot in the base_footprint frame.
119119
U and V are returned, where positive U is forward, positive V is to the left.
120120
"""
121121
ball = self.get_best_ball_point_stamped()
122-
try:
123-
ball_bfp = self._blackboard.tf_buffer.transform(
124-
ball, self.base_footprint_frame, timeout=Duration(seconds=0.2)
125-
).point
126-
except tf2.ExtrapolationException as e:
127-
self._node.get_logger().warn(str(e))
128-
self._node.get_logger().error("Severe transformation problem concerning the ball!")
129-
raise WorldModelBallTFError("Could not transform ball to base_footprint frame") from e
130-
return ball_bfp.x, ball_bfp.y
122+
assert ball.header.frame_id == self.map_frame, "Ball needs to be in the map frame"
123+
our_pose = self.get_current_position_transform()
124+
assert our_pose.header.frame_id == self.map_frame, "Our pose needs to be in the map frame"
125+
# Construct the homogeneous transformation matrix for the ball position
126+
ball_transform = np.eye(4)
127+
ball_transform[0, 3] = ball.point.x
128+
ball_transform[1, 3] = ball.point.y
129+
# Get the homogeneous transformation matrix for the robot's current position
130+
our_pose_transform = numpify(our_pose.transform)
131+
# Calculate the relative position of the ball to the robot
132+
relative_transform = np.linalg.inv(our_pose_transform) @ ball_transform
133+
return relative_transform[0, 3], relative_transform[1, 3]
131134

132135
def get_ball_distance(self) -> float:
133136
"""
@@ -171,9 +174,7 @@ def forget_ball(self) -> None:
171174
"""
172175
Forget that we saw a ball
173176
"""
174-
result: Trigger.Response = self.reset_ball_filter.call(Trigger.Request())
175-
if not result.success:
176-
self._node.get_logger().error(f"Ball filter reset failed with: '{result.message}'")
177+
self.reset_ball_filter.call_async(Trigger.Request())
177178

178179
########
179180
# Goal #
@@ -218,6 +219,7 @@ def get_map_based_opp_goal_right_post_uv(self) -> tuple[float, float]:
218219
# Pose #
219220
########
220221

222+
@cached_capsule_function
221223
def get_current_position(self) -> tuple[float, float, float]:
222224
"""
223225
Returns the current position on the field as determined by the localization.
@@ -228,6 +230,7 @@ def get_current_position(self) -> tuple[float, float, float]:
228230
theta = euler_from_quaternion(numpify(transform.transform.rotation))[2]
229231
return transform.transform.translation.x, transform.transform.translation.y, theta
230232

233+
@cached_capsule_function
231234
def get_current_position_pose_stamped(self) -> PoseStamped:
232235
"""
233236
Returns the current position as determined by the localization as a PoseStamped
@@ -241,6 +244,7 @@ def get_current_position_pose_stamped(self) -> PoseStamped:
241244
),
242245
)
243246

247+
@cached_capsule_function
244248
def get_current_position_transform(self) -> TransformStamped:
245249
"""
246250
Returns the current position as determined by the localization as a TransformStamped

bitbots_behavior/bitbots_body_behavior/bitbots_body_behavior/body_behavior.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from geometry_msgs.msg import PoseWithCovarianceStamped, Twist
99
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
1010
from rclpy.duration import Duration
11-
from rclpy.executors import MultiThreadedExecutor
11+
from rclpy.experimental.events_executor import EventsExecutor
1212
from rclpy.node import Node
1313
from soccer_vision_3d_msgs.msg import RobotArray
1414

@@ -22,7 +22,7 @@ def __init__(self, node: Node):
2222
self.step_running = False
2323
self.node = node
2424

25-
self.tf_buffer = Buffer(node, Duration(seconds=30))
25+
self.tf_buffer = Buffer(Duration(seconds=30), node)
2626

2727
blackboard = BodyBlackboard(node, self.tf_buffer)
2828
self.dsd = DSD(blackboard, "debug/dsd/body_behavior", node) # TODO: use config
@@ -85,6 +85,7 @@ def loop(self):
8585
self.counter = (self.counter + 1) % blackboard.config["time_to_ball_divider"]
8686
if self.counter == 0:
8787
blackboard.pathfinding.calculate_time_to_ball()
88+
blackboard.clear_cache()
8889
except Exception as e:
8990
import traceback
9091

@@ -97,12 +98,11 @@ def main(args=None):
9798
node = Node("body_behavior", automatically_declare_parameters_from_overrides=True)
9899
body_dsd = BodyDSD(node)
99100
node.create_timer(1 / 60.0, body_dsd.loop, callback_group=MutuallyExclusiveCallbackGroup(), clock=node.get_clock())
100-
# Number of executor threads is the number of MutuallyExclusiveCallbackGroups + 2 threads needed by the tf listener and executor
101-
multi_executor = MultiThreadedExecutor(num_threads=12)
102-
multi_executor.add_node(node)
103101

102+
executor = EventsExecutor()
103+
executor.add_node(node)
104104
try:
105-
multi_executor.spin()
105+
executor.spin()
106106
except KeyboardInterrupt:
107107
pass
108108

bitbots_navigation/bitbots_path_planning/bitbots_path_planning/path_planning.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(self) -> None:
2222
super().__init__("bitbots_path_planning")
2323

2424
# We need to create a tf buffer
25-
self.tf_buffer = Buffer(self, Duration(seconds=self.config.tf_buffer_duration))
25+
self.tf_buffer = Buffer(Duration(seconds=self.config.tf_buffer_duration), self)
2626

2727
self.planner = VisibilityPlanner(node=self, buffer=self.tf_buffer)
2828
self.controller = Controller(node=self, buffer=self.tf_buffer)

bitbots_team_communication/bitbots_team_communication/bitbots_team_communication/bitbots_team_communication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(self):
6060

6161
self.set_state_defaults()
6262

63-
self.tf_buffer = Buffer(self.node)
63+
self.tf_buffer = Buffer(node=self.node)
6464

6565
self.run_spin_in_thread()
6666
self.try_to_establish_connection()

bitbots_world_model/bitbots_ball_filter/bitbots_ball_filter/ball_filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self) -> None:
3232
"""
3333
super().__init__("ball_filter")
3434
self.logger = self.get_logger()
35-
self.tf_buffer = Buffer(self, Duration(seconds=2))
35+
self.tf_buffer = Buffer(Duration(seconds=2), self)
3636
# Setup dynamic reconfigure config
3737
self.param_listener = parameters.ParamListener(self)
3838

bitbots_world_model/bitbots_robot_filter/bitbots_robot_filter/filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class RobotFilter(Node):
2020
def __init__(self):
2121
super().__init__("bitbots_robot_filter")
2222

23-
self.tf_buffer = Buffer(self, Duration(seconds=10.0))
23+
self.tf_buffer = Buffer(Duration(seconds=10.0), self)
2424

2525
self.robots: list[tuple[sv3dm.Robot, Time]] = list()
2626
self.team: dict[int, TeamData] = dict()

0 commit comments

Comments
 (0)