-
Notifications
You must be signed in to change notification settings - Fork 24
Better defence, goalie and supporter positions #925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 29 commits
e74145f
5c21291
6cd3000
1ced7f6
61bf6e7
4fa2aed
eca2b97
775da12
842adfd
43c6e31
49ed7aa
6d616d6
251a18d
9002998
49d519a
eaaf36d
bfbf3c7
3fc4e2c
28dbf2e
ae03c94
d058eeb
b884405
656f27f
e2c3c4a
ab88f08
2ef42fe
b9bfea2
104cea8
b6b222d
ee12f90
c09ac88
1b03218
5190a13
09a1641
d86cc96
e10883c
fa5e0ce
9ddf72d
bedf8e9
5551f44
94408f2
85c0aaa
93cd9c4
7c664c7
5cf3bab
eeed157
a3f9344
8b63646
9a3099b
bcfdbb2
426c857
a093312
33a0ff2
0e885e3
5904a09
1b38e33
a99dc2c
6d3d637
6acf08a
d35ad73
888ea17
1ece6ee
3e11e17
ad9f591
6e49a6c
6818fa5
aaed253
463144b
53874db
bd8cb9c
4867dce
bdae2ba
7d52bbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| #!/usr/bin/env python3 | ||
| """Debug GUI for InnerPositioningCapsule. | ||
|
|
||
| Exercises the exact capsule code without starting the full stack. | ||
| Click the field to move the ball; use the sliders to tweak params. | ||
| """ | ||
|
|
||
| import numpy as np | ||
| from bitbots_blackboard.capsules.positioning_capsule import Field, InnerPositioningCapsule, Params | ||
|
|
||
| _inner = InnerPositioningCapsule() | ||
|
|
||
|
|
||
| def run_gui(): | ||
| import matplotlib.pyplot as plt | ||
| from matplotlib.patches import Circle, Rectangle | ||
| from matplotlib.widgets import CheckButtons, Slider | ||
|
|
||
| fld = Field() | ||
| params = Params() | ||
| state = {"ball": np.array([1.0, 0.5]), "n": 5, "prev": None} | ||
|
|
||
| colors = {"goalie": "#e6b800", "striker": "#d62728", "supporter": "#2ca02c"} | ||
|
|
||
| fig, ax = plt.subplots(figsize=(9, 9)) | ||
| plt.subplots_adjust(left=0.08, right=0.97, top=0.98, bottom=0.50) | ||
|
|
||
| def _ax(b): | ||
| return plt.axes([0.18, b, 0.72, 0.015]) | ||
|
|
||
| s_n = Slider(_ax(0.470), "players", 1, 8, valinit=state["n"], valstep=1) | ||
| s_sep = Slider(_ax(0.440), "min_sep", 0.3, 2.0, valinit=params.min_sep) | ||
| s_alpha = Slider(_ax(0.410), "push α", 0.1, 0.8, valinit=params.alpha) | ||
| s_dbias = Slider(_ax(0.380), "def fwd/back", -2.0, 3.0, valinit=params.depth_bias) | ||
| s_dside = Slider(_ax(0.350), "def side", 0.0, 2.0, valinit=params.def_side) | ||
| s_gap = Slider(_ax(0.320), "def gap", 0.5, 2.0, valinit=params.gap) | ||
| s_f = Slider(_ax(0.290), "supp lead", 0.0, 3.0, valinit=params.f) | ||
| s_side = Slider(_ax(0.260), "supp side", 0.0, 2.5, valinit=params.supp_side) | ||
| s_smax = Slider(_ax(0.230), "supp max x", 0.0, 4.2, valinit=params.supp_max_x) | ||
| s_pmarg = Slider(_ax(0.200), "post margin", 0.0, 1.3, valinit=params.post_margin) | ||
| s_back = Slider(_ax(0.170), "back dist", 0.0, 3.0, valinit=params.back_dist) | ||
| s_kclr = Slider(_ax(0.140), "kick clear", 0.0, 1.5, valinit=params.kick_clear) | ||
| s_gout = Slider(_ax(0.110), "goalie out", 0.2, 2.0, valinit=params.d_g) | ||
| check_fk = CheckButtons(plt.axes([0.18, 0.073, 0.20, 0.022]), ["freekick"], [False]) | ||
| s_fkcl = Slider(_ax(0.050), "fk clearance", 0.1, 2.0, valinit=params.freekick_clearance) | ||
|
|
||
| def draw(): | ||
| ax.clear() | ||
| ax.add_patch( | ||
| Rectangle((-fld.length / 2, -fld.width / 2), fld.length, fld.width, fill=False, color="white", lw=2) | ||
| ) | ||
| ax.axvline(0, color="white", lw=1) | ||
| ax.add_patch(Circle((0, 0), 0.75, fill=False, color="white", lw=1)) | ||
| for sgn in (-1, 1): | ||
| ax.plot( | ||
| [sgn * fld.length / 2] * 2, | ||
| [-fld.goal_width / 2, fld.goal_width / 2], | ||
| color="#4da6ff" if sgn < 0 else "#ff9999", | ||
| lw=6, | ||
| ) | ||
| ax.set_facecolor("#2e7d32") | ||
|
|
||
| params.min_sep, params.alpha, params.gap, params.f = s_sep.val, s_alpha.val, s_gap.val, s_f.val | ||
| params.depth_bias, params.supp_side, params.d_g = s_dbias.val, s_side.val, s_gout.val | ||
| params.supp_max_x, params.def_side = s_smax.val, s_dside.val | ||
| params.post_margin, params.back_dist = s_pmarg.val, s_back.val | ||
| params.kick_clear = s_kclr.val | ||
| params.freekick = check_fk.get_status()[0] | ||
| params.freekick_clearance = s_fkcl.val | ||
| n = int(s_n.val) | ||
|
|
||
| if params.freekick: | ||
| ax.add_patch( | ||
| Circle( | ||
| state["ball"], | ||
| params.freekick_clearance, | ||
| fill=False, | ||
| color="yellow", | ||
| lw=1.5, | ||
| ls="--", | ||
| zorder=2, | ||
| alpha=0.7, | ||
| ) | ||
| ) | ||
|
|
||
| form = _inner._compute_formation(state["ball"], fld, n, params) | ||
| new_items = list(form.items()) | ||
|
|
||
| prev = state["prev"] | ||
| if prev is not None and len(prev) == len(new_items): | ||
| for old_idx, new_pose, role in _inner._match_assignment(prev, new_items, state["ball"]): | ||
| old_pose = prev[old_idx] | ||
| c = colors.get(role.split("_")[0], "#1f77b4") | ||
| ax.plot([old_pose[0], new_pose[0]], [old_pose[1], new_pose[1]], ls=":", color=c, lw=1.5, zorder=3) | ||
| ax.add_patch(Circle(old_pose[:2], 0.10, color=c, alpha=0.55, zorder=4)) | ||
| state["prev"] = [pose for _role, pose in new_items] | ||
|
|
||
| ax.add_patch(Circle(state["ball"], 0.12, color="white", ec="black", zorder=5)) | ||
| if "striker" in form: | ||
| th = form["striker"][2] | ||
| aim = np.array([np.cos(th), np.sin(th)]) | ||
| perp = np.array([-aim[1], aim[0]]) | ||
| b = np.asarray(state["ball"]) | ||
| far = b + params.kick_range * aim | ||
| corner = perp * params.kick_clear | ||
| lane = np.array([b + corner, far + corner, far - corner, b - corner]) | ||
| ax.add_patch(plt.Polygon(lane, closed=True, color="white", alpha=0.10, zorder=1)) | ||
| ax.arrow( | ||
| *state["ball"], | ||
| *(1.4 * aim), | ||
| color="white", | ||
| width=0.02, | ||
| head_width=0.18, | ||
| length_includes_head=True, | ||
| zorder=8, | ||
| alpha=0.9, | ||
| ) | ||
| for role, pose in form.items(): | ||
| p, th = pose[:2], pose[2] | ||
| base = role.split("_")[0] | ||
| c = colors.get(base, "#1f77b4") | ||
| ax.add_patch(Circle(p, params.min_sep / 2, color=c, alpha=0.18, zorder=2)) | ||
| ax.add_patch(Circle(p, 0.16, color=c, ec="black", zorder=6)) | ||
| ax.plot( | ||
| [p[0], p[0] + 0.45 * np.cos(th)], | ||
| [p[1], p[1] + 0.45 * np.sin(th)], | ||
| color="black", | ||
| lw=2.5, | ||
| zorder=7, | ||
| solid_capstyle="round", | ||
| ) | ||
| ax.annotate( | ||
| role.replace("defender_", "D").replace("supporter", "supp"), | ||
| p, | ||
| color="white", | ||
| fontsize=8, | ||
| ha="center", | ||
| va="center", | ||
| zorder=8, | ||
| ) | ||
|
|
||
| ax.set_xlim(-fld.length / 2 - 0.5, fld.length / 2 + 0.5) | ||
| ax.set_ylim(-fld.width / 2 - 0.5, fld.width / 2 + 0.5) | ||
| ax.set_aspect("equal") | ||
| ax.set_title("click to move the ball · dots = previous assignment (dotted = who moves where)", color="black") | ||
| fig.canvas.draw_idle() | ||
|
|
||
| def on_click(event): | ||
| if event.inaxes is ax and event.xdata is not None: | ||
| state["ball"] = np.array([event.xdata, event.ydata]) | ||
| draw() | ||
|
|
||
| for s in ( | ||
| s_n, | ||
| s_sep, | ||
| s_alpha, | ||
| s_dbias, | ||
| s_dside, | ||
| s_gap, | ||
| s_f, | ||
| s_side, | ||
| s_smax, | ||
| s_pmarg, | ||
| s_back, | ||
| s_kclr, | ||
| s_gout, | ||
| s_fkcl, | ||
| ): | ||
| s.on_changed(lambda _v: draw()) | ||
| check_fk.on_clicked(lambda _label: draw()) | ||
| fig.canvas.mpl_connect("button_press_event", on_click) | ||
| draw() | ||
| plt.show() | ||
|
|
||
|
|
||
| def main(): | ||
| run_gui() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| from bitbots_blackboard.capsules.pathfinding_capsule import BallGoalType | ||
| from dynamic_stack_decider.abstract_action_element import AbstractActionElement | ||
| from geometry_msgs.msg import Vector3 | ||
| from tf2_geometry_msgs import PoseStamped | ||
| from std_msgs.msg import ColorRGBA | ||
| from visualization_msgs.msg import Marker | ||
|
|
||
|
|
@@ -25,7 +26,15 @@ def __init__(self, blackboard, dsd, parameters): | |
| self.side_offset = parameters.get("side_offset", 0.00) | ||
|
|
||
| def perform(self, reevaluate=False): | ||
| pose_msg = self.blackboard.pathfinding.get_ball_goal(self.target, self.distance, self.side_offset) | ||
| pose_msg = PoseStamped() | ||
| pose_msg.header.stamp = self.blackboard.node.get_clock().now().to_msg() | ||
| pose_msg.header.frame_id = self.blackboard.map_frame | ||
| optimal_positioning = self.blackboard.positioning.get_formation_assignment() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use the debounced assignment here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you mean by that? |
||
| own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()] | ||
| pose = own_position["goal_pose"] | ||
| pose_msg.pose.position.x = pose[0] | ||
| pose_msg.pose.position.y = pose[1] | ||
| pose_msg.pose.orientation.w = pose[2] | ||
| self.blackboard.pathfinding.publish(pose_msg) | ||
|
|
||
| approach_marker = Marker() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,27 @@ | |
| from tf2_geometry_msgs import PoseStamped | ||
|
|
||
|
|
||
| class GoToGoaliePosition(AbstractActionElement): | ||
| blackboard: BodyBlackboard | ||
|
|
||
| def __init__(self, blackboard, dsd, parameters): | ||
| super().__init__(blackboard, dsd, parameters) | ||
|
|
||
| def perform(self, reevaluate=False): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now it treads go to goalie, defense, etc all the same. That might be confusing, because somebody calls go to defense position to go explicitly there and he ends up at the goalie position.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer an explicit go to placement position. We can keep the old supporter position for the forced two robot ball touch, but that works outside of this placement. The PR is still pending, but it essentially removes the robot from the assignment after the ball was touched and goes to an explicit position to wait. While the others play with essentially one robot less. This can be done in the new concept, but we need the explicit support position for this. Other than that all other go to defense, block etc can be removed. Go to role needs to stay.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also need to think about what to do if we don't see a ball. The position assignment should still work based on the lastest ball. I think the striker should perform our normal ball discovery behavior (turn around, go to opponent goal, go to middle line on the side). The rest of the robots should go to their role positions to not interfere with the striker during this procedure.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the whole part with coding the positioning goals in the old GoTo decisions was only for testing :) I use GoToFormationPosition now in the most cases, we still need to discuss how to handle the supporter during no double touch, I would prefer a direct integration in the formation capsule |
||
| pose_msg = PoseStamped() | ||
| pose_msg.header.stamp = self.blackboard.node.get_clock().now().to_msg() | ||
| pose_msg.header.frame_id = self.blackboard.map_frame | ||
|
|
||
| optimal_positioning = self.blackboard.positioning.get_formation_assignment() | ||
| own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()] | ||
| pose = own_position["goal_pose"] | ||
| pose_msg.pose.position.x = pose[0] | ||
| pose_msg.pose.position.y = pose[1] | ||
| pose_msg.pose.orientation.w = pose[2] | ||
|
|
||
| self.blackboard.pathfinding.publish(pose_msg) | ||
|
|
||
|
|
||
| class GoToBlockPosition(AbstractActionElement): | ||
| blackboard: BodyBlackboard | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
|
|
||
| from bitbots_blackboard.body_blackboard import BodyBlackboard | ||
| from dynamic_stack_decider.abstract_action_element import AbstractActionElement | ||
| from tf2_geometry_msgs import PoseStamped | ||
|
|
||
|
|
||
| class GoToFormationPosition(AbstractActionElement): | ||
| blackboard: BodyBlackboard | ||
|
|
||
| def __init__(self, blackboard, dsd, parameters): | ||
| super().__init__(blackboard, dsd, parameters) | ||
|
|
||
| def perform(self, reevaluate=False): | ||
| pose_msg = PoseStamped() | ||
| pose_msg.header.stamp = self.blackboard.node.get_clock().now().to_msg() | ||
| pose_msg.header.frame_id = self.blackboard.map_frame | ||
|
|
||
| optimal_positioning = self.blackboard.positioning.get_formation_assignment() | ||
| own_position = optimal_positioning[self.blackboard.gamestate.get_own_id()] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
| pose = own_position["goal_pose"] | ||
| pose_msg.pose.position.x = pose[0] | ||
| pose_msg.pose.position.y = pose[1] | ||
| pose_msg.pose.orientation.w = pose[2] | ||
|
|
||
| self.blackboard.pathfinding.publish(pose_msg) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should stop if we reached the target pose roughly. Make the threshold and whole feature a parameter, because e.g. the striker will not stop when he wants to strike while the supporter and goaly should. This is fairly important as walking on place for too long might break the localization due to slight drift that is unaccounted for in the linear noise model.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right inside the action? how about a dsd decision either says stands or goes to formation position? |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specify the quaternion notation xyzw vs wxyz and maybe move/check but it's utils for these functions.