From a53d243992cc5f6f1f0e2eced2eb7b8b05d7bfa2 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia <65171491+MarcoLm993@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:53:12 +0100 Subject: [PATCH] Update uc2 models for paper and adjust random number generator (#77) Signed-off-by: Marco Lampacrescia --- docs/source/howto.rst | 24 ++++++++++- .../jani_convince_expression_expansion.py | 5 +-- .../jani_entries/jani_helpers.py | 12 +++--- .../scxml_helpers/scxml_event.py | 24 +++++++++++ .../scxml_helpers/scxml_tags.py | 5 ++- .../scxml_converter/scxml_entries/bt_utils.py | 10 +++++ .../scxml_entries/ros_utils.py | 21 ++++++++++ .../grid_robot_blackboard/world.scxml | 8 ++-- .../bt_update_goal_and_current_position.scxml | 12 +++--- .../uc2_assembly/Main/properties.jani | 42 ++++++++++++++++--- .../uc2_assembly/Skills/MoveBlockSkill.scxml | 8 ++-- .../test_systemtest_scxml_to_jani.py | 4 +- .../test_unittest_expression_expansion.py | 22 +++++----- 13 files changed, 153 insertions(+), 44 deletions(-) diff --git a/docs/source/howto.rst b/docs/source/howto.rst index ba2175c0..a0c6ae6e 100644 --- a/docs/source/howto.rst +++ b/docs/source/howto.rst @@ -311,7 +311,8 @@ As in the case of ROS functionalities, BT ports need to be declared before being .. code-block:: xml - + + Once declared, it is possible to reference to the port in multiple SCXML entries. @@ -337,8 +338,27 @@ Or we can use `start_value` to define the initial value of a variable. +Finally, we can store a specific value to the blackboard (only for output ports). -BT ports can also be linked to variables in the `BT Blackboard` by wrapping the variable name in curly braces in the BT XML file. However, this feature is not yet supported. +.. code-block:: xml + + + + ... + + ... + + ... + + + +BT Ports can be declared either as input or output ports: + +* input ports can refer to either fixed or mutable variables (i.e. blackboard variables) +* output ports on only refer to mutable variables + +When a BT plugin declares an output port, this must be referenced to a `BT Blackboard` variable. +This is defined in the BT XML file, by providing a blackboard variable name wrapped by curly braces. .. _main_xml_howto: diff --git a/src/as2fm/jani_generator/jani_entries/jani_convince_expression_expansion.py b/src/as2fm/jani_generator/jani_entries/jani_convince_expression_expansion.py index 22a877e1..d6526411 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_convince_expression_expansion.py +++ b/src/as2fm/jani_generator/jani_entries/jani_convince_expression_expansion.py @@ -508,7 +508,7 @@ def expand_expression( def expand_distribution_expressions( - expression: JaniExpression, *, n_options: int = 101 + expression: JaniExpression, *, n_options ) -> List[JaniExpression]: """ Traverse the expression and substitute each distribution with n expressions. @@ -543,8 +543,7 @@ def expand_distribution_expressions( dist_width = expression.get_dist_args()[1] - lower_bound # Generate a (constant) JaniExpression for each possible outcome return [ - JaniExpression(lower_bound + (x * dist_width / (n_options - 1))) - for x in range(n_options) + JaniExpression(lower_bound + (x * dist_width / (n_options))) for x in range(n_options) ] return [expression] diff --git a/src/as2fm/jani_generator/jani_entries/jani_helpers.py b/src/as2fm/jani_generator/jani_entries/jani_helpers.py index ed4b4772..12f8ce1e 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_helpers.py +++ b/src/as2fm/jani_generator/jani_entries/jani_helpers.py @@ -43,9 +43,7 @@ def _generate_new_edge_for_random_assignments( ) -def _expand_random_variables_in_edge( - jani_edge: JaniEdge, *, n_options: int = 101 -) -> List[JaniEdge]: +def _expand_random_variables_in_edge(jani_edge: JaniEdge, *, n_options: int) -> List[JaniEdge]: """ If there are random variables in the input JaniEdge, generate new edges to handle it. @@ -100,17 +98,19 @@ def _expand_random_variables_in_edge( return generated_edges -def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int = 101) -> None: +def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int) -> None: """Find all expression containing the 'distribution' expression and expand them.""" # Check that no global variable has a random value (not supported) for g_var_name, g_var in model.get_variables().items(): assert ( - len(expand_distribution_expressions(g_var.get_init_expr())) == 1 + len(expand_distribution_expressions(g_var.get_init_expr(), n_options=100)) == 1 ), f"Global variable {g_var_name} is init using a random value. This is unsupported." for automaton in model.get_automata(): # Also for automaton, check variables initialization for aut_var_name, aut_var in automaton.get_variables().items(): - assert len(expand_distribution_expressions(aut_var.get_init_expr())) == 1, ( + assert ( + len(expand_distribution_expressions(aut_var.get_init_expr(), n_options=100)) == 1 + ), ( f"Variable {aut_var_name} in automaton {automaton.get_name()} is init using random " f"values: init expr = '{aut_var.get_init_expr().as_dict()}'. This is unsupported." ) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_event.py b/src/as2fm/jani_generator/scxml_helpers/scxml_event.py index c640bd66..ab038d99 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_event.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_event.py @@ -21,6 +21,13 @@ from typing import Dict, List, Optional from as2fm.jani_generator.ros_helpers.ros_timer import ROS_TIMER_RATE_EVENT_PREFIX +from as2fm.scxml_converter.scxml_entries.bt_utils import is_bt_response_event, is_bt_tick_event +from as2fm.scxml_converter.scxml_entries.ros_utils import ( + is_action_request_event, + is_action_result_event, + is_action_thread_event, + is_srv_event, +) class EventSender: @@ -142,3 +149,20 @@ def get_events(self) -> Dict[str, Event]: def add_event(self, event: Event): assert event.name not in self._events, f"Event {event.name} must not be added twice." self._events[event.name] = event + + +def is_event_synched(event_name: str) -> bool: + """ + Check if the event is synched, hence there should not be autogenerated self loops. + + :param event_name: The name of the event to evaluate. + """ + # Action feedbacks are not considered synched, since a client might discard one or more of them + return ( + is_bt_tick_event(event_name) + or is_bt_response_event(event_name) + or is_action_request_event(event_name) + or is_action_result_event(event_name) + or is_action_thread_event(event_name) + or is_srv_event(event_name) + ) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py index dcdf92ba..474bb942 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py @@ -53,7 +53,7 @@ get_variable_type, is_variable_array, ) -from as2fm.jani_generator.scxml_helpers.scxml_event import Event, EventsHolder +from as2fm.jani_generator.scxml_helpers.scxml_event import Event, EventsHolder, is_event_synched from as2fm.jani_generator.scxml_helpers.scxml_expression import ( ArrayInfo, parse_ecmascript_to_jani_expression, @@ -627,6 +627,9 @@ def add_unhandled_transitions(self, transitions_set: Set[str]): if event_name in self._events_no_condition or len(event_name) == 0: continue guard_exp = self.get_guard_exp_for_prev_conditions(event_name) + # If the event was not handled in the state and is expected to be synched, skip it + if guard_exp is None and is_event_synched(event_name): + continue edges, locations = _append_scxml_body_to_jani_automaton( self.automaton, self.events_holder, diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 45bb2fcf..e5abe601 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -76,11 +76,21 @@ def generate_bt_tick_event(instance_id: str) -> str: return f"bt_{instance_id}_tick" +def is_bt_tick_event(event_name: str) -> bool: + """Check is the event is used for ticking a BT node.""" + return re.match(r"^bt_.+_tick$", event_name) is not None + + def generate_bt_response_event(instance_id: str) -> str: """Generate the BT response event name for a given BT node instance.""" return f"bt_{instance_id}_response" +def is_bt_response_event(event_name: str) -> bool: + """Check if the event name is for BT node's responses(success, failure, running).""" + return re.match(r"^bt_.+_response$", event_name) is not None + + def is_bt_event(event_name: str) -> bool: """Given an event name, returns whether it is related to a BT event or not.""" bt_events = [f"bt_{suffix}" for suffix in ["tick", "running", "success", "failure"]] diff --git a/src/as2fm/scxml_converter/scxml_entries/ros_utils.py b/src/as2fm/scxml_converter/scxml_entries/ros_utils.py index 56c9dd84..4f4352cd 100644 --- a/src/as2fm/scxml_converter/scxml_entries/ros_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/ros_utils.py @@ -15,6 +15,7 @@ """Collection of SCXML utilities related to ROS functionalities.""" +import re from typing import Any, Dict, List, Tuple, Type from as2fm.scxml_converter.scxml_entries import RosField, ScxmlBase @@ -196,6 +197,11 @@ def generate_srv_server_response_event(service_name: str) -> str: return f"srv_{sanitize_ros_interface_name(service_name)}_response" +def is_srv_event(event_name) -> str: + """Check whether the event name matches the ROS service plain events pattern.""" + return re.match(r"^srv_.+_(response|request|req_client).*$", event_name) is not None + + def generate_action_goal_req_event(action_name: str, client_name: str) -> str: """Generate the name of the event that sends an action goal from a client to the server.""" return f"action_{sanitize_ros_interface_name(action_name)}_goal_req_client_{client_name}" @@ -262,6 +268,21 @@ def generate_action_result_handle_event(action_name: str, automaton_name: str) - ) +def is_action_request_event(event_name: str) -> bool: + """Check whether the event name matches the ROS action plain events pattern.""" + return re.match(r"^action_.+_goal_.+$", event_name) is not None + + +def is_action_result_event(event_name: str) -> bool: + """Check whether the event name matches the ROS action plain events pattern.""" + return re.match(r"^action_.+_result.*$", event_name) is not None + + +def is_action_thread_event(event_name: str) -> bool: + """Check whether the event name matches the ROS action plain events pattern.""" + return re.match(r"^action_.+_thread_(start|free)$", event_name) is not None + + class ScxmlRosDeclarationsContainer: """Object that contains a description of the ROS declarations in the SCXML root.""" diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml index 8e7ecf02..29bdc6de 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -25,8 +25,8 @@ - - + + @@ -36,8 +36,8 @@ - - + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml index 3f56fcb5..7f61f3c8 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml @@ -28,8 +28,8 @@ - - + + @@ -37,8 +37,8 @@ - - + + @@ -48,8 +48,8 @@ - - + + diff --git a/test/jani_generator/_test_data/uc2_assembly/Main/properties.jani b/test/jani_generator/_test_data/uc2_assembly/Main/properties.jani index d1e4a592..f079c67d 100644 --- a/test/jani_generator/_test_data/uc2_assembly/Main/properties.jani +++ b/test/jani_generator/_test_data/uc2_assembly/Main/properties.jani @@ -1,14 +1,14 @@ { "properties": [ { - "name": "can_execute_recovery_branch", + "name": "executes_recovery_branch_or_success", "expression": { "op": "filter", "fun": "values", "values": { "op": "Pmin", "exp": { - "comment": "((abort_time > 0) ⇒ (clock < abort_time + 5000)) U (recover_block_running > 0)", + "comment": "((abort_time > 0) ⇒ (clock < abort_time + 5000)) U (recover_block_running > 0 || tree_success)", "op": "U", "left": { "op": "⇒", @@ -28,9 +28,41 @@ } }, "right": { - "op": ">", - "left": "topic_uc2__info__properties__recover_block_running_msg.ros_fields__data", - "right": 0 + "op": "∨", + "left": { + "op": ">", + "left": "topic_uc2__info__properties__recover_block_running_msg.ros_fields__data", + "right": 0 + }, + "right": { + "op": "=", + "left": "bt_1000_response.status", + "right": 1, + "comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}" + } + } + } + }, + "states": { + "op": "initial" + } + } + }, + { + "name": "move_success", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "comment": "The tree succeeds only in case MoveBlock succeeds, but currently not working, since after recovery the plugin MoveAction will FAIL anyway! This is a problem with the missing reset: when RecoverBlock runs, the MoveAction plugin shall be reset, preventing failure from happening...", + "op": "F", + "exp": { + "op": "=", + "left": "bt_1000_response.status", + "right": 1, + "comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}" } } }, diff --git a/test/jani_generator/_test_data/uc2_assembly/Skills/MoveBlockSkill.scxml b/test/jani_generator/_test_data/uc2_assembly/Skills/MoveBlockSkill.scxml index 1b00a2d1..9934df8e 100644 --- a/test/jani_generator/_test_data/uc2_assembly/Skills/MoveBlockSkill.scxml +++ b/test/jani_generator/_test_data/uc2_assembly/Skills/MoveBlockSkill.scxml @@ -10,7 +10,7 @@ - + @@ -38,15 +38,15 @@ - - + + - + diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index f134bf88..00339112 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -427,7 +427,7 @@ def test_uc2_assembly(self): self._test_with_main( os.path.join("uc2_assembly", "Main"), model_xml="main.xml", - property_name="can_execute_recovery_branch", + property_name="executes_recovery_branch_or_success", success=True, ) @@ -436,7 +436,7 @@ def test_uc2_assembly_with_bug(self): self._test_with_main( os.path.join("uc2_assembly", "Main"), model_xml="main_bug.xml", - property_name="can_execute_recovery_branch", + property_name="executes_recovery_branch_or_success", success=False, ) diff --git a/test/jani_generator/test_unittest_expression_expansion.py b/test/jani_generator/test_unittest_expression_expansion.py index 7ca8e3b4..f383e547 100644 --- a/test/jani_generator/test_unittest_expression_expansion.py +++ b/test/jani_generator/test_unittest_expression_expansion.py @@ -29,13 +29,13 @@ def test_jani_expression_expansion_no_distribution(): Test the expansion of an expression containing no distribution (should stay the same). """ jani_entry = generate_jani_expression(5) - jani_expressions = expand_distribution_expressions(jani_entry) + jani_expressions = expand_distribution_expressions(jani_entry, n_options=100) assert len(jani_expressions) == 1, "Expression without distribution should not be expanded!" assert jani_entry.as_dict() == jani_expressions[0].as_dict() jani_entry = generate_jani_expression( {"op": "*", "left": 2, "right": {"op": "floor", "exp": 1.1}} ) - jani_expressions = expand_distribution_expressions(jani_entry) + jani_expressions = expand_distribution_expressions(jani_entry, n_options=100) assert len(jani_expressions) == 1, "Expression without distribution should not be expanded!" assert jani_entry.as_dict() == jani_expressions[0].as_dict() @@ -45,13 +45,13 @@ def test_jani_expression_expansion_distribution(): Test the expansion of an expression with only a distribution. """ # Simplest case, just a distribution. Boundaries are included - n_options = 101 + n_options = 100 jani_distribution = generate_jani_expression({"distribution": "Uniform", "args": [1.0, 3.0]}) jani_expressions = expand_distribution_expressions(jani_distribution, n_options=n_options) assert len(jani_expressions) == n_options, "Base distribution was not expanded!" assert all(expr.as_literal() is not None for expr in jani_expressions) assert jani_expressions[0].as_literal().value() == pytest.approx(1.0) - assert jani_expressions[100].as_literal().value() == pytest.approx(3.0) + assert jani_expressions[99].as_literal().value() == pytest.approx(2.98) assert jani_expressions[10].as_literal().value() == pytest.approx(1.2) # Test a non trivial expression jani_distribution = generate_jani_expression( @@ -74,9 +74,9 @@ def test_jani_expression_expansion_distribution(): "op": "floor", "exp": {"op": "*", "left": 0.1, "right": 20}, } - assert jani_expressions[100].as_dict() == { + assert jani_expressions[99].as_dict() == { "op": "floor", - "exp": {"op": "*", "left": 1.0, "right": 20}, + "exp": {"op": "*", "left": 0.99, "right": 20}, } @@ -85,7 +85,7 @@ def test_jani_expression_expansion_expr_with_multiple_distribution(): Test the expansion of complex expressions with multiple distributions. """ # Multiple distributions at the same level - n_options = 21 + n_options = 20 jani_distribution = generate_jani_expression( { "op": "floor", @@ -104,11 +104,11 @@ def test_jani_expression_expansion_expr_with_multiple_distribution(): } assert jani_expressions[-1].as_dict() == { "op": "floor", - "exp": {"op": "*", "left": 20.0, "right": 10.0}, + "exp": {"op": "*", "left": 19.0, "right": 9.5}, } assert jani_expressions[-2].as_dict() == { "op": "floor", - "exp": {"op": "*", "left": 20.0, "right": 9.5}, + "exp": {"op": "*", "left": 19.0, "right": 9.0}, } # Multiple distributions at a different level jani_distribution = generate_jani_expression( @@ -131,8 +131,8 @@ def test_jani_expression_expansion_expr_with_multiple_distribution(): } assert jani_expressions[-1].as_dict() == { "op": "*", - "left": 20.0, - "right": {"op": "*", "left": 2, "right": 10.0}, + "left": 19.0, + "right": {"op": "*", "left": 2, "right": 9.5}, } assert jani_expressions[1].as_dict() == { "op": "*",