Skip to content

Commit

Permalink
Introduce action support (#17)
Browse files Browse the repository at this point in the history
Signed-off-by: Marco Lampacrescia <[email protected]>
Signed-off-by: Christian Henkel <[email protected]>
  • Loading branch information
MarcoLm993 authored Aug 6, 2024
1 parent 52b2c06 commit 8c6666a
Show file tree
Hide file tree
Showing 40 changed files with 2,370 additions and 738 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v2
with:
repository: boschresearch/bt_tools
ref: feature/fsm_conversion
ref: main
path: colcon_ws/src/bt_tools
# Compile bt_tools TODO: remove after the release of bt_tools
- name: Compile bt_tools
Expand Down
469 changes: 469 additions & 0 deletions docs/source/graphics/ros_service_to_scxml.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 21 additions & 5 deletions docs/source/scxml-jani-conversion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,6 @@ TODO
:alt: How execution blocks and conditions are translated
:align: center

Handling of (ROS) Timers
__________________________

TODO

Handling events
________________
.. _handling_events:
Expand Down Expand Up @@ -116,3 +111,24 @@ The Jani model resulting from applying the conversion strategies we just describ
:align: center

It can be noticed how new self loop edges are added in the `A_B_receiver` automaton (the dashed ones) and how the `ev_a_on_send` is now duplicated in the Composition table, one advancing the `A sender` automaton and the other advancing the `A_B sender` automaton.


Handling of (ROS) Timers
__________________________

TODO

Handling of (ROS) Services
_____________________________

TODO

ROS services, as well as ROS topics, can be handled directly in the ROS to Plain SCXML conversion, without the need of adding Jani-specific features, as for the ROS Timers.

The main structure of the generated Jani models can be seen in the diagram below:

.. image:: graphics/ros_service_to_scxml.drawio.svg
:alt: Handling of ROS Services
:align: center

The automata of clients and service are converted directly from the existing ROS-SCXML files, while the "Extra Service Handler" one, is autogenerated starting from the provided clients and services.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ def remove_edges_with_action_name(self, action_name: str):
assert isinstance(action_name, str), "Action name must be a string"
self._edges = [edge for edge in self._edges if edge.get_action() != action_name]

def remove_empty_self_loop_edges(self):
"""Remove all self-loop edges from the automaton."""
self._edges = [edge for edge in self._edges if not edge.is_empty_self_loop()]

def _generate_locations(self, location_list: List[str], initial_locations: List[str]):
for location in location_list:
self._locations.add(location["name"])
Expand All @@ -99,7 +103,6 @@ def _generate_variables(self, variable_list: List[dict]):
variable["name"], var_type, init_expr, is_transient)})

def _generate_edges(self, edge_list: List[dict]):
# TODO: Proposal -> Edges might require support variables? In case we want to provide standard ones...
for edge in edge_list:
jani_edge = JaniEdge(edge)
self.add_edge(jani_edge)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"≤": "≤",
"<=": "≤",
"=": "=",
"==": "=",
"≠": "≠",
"!=": "≠",
"!": "¬",
Expand All @@ -62,15 +63,25 @@

# Custom operators (CONVINCE, specific to mobile 2D robot use case)
def intersection_operator(left, right) -> JaniExpression:
return JaniExpression({"op": "intersect", "robot": JaniExpression(left), "barrier": JaniExpression(right)})
return JaniExpression({
"op": "intersect",
"robot": JaniExpression(left),
"barrier": JaniExpression(right)})


def distance_operator(left, right) -> JaniExpression:
return JaniExpression({"op": "distance", "robot": JaniExpression(left), "barrier": JaniExpression(right)})
return JaniExpression({
"op": "distance",
"robot": JaniExpression(left),
"barrier": JaniExpression(right)})


def distance_to_point_operator(robot, target_x, target_y) -> JaniExpression:
return JaniExpression({"op": "distance_to_point", "robot": JaniExpression(robot), "x": JaniExpression(target_x), "y": JaniExpression(target_y)})
return JaniExpression({
"op": "distance_to_point",
"robot": JaniExpression(robot),
"x": JaniExpression(target_x),
"y": JaniExpression(target_y)})


def norm2d_operator(x=None, y=None, *, exp=None) -> JaniExpression:
Expand Down Expand Up @@ -102,7 +113,8 @@ def cross2d_operator(x1=None, y1=None, x2=None, y2=None, *, exp=None) -> JaniExp
exp_y1 = y1
exp_x2 = x2
exp_y2 = y2
assert exp_x1 is not None and exp_y1 is not None and exp_x2 is not None and exp_y2 is not None, "The 2D vectors components must be provided"
assert all(exp is not None for exp in [exp_x1, exp_y1, exp_x2, exp_y2]), \
"The 2D vectors components must be provided"
return minus_operator(multiply_operator(exp_x1, exp_y2), multiply_operator(exp_y1, exp_x2))


Expand All @@ -119,7 +131,8 @@ def dot2d_operator(x1=None, y1=None, x2=None, y2=None, *, exp=None) -> JaniExpre
exp_y1 = y1
exp_x2 = x2
exp_y2 = y2
assert exp_x1 is not None and exp_y1 is not None and exp_x2 is not None and exp_y2 is not None, "The 2D vectors components must be provided"
assert all(exp is not None for exp in [exp_x1, exp_y1, exp_x2, exp_y2]), \
"The 2D vectors components must be provided"
return plus_operator(multiply_operator(exp_x1, exp_x2), multiply_operator(exp_y1, exp_y2))


Expand Down Expand Up @@ -176,7 +189,9 @@ def to_rad_operator(value=None, *, exp=None) -> JaniExpression:


# Functionalities for interpolation
def __expression_interpolation_single_boundary(jani_constants: Dict[str, JaniConstant], robot_name: str, boundary_id: int) -> JaniExpression:
def __expression_interpolation_single_boundary(
jani_constants: Dict[str, JaniConstant],
robot_name: str, boundary_id: int) -> JaniExpression:
n_vertices = jani_constants["boundaries.count"].value()
# Variables names
robot_radius = f"robots.{robot_name}.shape.radius"
Expand All @@ -203,7 +218,8 @@ def __expression_interpolation_single_boundary(jani_constants: Dict[str, JaniCon
# Boundary length
boundary_norm_exp = norm2d_operator(ab_x, ab_y)
# Distance from the robot to the boundary perpendicular to the boundary segment
v_dist_exp = divide_operator(abs_operator(cross2d_operator(ab_x, ab_y, ea_x, ea_y)), boundary_norm_exp)
v_dist_exp = divide_operator(
abs_operator(cross2d_operator(ab_x, ab_y, ea_x, ea_y)), boundary_norm_exp)
# Distance between the boundary extreme points and the robot parallel to the boundary segment
ha_dist_exp = divide_operator(dot2d_operator(ab_x, ab_y, ea_x, ea_y), boundary_norm_exp)
hb_dist_exp = divide_operator(dot2d_operator(ba_x, ba_y, eb_x, eb_y), boundary_norm_exp)
Expand All @@ -212,42 +228,53 @@ def __expression_interpolation_single_boundary(jani_constants: Dict[str, JaniCon
is_parallel_exp = equal_operator(cross2d_operator(ab_x, ab_y, es_x, es_y), 0.0)
# Interpolation factors
ha_interp_exp = if_operator(
and_operator(greater_equal_operator(ha_dist_exp, 0.0), lower_operator(ha_dist_exp, robot_radius)),
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius), dot2d_operator(ab_x, ab_y, ea_x, ea_y)),
and_operator(greater_equal_operator(ha_dist_exp, 0.0),
lower_operator(ha_dist_exp, robot_radius)),
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius),
dot2d_operator(ab_x, ab_y, ea_x, ea_y)),
dot2d_operator(ba_x, ba_y, es_x, es_y)),
1.0)
hb_interp_exp = if_operator(
and_operator(greater_equal_operator(hb_dist_exp, 0.0), lower_operator(hb_dist_exp, robot_radius)),
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius), dot2d_operator(ba_x, ba_y, eb_x, eb_y)),
and_operator(greater_equal_operator(hb_dist_exp, 0.0),
lower_operator(hb_dist_exp, robot_radius)),
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius),
dot2d_operator(ba_x, ba_y, eb_x, eb_y)),
dot2d_operator(ab_x, ab_y, es_x, es_y)),
1.0)
h_interp_exp = if_operator(is_perpendicular_exp, 1.0, min_operator(ha_interp_exp, hb_interp_exp))
h_interp_exp = if_operator(is_perpendicular_exp,
1.0, min_operator(ha_interp_exp, hb_interp_exp))
v_interp_exp = if_operator(
or_operator(is_parallel_exp, greater_equal_operator(v_dist_exp, robot_radius)),
1.0,
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius), abs_operator(cross2d_operator(ab_x, ab_y, ea_x, ea_y))),
divide_operator(minus_operator(multiply_operator(boundary_norm_exp, robot_radius),
abs_operator(cross2d_operator(ab_x, ab_y, ea_x, ea_y))),
abs_operator(cross2d_operator(ab_x, ab_y, es_x, es_y))))
return if_operator(
greater_equal_operator(max_operator(v_dist_exp, max_operator(ha_dist_exp, hb_dist_exp)), robot_radius),
greater_equal_operator(max_operator(v_dist_exp, max_operator(ha_dist_exp, hb_dist_exp)),
robot_radius),
0.0, min_operator(h_interp_exp, v_interp_exp))


def __expression_interpolation_next_boundaries(jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
def __expression_interpolation_next_boundaries(
jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
n_vertices = jani_constants["boundaries.count"].value()
assert isinstance(n_vertices, int) and n_vertices > 1, f"The number of boundaries ({n_vertices}) must greater than 1"
assert isinstance(n_vertices, int) and n_vertices > 1, \
f"The number of boundaries ({n_vertices}) must greater than 1"
if boundary_id >= n_vertices:
return JaniExpression(0.0)
return max_operator(
__expression_interpolation_single_boundary(jani_constants, robot_name, boundary_id),
__expression_interpolation_next_boundaries(jani_constants, robot_name, boundary_id + 1))


def __expression_interpolation_next_obstacles(jani_constants, robot_name, obstacle_id) -> JaniExpression:
def __expression_interpolation_next_obstacles(
jani_constants, robot_name, obstacle_id) -> JaniExpression:
# TODO
return JaniExpression(0.0)


def __expression_interpolation(jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
def __expression_interpolation(
jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
assert isinstance(jani_expression, JaniExpression), "The input must be a JaniExpression"
assert jani_expression.op == "intersect"
robot_name = jani_expression.operands["robot"].identifier
Expand All @@ -264,7 +291,8 @@ def __expression_interpolation(jani_expression: JaniExpression, jani_constants:


# Functionalities for validity check
def __expression_distance_single_boundary(jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
def __expression_distance_single_boundary(
jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
n_vertices = jani_constants["boundaries.count"].value()
# Variables names
robot_radius = f"robots.{robot_name}.shape.radius"
Expand All @@ -287,17 +315,20 @@ def __expression_distance_single_boundary(jani_constants: Dict[str, JaniConstant
# Boundary length
boundary_norm_exp = norm2d_operator(ab_x, ab_y)
# Distance from the robot to the boundary perpendicular to the boundary segment
v_dist_exp = divide_operator(abs_operator(cross2d_operator(ab_x, ab_y, ra_x, ra_y)), boundary_norm_exp)
v_dist_exp = divide_operator(abs_operator(cross2d_operator(ab_x, ab_y, ra_x, ra_y)),
boundary_norm_exp)
# Distance between the boundary extreme points and the robot parallel to the boundary segment
ha_dist_exp = divide_operator(dot2d_operator(ab_x, ab_y, ra_x, ra_y), boundary_norm_exp)
hb_dist_exp = divide_operator(dot2d_operator(ba_x, ba_y, rb_x, rb_y), boundary_norm_exp)
h_dist_exp = max_operator(max_operator(ha_dist_exp, hb_dist_exp), 0.0)
return minus_operator(norm2d_operator(h_dist_exp, v_dist_exp), robot_radius)


def __expression_distance_next_boundaries(jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
def __expression_distance_next_boundaries(
jani_constants: Dict[str, JaniConstant], robot_name, boundary_id) -> JaniExpression:
n_vertices = jani_constants["boundaries.count"].value()
assert isinstance(n_vertices, int) and n_vertices > 1, f"The number of boundaries ({n_vertices}) must greater than 1"
assert isinstance(n_vertices, int) and n_vertices > 1, \
f"The number of boundaries ({n_vertices}) must greater than 1"
if boundary_id >= n_vertices:
return JaniExpression(True)
return min_operator(
Expand All @@ -310,7 +341,8 @@ def __expression_distance_next_obstacles(jani_constants, robot_name, obstacle_id
return JaniExpression(True)


def __expression_distance(jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
def __expression_distance(
jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
assert isinstance(jani_expression, JaniExpression), "The input must be a JaniExpression"
assert jani_expression.op == "distance"
robot_name = jani_expression.operands["robot"].identifier
Expand All @@ -326,28 +358,34 @@ def __expression_distance(jani_expression: JaniExpression, jani_constants: Dict[
raise NotImplementedError("The barrier type is not implemented")


def __expression_distance_to_point(jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
def __expression_distance_to_point(
jani_expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
assert isinstance(jani_expression, JaniExpression), "The input must be a JaniExpression"
assert jani_expression.op == "distance_to_point"
robot_name = jani_expression.operands["robot"].identifier
target_x_cm = to_cm_operator(expand_expression(jani_expression.operands["x"], jani_constants))
target_y_cm = to_cm_operator(expand_expression(jani_expression.operands["y"], jani_constants))
robot_x_cm = f"robots.{robot_name}.pose.x_cm"
robot_y_cm = f"robots.{robot_name}.pose.y_cm"
return to_m_operator(norm2d_operator(minus_operator(robot_x_cm, target_x_cm), minus_operator(robot_y_cm, target_y_cm)))
return to_m_operator(norm2d_operator(minus_operator(robot_x_cm, target_x_cm),
minus_operator(robot_y_cm, target_y_cm)))


def __substitute_expression_op(expression: JaniExpression) -> JaniExpression:
assert isinstance(expression, JaniExpression), "The input must be a JaniExpression"
assert expression.op in BASIC_EXPRESSIONS_MAPPING, f"The operator {expression.op} is not supported"
assert expression.op in BASIC_EXPRESSIONS_MAPPING, \
f"The operator {expression.op} is not supported"
expression.op = BASIC_EXPRESSIONS_MAPPING[expression.op]
return expression


def expand_expression(expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
def expand_expression(
expression: JaniExpression, jani_constants: Dict[str, JaniConstant]) -> JaniExpression:
# Given a CONVINCE JaniExpression, expand it to a plain JaniExpression
assert isinstance(expression, JaniExpression), f"The expression should be a JaniExpression instance, found {type(expression)} instead."
assert expression.is_valid(), "The expression is not valid: it defines no value, nor variable, nor operation to be done."
assert isinstance(expression, JaniExpression), \
f"The expression should be a JaniExpression instance, found {type(expression)} instead."
assert expression.is_valid(), \
"The expression is not valid: it defines no value, nor variable, nor operation to be done."
if expression.op is None:
# It is either a variable/constant identifier or a value
return expression
Expand All @@ -357,7 +395,7 @@ def expand_expression(expression: JaniExpression, jani_constants: Dict[str, Jani
return __expression_distance(expression, jani_constants)
if expression.op == "distance_to_point":
return __expression_distance_to_point(expression, jani_constants)
# If the expressions is neither of the above, we expand the operands and then we return the expanded expression
# If the expressions is neither of the above, we expand the operands and return them
for key, value in expression.operands.items():
expression.operands[key] = expand_expression(value, jani_constants)
if expression.op == "norm2d":
Expand Down
5 changes: 5 additions & 0 deletions jani_generator/src/jani_generator/jani_entries/jani_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def get_action(self) -> Optional[str]:
"""Get the action name, if set."""
return self.action

def is_empty_self_loop(self) -> bool:
"""Check if the edge is an empty self loop (i.e. has no assignments)."""
return len(self.destinations) == 1 and self.location == self.destinations[0]["location"] \
and len(self.destinations[0]["assignments"]) == 0

def set_action(self, action_name: str):
"""Set the action name."""
self.action = action_name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@


class JaniExpression:
"""
Jani Expression class.
Content of an instance of this class can be:
- identifier: a string representing a reference to a constant or variable (literal)
or
- value: a JaniValue object
or
- op: a string representing an operator
- operands: a dictionary of operands, related to the specified operator
"""
def __init__(self, expression: Union[SupportedExp, 'JaniExpression', JaniValue]):
self.identifier: str = None
self.value: JaniValue = None
Expand Down Expand Up @@ -74,7 +85,7 @@ def _get_operands(self, expression_dict: dict):
"&&", "||", "and", "or", "∨", "∧",
"⇒", "=>", "=", "≠", "!=", "+", "-", "*", "%",
"pow", "log", "/", "min", "max",
"<", "≤", ">", "≥", "<=", ">=")):
"<", "≤", ">", "≥", "<=", ">=", "==")):
return {
"left": JaniExpression(expression_dict["left"]),
"right": JaniExpression(expression_dict["right"])}
Expand Down
Loading

0 comments on commit 8c6666a

Please sign in to comment.