Skip to content
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

Add BlackboardBehavior and initial AcquireFood tree #102

Merged
merged 14 commits into from
Sep 27, 2023

Conversation

egordon
Copy link
Collaborator

@egordon egordon commented Sep 21, 2023

Description

  1. Adds class behaviors.BlackboardBehavior, which exposes a concise way to map blackboard keys to local strings.
  2. Adds and hooks up the AcquireFood tree to the AcquireFood action server
  3. Creates the behavior ComputeFoodFrame which, given the goal, outputs the food reference frame. It also (optionally) publishes this frame to the TF tree for debugging purposes.
  4. Adds ComputeFoodFrame to the AcquireFoodTree
  5. Uses send_goal to write specific goal arguments to the local blackboard.

Proposal:
In general, I think goal and result variables should be read from / written to the current tree blackboard namespace. This better enables subtree inclusion, where previous nodes explicitly write subtree inputs to that subtree's local blackboard and read subtree outputs from that subtree's local blackboard namespace.

For right now, I think it's okay for feedback to utilize the tree root blackboard. However, I think it would be better going forward for us to attach a FeedbackVisitor to construct it every tick instead. Feedback in theory should ideally only be dependent on:

  • The currently running Behavior
  • The blackboard attached to the currently running Behavior
  • The previous Feedback

Note that, for subtrees, get_feedback will never be called, so get_feedback, get_result, send_goal should be free to assume that they are running on the main tree and act accordingly.

The upshot of all of this is that tree_root_name becomes deprecated, and all trees can treat their own self.blackboard as the root scope.

Testing procedure

  1. Use MoveAbovePlate before AcquireFood feeding_web_interface#89 (if not merged)
  2. Run the Mock FT Sensor: ros2 run ada_feeding dummy_ft_sensor.py
  3. Run the action servers: ros2 launch ada_feeding ada_feeding_launch.xml use_estop:=false
  4. Run MoveIt in sim: ros2 launch ada_moveit demo.launch.py sim:=mock
  5. Run this code with the default dummy carrot (oriented horizontally in the camera frame): ros2 launch feeding_web_app_ros2_test feeding_dummy_acquirefood_launch.py
  6. Verify the Goal Succeeds (status=0). Open RViz and add an Axis Frame "food" (rescale to 0.1 for visibility), verify that +Z is against gravity and +X is horizontal relative to the camera.
  7. Run this code with the dummy carrot 2 (oriented ~vertically in the camera frame): ros2 launch feeding_web_app_ros2_test feeding_dummy_acquirefood_launch.py request:=above_plate_2_carrot_request.pkl
  8. Verify the Goal Succeeds (status=0). Open RViz and add an Axis Frame "food" (rescale to 0.1 for visibility), verify that +Z is against gravity and +X is vertical relative to the camera and to the "lower right" in the camera frame.

Before opening a pull request

  • Format your code using black formatter python3 -m black .
  • Run your code through pylint and address all warnings/errors. The only warnings that are acceptable to not address is TODOs that should be addressed in a future PR. From the top-level ada_feeding directory, run: pylint --recursive=y --rcfile=.pylintrc ..

Before Merging

  • Squash & Merge

@egordon egordon marked this pull request as ready for review September 22, 2023 00:30
Copy link
Contributor

@amalnanavati amalnanavati left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, comments should be addressed, but nothing that requires a re-review.

Re. your idea of writing goals in local namespace as opposed to the tree root namespace, I think the only scenario in which that matters is if we use a tree within another tree, and I'm not sure how we would percolate the goal down into every subtree's blackboard. We should discuss more asynch (e.g., on a GitHub issue). For Feedback and Result, I agree that we can create a visitor for those.


class BlackboardBehavior(py_trees.behaviour.Behaviour):
"""
TODO
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add to the class docstring the following:

  1. self.remap, self.inputs, and self.outputs should not be overridden. I was hoping name mangling could allow us to prevent them from being overridden, but unfortunately that won't work (see example).
  2. blackboard values should only be accessed via blackboard_exists, blackboard_get, and blackboard_write, not by accessing self.blackboard directly.


def blackboard_inputs(self, **kwargs) -> None:
"""
Define and register all blackboard input keys.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an English explanation of what this does, in addition to the pseudocode explanation before? e.g.,

Each key of the kwargs is a str specifying the location on the blackboard for that variable to be stored. Each value of kwargs is either `BlackboardKey`, in which case it specifies a blackboard location to remap the key to, or another type, in which case it specifies a constant value to store at that key. Note that as opposed to setting constants on the blackboard, this behavior stores it in a local dict.


return self.blackboard.get(self.remap[key])

def blackboard_write(self, key: str, output: Any) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why blackboard_write instead of blackboard_set (which would seem to match better with blackboard_get)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or blackboard_read instead of blackboard_get. Basically I think we should either use read/write or get/set, not both.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch to blackboard_set, using get/set to match the underlying API


def blackboard_exists(self, key: str) -> bool:
"""
Check if a key is set in the blackboard or available locally.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add to blackboard_exists, blackboard_get, blackboard_write docstrings: Raises KeyError if the key has not been defined in blackboard_inputs/outputs

<key>: BlackboardKey = <default>

Call from the subclass using:
super().blackboard_inputs(**{key: value for key,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outputs, not inputs

"""
TODO
Computes the food reference frame.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please define the food reference frame here, or point to the file where it is defined (I believe in one of the message definitions).

raise KeyError(f"{key} is not a registered output: {self.outputs.keys()}")

if self.outputs[key] is not None:
self.blackboard.set(self.outputs[key], output)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should log an error/warning if blackboard_write is called when the default value was None. A programmer may have just forgotten to change the default output value, and a warning can help surface that issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected behavior, so I'll instead write it to debug()

request.food_context = mask
self.blackboard_write("action_select_request", request)

return py_trees.common.Status.SUCCESS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, once we have the transform how long does it take to do the computations? Because the tree is blocked until the computations are done. If it takes a while, we should instead make it an asynch function that does the computation, and return RUNNING until that asynch function succeeds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timed: 0.0005796703s (i.e. 580us)
I think we're fine

@@ -35,6 +54,42 @@
MOVE_TO_NAMESPACE_PREFIX = "move_to"


def quat_between_vectors(vec_from: Vector3, vec_to: Vector3) -> Quaternion:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trusting that this is correct, I didn't check the math on this. In the future we can consider writing a unit test for this to check it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added issue #110

@@ -73,9 +55,69 @@ def create_move_to_tree(
-------
tree: The behavior tree that acquires food
"""
# TODO: remove name and tree_root_name
# We have access to them implicitly via self.blackboard / self.blackboard_tree_root
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, feel free to create an issue for this.

Currently, self.blackboard.namespace and self.blackboard_tree_root.namespace are the ones that have the unchanged name. We should consider whether it should instead be .name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made issue #111

@egordon
Copy link
Collaborator Author

egordon commented Sep 22, 2023

Re. your idea of writing goals in local namespace as opposed to the tree root namespace, I think the only scenario in which that matters is if we use a tree within another tree, and I'm not sure how we would percolate the goal down into every subtree's blackboard. We should discuss more asynch (e.g., on a GitHub issue). For Feedback and Result, I agree that we can create a visitor for those.

We can discuss offline, though I want to flag that we shouldn't conceive of the input of the tree being the action goal specifically. Instead, we can consider a tree to have a set of inputs, like a function, that are implemented as expected variables written to the local blackboard prior to tree execution.

For the root tree, send_goal takes the goal object and writes those variables.

For sub-trees, it is up to the parent tree to write to those variables (by having the keys be the outputs of other nodes or written directly).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants