From b1c63c200c036743abdace290dc5711277b8390c Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 26 Nov 2024 15:57:26 +0100 Subject: [PATCH 01/58] Initial implementation of Blackboard example Co-authored-by: Christian Henkel Signed-off-by: Marco Lampacrescia --- .../grid_robot_interfaces/CMakeLists.txt | 42 ++++++++++++++++ .../grid_robot_interfaces/README.md | 1 + .../grid_robot_interfaces/msg/Int2D.msg | 2 + .../grid_robot_interfaces/package.xml | 22 +++++++++ .../_test_data/grid_robot_blackboard/bt.xml | 33 +++++++++++++ .../_test_data/grid_robot_blackboard/main.xml | 19 +++++++ .../grid_robot_blackboard/world.scxml | 49 +++++++++++++++++++ 7 files changed, 168 insertions(+) create mode 100644 ros_support_interfaces/grid_robot_interfaces/CMakeLists.txt create mode 100644 ros_support_interfaces/grid_robot_interfaces/README.md create mode 100644 ros_support_interfaces/grid_robot_interfaces/msg/Int2D.msg create mode 100644 ros_support_interfaces/grid_robot_interfaces/package.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/bt.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/main.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/world.scxml diff --git a/ros_support_interfaces/grid_robot_interfaces/CMakeLists.txt b/ros_support_interfaces/grid_robot_interfaces/CMakeLists.txt new file mode 100644 index 00000000..cd9e906b --- /dev/null +++ b/ros_support_interfaces/grid_robot_interfaces/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.5) +project(grid_robot_interfaces) + +# Default to C99 +if(NOT CMAKE_C_STANDARD) + set(CMAKE_C_STANDARD 99) +endif() + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +# uncomment the following section in order to fill in +# further dependencies manually. +find_package(std_msgs REQUIRED) + +find_package(rosidl_default_generators REQUIRED) + +rosidl_generate_interfaces(${PROJECT_NAME} + "msg/Int2D.msg" + DEPENDENCIES std_msgs + ) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # the following line skips the linter which checks for copyrights + # uncomment the line when a copyright and license is not present in all source files + #set(ament_cmake_copyright_FOUND TRUE) + # the following line skips cpplint (only works in a git repo) + # uncomment the line when this package is not in a git repo + #set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/ros_support_interfaces/grid_robot_interfaces/README.md b/ros_support_interfaces/grid_robot_interfaces/README.md new file mode 100644 index 00000000..b4a6a3b7 --- /dev/null +++ b/ros_support_interfaces/grid_robot_interfaces/README.md @@ -0,0 +1 @@ +Used in `test/jani_generator/_test_data/grid_robot_blackboard` diff --git a/ros_support_interfaces/grid_robot_interfaces/msg/Int2D.msg b/ros_support_interfaces/grid_robot_interfaces/msg/Int2D.msg new file mode 100644 index 00000000..10fb937a --- /dev/null +++ b/ros_support_interfaces/grid_robot_interfaces/msg/Int2D.msg @@ -0,0 +1,2 @@ +int32 x +int32 y diff --git a/ros_support_interfaces/grid_robot_interfaces/package.xml b/ros_support_interfaces/grid_robot_interfaces/package.xml new file mode 100644 index 00000000..3f8a6782 --- /dev/null +++ b/ros_support_interfaces/grid_robot_interfaces/package.xml @@ -0,0 +1,22 @@ + + + + grid_robot_interfaces + 0.0.0 + TODO: Package description + root + TODO: License declaration + + ament_cmake + std_msgs + ament_lint_auto + ament_lint_common + + rosidl_default_generators + rosidl_default_runtime + rosidl_interface_packages + + + ament_cmake + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml new file mode 100644 index 00000000..aba8d127 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/main.xml b/test/jani_generator/_test_data/grid_robot_blackboard/main.xml new file mode 100644 index 00000000..c03ce433 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/main.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml new file mode 100644 index 00000000..8adab62a --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f9d05bbadf251c9ca49247a2f941a171784cfb2d Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Tue, 26 Nov 2024 16:27:40 +0100 Subject: [PATCH 02/58] a brand new world Signed-off-by: Christian Henkel --- .../grid_robot_blackboard/world.scxml | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) 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 8adab62a..1f0eb530 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -5,45 +5,56 @@ name="world" initial="init"> - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + From 1b09d13ab2dc88e354d577996b6d68dffb123b31 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Tue, 26 Nov 2024 16:31:08 +0100 Subject: [PATCH 03/58] valid xml Signed-off-by: Christian Henkel --- .../grid_robot_blackboard/world.scxml | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) 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 1f0eb530..68d6902f 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -5,56 +5,56 @@ name="world" initial="init"> - - - - - - - + + + + + + + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + From d28b03da4da965bcfd9043c4c2f09e3b51bf9056 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 26 Nov 2024 17:37:31 +0100 Subject: [PATCH 04/58] Implement some plugins Signed-off-by: Marco Lampacrescia --- .../_test_data/grid_robot_blackboard/bt.xml | 10 +-- .../grid_robot_blackboard/bt_shall_move.scxml | 52 ++++++++++++++++ .../bt_update_goal_and_current_position.scxml | 61 +++++++++++++++++++ .../grid_robot_blackboard/world.scxml | 8 +-- 4 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml index aba8d127..e7a9668b 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt.xml @@ -2,7 +2,7 @@ - + - + - + - + - + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml new file mode 100644 index 00000000..549150b5 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml new file mode 100644 index 00000000..9192b845 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 68d6902f..1c34967e 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -7,10 +7,10 @@ - - - - + + + + From e949f5ba5caa8680b273e1a2f1ea2d7c39dd321b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 27 Nov 2024 11:21:42 +0100 Subject: [PATCH 05/58] Continue implementation Signed-off-by: Marco Lampacrescia --- .../grid_robot_blackboard/bt_move.scxml | 38 +++++++++++++++++++ .../grid_robot_blackboard/bt_shall_move.scxml | 10 ++--- .../bt_update_goal_and_current_position.scxml | 16 ++++---- .../_test_data/grid_robot_blackboard/main.xml | 6 ++- 4 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml new file mode 100644 index 00000000..3b9afcc6 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml index 549150b5..a3d60610 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml @@ -1,8 +1,8 @@ @@ -19,8 +19,6 @@ - - @@ -29,8 +27,8 @@ - - + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml index 9192b845..0f797429 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml @@ -1,8 +1,8 @@ @@ -25,27 +25,27 @@ - + - + - + - + - + - + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/main.xml b/test/jani_generator/_test_data/grid_robot_blackboard/main.xml index c03ce433..68f8d889 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/main.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/main.xml @@ -2,11 +2,15 @@ + + - + + + From 1fb5849fc0c0913dabe37cab160cc7d7bc050b11 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 27 Nov 2024 11:31:08 +0100 Subject: [PATCH 06/58] Check the BT Tree return status instead of publishing empty msgs Signed-off-by: Marco Lampacrescia --- .../uc1_docking/bt_publish_empty_msg.scxml | 25 ------------- .../_test_data/uc1_docking/main.xml | 1 - .../uc1_docking/main_with_problem.xml | 1 - .../_test_data/uc1_docking/policy.xml | 35 ++++++++----------- .../_test_data/uc1_docking/properties.jani | 7 +++- 5 files changed, 21 insertions(+), 48 deletions(-) delete mode 100644 test/jani_generator/_test_data/uc1_docking/bt_publish_empty_msg.scxml diff --git a/test/jani_generator/_test_data/uc1_docking/bt_publish_empty_msg.scxml b/test/jani_generator/_test_data/uc1_docking/bt_publish_empty_msg.scxml deleted file mode 100644 index 263a182c..00000000 --- a/test/jani_generator/_test_data/uc1_docking/bt_publish_empty_msg.scxml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/uc1_docking/main.xml b/test/jani_generator/_test_data/uc1_docking/main.xml index 0540a327..6b6cd750 100644 --- a/test/jani_generator/_test_data/uc1_docking/main.xml +++ b/test/jani_generator/_test_data/uc1_docking/main.xml @@ -13,7 +13,6 @@ - diff --git a/test/jani_generator/_test_data/uc1_docking/main_with_problem.xml b/test/jani_generator/_test_data/uc1_docking/main_with_problem.xml index 3fa69a13..941aa1b3 100644 --- a/test/jani_generator/_test_data/uc1_docking/main_with_problem.xml +++ b/test/jani_generator/_test_data/uc1_docking/main_with_problem.xml @@ -13,7 +13,6 @@ - diff --git a/test/jani_generator/_test_data/uc1_docking/policy.xml b/test/jani_generator/_test_data/uc1_docking/policy.xml index 7c61812e..11cbb5e2 100644 --- a/test/jani_generator/_test_data/uc1_docking/policy.xml +++ b/test/jani_generator/_test_data/uc1_docking/policy.xml @@ -1,24 +1,19 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/uc1_docking/properties.jani b/test/jani_generator/_test_data/uc1_docking/properties.jani index 57f65d54..8df0aa58 100644 --- a/test/jani_generator/_test_data/uc1_docking/properties.jani +++ b/test/jani_generator/_test_data/uc1_docking/properties.jani @@ -38,7 +38,12 @@ "op": "Pmin", "exp": { "op": "F", - "exp": "topic_tree_succeeded_msg.valid" + "exp": { + "op": "=", + "left": "bt_1000_response.status", + "right": 1, + "comment": "1: SUCCESS, 2: FAILURE, 3: RUNNING" + } } }, "states": { From 4c24685d5f0d9073a423f43dbf1ddf29de6f1069 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 27 Nov 2024 15:06:55 +0100 Subject: [PATCH 07/58] Some braindump about how to proceed with the blackboard implementation Signed-off-by: Marco Lampacrescia --- .../graphics/blackboard_to_scxml.drawio.svg | 105 ++++++++++++++++++ docs/source/scxml-jani-conversion.rst | 95 ++++++++++------ .../grid_robot_blackboard/bt_shall_move.scxml | 26 +++++ .../grid_robot_blackboard/properties.jani | 26 +++++ 4 files changed, 216 insertions(+), 36 deletions(-) create mode 100644 docs/source/graphics/blackboard_to_scxml.drawio.svg create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard/properties.jani diff --git a/docs/source/graphics/blackboard_to_scxml.drawio.svg b/docs/source/graphics/blackboard_to_scxml.drawio.svg new file mode 100644 index 00000000..2cd13407 --- /dev/null +++ b/docs/source/graphics/blackboard_to_scxml.drawio.svg @@ -0,0 +1,105 @@ + + + + + + + +
+
+
+ idle +
+
+
+
+ + idle + +
+
+ + + + + + + +
+
+
+ bt_blackboard_set_<bb_var_x> +
+ * assign bb_var_x = _event.data +
+
+
+
+ + bt_blackboard_set_<bb_var_x>... + +
+
+ + + + + +
+
+
+ bt_blackboard_get_<bb_var_x> +
+ * send bt_blackboard_var_<bb_var_x> +
+ * field data = bb_var_x +
+
+
+
+ + bt_blackboard_get_<bb_var_x>... + +
+
+ + + + + + +
+
+
+ + + datamodel + +
+
+ * bb_var_1 +
+ * bb_var_2 +
+ ... +
+ * bb_var_n +
+
+
+
+ + datamodel... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/source/scxml-jani-conversion.rst b/docs/source/scxml-jani-conversion.rst index b417000c..870864bf 100644 --- a/docs/source/scxml-jani-conversion.rst +++ b/docs/source/scxml-jani-conversion.rst @@ -33,14 +33,68 @@ Low-Level SCXML Conversion Low-Level SCXML is the standard SCXML format defined `here `_. -Our converter is able to transform high-level SCXML to low-level SCXML by translating the ROS specific features to standard SCXML features. -In case of timers, we need additional information that cannot be encoded in SCXML, such that information is generated at runtime. +Our converter is able to transform High-Level (HL) SCXML to Low-Level (LL) SCXML by translating ROS and BT specific features to standard SCXML features. +This applies also for timers: we generate an additional SCXML FSM encoding a global clock, sending out events to trigger the timers defined in the model at the correct rate. -The conversion between the two SCXML formats is implemented in ScxmlRoot.as_plain_scxml(). TODO: Link to API. +The next subsections describe our conversion strategy from HL-SCXML to LL-SCXML. +The entry-point for the conversion is implemented in ScxmlRoot.as_plain_scxml(). TODO: Link to API. -TODO: Describe how we translate the high-level SCXML to the low-level SCXML. +Handling of (ROS) Timers +__________________________ + +TODO + +Handling of (ROS) Services +_____________________________ + +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 SCXML related state machines can be inspected in the diagram below: + +.. image:: graphics/ros_service_to_scxml.drawio.svg + :alt: Handling of ROS Services + :align: center + +The automata of clients and services are converted directly from the existing ROS-SCXML files, while the "Extra Service Handler" is autogenerated starting from the provided clients and services. + + +Handling of (ROS) Actions +_____________________________ + +ROS actions are handled similarly to ROS Services: a ROS-SCXML description of the system is converted to plain SCXML, and an additional automaton is generated to handle the synchronization between the clients and the server. + +The structure of a client-server communication through actions and additional threads looks as follows: + +.. image:: graphics/ros_action_to_scxml.drawio.svg + :alt: Handling of ROS Actions + :align: center + + +Handling the BT Blackboard +_____________________________ + +The Blackboard is a container that shares variables across different BT plugins. The value if those variables normally changes over time, and is expected to be updated at each tick. + +In LL-SCXML, this is handled by an autogenerated SCXML FSM, that receives the updates from the various plugins and, upon request, provides the data. +This diagram summarizes the FSM structure. + +.. image:: graphics/blackboard_to_scxml.drawio.svg + :alt: Blackboard FSM + :align: center -TODO: Timers are useful for SCAN as well: instead of keeping them in a runtime object, we can consider to list them in an intermediary XML file. + +Setting Blackboard Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This can be done using the tag `bt_set_output` in HL-SCXML. In LL-SCXML this translates to a send event, that is received by the Blackboard FSM to update the internal data. + +Reading Blackboard Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At the current state, to read the internal variables there needs to be some message exchange between the plugin FSM and the Blackboard FSM. +This needs to happen for each and every Blackboard variable that is read. + +Optimization is nevertheless possible (sending the whole set of blackboard variables each time), but this would diverge from the Blackboard.CPP implementation, so it should rather be an automatic conversion. JANI Conversion ---------------- @@ -114,34 +168,3 @@ The JANI model resulting from applying the conversion strategies we just describ :align: center It can be seen 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 one advancing the `A_B sender` automaton. - - -Handling of (ROS) Timers -__________________________ - -TODO - -Handling of (ROS) Services -_____________________________ - -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 SCXML related state machines can be inspected in the diagram below: - -.. image:: graphics/ros_service_to_scxml.drawio.svg - :alt: Handling of ROS Services - :align: center - -The automata of clients and services are converted directly from the existing ROS-SCXML files, while the "Extra Service Handler" is autogenerated starting from the provided clients and services. - - -Handling of (ROS) Actions -_____________________________ - -ROS actions are handled similarly to ROS Services: a ROS-SCXML description of the system is converted to plain SCXML, and an additional automaton is generated to handle the synchronization between the clients and the server. - -The structure of a client-server communication through actions and additional threads looks as follows: - -.. image:: graphics/ros_action_to_scxml.drawio.svg - :alt: Handling of ROS Actions - :align: center diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml index a3d60610..90a8a178 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_shall_move.scxml @@ -25,11 +25,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani new file mode 100644 index 00000000..1094fe1c --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani @@ -0,0 +1,26 @@ +{ + "properties": [ + { + "name": "tree_success", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "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}" + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} From 87b8f09e5afbf2d40e49f83a7cdb42c1b7ea7652 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 28 Nov 2024 08:50:59 +0100 Subject: [PATCH 08/58] Enable the test (will fail) Signed-off-by: Marco Lampacrescia --- .../graphics/blackboard_to_scxml.drawio.svg | 20 ++++++++++--------- .../test_systemtest_scxml_to_jani.py | 9 +++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/source/graphics/blackboard_to_scxml.drawio.svg b/docs/source/graphics/blackboard_to_scxml.drawio.svg index 2cd13407..3e6dfaf1 100644 --- a/docs/source/graphics/blackboard_to_scxml.drawio.svg +++ b/docs/source/graphics/blackboard_to_scxml.drawio.svg @@ -1,4 +1,4 @@ - + @@ -45,25 +45,27 @@ -
+
- bt_blackboard_get_<bb_var_x> + bt_blackboard_req
- * send bt_blackboard_var_<bb_var_x> + * send bt_blackboard_get
- * field data = bb_var_x + * field bb_var_1 = bb_var_1 +
+ * .... +
+ * field bb_var_n = bb_var_n
- - bt_blackboard_get_<bb_var_x>... + + bt_blackboard_req... - - diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 5a69f6d8..8e44204b 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -434,6 +434,15 @@ def test_uc2_assembly_with_bug(self): success=False, ) + def test_grid_robot_blackboard(self): + """Test the grid_robot_blackboard model (BT + Blackboard).""" + self._test_with_main( + "grid_robot_blackboard", + model_xml="main.xml", + property_name="tree_success", + success=True, + ) + def test_command_line_output_with_line_numbers(self): """Test the command line output with line numbers for the main.xml file.""" tmp_test_dir = os.path.join("/tmp", "test_as2fm") From 6d690ebc48717531fdadc140ec80881982ba0abc Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 28 Nov 2024 13:35:47 +0100 Subject: [PATCH 09/58] Implemented unctionality to extract BT variables information from fsms in the model Signed-off-by: Marco Lampacrescia --- .../scxml_helpers/top_level_interpreter.py | 4 ++- src/as2fm/scxml_converter/bt_converter.py | 26 +++++++++++++++++++ .../scxml_converter/scxml_entries/bt_utils.py | 12 ++++++++- .../scxml_entries/scxml_root.py | 8 ++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py index 9c4f1ee2..b98faec8 100644 --- a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py +++ b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py @@ -36,7 +36,7 @@ from as2fm.jani_generator.ros_helpers.ros_service_handler import RosServiceHandler from as2fm.jani_generator.ros_helpers.ros_timer import RosTimer, make_global_timer_scxml from as2fm.jani_generator.scxml_helpers.scxml_to_jani import convert_multiple_scxmls_to_jani -from as2fm.scxml_converter.bt_converter import bt_converter +from as2fm.scxml_converter.bt_converter import bt_converter, get_blackboard_variables_from_models from as2fm.scxml_converter.scxml_entries import EventsToAutomata, ScxmlRoot @@ -174,6 +174,8 @@ def generate_plain_scxml_models_and_timers( all_timers: List[RosTimer] = [] all_services: Dict[str, RosCommunicationHandler] = {} all_actions: Dict[str, RosCommunicationHandler] = {} + bt_blackboard_vars: Dict[str, str] = get_blackboard_variables_from_models(ros_scxmls) + print(bt_blackboard_vars) for scxml_entry in ros_scxmls: plain_scxmls, ros_declarations = scxml_entry.to_plain_scxml_and_declarations() # Handle ROS timers diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 38645fec..566a4831 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -33,10 +33,36 @@ ScxmlRoot, ScxmlState, ) +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + get_blackboard_variable_name, + is_blackboard_reference, +) BT_ROOT_PREFIX = "bt_root_fsm_" +def get_blackboard_variables_from_models(models: List[ScxmlRoot]) -> Dict[str, str]: + """ + Collect all blackboard variables and return them as a dictionary. + + :param models: List of ScxmlModel to extract the information from. + :return: Dictionary with name and type of the detected blackboard variable. + """ + blackboard_vars: Dict[str, str] = {} + for scxml_model in models: + declared_ports: List[Tuple[str, str, str]] = scxml_model.get_bt_ports_types_values() + for p_name, p_type, p_value in declared_ports: + assert ( + p_value is not None + ), f"Error in model {scxml_model.get_name()}: undefined value in {p_name} BT port." + if is_blackboard_reference(p_value): + var_name = get_blackboard_variable_name(p_value) + existing_bt_type = blackboard_vars.get(var_name) + assert existing_bt_type is None or existing_bt_type == p_type + blackboard_vars.update({var_name: p_type}) + return blackboard_vars + + def is_bt_root_scxml(scxml_name: str) -> bool: """ Check if the SCXML name matches with the BT root SCXML name pattern. diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index fe587ddc..189fb770 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -82,6 +82,12 @@ def is_blackboard_reference(port_value: str) -> bool: return re.match(r"\{.+\}", port_value) is not None +def get_blackboard_variable_name(port_value: str) -> str: + assert is_blackboard_reference( + port_value + ), f"Error: expected '{port_value}' to be a reference to a blackboard variable." + + class BtPortsHandler: """Collector for declared BT ports and their assigned value.""" @@ -97,7 +103,7 @@ def check_port_name_allowed(port_name: str) -> None: def __init__(self): # For each port name, store the port type string and value. self._in_ports: Dict[str, Tuple[str, str]] = {} - self._out_ports: Dict[str, Tuple[Type, str]] = {} + self._out_ports: Dict[str, Tuple[str, str]] = {} def in_port_exists(self, port_name: str) -> bool: """Check if an input port exists.""" @@ -144,6 +150,10 @@ def get_port_value(self, port_name: str) -> str: else: raise RuntimeError(f"Error: Port {port_name} is not declared.") + def get_all_ports(self) -> Dict[str, Tuple[str, str]]: + """Get all declaed ports as a dict referencing port names to type and value.""" + return self._in_ports | self._out_ports + def get_in_port_value(self, port_name: str) -> str: """Get the value of an input port.""" assert self.in_port_exists( diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 7a144dea..a9bb689e 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -235,6 +235,14 @@ def set_bt_ports_values(self, ports_values: List[Tuple[str, str]]): for port_name, port_value in ports_values: self.set_bt_port_value(port_name, port_value) + def get_bt_ports_types_values(self) -> List[Tuple[str, str, str]]: + """ + Get information about the BT ports in the model. + + :return: A list of Tuples containing bt_port_name, type and value. + """ + return [(p_name, p_type, p_value) for p_name, (p_type, p_value) in self._bt_ports_handler] + def append_bt_child_id(self, child_id: int): """Append a child ID to the list of child IDs.""" assert isinstance(child_id, int), "Error: SCXML root: invalid child ID type." From 2b0df5fcd55fbce4f144bce6374d72a2c74c53c6 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 28 Nov 2024 13:57:52 +0100 Subject: [PATCH 10/58] Remove assertions for Blackboard variables in BtPortsHandler Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 189fb770..676ddc50 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -165,7 +165,15 @@ def get_in_port_value(self, port_name: str) -> str: def get_out_port_value(self, port_name: str) -> str: """Get the value of an output port.""" - raise NotImplementedError("Error: Output ports are not supported yet.") + assert self.out_port_exists( + port_name + ), f"Error: Port {port_name} is not declared as input port." + port_value = self._out_ports[port_name][1] + assert port_value is not None, f"Error: Port {port_name} has no assigned value." + assert is_blackboard_reference( + port_value + ), f"Error: Port {port_name} should be a blackboard reference, found value {port_value}" + return port_value def set_port_value(self, port_name: str, port_value: str) -> None: """Set the value of a port.""" @@ -175,8 +183,7 @@ def set_port_value(self, port_name: str, port_value: str) -> None: self._set_out_port_value(port_name, port_value) else: # The reserved port IDs can be set in the bt.xml even if they are unused in the plugin - if port_name not in RESERVED_BT_PORT_NAMES: - raise RuntimeError(f"Error: Port {port_name} is not declared.") + assert port_name in RESERVED_BT_PORT_NAMES, f"Error: Port {port_name} is not declared." def _set_in_port_value(self, port_name: str, port_value: str): """Set the value of an input port.""" @@ -185,16 +192,20 @@ def _set_in_port_value(self, port_name: str, port_value: str): ), f"Error: Port {port_name} is not declared as input port." assert ( self._in_ports[port_name][1] is None - ), f"Error: Port {port_name} already has a value assigned." + ), f"Error: Value of port {port_name} already assigned." port_type = self._in_ports[port_name][0] - # Ensure this is not a Blackboard variable reference: currently not supported - if is_blackboard_reference(port_value): - raise NotImplementedError( - f"Error: {port_value} assigns a Blackboard variable to {port_name}. " - "This is not yet supported." - ) self._in_ports[port_name] = (port_type, port_value) def _set_out_port_value(self, port_name: str, port_value: str): """Set the value of an output port.""" - raise NotImplementedError("Error: Output ports are not supported yet.") + assert self.out_port_exists( + port_name + ), f"Error: Port {port_name} is not declared as output port." + assert ( + self._out_ports[port_name][1] is None + ), f"Error: Value of port {port_name} already assigned." + assert is_blackboard_reference( + port_value + ), f"Error: value of output port {port_name} must be a blackboard variable." + port_type = self._out_ports[port_name][0] + self._out_ports[port_name] = (port_type, port_value) From e1c29ba671fd254e6ae4aafef20869e8830fa669 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 28 Nov 2024 14:03:49 +0100 Subject: [PATCH 11/58] Add flag to check if there are blackboard variables or not Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/scxml_entries/bt_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 676ddc50..b3fb7158 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -104,6 +104,11 @@ def __init__(self): # For each port name, store the port type string and value. self._in_ports: Dict[str, Tuple[str, str]] = {} self._out_ports: Dict[str, Tuple[str, str]] = {} + self._has_bt_references: bool = False + + def has_bt_references(self) -> bool: + """Boolean check reporting whether any port references blackboard variables or not.""" + return self._has_bt_references def in_port_exists(self, port_name: str) -> bool: """Check if an input port exists.""" @@ -184,6 +189,8 @@ def set_port_value(self, port_name: str, port_value: str) -> None: else: # The reserved port IDs can be set in the bt.xml even if they are unused in the plugin assert port_name in RESERVED_BT_PORT_NAMES, f"Error: Port {port_name} is not declared." + # Update flag to track whether we added a blackboard variable or not + self._has_bt_references = self._has_bt_references or is_blackboard_reference(port_value) def _set_in_port_value(self, port_name: str, port_value: str): """Set the value of an input port.""" From d8cb76d2ed45a65eba66950e1d91a2479c5dcd9f Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Thu, 28 Nov 2024 15:15:47 +0100 Subject: [PATCH 12/58] Prepare state bt processing for generating additional states Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/scxml_root.py | 13 ++++++++++--- .../scxml_converter/scxml_entries/scxml_state.py | 7 ++++++- .../jani_generator/test_systemtest_scxml_to_jani.py | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index a9bb689e..af207134 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -241,7 +241,10 @@ def get_bt_ports_types_values(self) -> List[Tuple[str, str, str]]: :return: A list of Tuples containing bt_port_name, type and value. """ - return [(p_name, p_type, p_value) for p_name, (p_type, p_value) in self._bt_ports_handler] + return [ + (p_name, p_type, p_value) + for p_name, (p_type, p_value) in self._bt_ports_handler.get_all_ports().items() + ] def append_bt_child_id(self, child_id: int): """Append a child ID to the list of child IDs.""" @@ -261,9 +264,13 @@ def instantiate_bt_information(self): ros_decl_scxml.update_bt_ports_values(self._bt_ports_handler) for scxml_thread in self._additional_threads: scxml_thread.update_bt_ports_values(self._bt_ports_handler) + processed_states: List[ScxmlState] = [] for state in self._states: - state.instantiate_bt_events(self._bt_plugin_id, self._bt_children_ids) - state.update_bt_ports_values(self._bt_ports_handler) + processed_states.extend( + state.instantiate_bt_events( + self._bt_plugin_id, self._bt_children_ids, self._bt_ports_handler + ) + ) def _generate_ros_declarations_helper(self) -> Optional[ScxmlRosDeclarationsContainer]: """Generate a HelperRosDeclarations object from the existing ROS declarations.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index c03c9dc0..92271000 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -142,7 +142,9 @@ def set_thread_id(self, thread_idx: int): if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_idx) - def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> None: + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int], bt_ports_handler: BtPortsHandler + ) -> List["ScxmlState"]: """Instantiate the BT events in all entries belonging to a state.""" instantiated_transitions: List[ScxmlTransition] = [] for transition in self._body: @@ -154,6 +156,9 @@ def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> No self._body = instantiated_transitions instantiate_exec_body_bt_events(self._on_entry, instance_id, children_ids) instantiate_exec_body_bt_events(self._on_exit, instance_id, children_ids) + self.update_bt_ports_values(bt_ports_handler) + # TODO: Split this state in two parts if there are blackboard vars + return [self] def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 8e44204b..f0da2cb7 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -434,6 +434,7 @@ def test_uc2_assembly_with_bug(self): success=False, ) + @pytest.mark.skip(reason="WIP: Blackboard support.") def test_grid_robot_blackboard(self): """Test the grid_robot_blackboard model (BT + Blackboard).""" self._test_with_main( From 2ec87a7ba0bb06d4da8f1927356aaeb11df3b1cd Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 10:01:31 +0100 Subject: [PATCH 13/58] Code cleanup Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_root.py | 1 + .../scxml_entries/scxml_state.py | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index af207134..47e84dde 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -271,6 +271,7 @@ def instantiate_bt_information(self): self._bt_plugin_id, self._bt_children_ids, self._bt_ports_handler ) ) + self._states = processed_states def _generate_ros_declarations_helper(self) -> Optional[ScxmlRosDeclarationsContainer]: """Generate a HelperRosDeclarations object from the existing ROS declarations.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index 92271000..bdfae1d4 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -142,10 +142,17 @@ def set_thread_id(self, thread_idx: int): if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_idx) - def instantiate_bt_events( - self, instance_id: int, children_ids: List[int], bt_ports_handler: BtPortsHandler + def _generate_blackboard_retrieval( + self, bt_ports_handler: BtPortsHandler ) -> List["ScxmlState"]: - """Instantiate the BT events in all entries belonging to a state.""" + if bt_ports_handler.has_bt_references(): + # TODO: Split the state in 2 parts + raise NotImplementedError + return [self] + + def _substitute_bt_events_and_ports( + self, instance_id: int, children_ids: List[int], bt_ports_handler: BtPortsHandler + ) -> None: instantiated_transitions: List[ScxmlTransition] = [] for transition in self._body: new_transitions = transition.instantiate_bt_events(instance_id, children_ids) @@ -156,11 +163,9 @@ def instantiate_bt_events( self._body = instantiated_transitions instantiate_exec_body_bt_events(self._on_entry, instance_id, children_ids) instantiate_exec_body_bt_events(self._on_exit, instance_id, children_ids) - self.update_bt_ports_values(bt_ports_handler) - # TODO: Split this state in two parts if there are blackboard vars - return [self] + self._update_bt_ports_values(bt_ports_handler) - def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: + def _update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" for transition in self._body: transition.update_bt_ports_values(bt_ports_handler) @@ -169,6 +174,15 @@ def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: for entry in self._on_exit: entry.update_bt_ports_values(bt_ports_handler) + def instantiate_bt_events( + self, instance_id: int, children_ids: List[int], bt_ports_handler: BtPortsHandler + ) -> List["ScxmlState"]: + """Instantiate the BT events in all entries belonging to a state.""" + generated_states = self._generate_blackboard_retrieval(bt_ports_handler) + for state in generated_states: + state._substitute_bt_events_and_ports(instance_id, children_ids, bt_ports_handler) + return generated_states + def add_transition(self, transition: ScxmlTransition) -> None: self._body.append(transition) From 9717cdc63428a9b0b305d972a1a40408198a0fa1 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 14:16:41 +0100 Subject: [PATCH 14/58] Implement the additional states generation Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 14 +++++---- .../scxml_entries/scxml_state.py | 30 ++++++++++++++++--- .../scxml_entries/scxml_transition.py | 3 ++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index b3fb7158..56551527 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -104,11 +104,11 @@ def __init__(self): # For each port name, store the port type string and value. self._in_ports: Dict[str, Tuple[str, str]] = {} self._out_ports: Dict[str, Tuple[str, str]] = {} - self._has_bt_references: bool = False + self._has_blackboard_inputs: bool = False - def has_bt_references(self) -> bool: - """Boolean check reporting whether any port references blackboard variables or not.""" - return self._has_bt_references + def has_blackboard_inputs(self) -> bool: + """Boolean check reporting whether any input port references blackboard variables.""" + return self._has_blackboard_inputs def in_port_exists(self, port_name: str) -> bool: """Check if an input port exists.""" @@ -189,8 +189,6 @@ def set_port_value(self, port_name: str, port_value: str) -> None: else: # The reserved port IDs can be set in the bt.xml even if they are unused in the plugin assert port_name in RESERVED_BT_PORT_NAMES, f"Error: Port {port_name} is not declared." - # Update flag to track whether we added a blackboard variable or not - self._has_bt_references = self._has_bt_references or is_blackboard_reference(port_value) def _set_in_port_value(self, port_name: str, port_value: str): """Set the value of an input port.""" @@ -202,6 +200,10 @@ def _set_in_port_value(self, port_name: str, port_value: str): ), f"Error: Value of port {port_name} already assigned." port_type = self._in_ports[port_name][0] self._in_ports[port_name] = (port_type, port_value) + # Update flag to track whether we added a blackboard variable or not + self._has_blackboard_inputs = self._has_blackboard_inputs or is_blackboard_reference( + port_value + ) def _set_out_port_value(self, port_name: str, port_value: str): """Set the value of an output port.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index bdfae1d4..184f847f 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -28,6 +28,7 @@ ScxmlExecutableEntry, ScxmlExecutionBody, ScxmlRosDeclarationsContainer, + ScxmlSend, ScxmlTransition, ) from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler @@ -142,13 +143,34 @@ def set_thread_id(self, thread_idx: int): if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_idx) + def _is_blackboard_required(self, bt_ports_handler: BtPortsHandler) -> List["ScxmlState"]: + # TODO(Blackboard-optimization): We should generate the additional state only in case + # there is a bt_get_input targeting a blackboard variable, this requires adding support + # to retrieve this information from the state's children. + # TODO: Additionally, we assume the bt is read only during ticks, but we aren't verifying + # this assumption + return bt_ports_handler.has_blackboard_inputs() and self.has_bt_tick_transitions() + def _generate_blackboard_retrieval( self, bt_ports_handler: BtPortsHandler ) -> List["ScxmlState"]: - if bt_ports_handler.has_bt_references(): - # TODO: Split the state in 2 parts - raise NotImplementedError - return [self] + generated_states: List[ScxmlState] = [self] + if self._is_blackboard_required(bt_ports_handler): + for transition in self._body: + if isinstance(transition, BtTick): + # TODO: Write the transitions names in a variable + new_state_id = f"{self.get_id}_on_tick_{len(generated_states)}" + new_state = ScxmlState(new_state_id) + blackboard_transition = ScxmlTransition( + transition.get_target_state_id(), + ["bt_blackboard_get"], + body=transition.get_body(), + ) + new_state.add_transition(blackboard_transition) + transition.set_target_state_id(new_state_id) + transition.set_body([ScxmlSend("bt_blackboard_req")]) + generated_states.append(new_state) + return generated_states def _substitute_bt_events_and_ports( self, instance_id: int, children_ids: List[int], bt_ports_handler: BtPortsHandler diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index d853139f..b62ff01a 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -105,6 +105,9 @@ def get_target_state_id(self) -> str: """Return the ID of the target state of this transition.""" return self._target + def set_target_state_id(self, state_id: str): + self._target = state_id + def get_events(self) -> List[str]: """Return the events that trigger this transition (if any).""" return self._events From 19fe5adf27b58883fd4530c131ae0473c460a9b3 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 14:35:23 +0100 Subject: [PATCH 15/58] Generate class to set value through a port Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/__init__.py | 1 + .../scxml_converter/scxml_entries/bt_utils.py | 14 ++++++++ .../scxml_entries/scxml_bt_ports.py | 34 +++++++++++++++++++ .../scxml_entries/scxml_state.py | 10 ++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/__init__.py b/src/as2fm/scxml_converter/scxml_entries/__init__.py index 35332f39..c698d031 100644 --- a/src/as2fm/scxml_converter/scxml_entries/__init__.py +++ b/src/as2fm/scxml_converter/scxml_entries/__init__.py @@ -8,6 +8,7 @@ BtPortDeclarations, BtOutputPortDeclaration, BtGetValueInputPort, + BtSetValueOutputPort, ) # noqa: F401 from .scxml_param import ScxmlParam # noqa: F401 from .scxml_ros_field import RosField # noqa: F401 diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 56551527..547588ef 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -27,6 +27,10 @@ # List of keys that are not going to be read as BT ports from the BT XML definition. RESERVED_BT_PORT_NAMES = ["ID", "name"] +# Blackboard-related autogenerated events +BT_BLACKBOARD_REQUEST = "bt_blackboard_req" +BT_BLACKBOARD_GET = "bt_blackboard_get" + class BtResponse(Enum): """Enumeration of possible BT responses.""" @@ -51,6 +55,16 @@ def process_expr(expr: str) -> str: return expr +def generate_bt_blackboard_set(bt_bb_ref_name: str) -> str: + """ + Generate the name of the evnt setting a specific Blackboard variable. + + :param bt_bb_ref_name: The name of the blackboard variable to set. + :return: The name of the event to use to generate the specific variable. + """ + return f"bt_blackboard_set_{bt_bb_ref_name}" + + def generate_bt_tick_event(instance_id: str) -> str: """Generate the BT tick event name for a given BT node instance.""" return f"bt_{instance_id}_tick" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py index 58d00570..ea43976b 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py @@ -146,4 +146,38 @@ def as_xml(self) -> ET.Element: return xml_bt_in_port +class BtSetValueOutputPort(ScxmlBase): + """ + Get the value of an input port in a bt plugin. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_set_output" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtSetValueOutputPort": + assert_xml_tag_ok(BtSetValueOutputPort, xml_tree) + key_str = get_xml_argument(BtSetValueOutputPort, xml_tree, "key") + return BtSetValueOutputPort(key_str) + + def __init__(self, key_str: str): + self._key = key_str + + def check_validity(self) -> bool: + return is_non_empty_string(BtSetValueOutputPort, "key", self._key) + + def get_key_name(self) -> str: + return self._key + + def as_plain_scxml(self, _) -> ScxmlBase: + # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot + raise RuntimeError("Error: SCXML BT Port value setter cannot be converted to plain SCXML.") + + def as_xml(self) -> ET.Element: + assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." + xml_bt_in_port = ET.Element(BtSetValueOutputPort.get_tag_name(), {"key": self._key}) + return xml_bt_in_port + + BtPortDeclarations = Union[BtInputPortDeclaration, BtOutputPortDeclaration] diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index 184f847f..a35a0a05 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -31,7 +31,11 @@ ScxmlSend, ScxmlTransition, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BT_BLACKBOARD_GET, + BT_BLACKBOARD_REQUEST, + BtPortsHandler, +) from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import ( as_plain_execution_body, execution_body_from_xml, @@ -163,12 +167,12 @@ def _generate_blackboard_retrieval( new_state = ScxmlState(new_state_id) blackboard_transition = ScxmlTransition( transition.get_target_state_id(), - ["bt_blackboard_get"], + [BT_BLACKBOARD_GET], body=transition.get_body(), ) new_state.add_transition(blackboard_transition) transition.set_target_state_id(new_state_id) - transition.set_body([ScxmlSend("bt_blackboard_req")]) + transition.set_body([ScxmlSend(BT_BLACKBOARD_REQUEST)]) generated_states.append(new_state) return generated_states From 89d02c7293d39c0e59cd5f4a85ac761503958942 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 15:25:20 +0100 Subject: [PATCH 16/58] Implement reading from blackboard Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 17 ++++++++++++++++- .../scxml_converter/scxml_entries/scxml_data.py | 14 +++++++++++++- .../scxml_entries/scxml_executable_entries.py | 10 ++++++++-- .../scxml_entries/scxml_param.py | 9 +++++++-- .../scxml_entries/scxml_ros_base.py | 10 ++++++---- .../scxml_entries/scxml_ros_field.py | 6 ------ 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 547588ef..9fd38a58 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -19,7 +19,10 @@ from enum import Enum, auto from typing import Dict, Tuple, Type -from as2fm.scxml_converter.scxml_entries.utils import SCXML_DATA_STR_TO_TYPE +from as2fm.scxml_converter.scxml_entries.utils import ( + PLAIN_SCXML_EVENT_DATA_PREFIX, + SCXML_DATA_STR_TO_TYPE, +) VALID_BT_INPUT_PORT_TYPES: Dict[str, Type] = SCXML_DATA_STR_TO_TYPE | {"string": str} VALID_BT_OUTPUT_PORT_TYPES: Dict[str, Type] = SCXML_DATA_STR_TO_TYPE @@ -100,6 +103,18 @@ def get_blackboard_variable_name(port_value: str) -> str: assert is_blackboard_reference( port_value ), f"Error: expected '{port_value}' to be a reference to a blackboard variable." + return port_value.removeprefix("{").removesuffix("}") + + +def get_input_variable_as_scxml_expression(port_value: str) -> str: + """ + Given an input variable it generates an expression as event data or single value. + + The outcome depends on whether port value refers to the BT blackboard or not. + """ + if is_blackboard_reference(port_value): + return PLAIN_SCXML_EVENT_DATA_PREFIX + get_blackboard_variable_name(port_value) + return port_value class BtPortsHandler: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_data.py b/src/as2fm/scxml_converter/scxml_entries/scxml_data.py index 13e18e88..0d857e2b 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_data.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_data.py @@ -24,7 +24,7 @@ from as2fm.as2fm_common.common import is_array_type, is_comment from as2fm.scxml_converter.scxml_entries import BtGetValueInputPort, ScxmlBase -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler +from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_blackboard_reference from as2fm.scxml_converter.scxml_entries.utils import ( convert_string_to_type, get_array_max_size, @@ -203,7 +203,19 @@ def as_plain_scxml(self, _): def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): if isinstance(self._expr, BtGetValueInputPort): self._expr = bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + assert not is_blackboard_reference(self._expr), ( + f"Error: SCXML Data: '{self._id}': cannot set the initial expression from " + f" the BT blackboard variable {self._expr}" + ) if isinstance(self._lower_bound, BtGetValueInputPort): self._lower_bound = bt_ports_handler.get_in_port_value(self._lower_bound.get_key_name()) + assert not is_blackboard_reference(self._lower_bound), ( + f"Error: SCXML Data: '{self._id}': cannot set the lower bound from " + f" the BT blackboard variable {self._lower_bound}" + ) if isinstance(self._upper_bound, BtGetValueInputPort): self._upper_bound = bt_ports_handler.get_in_port_value(self._upper_bound.get_key_name()) + assert not is_blackboard_reference(self._upper_bound), ( + f"Error: SCXML Data: '{self._id}': cannot set the upper bound from " + f" the BT blackboard variable {self._upper_bound}" + ) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index 9c8f9a53..fdb6a4fe 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -30,7 +30,11 @@ ScxmlParam, ScxmlRosDeclarationsContainer, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_bt_event +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BtPortsHandler, + get_input_variable_as_scxml_expression, + is_bt_event, +) from as2fm.scxml_converter.scxml_entries.utils import ( CallbackType, get_plain_expression, @@ -420,7 +424,9 @@ def instantiate_bt_events(self, _, __) -> "ScxmlAssign": def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" if isinstance(self._expr, BtGetValueInputPort): - self._expr = bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + self._expr = get_input_variable_as_scxml_expression( + bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + ) def check_validity(self) -> bool: # TODO: Check that the location to assign exists in the data-model diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py index c9388a05..deddc8db 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py @@ -22,7 +22,10 @@ from lxml import etree as ET from as2fm.scxml_converter.scxml_entries import BtGetValueInputPort, ScxmlBase -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BtPortsHandler, + get_input_variable_as_scxml_expression, +) from as2fm.scxml_converter.scxml_entries.utils import CallbackType, is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import ( assert_xml_tag_ok, @@ -86,7 +89,9 @@ def get_location(self) -> Optional[str]: def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): """Update the values of potential entries making use of BT ports.""" if isinstance(self._expr, BtGetValueInputPort): - self._expr = bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + self._expr = get_input_variable_as_scxml_expression( + bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + ) def check_validity(self) -> bool: valid_name = is_non_empty_string(ScxmlParam, "name", self._name) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py index ca4c9fd7..512d6560 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py @@ -30,7 +30,7 @@ ScxmlSend, ScxmlTransition, ) -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler +from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_blackboard_reference from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import ( as_plain_execution_body, execution_body_from_xml, @@ -140,9 +140,11 @@ def check_valid_instantiation(self) -> bool: def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" if isinstance(self._interface_name, BtGetValueInputPort): - self._interface_name = bt_ports_handler.get_in_port_value( - self._interface_name.get_key_name() - ) + port_value = bt_ports_handler.get_in_port_value(self._interface_name.get_key_name()) + assert not is_blackboard_reference( + port_value + ), f"Error: SCXML {self.__class__.__name__}: interface can't come from BT Blackboard." + self._interface_name = port_value def as_plain_scxml(self, _) -> ScxmlBase: # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_field.py b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_field.py index 1f69e9a5..5bfd1064 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_field.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_field.py @@ -20,7 +20,6 @@ from lxml import etree as ET from as2fm.scxml_converter.scxml_entries import BtGetValueInputPort, ScxmlParam -from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler from as2fm.scxml_converter.scxml_entries.utils import ( ROS_FIELD_PREFIX, CallbackType, @@ -64,11 +63,6 @@ def check_validity(self) -> bool: ) return valid_name and valid_expr - def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): - """Update the values of potential entries making use of BT ports.""" - if isinstance(self._expr, BtGetValueInputPort): - self._expr = bt_ports_handler.get_in_port_value(self._expr.get_key_name()) - def as_plain_scxml(self, _) -> ScxmlParam: # In order to distinguish the message body from additional entries, add a prefix to the name assert ( From a969129b6311e5f6d7bbc93bf771a4f60fc039a3 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 15:36:02 +0100 Subject: [PATCH 17/58] Split BT ports files to break cyclic dependencies Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/__init__.py | 6 +- .../scxml_entries/scxml_bt_in_port.py | 58 +++++++++++++++ .../scxml_entries/scxml_bt_out_port.py | 58 +++++++++++++++ ..._ports.py => scxml_bt_port_declaration.py} | 70 +------------------ 4 files changed, 120 insertions(+), 72 deletions(-) create mode 100644 src/as2fm/scxml_converter/scxml_entries/scxml_bt_in_port.py create mode 100644 src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py rename src/as2fm/scxml_converter/scxml_entries/{scxml_bt_ports.py => scxml_bt_port_declaration.py} (62%) diff --git a/src/as2fm/scxml_converter/scxml_entries/__init__.py b/src/as2fm/scxml_converter/scxml_entries/__init__.py index c698d031..b4a07c4f 100644 --- a/src/as2fm/scxml_converter/scxml_entries/__init__.py +++ b/src/as2fm/scxml_converter/scxml_entries/__init__.py @@ -3,13 +3,12 @@ from .scxml_base import ScxmlBase # noqa: F401 from .utils import CallbackType # noqa: F401 from .bt_utils import RESERVED_BT_PORT_NAMES # noqa: F401 -from .scxml_bt_ports import ( # noqa: F401 +from .scxml_bt_port_declaration import ( # noqa: F401 BtInputPortDeclaration, BtPortDeclarations, BtOutputPortDeclaration, - BtGetValueInputPort, - BtSetValueOutputPort, ) # noqa: F401 +from .scxml_bt_in_port import BtGetValueInputPort # noqa: F401 from .scxml_param import ScxmlParam # noqa: F401 from .scxml_ros_field import RosField # noqa: F401 from .scxml_data import ScxmlData # noqa: F401 @@ -21,6 +20,7 @@ ScxmlExecutionBody, EventsToAutomata, ) # noqa: F401 +from .scxml_bt_out_port import BtSetValueOutputPort # noqa: F401 from .scxml_executable_entries import ( # noqa: F401 execution_body_from_xml, as_plain_execution_body, diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_in_port.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_in_port.py new file mode 100644 index 00000000..923620fb --- /dev/null +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_in_port.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +SCXML get input for Behavior Trees' Ports. +""" + +from lxml import etree as ET + +from as2fm.scxml_converter.scxml_entries import ScxmlBase +from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string +from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument + + +class BtGetValueInputPort(ScxmlBase): + """ + Get the value of an input port in a bt plugin. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_get_input" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtGetValueInputPort": + assert_xml_tag_ok(BtGetValueInputPort, xml_tree) + key_str = get_xml_argument(BtGetValueInputPort, xml_tree, "key") + return BtGetValueInputPort(key_str) + + def __init__(self, key_str: str): + self._key = key_str + + def check_validity(self) -> bool: + return is_non_empty_string(BtGetValueInputPort, "key", self._key) + + def get_key_name(self) -> str: + return self._key + + def as_plain_scxml(self, _) -> ScxmlBase: + # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot + raise RuntimeError("Error: SCXML BT Port value getter cannot be converted to plain SCXML.") + + def as_xml(self) -> ET.Element: + assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." + xml_bt_in_port = ET.Element(BtGetValueInputPort.get_tag_name(), {"key": self._key}) + return xml_bt_in_port diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py new file mode 100644 index 00000000..f3708f76 --- /dev/null +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +SCXML set output for Behavior Trees' Ports. +""" + +from lxml import etree as ET + +from as2fm.scxml_converter.scxml_entries import ScxmlSend +from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string +from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument + + +class BtSetValueOutputPort(ScxmlSend): + """ + Get the value of an input port in a bt plugin. + """ + + @staticmethod + def get_tag_name() -> str: + return "bt_set_output" + + @staticmethod + def from_xml_tree(xml_tree: ET.Element) -> "BtSetValueOutputPort": + assert_xml_tag_ok(BtSetValueOutputPort, xml_tree) + key_str = get_xml_argument(BtSetValueOutputPort, xml_tree, "key") + return BtSetValueOutputPort(key_str) + + def __init__(self, key_str: str): + self._key = key_str + + def check_validity(self) -> bool: + return is_non_empty_string(BtSetValueOutputPort, "key", self._key) + + def get_key_name(self) -> str: + return self._key + + def as_plain_scxml(self, _) -> ScxmlSend: + # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot + raise RuntimeError("Error: SCXML BT Port value setter cannot be converted to plain SCXML.") + + def as_xml(self) -> ET.Element: + assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." + xml_bt_in_port = ET.Element(BtSetValueOutputPort.get_tag_name(), {"key": self._key}) + return xml_bt_in_port diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_port_declaration.py similarity index 62% rename from src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py rename to src/as2fm/scxml_converter/scxml_entries/scxml_bt_port_declaration.py index ea43976b..efdd3855 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ports.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_port_declaration.py @@ -14,7 +14,7 @@ # limitations under the License. """ -SCXML entries related to Behavior Trees' Ports. +SCXML entries related to Behavior Trees' Ports declaration. """ from typing import Union @@ -112,72 +112,4 @@ def as_xml(self) -> ET.Element: return xml_bt_in_port -class BtGetValueInputPort(ScxmlBase): - """ - Get the value of an input port in a bt plugin. - """ - - @staticmethod - def get_tag_name() -> str: - return "bt_get_input" - - @staticmethod - def from_xml_tree(xml_tree: ET.Element) -> "BtGetValueInputPort": - assert_xml_tag_ok(BtGetValueInputPort, xml_tree) - key_str = get_xml_argument(BtGetValueInputPort, xml_tree, "key") - return BtGetValueInputPort(key_str) - - def __init__(self, key_str: str): - self._key = key_str - - def check_validity(self) -> bool: - return is_non_empty_string(BtGetValueInputPort, "key", self._key) - - def get_key_name(self) -> str: - return self._key - - def as_plain_scxml(self, _) -> ScxmlBase: - # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot - raise RuntimeError("Error: SCXML BT Port value getter cannot be converted to plain SCXML.") - - def as_xml(self) -> ET.Element: - assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." - xml_bt_in_port = ET.Element(BtGetValueInputPort.get_tag_name(), {"key": self._key}) - return xml_bt_in_port - - -class BtSetValueOutputPort(ScxmlBase): - """ - Get the value of an input port in a bt plugin. - """ - - @staticmethod - def get_tag_name() -> str: - return "bt_set_output" - - @staticmethod - def from_xml_tree(xml_tree: ET.Element) -> "BtSetValueOutputPort": - assert_xml_tag_ok(BtSetValueOutputPort, xml_tree) - key_str = get_xml_argument(BtSetValueOutputPort, xml_tree, "key") - return BtSetValueOutputPort(key_str) - - def __init__(self, key_str: str): - self._key = key_str - - def check_validity(self) -> bool: - return is_non_empty_string(BtSetValueOutputPort, "key", self._key) - - def get_key_name(self) -> str: - return self._key - - def as_plain_scxml(self, _) -> ScxmlBase: - # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot - raise RuntimeError("Error: SCXML BT Port value setter cannot be converted to plain SCXML.") - - def as_xml(self) -> ET.Element: - assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." - xml_bt_in_port = ET.Element(BtSetValueOutputPort.get_tag_name(), {"key": self._key}) - return xml_bt_in_port - - BtPortDeclarations = Union[BtInputPortDeclaration, BtOutputPortDeclaration] From 1adf823d84864c3067ef53a85b0fe6bcac841a2a Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 16:00:52 +0100 Subject: [PATCH 18/58] First complete bt_out_port scxml class Signed-off-by: Marco Lampacrescia --- .../graphics/blackboard_to_scxml.drawio.svg | 4 +- .../scxml_entries/scxml_bt_out_port.py | 44 +++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docs/source/graphics/blackboard_to_scxml.drawio.svg b/docs/source/graphics/blackboard_to_scxml.drawio.svg index 3e6dfaf1..4b31c576 100644 --- a/docs/source/graphics/blackboard_to_scxml.drawio.svg +++ b/docs/source/graphics/blackboard_to_scxml.drawio.svg @@ -1,4 +1,4 @@ - + @@ -30,7 +30,7 @@
bt_blackboard_set_<bb_var_x>
- * assign bb_var_x = _event.data + * assign bb_var_x = _event.data.value
diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py index f3708f76..fc01de0c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py @@ -19,7 +19,13 @@ from lxml import etree as ET -from as2fm.scxml_converter.scxml_entries import ScxmlSend +from as2fm.scxml_converter.scxml_entries import ScxmlParam, ScxmlSend +from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BtPortsHandler, + generate_bt_blackboard_set, + get_blackboard_variable_name, + is_blackboard_reference, +) from as2fm.scxml_converter.scxml_entries.utils import is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import assert_xml_tag_ok, get_xml_argument @@ -37,22 +43,42 @@ def get_tag_name() -> str: def from_xml_tree(xml_tree: ET.Element) -> "BtSetValueOutputPort": assert_xml_tag_ok(BtSetValueOutputPort, xml_tree) key_str = get_xml_argument(BtSetValueOutputPort, xml_tree, "key") - return BtSetValueOutputPort(key_str) + expr_str = get_xml_argument(BtSetValueOutputPort, xml_tree, "expr") + return BtSetValueOutputPort(key_str, expr_str) - def __init__(self, key_str: str): + def __init__(self, key_str: str, expr_str: str): self._key = key_str + self._expr = expr_str + self._blackboard_reference = None def check_validity(self) -> bool: - return is_non_empty_string(BtSetValueOutputPort, "key", self._key) + return is_non_empty_string(BtSetValueOutputPort, "key", self._key) and is_non_empty_string( + BtSetValueOutputPort, "expr", self._expr + ) - def get_key_name(self) -> str: - return self._key + def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: + assert bt_ports_handler.out_port_exists( + self._key + ), f"Error: SCXML BT Port {self._key} is not declared as output port." + port_value = bt_ports_handler.get_out_port_value(self._key) + assert is_blackboard_reference( + port_value + ), f"Error: SCXML BT Port {self._key} is not referencing a blackboard variable." + self._blackboard_reference = get_blackboard_variable_name(port_value) def as_plain_scxml(self, _) -> ScxmlSend: # This is discarded in the to_plain_scxml_and_declarations method from ScxmlRoot - raise RuntimeError("Error: SCXML BT Port value setter cannot be converted to plain SCXML.") + assert ( + self._blackboard_reference is not None + ), "Error: SCXML BT Output Port: must run 'update_bt_ports_values' before 'as_plain_scxml'" + return ScxmlSend( + generate_bt_blackboard_set(self._blackboard_reference), + [ScxmlParam("value", expr=self._expr)], + ) def as_xml(self) -> ET.Element: - assert self.check_validity(), "Error: SCXML BT Input Port: invalid parameters." - xml_bt_in_port = ET.Element(BtSetValueOutputPort.get_tag_name(), {"key": self._key}) + assert self.check_validity(), "Error: SCXML BT Output Port: invalid parameters." + xml_bt_in_port = ET.Element( + BtSetValueOutputPort.get_tag_name(), {"key": self._key, "expr": self._expr} + ) return xml_bt_in_port From 395f2bc0355fc6d5037915762993a647aee77d52 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 17:18:39 +0100 Subject: [PATCH 19/58] Add Scxml Blackboard FSM Signed-off-by: Marco Lampacrescia --- .../scxml_helpers/top_level_interpreter.py | 10 +++- src/as2fm/scxml_converter/bt_converter.py | 47 +++++++++++++++++++ .../scxml_converter/scxml_entries/bt_utils.py | 3 ++ .../scxml_entries/scxml_bt_out_port.py | 3 +- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py index b98faec8..2a076242 100644 --- a/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py +++ b/src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py @@ -36,7 +36,11 @@ from as2fm.jani_generator.ros_helpers.ros_service_handler import RosServiceHandler from as2fm.jani_generator.ros_helpers.ros_timer import RosTimer, make_global_timer_scxml from as2fm.jani_generator.scxml_helpers.scxml_to_jani import convert_multiple_scxmls_to_jani -from as2fm.scxml_converter.bt_converter import bt_converter, get_blackboard_variables_from_models +from as2fm.scxml_converter.bt_converter import ( + bt_converter, + generate_blackboard_scxml, + get_blackboard_variables_from_models, +) from as2fm.scxml_converter.scxml_entries import EventsToAutomata, ScxmlRoot @@ -175,7 +179,6 @@ def generate_plain_scxml_models_and_timers( all_services: Dict[str, RosCommunicationHandler] = {} all_actions: Dict[str, RosCommunicationHandler] = {} bt_blackboard_vars: Dict[str, str] = get_blackboard_variables_from_models(ros_scxmls) - print(bt_blackboard_vars) for scxml_entry in ros_scxmls: plain_scxmls, ros_declarations = scxml_entry.to_plain_scxml_and_declarations() # Handle ROS timers @@ -199,6 +202,9 @@ def generate_plain_scxml_models_and_timers( ros_declarations._action_clients, ) plain_scxml_models.extend(plain_scxmls) + # Generate sync SCXML model for BT Blackboard (if needed) + if len(bt_blackboard_vars) > 0: + plain_scxml_models.append(generate_blackboard_scxml(bt_blackboard_vars)) # Generate sync SCXML models for services and actions for plain_scxml in generate_plain_scxml_from_handlers(all_services | all_actions): plain_scxml_models.append(plain_scxml) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 566a4831..60e04d6a 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -24,19 +24,31 @@ from lxml import etree as ET +from as2fm.as2fm_common.common import get_default_expression_for_type, value_to_string from as2fm.scxml_converter.scxml_entries import ( BtChildStatus, BtTickChild, RosRateCallback, RosTimeRate, + ScxmlAssign, + ScxmlData, + ScxmlDataModel, ScxmlExecutionBody, + ScxmlParam, ScxmlRoot, + ScxmlSend, ScxmlState, + ScxmlTransition, ) from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BT_BLACKBOARD_EVENT_VALUE, + BT_BLACKBOARD_GET, + BT_BLACKBOARD_REQUEST, + generate_bt_blackboard_set, get_blackboard_variable_name, is_blackboard_reference, ) +from as2fm.scxml_converter.scxml_entries.utils import SCXML_DATA_STR_TO_TYPE BT_ROOT_PREFIX = "bt_root_fsm_" @@ -63,6 +75,41 @@ def get_blackboard_variables_from_models(models: List[ScxmlRoot]) -> Dict[str, s return blackboard_vars +def generate_blackboard_scxml(bt_blackboard_vars: Dict[str, str]) -> ScxmlRoot: + """Generate an SCXML model that handles all BT related synchronization.""" + assert len(bt_blackboard_vars) > 0, "Cannot generate BT Blackboard, no variables" + # TODO: Append the name of the related BT, as in generate_bt_root_scxml + scxml_model_name = "bt_blackboard_fsm" + state_name = "idle" + idle_state = ScxmlState(state_name) + bt_data: List[ScxmlData] = [] + bt_bb_param_list: List[ScxmlParam] = [] + for bb_key, bb_type in bt_blackboard_vars.items(): + default_value = value_to_string( + get_default_expression_for_type(SCXML_DATA_STR_TO_TYPE[bb_type]) + ) + bt_data.append(ScxmlData(bb_key, default_value, bb_type)) + bt_bb_param_list.append(ScxmlParam(bb_key, expr=bb_key)) + idle_state.add_transition( + ScxmlTransition( + state_name, + [generate_bt_blackboard_set(bb_key)], + body=[ScxmlAssign(bb_key, BT_BLACKBOARD_EVENT_VALUE)], + ) + ) + idle_state.add_transition( + ScxmlTransition( + state_name, + [BT_BLACKBOARD_REQUEST], + body=[ScxmlSend(BT_BLACKBOARD_GET, bt_bb_param_list)], + ) + ) + bt_root = ScxmlRoot(scxml_model_name) + bt_root.set_data_model(ScxmlDataModel(bt_data)) + bt_root.add_state(idle_state, initial=True) + return bt_root + + def is_bt_root_scxml(scxml_name: str) -> bool: """ Check if the SCXML name matches with the BT root SCXML name pattern. diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 9fd38a58..581d6357 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -34,6 +34,9 @@ BT_BLACKBOARD_REQUEST = "bt_blackboard_req" BT_BLACKBOARD_GET = "bt_blackboard_get" +BT_SET_BLACKBOARD_PARAM = "value" +BT_BLACKBOARD_EVENT_VALUE = PLAIN_SCXML_EVENT_DATA_PREFIX + BT_SET_BLACKBOARD_PARAM + class BtResponse(Enum): """Enumeration of possible BT responses.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py index fc01de0c..ddc44449 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py @@ -21,6 +21,7 @@ from as2fm.scxml_converter.scxml_entries import ScxmlParam, ScxmlSend from as2fm.scxml_converter.scxml_entries.bt_utils import ( + BT_SET_BLACKBOARD_PARAM, BtPortsHandler, generate_bt_blackboard_set, get_blackboard_variable_name, @@ -73,7 +74,7 @@ def as_plain_scxml(self, _) -> ScxmlSend: ), "Error: SCXML BT Output Port: must run 'update_bt_ports_values' before 'as_plain_scxml'" return ScxmlSend( generate_bt_blackboard_set(self._blackboard_reference), - [ScxmlParam("value", expr=self._expr)], + [ScxmlParam(BT_SET_BLACKBOARD_PARAM, expr=self._expr)], ) def as_xml(self) -> ET.Element: From bf596da5a1b493e76dba550cb4992f3c7e622f72 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 17:20:09 +0100 Subject: [PATCH 20/58] Enable test Signed-off-by: Marco Lampacrescia --- test/jani_generator/test_systemtest_scxml_to_jani.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index f0da2cb7..8e44204b 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -434,7 +434,6 @@ def test_uc2_assembly_with_bug(self): success=False, ) - @pytest.mark.skip(reason="WIP: Blackboard support.") def test_grid_robot_blackboard(self): """Test the grid_robot_blackboard model (BT + Blackboard).""" self._test_with_main( From f6687484d4055dd49fae1b380eeae221118930e2 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 17:42:50 +0100 Subject: [PATCH 21/58] Fixed model Signed-off-by: Marco Lampacrescia --- .../grid_robot_blackboard/bt_move.scxml | 4 +-- .../grid_robot_blackboard/world.scxml | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml index 3b9afcc6..9462122c 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_move.scxml @@ -2,7 +2,7 @@ @@ -24,7 +24,7 @@ - + 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 1c34967e..30b457a5 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -6,11 +6,11 @@ initial="init"> - - - - - + + + + + @@ -23,12 +23,12 @@ - + - - - + + + @@ -36,17 +36,17 @@ - - + + - + - + - + From 4be98dc16d4c663b4422d90eaf6c1701e7f8443f Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Fri, 29 Nov 2024 18:24:21 +0100 Subject: [PATCH 22/58] Fixed models Signed-off-by: Marco Lampacrescia --- .../bt_update_goal_and_current_position.scxml | 7 +++-- .../grid_robot_blackboard/world.scxml | 27 ++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml index 0f797429..474fe424 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/bt_update_goal_and_current_position.scxml @@ -14,7 +14,8 @@ - + + @@ -38,15 +39,17 @@ + + - + 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 30b457a5..21bd48cc 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -9,8 +9,8 @@ - - + + @@ -25,17 +25,21 @@ - - - - - - - - - + + + + + + + + + + + + + @@ -55,6 +59,5 @@ - From ac88e3a9e363704818a6389bf9611baae9688ce0 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 9 Dec 2024 15:12:58 +0100 Subject: [PATCH 23/58] Quick pass over documentation Signed-off-by: Marco Lampacrescia --- docs/source/scxml-jani-conversion.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/source/scxml-jani-conversion.rst b/docs/source/scxml-jani-conversion.rst index 870864bf..f8d35ee8 100644 --- a/docs/source/scxml-jani-conversion.rst +++ b/docs/source/scxml-jani-conversion.rst @@ -6,18 +6,18 @@ SCXML and JANI In CONVINCE, we expect developers to use Behavior Trees and SCXML to model the different parts of a robotic systems. -SCXML (Scope XML) is a high level format that describes a single state machine, and allows it to exchange information with other state machines using events. Each SCXML file defines its variables (datamodel), states, and transitions. +SCXML (Scope XML) is an XML format that describes a single state machine, and allows it to exchange information with other SCXML state machines using events. Each SCXML file defines its variables (datamodel), states, and transitions. -With SCXML, the system consists of a set of state machines, each one represented by an SCXML file, which are synchronized together using events. Operations are carried out when the execution of a state machine receives an event, enters a state, or exits a state. +Using SCXML, the system can be modeled as a set of state machines, each one represented by an SCXML file, which are synchronized together using events. Operations are carried out when the execution of a state machine receives an event, enters a state, or exits a state. -With JANI, the whole system model is contained in a single JSON file, consisting of a set of global variables, automata (equivalent to state machines) with their edges (equivalent to transitions), and a composition description, specifying how the automata should be synchronized by advancing specific edges at the same time synchronously. +Using AS2FM, we can convert the model described using SCXML to JANI, that is a JSON-based format for describing a system as a formal model. With JANI, the whole system model is contained in a single JSON file, consisting of a set of global variables, automata (equivalent to state machines) with their edges (equivalent to transitions), and a composition description, specifying how the automata should be synchronized by advancing specific edges at the same time synchronously. The main difference between SCXML and JANI is that in JANI there is no concept of events, so synchronization must be achieved using the global variables and composition description. High-Level (ROS) SCXML Implementation --------------------------------------- -In CONVINCE, we extended the standard SCXML format defined `here `_ with ROS specific features, to make it easier for ROS developers to model ROS-based systems. +In CONVINCE, we extended the standard SCXML format defined `here `_ with ROS and Behavior Tree (BT) specific features, to make it easier for robot developers to model their systems using both ROS and BT. In this guide we will refer to the extended SCXML format as high-level SCXML and to the standard SCXML format as low-level SCXML. @@ -25,8 +25,12 @@ Currently, the supported ROS-features are: * ROS Topics * ROS Timers (Rate-callbacks) - -TODO: Example of Topic and Timer declaration + usage. +* ROS Service +* ROS Actions +* BT Ticks +* BT Responses +* BT Ports +* BT Blackboard Low-Level SCXML Conversion ---------------------------- @@ -47,7 +51,7 @@ TODO Handling of (ROS) Services _____________________________ -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. +ROS services, as well as ROS topics, can be handled directly in the conversion from HL-SCXML to LL-SCXML. The main structure of the SCXML related state machines can be inspected in the diagram below: @@ -55,13 +59,13 @@ The main structure of the SCXML related state machines can be inspected in the d :alt: Handling of ROS Services :align: center -The automata of clients and services are converted directly from the existing ROS-SCXML files, while the "Extra Service Handler" is autogenerated starting from the provided clients and services. +The automata of clients and services are converted directly from the existing ROS-SCXML files, while the "Extra Service Handler" is autogenerated starting from the provided clients' and services' declarations. Handling of (ROS) Actions _____________________________ -ROS actions are handled similarly to ROS Services: a ROS-SCXML description of the system is converted to plain SCXML, and an additional automaton is generated to handle the synchronization between the clients and the server. +ROS actions are handled similarly to ROS Services: a HL-SCXML description of the system is converted to LL-SCXML, and an additional automaton is generated to handle the synchronization between the clients and the server. The structure of a client-server communication through actions and additional threads looks as follows: @@ -73,7 +77,7 @@ The structure of a client-server communication through actions and additional th Handling the BT Blackboard _____________________________ -The Blackboard is a container that shares variables across different BT plugins. The value if those variables normally changes over time, and is expected to be updated at each tick. +The Blackboard is a container that shares variables across different BT plugins. The value of those variables normally changes over time, and is expected to be updated at each tick. In LL-SCXML, this is handled by an autogenerated SCXML FSM, that receives the updates from the various plugins and, upon request, provides the data. This diagram summarizes the FSM structure. From 7e37d91cefaa88fd5b45abaa53e98064f336459f Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 9 Dec 2024 15:45:36 +0100 Subject: [PATCH 24/58] Additional property to check actual functionality Signed-off-by: Marco Lampacrescia --- .../grid_robot_blackboard/properties.jani | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani index 1094fe1c..591adf9f 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani +++ b/test/jani_generator/_test_data/grid_robot_blackboard/properties.jani @@ -21,6 +21,39 @@ "op": "initial" } } + }, + { + "name": "at_goal", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "op": "F", + "exp": { + "op": "∧", + "left": "topic_goal_msg.valid", + "right": { + "op": "∧", + "left": { + "op": "=", + "left": "topic_pose_msg.ros_fields__x", + "right": "topic_goal_msg.ros_fields__x" + }, + "right": { + "op": "=", + "left": "topic_pose_msg.ros_fields__y", + "right": "topic_goal_msg.ros_fields__y" + } + } + } + } + }, + "states": { + "op": "initial" + } + } } ] } From 9ad0363f854b2b4a1086c0a8c105c28d1f44a77b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 9 Dec 2024 16:37:20 +0100 Subject: [PATCH 25/58] Comments for readability Signed-off-by: Marco Lampacrescia --- src/as2fm/scxml_converter/scxml_entries/scxml_state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index a35a0a05..6160284f 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -162,8 +162,9 @@ def _generate_blackboard_retrieval( if self._is_blackboard_required(bt_ports_handler): for transition in self._body: if isinstance(transition, BtTick): - # TODO: Write the transitions names in a variable - new_state_id = f"{self.get_id}_on_tick_{len(generated_states)}" + # Prepare the new state using the received BT info + states_count = len(generated_states) + new_state_id = f"{self.get_id}_{transition.get_tag_name()}_{states_count}" new_state = ScxmlState(new_state_id) blackboard_transition = ScxmlTransition( transition.get_target_state_id(), @@ -171,9 +172,10 @@ def _generate_blackboard_retrieval( body=transition.get_body(), ) new_state.add_transition(blackboard_transition) + generated_states.append(new_state) + # Set the new target and body to the original transition transition.set_target_state_id(new_state_id) transition.set_body([ScxmlSend(BT_BLACKBOARD_REQUEST)]) - generated_states.append(new_state) return generated_states def _substitute_bt_events_and_ports( From ac9038e5a1183edc8bced5ed0c37831b99f650b0 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 9 Dec 2024 18:01:53 +0100 Subject: [PATCH 26/58] Prepare method for checking the need of reading from the blackboard Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_executable_entries.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index fdb6a4fe..46789f33 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -599,3 +599,18 @@ def add_targets_to_scxml_send( else: raise ValueError(f"Error: SCXML send: invalid entry type {type(entry)}.") return new_body + + +def has_bt_blackboard_input( + exec_body: Optional[ScxmlExecutionBody], bt_ports_info: BtPortsHandler +) -> bool: + """ + Check if any entry in the execution body requires reading from the blackboard. + """ + if exec_body is None: + return False + for entry in exec_body: + # If any entry in the executable body requires reading from the blackboard, report it + if entry.has_bt_blackboard_input(bt_ports_info): + return True + return False From 09f5a56283ead4560994161046abc7fe18126afb Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 10 Dec 2024 14:37:07 +0100 Subject: [PATCH 27/58] Add support for reading blackboard from all other transitions Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_executable_entries.py | 51 +++++++++++++------ .../scxml_entries/scxml_param.py | 6 +++ .../scxml_entries/scxml_ros_base.py | 7 +++ .../scxml_entries/scxml_state.py | 21 ++++---- .../scxml_entries/scxml_transition.py | 4 ++ 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index 46789f33..b6f6d668 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -33,6 +33,7 @@ from as2fm.scxml_converter.scxml_entries.bt_utils import ( BtPortsHandler, get_input_variable_as_scxml_expression, + is_blackboard_reference, is_bt_event, ) from as2fm.scxml_converter.scxml_entries.utils import ( @@ -81,6 +82,21 @@ def update_exec_body_bt_ports_values( entry.update_bt_ports_values(bt_ports_handler) +def has_bt_blackboard_input( + exec_body: Optional[ScxmlExecutionBody], bt_ports_info: BtPortsHandler +) -> bool: + """ + Check if any entry in the execution body requires reading from the blackboard. + """ + if exec_body is None: + return False + for entry in exec_body: + # If any entry in the executable body requires reading from the blackboard, report it + if entry.has_bt_blackboard_input(bt_ports_info): + return True + return False + + class ScxmlIf(ScxmlBase): """This class represents SCXML conditionals.""" @@ -158,6 +174,13 @@ def get_else_execution(self) -> ScxmlExecutionBody: """Get the else execution.""" return self._else_execution + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + """Check whether the If entry reads content from the BT Blackboard.""" + for _, cond_body in self._conditional_executions: + if has_bt_blackboard_input(cond_body, bt_ports_handler): + return True + return has_bt_blackboard_input(self._else_execution, bt_ports_handler) + def instantiate_bt_events(self, instance_id: int, children_ids: List[int]) -> "ScxmlIf": """Instantiate the behavior tree events in the If action, if available.""" for _, exec_body in self._conditional_executions: @@ -306,6 +329,13 @@ def set_target_automaton(self, target_automaton: str) -> None: """Set the target automata associated to this send event.""" self._target_automaton = target_automaton + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + """Check whether the If entry reads content from the BT Blackboard.""" + for param in self._params: + if param.has_bt_blackboard_input(bt_ports_handler): + return True + return False + def instantiate_bt_events(self, instance_id: int, _) -> "ScxmlSend": """Instantiate the behavior tree events in the send action, if available.""" # Support for deprecated BT events handling. Remove the whole if block once transition done. @@ -417,6 +447,12 @@ def get_expr(self) -> Union[str, BtGetValueInputPort]: """Get the expression to assign.""" return self._expr + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + """Check whether the If entry reads content from the BT Blackboard.""" + return isinstance(self._expr, BtGetValueInputPort) and is_blackboard_reference( + bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + ) + def instantiate_bt_events(self, _, __) -> "ScxmlAssign": """This functionality is not needed in this class.""" return self @@ -599,18 +635,3 @@ def add_targets_to_scxml_send( else: raise ValueError(f"Error: SCXML send: invalid entry type {type(entry)}.") return new_body - - -def has_bt_blackboard_input( - exec_body: Optional[ScxmlExecutionBody], bt_ports_info: BtPortsHandler -) -> bool: - """ - Check if any entry in the execution body requires reading from the blackboard. - """ - if exec_body is None: - return False - for entry in exec_body: - # If any entry in the executable body requires reading from the blackboard, report it - if entry.has_bt_blackboard_input(bt_ports_info): - return True - return False diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py index deddc8db..c2fa1bff 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py @@ -25,6 +25,7 @@ from as2fm.scxml_converter.scxml_entries.bt_utils import ( BtPortsHandler, get_input_variable_as_scxml_expression, + is_blackboard_reference, ) from as2fm.scxml_converter.scxml_entries.utils import CallbackType, is_non_empty_string from as2fm.scxml_converter.scxml_entries.xml_utils import ( @@ -86,6 +87,11 @@ def get_expr(self) -> Optional[str]: def get_location(self) -> Optional[str]: return self._location + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + return isinstance(self._expr, BtGetValueInputPort) and is_blackboard_reference( + bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + ) + def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): """Update the values of potential entries making use of BT ports.""" if isinstance(self._expr, BtGetValueInputPort): diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py index 512d6560..482afa24 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_base.py @@ -370,6 +370,13 @@ def append_field(self, field: RosField) -> None: field.set_callback_type(self._cb_type) self._fields.append(field) + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + """Check whether the If entry reads content from the BT Blackboard.""" + for field in self._fields: + if field.has_bt_blackboard_input(bt_ports_handler): + return True + return False + def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): """Update the values of potential entries making use of BT ports.""" for field in self._fields: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index 6160284f..71cdf07d 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -39,6 +39,7 @@ from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import ( as_plain_execution_body, execution_body_from_xml, + has_bt_blackboard_input, instantiate_exec_body_bt_events, set_execution_body_callback_type, valid_execution_body, @@ -147,21 +148,21 @@ def set_thread_id(self, thread_idx: int): if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_idx) - def _is_blackboard_required(self, bt_ports_handler: BtPortsHandler) -> List["ScxmlState"]: - # TODO(Blackboard-optimization): We should generate the additional state only in case - # there is a bt_get_input targeting a blackboard variable, this requires adding support - # to retrieve this information from the state's children. - # TODO: Additionally, we assume the bt is read only during ticks, but we aren't verifying - # this assumption - return bt_ports_handler.has_blackboard_inputs() and self.has_bt_tick_transitions() - def _generate_blackboard_retrieval( self, bt_ports_handler: BtPortsHandler ) -> List["ScxmlState"]: generated_states: List[ScxmlState] = [self] - if self._is_blackboard_required(bt_ports_handler): + if bt_ports_handler.has_blackboard_inputs(): + assert not has_bt_blackboard_input(self._on_entry, bt_ports_handler), ( + f"Error: SCXML state {self.get_id()}: reading blackboard variables from onentry. " + "This isn't yet supported." + ) + assert not has_bt_blackboard_input(self._on_exit, bt_ports_handler), ( + f"Error: SCXML state {self.get_id()}: reading blackboard variables from onexit. " + "This isn't yet supported." + ) for transition in self._body: - if isinstance(transition, BtTick): + if transition.has_bt_blackboard_input(bt_ports_handler): # Prepare the new state using the received BT info states_count = len(generated_states) new_state_id = f"{self.get_id}_{transition.get_tag_name()}_{states_count}" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index b62ff01a..f58f0d27 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -31,6 +31,7 @@ from as2fm.scxml_converter.scxml_entries.bt_utils import BtPortsHandler, is_bt_event from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import ( execution_body_from_xml, + has_bt_blackboard_input, instantiate_exec_body_bt_events, set_execution_body_callback_type, valid_execution_body, @@ -124,6 +125,9 @@ def set_body(self, body: ScxmlExecutionBody) -> None: """Set the body of this transition.""" self._body = body + def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): + return has_bt_blackboard_input(self._body, bt_ports_handler) + def instantiate_bt_events( self, instance_id: int, children_ids: List[int] ) -> List["ScxmlTransition"]: From 8736824ac544cb8012914430e82f0b27e780f099 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 10 Dec 2024 14:44:27 +0100 Subject: [PATCH 28/58] Update property name in test of grid_world Signed-off-by: Marco Lampacrescia --- test/jani_generator/test_systemtest_scxml_to_jani.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 8e44204b..2de8747c 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -439,7 +439,7 @@ def test_grid_robot_blackboard(self): self._test_with_main( "grid_robot_blackboard", model_xml="main.xml", - property_name="tree_success", + property_name="at_goal", success=True, ) From 1f8819ec05541b89686f3498243594c774efc8f1 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Tue, 10 Dec 2024 17:41:29 +0100 Subject: [PATCH 29/58] Add test and bug circumventing Signed-off-by: Marco Lampacrescia Signed-off-by: Christian Henkel --- .../scxml_entries/scxml_bt_out_port.py | 4 ++ .../scxml_entries/scxml_bt_ticks.py | 4 ++ .../scxml_entries/scxml_root.py | 5 ++- .../_test_data/blackboard_test/bt.xml | 9 ++++ .../blackboard_test/bt_read_bb.scxml | 43 +++++++++++++++++++ .../blackboard_test/bt_write_bb.scxml | 23 ++++++++++ .../_test_data/blackboard_test/main.xml | 17 ++++++++ .../blackboard_test/properties.jani | 26 +++++++++++ .../test_systemtest_scxml_to_jani.py | 9 ++++ 9 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 test/jani_generator/_test_data/blackboard_test/bt.xml create mode 100644 test/jani_generator/_test_data/blackboard_test/bt_read_bb.scxml create mode 100644 test/jani_generator/_test_data/blackboard_test/bt_write_bb.scxml create mode 100644 test/jani_generator/_test_data/blackboard_test/main.xml create mode 100644 test/jani_generator/_test_data/blackboard_test/properties.jani diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py index ddc44449..e0fbbe13 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_out_port.py @@ -57,6 +57,10 @@ def check_validity(self) -> bool: BtSetValueOutputPort, "expr", self._expr ) + def has_bt_blackboard_input(self, _) -> bool: + """We do not expect reading from BT Ports here. Return False!""" + return False + def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: assert bt_ports_handler.out_port_exists( self._key diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index a0ef8d81..7a9ba005 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -275,6 +275,10 @@ def __init__(self, status: str): def check_validity(self) -> bool: return True + def has_bt_blackboard_input(self, _) -> bool: + """We do not expect reading from BT Ports here. Return False!""" + return False + def instantiate_bt_events(self, instance_id: int, _) -> ScxmlSend: return ScxmlSend( generate_bt_response_event(instance_id), diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index 47e84dde..c4a9db99 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -343,8 +343,9 @@ def to_plain_scxml_and_declarations( - a list of ScxmlRoot objects with all ROS specific entries converted to plain SCXML - The Ros declarations contained in the original SCXML object """ - if self.is_plain_scxml(): - return [self], ScxmlRosDeclarationsContainer(self._name) + # TODO: The check for BtTicks is not working, since we convert it at instantiate_bt_events + # if self.is_plain_scxml(): + # return [self], ScxmlRosDeclarationsContainer(self._name) converted_scxmls: List[ScxmlRoot] = [] # Convert the ROS specific entries to plain SCXML main_scxml = ScxmlRoot(self._name) diff --git a/test/jani_generator/_test_data/blackboard_test/bt.xml b/test/jani_generator/_test_data/blackboard_test/bt.xml new file mode 100644 index 00000000..4712d2dd --- /dev/null +++ b/test/jani_generator/_test_data/blackboard_test/bt.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/jani_generator/_test_data/blackboard_test/bt_read_bb.scxml b/test/jani_generator/_test_data/blackboard_test/bt_read_bb.scxml new file mode 100644 index 00000000..bb96244b --- /dev/null +++ b/test/jani_generator/_test_data/blackboard_test/bt_read_bb.scxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/blackboard_test/bt_write_bb.scxml b/test/jani_generator/_test_data/blackboard_test/bt_write_bb.scxml new file mode 100644 index 00000000..74781bcb --- /dev/null +++ b/test/jani_generator/_test_data/blackboard_test/bt_write_bb.scxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/blackboard_test/main.xml b/test/jani_generator/_test_data/blackboard_test/main.xml new file mode 100644 index 00000000..9b822905 --- /dev/null +++ b/test/jani_generator/_test_data/blackboard_test/main.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/blackboard_test/properties.jani b/test/jani_generator/_test_data/blackboard_test/properties.jani new file mode 100644 index 00000000..1094fe1c --- /dev/null +++ b/test/jani_generator/_test_data/blackboard_test/properties.jani @@ -0,0 +1,26 @@ +{ + "properties": [ + { + "name": "tree_success", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "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}" + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 2de8747c..d4805c18 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -434,6 +434,15 @@ def test_uc2_assembly_with_bug(self): success=False, ) + def test_blackboard_features(self): + """Test the grid_robot_blackboard model (BT + Blackboard).""" + self._test_with_main( + "blackboard_test", + model_xml="main.xml", + property_name="tree_success", + success=True, + ) + def test_grid_robot_blackboard(self): """Test the grid_robot_blackboard model (BT + Blackboard).""" self._test_with_main( From b87bef288f07ddfbb3cf3b9dde5b3abc0a26cf94 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Wed, 11 Dec 2024 09:19:06 +0100 Subject: [PATCH 30/58] Proper check for plain scxml of scxml_root object Signed-off-by: Marco Lampacrescia Signed-off-by: Christian Henkel --- .../scxml_entries/scxml_executable_entries.py | 24 +++++++++++++++++++ .../scxml_entries/scxml_param.py | 4 ++-- .../scxml_entries/scxml_root.py | 13 +++++----- .../scxml_entries/scxml_state.py | 11 +++++---- .../scxml_entries/scxml_transition.py | 5 ++++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index b6f6d668..3451e814 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -232,6 +232,13 @@ def set_thread_id(self, thread_id: int) -> None: if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_id) + def is_plain_scxml(self) -> bool: + if type(self) is ScxmlIf: + return all( + is_plain_execution_body(body) for _, body in self._conditional_executions + ) and is_plain_execution_body(self._else_execution) + return False + def as_plain_scxml(self, ros_declarations: ScxmlRosDeclarationsContainer) -> "ScxmlIf": assert self._cb_type is not None, "Error: SCXML if: callback type not set." conditional_executions = [] @@ -388,6 +395,11 @@ def append_param(self, param: ScxmlParam) -> None: assert isinstance(param, ScxmlParam), "Error: SCXML send: invalid param." self._params.append(param) + def is_plain_scxml(self) -> bool: + if type(self) is ScxmlSend: + return all(isinstance(param.get_expr(), str) for param in self._params) + return False + def as_plain_scxml(self, _) -> "ScxmlSend": # For now we don't need to do anything here. Change this to handle ros expr in scxml params. assert self._cb_type is not None, "Error: SCXML send: callback type not set." @@ -475,6 +487,11 @@ def check_valid_ros_instantiations(self, _) -> bool: # This has nothing to do with ROS. Return always True return True + def is_plain_scxml(self) -> bool: + if type(self) is ScxmlAssign: + return isinstance(self._expr, str) + return False + def as_plain_scxml(self, _) -> "ScxmlAssign": # TODO: Might make sense to check if the assignment happens in a topic callback assert self._cb_type is not None, "Error: SCXML assign: callback type not set." @@ -590,6 +607,13 @@ def set_execution_body_callback_type(exec_body: ScxmlExecutionBody, cb_type: Cal entry.set_callback_type(cb_type) +def is_plain_execution_body(exec_body: Optional[ScxmlExecutionBody]) -> bool: + """Check if al entries in the exec body are plain scxml.""" + if exec_body is None: + return True + return all(entry.is_plain_scxml() for entry in exec_body) + + def as_plain_execution_body( exec_body: Optional[ScxmlExecutionBody], ros_declarations: ScxmlRosDeclarationsContainer ) -> Optional[ScxmlExecutionBody]: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py index c2fa1bff..526baa5f 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py @@ -63,7 +63,7 @@ def __init__( """ Initialize the SCXML Parameter object. - The location entryu is kept for consistency, but using expr achieves the same result. + The 'location' entry is kept for consistency, but using expr achieves the same result. :param name: The name of the parameter. :param expr: The expression to assign to the parameter. Can come from a BT port. @@ -81,7 +81,7 @@ def set_callback_type(self, cb_type: CallbackType): def get_name(self) -> str: return self._name - def get_expr(self) -> Optional[str]: + def get_expr(self) -> Optional[Union[BtGetValueInputPort, str]]: return self._expr def get_location(self) -> Optional[str]: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py index c4a9db99..e3d39d6f 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_root.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_root.py @@ -327,11 +327,11 @@ def _check_valid_ros_declarations(self) -> bool: return True def is_plain_scxml(self) -> bool: - """Check whether there are ROS specific features or all entries are plain SCXML.""" + """Check whether there are ROS or BT specific tags in the SCXML model.""" assert self.check_validity(), "SCXML: found invalid root object." - has_ros_entries = len(self._ros_declarations) > 0 or len(self._additional_threads) > 0 - has_bt_entries = any(state.has_bt_tick_transitions() for state in self._states) - return not (has_ros_entries or has_bt_entries) + no_ros_declarations = (len(self._ros_declarations) + len(self._additional_threads)) == 0 + all_states_plain = all(state.is_plain_scxml() for state in self._states) + return no_ros_declarations and all_states_plain def to_plain_scxml_and_declarations( self, @@ -343,9 +343,8 @@ def to_plain_scxml_and_declarations( - a list of ScxmlRoot objects with all ROS specific entries converted to plain SCXML - The Ros declarations contained in the original SCXML object """ - # TODO: The check for BtTicks is not working, since we convert it at instantiate_bt_events - # if self.is_plain_scxml(): - # return [self], ScxmlRosDeclarationsContainer(self._name) + if self.is_plain_scxml(): + return [self], ScxmlRosDeclarationsContainer(self._name) converted_scxmls: List[ScxmlRoot] = [] # Convert the ROS specific entries to plain SCXML main_scxml = ScxmlRoot(self._name) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index 71cdf07d..e2111a03 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -23,7 +23,6 @@ from as2fm.as2fm_common.common import is_comment from as2fm.scxml_converter.scxml_entries import ( - BtTick, ScxmlBase, ScxmlExecutableEntry, ScxmlExecutionBody, @@ -41,6 +40,7 @@ execution_body_from_xml, has_bt_blackboard_input, instantiate_exec_body_bt_events, + is_plain_execution_body, set_execution_body_callback_type, valid_execution_body, ) @@ -277,9 +277,12 @@ def _check_valid_ros_instantiations( entry.check_valid_ros_instantiations(ros_declarations) for entry in body ) - def has_bt_tick_transitions(self) -> bool: - """Check if the state has BT tick transitions.""" - return any(isinstance(entry, BtTick) for entry in self._body) + def is_plain_scxml(self) -> bool: + """Check if all SCXML entries in the state are plain scxml.""" + plain_entry = is_plain_execution_body(self._on_entry) + plain_exit = is_plain_execution_body(self._on_exit) + plain_body = all(transition.is_plain_scxml() for transition in self._body) + return plain_entry and plain_exit and plain_body def as_plain_scxml(self, ros_declarations: ScxmlRosDeclarationsContainer) -> "ScxmlState": """Convert the ROS-specific entries to be plain SCXML""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py index f58f0d27..1cb6a81d 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_transition.py @@ -33,6 +33,7 @@ execution_body_from_xml, has_bt_blackboard_input, instantiate_exec_body_bt_events, + is_plain_execution_body, set_execution_body_callback_type, valid_execution_body, valid_execution_body_entry_types, @@ -204,6 +205,10 @@ def set_thread_id(self, thread_id: int) -> None: if hasattr(entry, "set_thread_id"): entry.set_thread_id(thread_id) + def is_plain_scxml(self) -> bool: + """Check if the transition is a plain scxml entry and contains only plain scxml.""" + return type(self) is ScxmlTransition and is_plain_execution_body(self._body) + def as_plain_scxml(self, ros_declarations: ScxmlRosDeclarationsContainer) -> "ScxmlTransition": assert isinstance( ros_declarations, ScxmlRosDeclarationsContainer From c8aaaaae4329888f7ab7ce108ac02711c2a9477d Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Fri, 13 Dec 2024 17:31:11 +0100 Subject: [PATCH 31/58] a simple grid example that doesnt work right now Signed-off-by: Christian Henkel --- .../grid_robot_blackboard_simpler/bt.xml | 33 +++++ .../bt_move.scxml | 49 ++++++++ .../bt_shall_move.scxml | 76 ++++++++++++ .../bt_update_goal_and_current_position.scxml | 117 ++++++++++++++++++ .../grid_robot_blackboard_simpler/main.xml | 23 ++++ .../properties.jani | 47 +++++++ .../grid_robot_blackboard_simpler/world.scxml | 63 ++++++++++ .../test_systemtest_scxml_to_jani.py | 11 +- 8 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml new file mode 100644 index 00000000..31151fc4 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml new file mode 100644 index 00000000..5facc523 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml new file mode 100644 index 00000000..90a8a178 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml new file mode 100644 index 00000000..38bf9370 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml new file mode 100644 index 00000000..9a3ce9cf --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani new file mode 100644 index 00000000..f4670774 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani @@ -0,0 +1,47 @@ +{ + "properties": [ + { + "expression": { + "fun": "values", + "op": "filter", + "states": { + "op": "initial" + }, + "values": { + "exp": { + "exp": { + "comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}", + "left": "bt_1000_response.status", + "op": "=", + "right": 1 + }, + "op": "F" + }, + "op": "Pmin" + } + }, + "name": "tree_success" + }, + { + "expression": { + "fun": "values", + "op": "filter", + "states": { + "op": "initial" + }, + "values": { + "exp": { + "exp": { + "left": "topic_at_goal_msg.valid", + "op": "∧", + "right": "topic_at_goal_msg.data" + }, + "op": "F" + }, + "op": "Pmin" + } + }, + "name": "at_goal" + } + ] +} diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml new file mode 100644 index 00000000..21bd48cc --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index d4805c18..213149d1 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -435,7 +435,7 @@ def test_uc2_assembly_with_bug(self): ) def test_blackboard_features(self): - """Test the grid_robot_blackboard model (BT + Blackboard).""" + """Test the blackboard features.""" self._test_with_main( "blackboard_test", model_xml="main.xml", @@ -452,6 +452,15 @@ def test_grid_robot_blackboard(self): success=True, ) + def test_grid_robot_blackboard_simpler(self): + """Test the simpler grid_robot_blackboard model (BT + Blackboard).""" + self._test_with_main( + "grid_robot_blackboard_simpler", + model_xml="main.xml", + property_name="at_goal", + success=True, + ) + def test_command_line_output_with_line_numbers(self): """Test the command line output with line numbers for the main.xml file.""" tmp_test_dir = os.path.join("/tmp", "test_as2fm") From fbfbb69010524219e34bad58607acb43ce3df722 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Sat, 14 Dec 2024 10:47:05 +0100 Subject: [PATCH 32/58] a bug Signed-off-by: Christian Henkel --- src/as2fm/scxml_converter/scxml_entries/scxml_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index e2111a03..d2c60aa2 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -165,7 +165,7 @@ def _generate_blackboard_retrieval( if transition.has_bt_blackboard_input(bt_ports_handler): # Prepare the new state using the received BT info states_count = len(generated_states) - new_state_id = f"{self.get_id}_{transition.get_tag_name()}_{states_count}" + new_state_id = f"{self.get_id()}_{transition.get_tag_name()}_{states_count}" new_state = ScxmlState(new_state_id) blackboard_transition = ScxmlTransition( transition.get_target_state_id(), From 59a28f4eaee762331b9585a58e10f84a4dcb0e53 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Sat, 14 Dec 2024 10:47:27 +0100 Subject: [PATCH 33/58] bug .. Unknown identifier _event.data.move Signed-off-by: Christian Henkel --- .../bt_update_goal_and_current_position.scxml | 38 +++++++++---------- .../properties.jani | 2 +- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index 38bf9370..2ccb6273 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -10,7 +10,7 @@ - + + - + - - - + + + + + + + + + + + - + @@ -98,10 +102,6 @@ - - - - - + + + + + + + + @@ -70,21 +78,16 @@ - - - - - - - - - - + + + + + - + @@ -98,6 +101,7 @@ + From 6cc80e81b33ae13f99c6cecbd65c98a2a8aa758d Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 10:24:21 +0100 Subject: [PATCH 39/58] Introduce support for randomness in the model Signed-off-by: Marco Lampacrescia --- .../as2fm_common/ecmascript_interpretation.py | 5 +- .../jani_generator/jani_entries/__init__.py | 7 +- .../jani_entries/jani_assignment.py | 17 ++- .../jani_entries/jani_automaton.py | 6 + .../jani_convince_expression_expansion.py | 64 +++++++- .../jani_generator/jani_entries/jani_edge.py | 8 +- .../jani_entries/jani_expression.py | 124 +++++++++++---- .../jani_entries/jani_expression_generator.py | 12 +- .../jani_entries/jani_helpers.py | 121 +++++++++++++++ .../scxml_helpers/scxml_tags.py | 22 ++- .../scxml_helpers/scxml_to_jani.py | 2 + .../grid_robot_blackboard/world.scxml | 30 ++-- .../test_unittest_expression_expansion.py | 141 ++++++++++++++++++ 13 files changed, 504 insertions(+), 55 deletions(-) create mode 100644 src/as2fm/jani_generator/jani_entries/jani_helpers.py create mode 100644 test/jani_generator/test_unittest_expression_expansion.py diff --git a/src/as2fm/as2fm_common/ecmascript_interpretation.py b/src/as2fm/as2fm_common/ecmascript_interpretation.py index 1437f3db..c0e963f7 100644 --- a/src/as2fm/as2fm_common/ecmascript_interpretation.py +++ b/src/as2fm/as2fm_common/ecmascript_interpretation.py @@ -45,7 +45,10 @@ def interpret_ecma_script_expr( msg_addition = "" if expr in ("True", "False"): msg_addition = "Did you mean to use 'true' or 'false' instead?" - raise RuntimeError(f"Failed to interpret JS expression: 'result = {expr}'. {msg_addition}") + raise RuntimeError( + f"Failed to interpret JS expression using variables {variables}: ", + f"'result = {expr}'. {msg_addition}", + ) expr_result = context.result if isinstance(expr_result, BasicJsTypes): return expr_result diff --git a/src/as2fm/jani_generator/jani_entries/__init__.py b/src/as2fm/jani_generator/jani_entries/__init__.py index a947bb31..04f109ae 100644 --- a/src/as2fm/jani_generator/jani_entries/__init__.py +++ b/src/as2fm/jani_generator/jani_entries/__init__.py @@ -1,7 +1,12 @@ # isort: skip_file # Skipping file to avoid circular import problem from .jani_value import JaniValue # noqa: F401 -from .jani_expression import JaniExpression, JaniExpressionType # noqa: F401 +from .jani_expression import ( # noqa: F401 + JaniExpression, + JaniExpressionType, + JaniDistribution, + generate_jani_expression, +) # noqa: F401 from .jani_constant import JaniConstant # noqa: F401 from .jani_variable import JaniVariable # noqa: F401 from .jani_assignment import JaniAssignment # noqa: F401 diff --git a/src/as2fm/jani_generator/jani_entries/jani_assignment.py b/src/as2fm/jani_generator/jani_entries/jani_assignment.py index f6afca24..40bc94a3 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_assignment.py +++ b/src/as2fm/jani_generator/jani_entries/jani_assignment.py @@ -19,7 +19,7 @@ from typing import Dict -from as2fm.jani_generator.jani_entries import JaniConstant, JaniExpression +from as2fm.jani_generator.jani_entries import JaniConstant, JaniExpression, generate_jani_expression from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import expand_expression @@ -30,12 +30,23 @@ class JaniAssignment: def __init__(self, assignment_dict: dict): """Initialize the assignment from a dictionary""" - self._var_name = JaniExpression(assignment_dict["ref"]) - self._value = JaniExpression(assignment_dict["value"]) + self._var_name = generate_jani_expression(assignment_dict["ref"]) + self._value: JaniExpression = generate_jani_expression(assignment_dict["value"]) self._index = 0 if "index" in assignment_dict: self._index = assignment_dict["index"] + def get_target(self): + """Return the variable storing the expression result.""" + return self._var_name + + def get_expression(self): + """Return the expression assigned to the target variable (or array entry)""" + return self._value + + def get_index(self) -> int: + return self._index + def as_dict(self, constants: Dict[str, JaniConstant]): """Transform the assignment to a dictionary""" expanded_value = expand_expression(self._value, constants) diff --git a/src/as2fm/jani_generator/jani_entries/jani_automaton.py b/src/as2fm/jani_generator/jani_entries/jani_automaton.py index eb4563c0..ea8716eb 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_automaton.py +++ b/src/as2fm/jani_generator/jani_entries/jani_automaton.py @@ -78,6 +78,12 @@ def add_edge(self, edge: JaniEdge): self._edge_id += 1 self._edges.append(edge) + def set_edges(self, new_edges: List[JaniEdge]) -> None: + """Replace the edges in the Automaton.""" + self._edges = [] + for edge in new_edges: + self.add_edge(edge) + def get_edges(self) -> List[JaniEdge]: return self._edges 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 67a88fed..22a877e1 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 @@ -15,15 +15,22 @@ """Expand expressions into jani.""" +from copy import deepcopy from math import pi -from typing import Callable, Dict, Union +from typing import Callable, Dict, List -from as2fm.jani_generator.jani_entries import JaniConstant, JaniExpression, JaniValue +from as2fm.jani_generator.jani_entries import ( + JaniConstant, + JaniDistribution, + JaniExpression, + JaniExpressionType, +) from as2fm.jani_generator.jani_entries.jani_expression_generator import ( abs_operator, and_operator, ceil_operator, cos_operator, + distribution_expression, divide_operator, equal_operator, floor_operator, @@ -83,6 +90,11 @@ } +def random_operator() -> JaniDistribution: + """Function to get a random number between 0 and 1 in the Jani Model.""" + return distribution_expression("Uniform", [0.0, 1.0]) + + # Custom operators (CONVINCE, specific to mobile 2D robot use case) def intersection_operator(left, right) -> JaniExpression: return JaniExpression( @@ -453,7 +465,7 @@ def __substitute_expression_op(expression: JaniExpression) -> JaniExpression: def expand_expression( - expression: Union[JaniExpression, JaniValue], jani_constants: Dict[str, JaniConstant] + expression: JaniExpression, jani_constants: Dict[str, JaniConstant] ) -> JaniExpression: # Given a CONVINCE JaniExpression, expand it to a plain JaniExpression assert isinstance( @@ -462,6 +474,9 @@ def expand_expression( assert ( expression.is_valid() ), "The expression is not valid: it defines no value, nor variable, nor operation to be done." + if expression.get_expression_type() == JaniExpressionType.DISTRIBUTION: + # For now this is fine, since we expect only real values in the args + return expression if expression.op is None: # It is either a variable/constant identifier or a value return expression @@ -492,6 +507,48 @@ def expand_expression( return __substitute_expression_op(expression) +def expand_distribution_expressions( + expression: JaniExpression, *, n_options: int = 101 +) -> List[JaniExpression]: + """ + Traverse the expression and substitute each distribution with n expressions. + + This is a workaround, until we can support it in our model checker. + + :param expression: The expression to expand. + :param n_options: How many options to generate for each encountered distribution. + :return: One expression, if no distribution is found, n_options^n_distributions expr. otherwise. + """ + assert isinstance( + expression, JaniExpression + ), f"Unexpected expression type: {type(expression)} != (JaniExpression, JaniDistribution)." + assert expression.is_valid(), f"Invalid expression found: {expression}." + expr_type = expression.get_expression_type() + if expr_type == JaniExpressionType.OPERATOR: + # Generate all possible expressions, if expansion returns many expressions for an operand + expanded_expressions: List[JaniExpression] = [deepcopy(expression)] + for key, value in expression.operands.items(): + expanded_operand = expand_distribution_expressions(value, n_options=n_options) + base_expressions = expanded_expressions + expanded_expressions = [] + for expr in base_expressions: + for key_value in expanded_operand: + expr.operands[key] = key_value + expanded_expressions.append(deepcopy(expr)) + return expanded_expressions + elif expr_type == JaniExpressionType.DISTRIBUTION: + # Here we need to substitute the distribution with a number of constants + assert isinstance(expression, JaniDistribution) and expression.is_valid() + lower_bound = expression.get_dist_args()[0] + 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) + ] + return [expression] + + # Map each function name to the corresponding Expression generator CALLABLE_OPERATORS_MAP: Dict[str, Callable] = { "abs": abs_operator, @@ -503,4 +560,5 @@ def expand_expression( "pow": pow_operator, "min": min_operator, "max": max_operator, + "random": random_operator, } diff --git a/src/as2fm/jani_generator/jani_entries/jani_edge.py b/src/as2fm/jani_generator/jani_entries/jani_edge.py index b4296d5b..85cf5e16 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_edge.py +++ b/src/as2fm/jani_generator/jani_entries/jani_edge.py @@ -15,7 +15,7 @@ """And edge defining the possible transition from one state to another in jani.""" -from typing import Dict, Optional +from typing import Dict, List, Optional from as2fm.jani_generator.jani_entries import ( JaniAssignment, @@ -52,6 +52,7 @@ def __init__(self, edge_dict: dict): jani_destination["assignments"].append(assignment) else: raise RuntimeError(f"Unexpected type {type(assignment)} in assignments") + _sort_assignments_by_index(jani_destination["assignments"]) self.destinations.append(jani_destination) def get_action(self) -> Optional[str]: @@ -93,3 +94,8 @@ def as_dict(self, constants: Dict[str, JaniConstant]): single_destination.update({"assignments": expanded_assignments}) edge_dict["destinations"].append(single_destination) return edge_dict + + +def _sort_assignments_by_index(assignments: List[JaniAssignment]) -> None: + """Sorts a list of assignments by assignment index.""" + assignments.sort(key=lambda assignment: assignment.get_index()) diff --git a/src/as2fm/jani_generator/jani_entries/jani_expression.py b/src/as2fm/jani_generator/jani_entries/jani_expression.py index 46eb782a..de98c59a 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_expression.py +++ b/src/as2fm/jani_generator/jani_entries/jani_expression.py @@ -18,7 +18,7 @@ """ from enum import Enum -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from as2fm.jani_generator.jani_entries import JaniValue from as2fm.scxml_converter.scxml_entries.utils import PLAIN_SCXML_EVENT_DATA_PREFIX @@ -32,6 +32,7 @@ class JaniExpressionType(Enum): IDENTIFIER = 1 # Reference to a constant or variable id LITERAL = 2 # Reference to a literal value OPERATOR = 3 # Reference to an operator (a composition of expressions) + DISTRIBUTION = 4 # A random number from a distribution class JaniExpression: @@ -53,6 +54,9 @@ def __init__(self, expression: Union[SupportedExp, "JaniExpression", JaniValue]) self.op: Optional[str] = None self.operands: Dict[str, JaniExpression] = {} if isinstance(expression, JaniExpression): + assert ( + expression.get_expression_type() != JaniExpressionType.DISTRIBUTION + ), "Cannot convert a JaniDistribution to a JaniExpression explicitly." self.identifier = expression.identifier self.value = expression.value self.op = expression.op @@ -87,15 +91,15 @@ def _get_operands(self, expression_dict: dict) -> Dict[str, "JaniExpression"]: # intersection occurs at the start distance: returns the distance between the robot and # the barrier. return { - "robot": JaniExpression(expression_dict["robot"]), - "barrier": JaniExpression(expression_dict["barrier"]), + "robot": generate_jani_expression(expression_dict["robot"]), + "barrier": generate_jani_expression(expression_dict["barrier"]), } if self.op in ("distance_to_point"): # distance between robot outer radius and point x-y coords return { - "robot": JaniExpression(expression_dict["robot"]), - "x": JaniExpression(expression_dict["x"]), - "y": JaniExpression(expression_dict["y"]), + "robot": generate_jani_expression(expression_dict["robot"]), + "x": generate_jani_expression(expression_dict["x"]), + "y": generate_jani_expression(expression_dict["y"]), } if self.op in ( "&&", @@ -127,8 +131,8 @@ def _get_operands(self, expression_dict: dict) -> Dict[str, "JaniExpression"]: "==", ): return { - "left": JaniExpression(expression_dict["left"]), - "right": JaniExpression(expression_dict["right"]), + "left": generate_jani_expression(expression_dict["left"]), + "right": generate_jani_expression(expression_dict["right"]), } if self.op in ( "!", @@ -143,39 +147,39 @@ def _get_operands(self, expression_dict: dict) -> Dict[str, "JaniExpression"]: "to_deg", "to_rad", ): - return {"exp": JaniExpression(expression_dict["exp"])} + return {"exp": generate_jani_expression(expression_dict["exp"])} if self.op in ("ite"): return { - "if": JaniExpression(expression_dict["if"]), - "then": JaniExpression(expression_dict["then"]), - "else": JaniExpression(expression_dict["else"]), + "if": generate_jani_expression(expression_dict["if"]), + "then": generate_jani_expression(expression_dict["then"]), + "else": generate_jani_expression(expression_dict["else"]), } # Array-specific expressions if self.op == "ac": return { - "var": JaniExpression(expression_dict["var"]), - "length": JaniExpression(expression_dict["length"]), - "exp": JaniExpression(expression_dict["exp"]), + "var": generate_jani_expression(expression_dict["var"]), + "length": generate_jani_expression(expression_dict["length"]), + "exp": generate_jani_expression(expression_dict["exp"]), } if self.op == "aa": return { - "exp": JaniExpression(expression_dict["exp"]), - "index": JaniExpression(expression_dict["index"]), + "exp": generate_jani_expression(expression_dict["exp"]), + "index": generate_jani_expression(expression_dict["index"]), } if self.op == "av": - return {"elements": JaniExpression(expression_dict["elements"])} + return {"elements": generate_jani_expression(expression_dict["elements"])} # Convince specific expressions if self.op in ("norm2d"): return { - "x": JaniExpression(expression_dict["x"]), - "y": JaniExpression(expression_dict["y"]), + "x": generate_jani_expression(expression_dict["x"]), + "y": generate_jani_expression(expression_dict["y"]), } if self.op in ("dot2d", "cross2d"): return { - "x1": JaniExpression(expression_dict["x1"]), - "y1": JaniExpression(expression_dict["y1"]), - "x2": JaniExpression(expression_dict["x2"]), - "y2": JaniExpression(expression_dict["y2"]), + "x1": generate_jani_expression(expression_dict["x1"]), + "y1": generate_jani_expression(expression_dict["y1"]), + "x2": generate_jani_expression(expression_dict["x2"]), + "y2": generate_jani_expression(expression_dict["y2"]), } assert False, f'Unknown operator "{self.op}" found.' @@ -190,7 +194,7 @@ def get_expression_type(self) -> JaniExpressionType: return JaniExpressionType.OPERATOR raise RuntimeError("Unknown expression type") - def replace_event(self, replacement: Optional[str]): + def replace_event(self, replacement: Optional[str]) -> "JaniExpression": """Replace the default SCXML event prefix with the provided replacement. Within a transitions, scxml can access to the event's parameters using a specific prefix. @@ -250,6 +254,74 @@ def as_dict(self) -> Union[str, int, float, bool, dict]: assert isinstance( op_value, JaniExpression ), f"Expected an expression, found {type(op_value)} for {op_key}" - assert hasattr(op_value, "identifier"), f"Identifier not set for {op_key}" + assert op_value.is_valid(), f"Expression's {op_key}'s value is invalid: {op_value}" op_dict.update({op_key: op_value.as_dict()}) return op_dict + + +class JaniDistribution(JaniExpression): + """ + A class representing a Jani Distribution (a random variable). + + At the moment, this is only meant to support Uniform distributions between 0.0 and 1.0 + """ + + def __init__(self, expression: dict): + self._distribution = expression.get("distribution") + self._args = expression.get("args") + assert ( + self._distribution == "Uniform" + ), f"Expected distribution to be Uniform, found {self._distribution}." + assert ( + isinstance(self._args, list) and len(self._args) == 2 + ), f"Unexpected arguments for Uniform distribution expression: {self._args}." + assert self.is_valid(), "Invalid arguments provided: expected args[0] <= args[1]." + + def is_valid(self): + # All other checks are carried out in the constructor + return all(isinstance(argument, (int, float)) for argument in self._args) and ( + self._args[0] <= self._args[1] + ) + + def get_expression_type(self) -> JaniExpressionType: + """Get the type of the expression.""" + assert self.is_valid(), "Expression is not valid" + return JaniExpressionType.DISTRIBUTION + + def replace_event(self, _: Optional[str]) -> "JaniDistribution": + """Replace the default SCXML event prefix with the provided replacement.""" + return self + + def as_literal(self) -> None: + """Provide the expression as a literal (JaniValue), if possible. None otherwise.""" + return None + + def as_identifier(self) -> None: + """Provide the expression as an identifier, if possible. None otherwise.""" + return None + + def as_operator(self) -> None: + """Provide the expression as an operator, if possible. None otherwise.""" + return None + + def get_dist_type(self) -> str: + return self._distribution + + def get_dist_args(self) -> List[Union[int, float]]: + return self._args + + def as_dict(self) -> Dict[str, Any]: + assert self.is_valid(), "Expected distribution to be valid." + return {"distribution": self._distribution, "args": self._args} + + +def generate_jani_expression(expr: SupportedExp) -> JaniExpression: + """Generate a JaniExpression or a JaniDistribution, depending on the input.""" + if isinstance(expr, JaniExpression): + return expr + if isinstance(expr, (str, JaniValue)) or JaniValue(expr).is_valid(): + return JaniExpression(expr) + assert isinstance(expr, dict), f"Unsupported expression provided: {expr}." + if "distribution" in expr: + return JaniDistribution(expr) + return JaniExpression(expr) diff --git a/src/as2fm/jani_generator/jani_entries/jani_expression_generator.py b/src/as2fm/jani_generator/jani_entries/jani_expression_generator.py index 06cc4c45..bc34bca3 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_expression_generator.py +++ b/src/as2fm/jani_generator/jani_entries/jani_expression_generator.py @@ -17,7 +17,7 @@ Generate full expressions in Jani """ -from as2fm.jani_generator.jani_entries import JaniExpression +from as2fm.jani_generator.jani_entries import JaniDistribution, JaniExpression # Math operators @@ -152,3 +152,13 @@ def array_value_operator(elements) -> JaniExpression: :param elements: The elements of the array """ return JaniExpression({"op": "av", "elements": elements}) + + +def distribution_expression(distribution: str, arguments: list) -> JaniDistribution: + """ + Generate a distribution expression + + :param distribution: The statistical distribution to pick from + :param arguments: The parameters for configuring the statistical distribution + """ + return JaniDistribution({"distribution": distribution, "args": arguments}) diff --git a/src/as2fm/jani_generator/jani_entries/jani_helpers.py b/src/as2fm/jani_generator/jani_entries/jani_helpers.py new file mode 100644 index 00000000..b6cee5c3 --- /dev/null +++ b/src/as2fm/jani_generator/jani_entries/jani_helpers.py @@ -0,0 +1,121 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Union + +from as2fm.jani_generator.jani_entries import JaniAssignment, JaniEdge, JaniExpression, JaniModel +from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import ( + expand_distribution_expressions, +) + + +def _generate_new_edge_for_random_assignments( + edge_location: str, + edge_target: str, + assignment_var: Union[str, JaniExpression], + assignment_possibilities: List[JaniExpression], +) -> JaniEdge: + probability = 1.0 / len(assignment_possibilities) + return JaniEdge( + { + "location": edge_location, + "destinations": [ + { + "location": edge_target, + "probability": {"exp": probability}, + "assignments": [{"ref": assignment_var, "value": assignment_value}], + } + for assignment_value in assignment_possibilities + ], + } + ) + + +def _expand_random_variables_in_edge(jani_edge: JaniEdge) -> List[JaniEdge]: + """ + If there are random variables in the input JaniEdge, generate new edges to handle it. + + :param jani_edge: The edge to expand + :return: All the edges resulting from the input. + """ + generated_edges: List[JaniEdge] = [jani_edge] + edge_location = jani_edge.location + edge_action = jani_edge.action + assert edge_action is not None, "Expected edge actions to be always defined." + edge_id = f"{edge_location}_{edge_action}" + + for dest_id, dest_val in enumerate(jani_edge.destinations): + jani_assignments: List[JaniAssignment] = dest_val["assignments"] + curr_assign_idx = 0 + while curr_assign_idx < len(jani_assignments): + expanded_assignments = expand_distribution_expressions( + jani_assignments[curr_assign_idx].get_expression() + ) + if len(expanded_assignments) > 1: + # In this case, we expanded the assignments, and we need to generate new edges + original_target_loc = dest_val["location"] + expanded_edge_loc = f"{edge_id}_dest_{dest_id}_expanded_assign_{curr_assign_idx}" + next_target_edge_loc = f"{edge_id}_dest_{dest_id}_after_assign_{curr_assign_idx}" + expanded_edge = _generate_new_edge_for_random_assignments( + expanded_edge_loc, + next_target_edge_loc, + jani_assignments[curr_assign_idx].get_target(), + expanded_assignments, + ) + next_assign_idx = curr_assign_idx + 1 + continuation_edge = JaniEdge( + { + "location": next_target_edge_loc, + "action": "act", # Keep it simple, due to the location naming scheme + "destinations": [ + { + "location": original_target_loc, + "assignments": jani_assignments[next_assign_idx:], + } + ], + } + ) + dest_val["location"] = expanded_edge_loc + dest_val["assignments"] = dest_val["assignments"][0:curr_assign_idx] + generated_edges.append(expanded_edge) + generated_edges.extend(_expand_random_variables_in_edge(continuation_edge)) + break + curr_assign_idx += 1 + return generated_edges + + +def expand_random_variables_in_jani_model(model: JaniModel) -> 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 + ), 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, ( + 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." + ) + # Edges created to handle random distributions + new_edges: List[JaniEdge] = [] + for edge in automaton.get_edges(): + generated_edges = _expand_random_variables_in_edge(edge) + for gen_edge in generated_edges: + automaton.add_location(gen_edge.location) + new_edges.extend(generated_edges) + automaton.set_edges(new_edges) + model._generate_missing_syncs() diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py index c8a710d8..5b8ec356 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py @@ -19,12 +19,17 @@ from array import ArrayType from hashlib import sha256 -from typing import Dict, List, Optional, Set, Tuple, Union, get_args +from typing import Any, Dict, List, Optional, Set, Tuple, Union, get_args import lxml.etree as ET from lxml.etree import _Element as Element -from as2fm.as2fm_common.common import check_value_type_compatible, string_to_value, value_to_type +from as2fm.as2fm_common.common import ( + check_value_type_compatible, + get_default_expression_for_type, + string_to_value, + value_to_type, +) from as2fm.as2fm_common.ecmascript_interpretation import interpret_ecma_script_expr from as2fm.jani_generator.jani_entries import ( JaniAssignment, @@ -448,6 +453,8 @@ def get_children(self) -> List[ScxmlBase]: return [] def write_model(self): + # A collection of the variables read from the datamodel so far + read_vars: Dict[str, Any] = {} for scxml_data in self.element.get_data_entries(): assert isinstance(scxml_data, ScxmlData), "Unexpected element in the DataModel." assert scxml_data.check_validity(), "Found invalid data entry." @@ -465,12 +472,10 @@ def write_model(self): expected_type = ArrayType array_info = ArrayInfo(array_type, max_array_size) init_value = parse_ecmascript_to_jani_expression(scxml_data.get_expr(), array_info) - expr_type = type(interpret_ecma_script_expr(scxml_data.get_expr())) - assert check_value_type_compatible( - interpret_ecma_script_expr(scxml_data.get_expr()), expected_type - ), ( + evaluated_expr = interpret_ecma_script_expr(scxml_data.get_expr(), read_vars) + assert check_value_type_compatible(evaluated_expr, expected_type), ( f"Invalid value for {scxml_data.get_name()}: " - f"Expected type {expected_type}, got {expr_type}." + f"Expected type {expected_type}, got {type(evaluated_expr)}." ) # TODO: Add support for lower and upper bounds self.automaton.add_variable( @@ -484,6 +489,9 @@ def write_model(self): self.automaton.add_variable( JaniVariable(f"{scxml_data.get_name()}.length", int, JaniValue(len(init_expr))) ) + read_vars.update( + {scxml_data.get_name(): get_default_expression_for_type(scxml_data.get_type())} + ) class ScxmlTag(BaseTag): diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py b/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py index 3a9a5069..664a3c34 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py @@ -20,6 +20,7 @@ from typing import List from as2fm.jani_generator.jani_entries.jani_automaton import JaniAutomaton +from as2fm.jani_generator.jani_entries.jani_helpers import expand_random_variables_in_jani_model from as2fm.jani_generator.jani_entries.jani_model import JaniModel from as2fm.jani_generator.ros_helpers.ros_communication_handler import ( remove_empty_self_loops_from_interface_handlers_in_jani, @@ -82,4 +83,5 @@ def convert_multiple_scxmls_to_jani( base_model.add_jani_automaton(timer_automaton) implement_scxml_events_as_jani_syncs(events_holder, timers, max_array_size, base_model) remove_empty_self_loops_from_interface_handlers_in_jani(base_model) + expand_random_variables_in_jani_model(base_model) return base_model 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 21bd48cc..8e7ecf02 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard/world.scxml @@ -7,8 +7,8 @@ - - + + @@ -24,23 +24,29 @@ - - - - - - - + + + - - + + + + + + + + + + + + - + diff --git a/test/jani_generator/test_unittest_expression_expansion.py b/test/jani_generator/test_unittest_expression_expansion.py new file mode 100644 index 00000000..7ca8e3b4 --- /dev/null +++ b/test/jani_generator/test_unittest_expression_expansion.py @@ -0,0 +1,141 @@ +# Copyright (c) 2024 - for information on the respective copyright owner +# see the NOTICE file + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""""Test the SCXML data conversion""" + + +import pytest + +from as2fm.jani_generator.jani_entries import generate_jani_expression +from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import ( + expand_distribution_expressions, +) + + +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) + 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) + assert len(jani_expressions) == 1, "Expression without distribution should not be expanded!" + assert jani_entry.as_dict() == jani_expressions[0].as_dict() + + +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 + 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[10].as_literal().value() == pytest.approx(1.2) + # Test a non trivial expression + jani_distribution = generate_jani_expression( + { + "op": "floor", + "exp": { + "op": "*", + "left": {"distribution": "Uniform", "args": [0.0, 1.0]}, + "right": 20, + }, + } + ) + jani_expressions = expand_distribution_expressions(jani_distribution, n_options=n_options) + assert len(jani_expressions) == n_options, "Base distribution was not expanded!" + assert jani_expressions[0].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 0.0, "right": 20}, + } + assert jani_expressions[10].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 0.1, "right": 20}, + } + assert jani_expressions[100].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 1.0, "right": 20}, + } + + +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 + jani_distribution = generate_jani_expression( + { + "op": "floor", + "exp": { + "op": "*", + "left": {"distribution": "Uniform", "args": [0.0, 20.0]}, + "right": {"distribution": "Uniform", "args": [0.0, 10.0]}, + }, + } + ) + jani_expressions = expand_distribution_expressions(jani_distribution, n_options=n_options) + assert len(jani_expressions) == n_options**2, "Base distribution was not expanded!" + assert jani_expressions[0].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 0.0, "right": 0.0}, + } + assert jani_expressions[-1].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 20.0, "right": 10.0}, + } + assert jani_expressions[-2].as_dict() == { + "op": "floor", + "exp": {"op": "*", "left": 20.0, "right": 9.5}, + } + # Multiple distributions at a different level + jani_distribution = generate_jani_expression( + { + "op": "*", + "left": {"distribution": "Uniform", "args": [0.0, 20.0]}, + "right": { + "op": "*", + "left": 2, + "right": {"distribution": "Uniform", "args": [0.0, 10.0]}, + }, + } + ) + jani_expressions = expand_distribution_expressions(jani_distribution, n_options=n_options) + assert len(jani_expressions) == n_options**2, "Base distribution was not expanded!" + assert jani_expressions[0].as_dict() == { + "op": "*", + "left": 0.0, + "right": {"op": "*", "left": 2, "right": 0.0}, + } + assert jani_expressions[-1].as_dict() == { + "op": "*", + "left": 20.0, + "right": {"op": "*", "left": 2, "right": 10.0}, + } + assert jani_expressions[1].as_dict() == { + "op": "*", + "left": 0.0, + "right": {"op": "*", "left": 2, "right": 0.5}, + } From c0c4e62a08ddb1f191426203b137cb8053a198e9 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Sun, 15 Dec 2024 21:43:38 +0100 Subject: [PATCH 40/58] this makes a bit more sense Signed-off-by: Christian Henkel --- .../bt_update_goal_and_current_position.scxml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index fcb23660..70f9e68f 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -27,12 +27,12 @@ - - + + - + @@ -72,8 +72,6 @@ - - From ecfefc135aa66b8c802951f3fb95c9678206425f Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 12:26:22 +0100 Subject: [PATCH 41/58] Differentiating between trigger and data events in executable content Co-authored-by: Marco Lampacrescia Signed-off-by: Christian Henkel --- .../scxml_helpers/scxml_tags.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py index 5b8ec356..dcdf92ba 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_tags.py @@ -212,27 +212,41 @@ def _append_scxml_body_to_jani_automaton( hash_str: str, guard_exp: Optional[JaniExpression], trigger_event: Optional[str], + data_event: Optional[str], max_array_size: int, ) -> Tuple[List[JaniEdge], List[str]]: """ Converts the body of an SCXML element to a set of locations and edges. They need to be added to a JaniAutomaton later on. + + :param jani_automaton: The single automaton hosting the generated edges and locations. + :param events_holder: A data structure describing the events generated in the automaton. + :param body: A list of SCXML entries to be translated into Jani. + :param source: The location we are starting executing the body from. + :param target: The location we are ending up in after executing the body. + :param hash_str: Additional hash to ensure a unique action identifier to executing the body. + :param guard_exp: An expression that needs to hold before executing this action. + :param trigger_event: The event starting the exec. block (use only from ScxmlTransition). + :param data_event: The event carrying the data, that might be read in the exec block. + :param max_array_size: The maximum allowed array size (for unbounded arrays). """ - edge_action_name = f"{source}-{target}-{hash_str}" - trigger_event_action = ( - edge_action_name if trigger_event is None else f"{trigger_event}_on_receive" + jani_action_name = ( + f"{trigger_event}_on_receive" + if trigger_event is not None + else f"{source}-{target}-parent-{hash_str}" ) + new_edges = [] new_locations = [] if guard_exp is not None: - guard_exp.replace_event(trigger_event) + guard_exp.replace_event(data_event) # First edge. Has to evaluate guard and trigger event of original transition. new_edges.append( JaniEdge( { "location": source, - "action": trigger_event_action, + "action": jani_action_name, "guard": JaniGuard(guard_exp), "destinations": [{"location": None, "assignments": []}], } @@ -241,7 +255,7 @@ def _append_scxml_body_to_jani_automaton( for i, ec in enumerate(body): if isinstance(ec, ScxmlAssign): assign_idx = len(new_edges[-1].destinations[0]["assignments"]) - jani_assigns = _interpret_scxml_assign(ec, jani_automaton, trigger_event, assign_idx) + jani_assigns = _interpret_scxml_assign(ec, jani_automaton, data_event, assign_idx) new_edges[-1].destinations[0]["assignments"].extend(jani_assigns) elif isinstance(ec, ScxmlSend): event_name = ec.get_event() @@ -273,7 +287,7 @@ def _append_scxml_body_to_jani_automaton( if isinstance(res_eval_value, ArrayType): array_info = ArrayInfo(get_args(res_eval_type)[0], max_array_size) jani_expr = parse_ecmascript_to_jani_expression(expr, array_info).replace_event( - trigger_event + data_event ) new_edge.destinations[0]["assignments"].append( JaniAssignment({"ref": param_assign_name, "value": jani_expr}) @@ -293,7 +307,7 @@ def _append_scxml_body_to_jani_automaton( ) ) elif jani_expr_type == JaniExpressionType.OPERATOR: - op_type, operands = jani_expr.as_operator() + op_type, _ = jani_expr.as_operator() if op_type == "av": assert isinstance( res_eval_value, ArrayType @@ -329,7 +343,7 @@ def _append_scxml_body_to_jani_automaton( for if_idx, (cond_str, conditional_body) in enumerate(ec.get_conditional_executions()): current_cond = parse_ecmascript_to_jani_expression(cond_str) jani_cond = _merge_conditions(previous_conditions, current_cond).replace_event( - trigger_event + data_event ) sub_edges, sub_locs = _append_scxml_body_to_jani_automaton( jani_automaton, @@ -339,7 +353,9 @@ def _append_scxml_body_to_jani_automaton( interm_loc_after, "-".join([hash_str, _hash_element(ec), str(if_idx)]), jani_cond, - None, + None, # This is not triggered by an event, even under a transition. Because + # the event triggering the transition is handled at the top of this function. + data_event, max_array_size, ) new_edges.extend(sub_edges) @@ -349,7 +365,7 @@ def _append_scxml_body_to_jani_automaton( else_execution_body = ec.get_else_execution() else_execution_id = str(len(ec.get_conditional_executions())) else_execution_body = [] if else_execution_body is None else else_execution_body - jani_cond = _merge_conditions(previous_conditions).replace_event(trigger_event) + jani_cond = _merge_conditions(previous_conditions).replace_event(data_event) sub_edges, sub_locs = _append_scxml_body_to_jani_automaton( jani_automaton, events_holder, @@ -359,16 +375,18 @@ def _append_scxml_body_to_jani_automaton( "-".join([hash_str, _hash_element(ec), else_execution_id]), jani_cond, None, + data_event, max_array_size, ) new_edges.extend(sub_edges) new_locations.extend(sub_locs) # Prepare the edge from the end of the if-else block + end_edge_action_name = f"{source}-{target}-{hash_str}" new_edges.append( JaniEdge( { "location": interm_loc_after, - "action": edge_action_name, + "action": end_edge_action_name, "guard": None, "destinations": [{"location": None, "assignments": []}], } @@ -528,6 +546,7 @@ def handle_entry_state(self): hash_str, None, None, + None, self.max_array_size, ) # Add the initial state and start sequence to the automaton @@ -617,6 +636,7 @@ def add_unhandled_transitions(self, transitions_set: Set[str]): "", guard_exp, event_name, + event_name, self.max_array_size, ) assert ( @@ -753,6 +773,7 @@ def write_model(self): hash_str, guard, transition_trigger_event, + transition_trigger_event, self.max_array_size, ) for edge in new_edges: From 4d22d7591021ccc238f044256dc85c3fa6e6856b Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 13:04:09 +0100 Subject: [PATCH 42/58] Allow reading output BT variables Signed-off-by: Marco Lampacrescia --- .../scxml_entries/scxml_executable_entries.py | 4 +- .../scxml_entries/scxml_param.py | 4 +- .../grid_robot_blackboard_simple/bt.xml | 33 ++++++++ .../bt_move.scxml | 56 ++++++++++++++ .../bt_shall_move.scxml | 76 +++++++++++++++++++ .../bt_update_goal_and_current_position.scxml | 53 +++++++++++++ .../grid_robot_blackboard_simple/main.xml | 22 ++++++ .../properties.jani | 59 ++++++++++++++ .../grid_robot_blackboard_simpler/world.scxml | 63 --------------- 9 files changed, 303 insertions(+), 67 deletions(-) create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_move.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_shall_move.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/main.xml create mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py index 3451e814..2601eb71 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_executable_entries.py @@ -462,7 +462,7 @@ def get_expr(self) -> Union[str, BtGetValueInputPort]: def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): """Check whether the If entry reads content from the BT Blackboard.""" return isinstance(self._expr, BtGetValueInputPort) and is_blackboard_reference( - bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + bt_ports_handler.get_port_value(self._expr.get_key_name()) ) def instantiate_bt_events(self, _, __) -> "ScxmlAssign": @@ -473,7 +473,7 @@ def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler) -> None: """Update the values of potential entries making use of BT ports.""" if isinstance(self._expr, BtGetValueInputPort): self._expr = get_input_variable_as_scxml_expression( - bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + bt_ports_handler.get_port_value(self._expr.get_key_name()) ) def check_validity(self) -> bool: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py index 526baa5f..1127e938 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_param.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_param.py @@ -89,14 +89,14 @@ def get_location(self) -> Optional[str]: def has_bt_blackboard_input(self, bt_ports_handler: BtPortsHandler): return isinstance(self._expr, BtGetValueInputPort) and is_blackboard_reference( - bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + bt_ports_handler.get_port_value(self._expr.get_key_name()) ) def update_bt_ports_values(self, bt_ports_handler: BtPortsHandler): """Update the values of potential entries making use of BT ports.""" if isinstance(self._expr, BtGetValueInputPort): self._expr = get_input_variable_as_scxml_expression( - bt_ports_handler.get_in_port_value(self._expr.get_key_name()) + bt_ports_handler.get_port_value(self._expr.get_key_name()) ) def check_validity(self) -> bool: diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml new file mode 100644 index 00000000..5562e011 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_move.scxml new file mode 100644 index 00000000..9ed117cd --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_move.scxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_shall_move.scxml new file mode 100644 index 00000000..90a8a178 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_shall_move.scxml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..80a1b35c --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt_update_goal_and_current_position.scxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/main.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/main.xml new file mode 100644 index 00000000..0c3c9171 --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/main.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani new file mode 100644 index 00000000..591adf9f --- /dev/null +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani @@ -0,0 +1,59 @@ +{ + "properties": [ + { + "name": "tree_success", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "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}" + } + } + }, + "states": { + "op": "initial" + } + } + }, + { + "name": "at_goal", + "expression": { + "op": "filter", + "fun": "values", + "values": { + "op": "Pmin", + "exp": { + "op": "F", + "exp": { + "op": "∧", + "left": "topic_goal_msg.valid", + "right": { + "op": "∧", + "left": { + "op": "=", + "left": "topic_pose_msg.ros_fields__x", + "right": "topic_goal_msg.ros_fields__x" + }, + "right": { + "op": "=", + "left": "topic_pose_msg.ros_fields__y", + "right": "topic_goal_msg.ros_fields__y" + } + } + } + } + }, + "states": { + "op": "initial" + } + } + } + ] +} diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml deleted file mode 100644 index 21bd48cc..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/world.scxml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 2953e421689abd721657c8de5c8f1ea89eacd8e6 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 13:44:07 +0100 Subject: [PATCH 43/58] cleaning up example Signed-off-by: Christian Henkel --- .../grid_robot_blackboard_simpler/bt.xml | 11 +++--- .../bt_move.scxml | 28 ++++----------- .../bt_update_goal_and_current_position.scxml | 34 ------------------- 3 files changed, 13 insertions(+), 60 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml index e5be6095..99af0d3e 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml @@ -5,12 +5,13 @@ + diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml index 5facc523..e6dc37a4 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml @@ -6,36 +6,22 @@ model_src="" xmlns="http://www.w3.org/2005/07/scxml"> - - + - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index 70f9e68f..5142d8ab 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -19,13 +19,10 @@ 3 - down 4 - stop --> - - @@ -36,33 +33,6 @@ - - @@ -105,10 +75,6 @@ - From d9f60841f3cfe0303066698f995961ae83fd7485 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 13:47:05 +0100 Subject: [PATCH 44/58] works when smaller Signed-off-by: Christian Henkel --- .../bt_update_goal_and_current_position.scxml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index 5142d8ab..82c1ac38 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -24,12 +24,12 @@ - - + + - + From c4d62368c014313024de923fca60294605554133 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 13:50:02 +0100 Subject: [PATCH 45/58] updating ground truth after ecfefc135aa66b8c802951f3fb95c9678206425f Signed-off-by: Christian Henkel --- .../battery_example/output_GROUND_TRUTH.jani | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/jani_generator/_test_data/battery_example/output_GROUND_TRUTH.jani b/test/jani_generator/_test_data/battery_example/output_GROUND_TRUTH.jani index d64d2935..c6b0c02f 100644 --- a/test/jani_generator/_test_data/battery_example/output_GROUND_TRUTH.jani +++ b/test/jani_generator/_test_data/battery_example/output_GROUND_TRUTH.jani @@ -2,7 +2,10 @@ "jani-version": 1, "name": "", "type": "mdp", - "features": ["arrays", "trigonometric-functions"], + "features": [ + "arrays", + "trigonometric-functions" + ], "metadata": { "description": "Autogenerated with CONVINCE toolchain" }, @@ -29,10 +32,10 @@ "name": "level_on_send" }, { - "name": "use_battery-first-exec-use_battery-766fa6e4" + "name": "use_battery-first-exec-use_battery-parent-766fa6e4" }, { - "name": "use_battery-use_battery-cf7e7c41" + "name": "use_battery-use_battery-parent-cf7e7c41" } ], "automata": [ @@ -74,7 +77,7 @@ ] } ], - "action": "use_battery-use_battery-cf7e7c41" + "action": "use_battery-use_battery-parent-cf7e7c41" }, { "location": "use_battery-1-cf7e7c41", @@ -105,7 +108,7 @@ "assignments": [] } ], - "action": "use_battery-first-exec-use_battery-766fa6e4" + "action": "use_battery-first-exec-use_battery-parent-766fa6e4" }, { "location": "use_battery-first-exec-0-766fa6e4", @@ -252,17 +255,17 @@ ] }, { - "result": "use_battery-first-exec-use_battery-766fa6e4", + "result": "use_battery-first-exec-use_battery-parent-766fa6e4", "synchronise": [ - "use_battery-first-exec-use_battery-766fa6e4", + "use_battery-first-exec-use_battery-parent-766fa6e4", null, null ] }, { - "result": "use_battery-use_battery-cf7e7c41", + "result": "use_battery-use_battery-parent-cf7e7c41", "synchronise": [ - "use_battery-use_battery-cf7e7c41", + "use_battery-use_battery-parent-cf7e7c41", null, null ] From ff2690030accd0f5ff809ce135d9a669ed5606b3 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 14:02:52 +0100 Subject: [PATCH 46/58] Remove flag for input blackboards in the full model Signed-off-by: Marco Lampacrescia --- .../scxml_converter/scxml_entries/bt_utils.py | 9 ---- .../scxml_entries/scxml_bt_ticks.py | 4 ++ .../scxml_entries/scxml_state.py | 49 +++++++++---------- .../properties.jani | 33 ------------- 4 files changed, 28 insertions(+), 67 deletions(-) diff --git a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py index 581d6357..45bb2fcf 100644 --- a/src/as2fm/scxml_converter/scxml_entries/bt_utils.py +++ b/src/as2fm/scxml_converter/scxml_entries/bt_utils.py @@ -136,11 +136,6 @@ def __init__(self): # For each port name, store the port type string and value. self._in_ports: Dict[str, Tuple[str, str]] = {} self._out_ports: Dict[str, Tuple[str, str]] = {} - self._has_blackboard_inputs: bool = False - - def has_blackboard_inputs(self) -> bool: - """Boolean check reporting whether any input port references blackboard variables.""" - return self._has_blackboard_inputs def in_port_exists(self, port_name: str) -> bool: """Check if an input port exists.""" @@ -232,10 +227,6 @@ def _set_in_port_value(self, port_name: str, port_value: str): ), f"Error: Value of port {port_name} already assigned." port_type = self._in_ports[port_name][0] self._in_ports[port_name] = (port_type, port_value) - # Update flag to track whether we added a blackboard variable or not - self._has_blackboard_inputs = self._has_blackboard_inputs or is_blackboard_reference( - port_value - ) def _set_out_port_value(self, port_name: str, port_value: str): """Set the value of an output port.""" diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py index 7a9ba005..f5ed8e5c 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_bt_ticks.py @@ -133,6 +133,10 @@ def __init__(self, child_seq_id: Union[str, int]): def check_validity(self) -> bool: return True + def has_bt_blackboard_input(self, _): + """Check whether the If entry reads content from the BT Blackboard.""" + return False + def instantiate_bt_events( self, instance_id: int, children_ids: List[int] ) -> Union[ScxmlIf, ScxmlSend]: diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py index d2c60aa2..10b5272a 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_state.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_state.py @@ -152,31 +152,30 @@ def _generate_blackboard_retrieval( self, bt_ports_handler: BtPortsHandler ) -> List["ScxmlState"]: generated_states: List[ScxmlState] = [self] - if bt_ports_handler.has_blackboard_inputs(): - assert not has_bt_blackboard_input(self._on_entry, bt_ports_handler), ( - f"Error: SCXML state {self.get_id()}: reading blackboard variables from onentry. " - "This isn't yet supported." - ) - assert not has_bt_blackboard_input(self._on_exit, bt_ports_handler), ( - f"Error: SCXML state {self.get_id()}: reading blackboard variables from onexit. " - "This isn't yet supported." - ) - for transition in self._body: - if transition.has_bt_blackboard_input(bt_ports_handler): - # Prepare the new state using the received BT info - states_count = len(generated_states) - new_state_id = f"{self.get_id()}_{transition.get_tag_name()}_{states_count}" - new_state = ScxmlState(new_state_id) - blackboard_transition = ScxmlTransition( - transition.get_target_state_id(), - [BT_BLACKBOARD_GET], - body=transition.get_body(), - ) - new_state.add_transition(blackboard_transition) - generated_states.append(new_state) - # Set the new target and body to the original transition - transition.set_target_state_id(new_state_id) - transition.set_body([ScxmlSend(BT_BLACKBOARD_REQUEST)]) + assert not has_bt_blackboard_input(self._on_entry, bt_ports_handler), ( + f"Error: SCXML state {self.get_id()}: reading blackboard variables from onentry. " + "This isn't yet supported." + ) + assert not has_bt_blackboard_input(self._on_exit, bt_ports_handler), ( + f"Error: SCXML state {self.get_id()}: reading blackboard variables from onexit. " + "This isn't yet supported." + ) + for transition in self._body: + if transition.has_bt_blackboard_input(bt_ports_handler): + # Prepare the new state using the received BT info + states_count = len(generated_states) + new_state_id = f"{self.get_id()}_{transition.get_tag_name()}_{states_count}" + new_state = ScxmlState(new_state_id) + blackboard_transition = ScxmlTransition( + transition.get_target_state_id(), + [BT_BLACKBOARD_GET], + body=transition.get_body(), + ) + new_state.add_transition(blackboard_transition) + generated_states.append(new_state) + # Set the new target and body to the original transition + transition.set_target_state_id(new_state_id) + transition.set_body([ScxmlSend(BT_BLACKBOARD_REQUEST)]) return generated_states def _substitute_bt_events_and_ports( diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani index 591adf9f..1094fe1c 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/properties.jani @@ -21,39 +21,6 @@ "op": "initial" } } - }, - { - "name": "at_goal", - "expression": { - "op": "filter", - "fun": "values", - "values": { - "op": "Pmin", - "exp": { - "op": "F", - "exp": { - "op": "∧", - "left": "topic_goal_msg.valid", - "right": { - "op": "∧", - "left": { - "op": "=", - "left": "topic_pose_msg.ros_fields__x", - "right": "topic_goal_msg.ros_fields__x" - }, - "right": { - "op": "=", - "left": "topic_pose_msg.ros_fields__y", - "right": "topic_goal_msg.ros_fields__y" - } - } - } - } - }, - "states": { - "op": "initial" - } - } } ] } From b932b4ca51fac098ffdff8bf98a4e321667d71e4 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 14:09:31 +0100 Subject: [PATCH 47/58] running bigger tests Signed-off-by: Christian Henkel --- test/as2fm_common/test_utilities_smc_storm.py | 2 +- .../bt_update_goal_and_current_position.scxml | 19 +++++++++++-------- .../test_systemtest_scxml_to_jani.py | 5 ++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/test/as2fm_common/test_utilities_smc_storm.py b/test/as2fm_common/test_utilities_smc_storm.py index 7bae2b04..e35e9959 100644 --- a/test/as2fm_common/test_utilities_smc_storm.py +++ b/test/as2fm_common/test_utilities_smc_storm.py @@ -38,7 +38,7 @@ def _interpret_output(output: str, expected_content: List[str], not_expected_con def _run_smc_storm(args: str) -> Tuple[str, str, int]: """Run smc_storm with the given arguments and return the stdout, stderr and return code.""" - command = f"smc_storm {args} --max-trace-length 10000 --max-n-traces 10000" + command = f"smc_storm {args}" print("Running command: ", command) with subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index 82c1ac38..21110669 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -24,12 +24,12 @@ - - + + - + @@ -46,11 +46,14 @@ - - - - - + + + + + + + + diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index 213149d1..d3c93049 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -213,6 +213,7 @@ def _test_with_main( skip_smc: bool = False, property_name: str, success: bool, + size_limit: int = 10_000, ): """ Testing the conversion of the model xml file with the entrypoint. @@ -257,7 +258,8 @@ def _test_with_main( pos_res = "Result: 1" if success else "Result: 0" neg_res = "Result: 0" if success else "Result: 1" run_smc_storm_with_output( - f"--model {output_path} --properties-names {property_name}", + f"--model {output_path} --properties-names {property_name} " + + f"--max-trace-length {size_limit} --max-n-traces {size_limit}", [property_name, output_path, pos_res], [neg_res], ) @@ -459,6 +461,7 @@ def test_grid_robot_blackboard_simpler(self): model_xml="main.xml", property_name="at_goal", success=True, + size_limit=1_000_000, ) def test_command_line_output_with_line_numbers(self): From efadf656cef34d3e877e0e22308cdb720d9bad23 Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 14:27:15 +0100 Subject: [PATCH 48/58] handling has_bt_blackboard_input for RosActionHandleGoalResponse Signed-off-by: Christian Henkel --- .../scxml_entries/scxml_ros_action_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py index 61d43b6c..1e5ea105 100644 --- a/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py +++ b/src/as2fm/scxml_converter/scxml_entries/scxml_ros_action_client.py @@ -33,6 +33,7 @@ generate_action_result_handle_event, is_action_type_known, ) +from as2fm.scxml_converter.scxml_entries.scxml_executable_entries import execution_body_from_xml from as2fm.scxml_converter.scxml_entries.scxml_ros_base import ( RosCallback, RosDeclaration, @@ -104,6 +105,9 @@ def from_xml_tree(xml_tree: ET.Element) -> "RosActionHandleGoalResponse": action_name = get_xml_argument(RosActionHandleGoalResponse, xml_tree, "name") accept_target = get_xml_argument(RosActionHandleGoalResponse, xml_tree, "accept") reject_target = get_xml_argument(RosActionHandleGoalResponse, xml_tree, "reject") + assert ( + len(execution_body_from_xml(xml_tree)) == 0 + ), "Error: SCXML RosActionHandleGoalResponse can not have an execution body." return RosActionHandleGoalResponse(action_name, accept_target, reject_target) def __init__( @@ -144,6 +148,10 @@ def update_bt_ports_values(self, _) -> None: # We do not expect a body with BT ports to be substituted pass + def has_bt_blackboard_input(self, _) -> bool: + """This can not have a body, so it can not have BT blackboard input.""" + return False + def check_valid_ros_instantiations( self, ros_declarations: ScxmlRosDeclarationsContainer ) -> bool: From f80e1fccd47c742410407219f48c277c9bacc51e Mon Sep 17 00:00:00 2001 From: Christian Henkel Date: Mon, 16 Dec 2024 14:37:14 +0100 Subject: [PATCH 49/58] using randomness Signed-off-by: Christian Henkel --- .../bt_update_goal_and_current_position.scxml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml index 21110669..5d42f9a2 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml @@ -1,6 +1,6 @@ - - + + + - + + + + From b26c36b7096abc9556318352745c00e389a1a412 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 14:38:56 +0100 Subject: [PATCH 50/58] Running 3 goal example Signed-off-by: Marco Lampacrescia --- .../grid_robot_blackboard_simple/bt.xml | 53 +++++++++---------- .../bt_update_goal_and_current_position.scxml | 26 ++++++++- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml index 5562e011..f8d331ec 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml @@ -1,33 +1,32 @@ - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + 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 80a1b35c..3f56fcb5 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 @@ -23,6 +23,7 @@ + @@ -44,9 +45,32 @@ + + + + + + + + + - + + + + + + + + + + + + + + + From 6e005f9c1d2e88925b7baa468558c74fc3770beb Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 15:26:40 +0100 Subject: [PATCH 51/58] Making n options configurable and fix bt Signed-off-by: Marco Lampacrescia --- .../jani_generator/jani_entries/jani_helpers.py | 14 +++++++++----- .../jani_generator/scxml_helpers/scxml_to_jani.py | 2 +- .../_test_data/grid_robot_blackboard_simple/bt.xml | 7 +++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/as2fm/jani_generator/jani_entries/jani_helpers.py b/src/as2fm/jani_generator/jani_entries/jani_helpers.py index b6cee5c3..ec499782 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_helpers.py +++ b/src/as2fm/jani_generator/jani_entries/jani_helpers.py @@ -43,7 +43,9 @@ def _generate_new_edge_for_random_assignments( ) -def _expand_random_variables_in_edge(jani_edge: JaniEdge) -> List[JaniEdge]: +def _expand_random_variables_in_edge( + jani_edge: JaniEdge, *, n_options: int = 100 +) -> List[JaniEdge]: """ If there are random variables in the input JaniEdge, generate new edges to handle it. @@ -61,7 +63,7 @@ def _expand_random_variables_in_edge(jani_edge: JaniEdge) -> List[JaniEdge]: curr_assign_idx = 0 while curr_assign_idx < len(jani_assignments): expanded_assignments = expand_distribution_expressions( - jani_assignments[curr_assign_idx].get_expression() + jani_assignments[curr_assign_idx].get_expression(), n_options=n_options ) if len(expanded_assignments) > 1: # In this case, we expanded the assignments, and we need to generate new edges @@ -90,13 +92,15 @@ def _expand_random_variables_in_edge(jani_edge: JaniEdge) -> List[JaniEdge]: dest_val["location"] = expanded_edge_loc dest_val["assignments"] = dest_val["assignments"][0:curr_assign_idx] generated_edges.append(expanded_edge) - generated_edges.extend(_expand_random_variables_in_edge(continuation_edge)) + generated_edges.extend( + _expand_random_variables_in_edge(continuation_edge, n_options=n_options) + ) break curr_assign_idx += 1 return generated_edges -def expand_random_variables_in_jani_model(model: JaniModel) -> None: +def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int = 100) -> 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(): @@ -113,7 +117,7 @@ def expand_random_variables_in_jani_model(model: JaniModel) -> None: # Edges created to handle random distributions new_edges: List[JaniEdge] = [] for edge in automaton.get_edges(): - generated_edges = _expand_random_variables_in_edge(edge) + generated_edges = _expand_random_variables_in_edge(edge, n_options=n_options) for gen_edge in generated_edges: automaton.add_location(gen_edge.location) new_edges.extend(generated_edges) diff --git a/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py b/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py index 664a3c34..9afd8376 100644 --- a/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py +++ b/src/as2fm/jani_generator/scxml_helpers/scxml_to_jani.py @@ -83,5 +83,5 @@ def convert_multiple_scxmls_to_jani( base_model.add_jani_automaton(timer_automaton) implement_scxml_events_as_jani_syncs(events_holder, timers, max_array_size, base_model) remove_empty_self_loops_from_interface_handlers_in_jani(base_model) - expand_random_variables_in_jani_model(base_model) + expand_random_variables_in_jani_model(base_model, n_options=100) return base_model diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml index f8d331ec..50f3f723 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml @@ -2,7 +2,7 @@ - + + + + @@ -78,9 +79,14 @@ - - - + + + + + + + + From afc8035216d4e9f2f6c5cb581aee7b6ba188dd9c Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 15:39:45 +0100 Subject: [PATCH 53/58] Developed a simple and fast approach, with 3 consecutive goals Signed-off-by: Marco Lampacrescia --- .../grid_robot_blackboard_simple/bt.xml | 2 +- .../grid_robot_blackboard_simpler/bt.xml | 34 ------- .../bt_move.scxml | 35 ------- .../bt_shall_move.scxml | 76 --------------- .../bt_update_goal_and_current_position.scxml | 94 ------------------- .../grid_robot_blackboard_simpler/main.xml | 23 ----- .../properties.jani | 47 ---------- .../test_systemtest_scxml_to_jani.py | 6 +- 8 files changed, 4 insertions(+), 313 deletions(-) delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt.xml delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml delete mode 100644 test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml index 50f3f723..9472dc78 100644 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml +++ b/test/jani_generator/_test_data/grid_robot_blackboard_simple/bt.xml @@ -2,7 +2,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml deleted file mode 100644 index e6dc37a4..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_move.scxml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml deleted file mode 100644 index 90a8a178..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_shall_move.scxml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml deleted file mode 100644 index 07b1d82c..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/bt_update_goal_and_current_position.scxml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml deleted file mode 100644 index 9a3ce9cf..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/main.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani b/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani deleted file mode 100644 index 23996849..00000000 --- a/test/jani_generator/_test_data/grid_robot_blackboard_simpler/properties.jani +++ /dev/null @@ -1,47 +0,0 @@ -{ - "properties": [ - { - "expression": { - "fun": "values", - "op": "filter", - "states": { - "op": "initial" - }, - "values": { - "exp": { - "exp": { - "comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}", - "left": "bt_1000_response.status", - "op": "=", - "right": 1 - }, - "op": "F" - }, - "op": "Pmin" - } - }, - "name": "tree_success" - }, - { - "expression": { - "fun": "values", - "op": "filter", - "states": { - "op": "initial" - }, - "values": { - "exp": { - "exp": { - "left": "topic_at_goal_msg.valid", - "op": "∧", - "right": "topic_at_goal_msg.ros_fields__data" - }, - "op": "F" - }, - "op": "Pmin" - } - }, - "name": "at_goal" - } - ] -} diff --git a/test/jani_generator/test_systemtest_scxml_to_jani.py b/test/jani_generator/test_systemtest_scxml_to_jani.py index d3c93049..cc7020f2 100644 --- a/test/jani_generator/test_systemtest_scxml_to_jani.py +++ b/test/jani_generator/test_systemtest_scxml_to_jani.py @@ -454,12 +454,12 @@ def test_grid_robot_blackboard(self): success=True, ) - def test_grid_robot_blackboard_simpler(self): + def test_grid_robot_blackboard_simple(self): """Test the simpler grid_robot_blackboard model (BT + Blackboard).""" self._test_with_main( - "grid_robot_blackboard_simpler", + "grid_robot_blackboard_simple", model_xml="main.xml", - property_name="at_goal", + property_name="tree_success", success=True, size_limit=1_000_000, ) From 36ad8422126e39be9dd2b0fd65a1a364dec7dfaa Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 16:08:39 +0100 Subject: [PATCH 54/58] Naming error fix Signed-off-by: Marco Lampacrescia --- docs/source/scxml-jani-conversion.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/scxml-jani-conversion.rst b/docs/source/scxml-jani-conversion.rst index f8d35ee8..e5786494 100644 --- a/docs/source/scxml-jani-conversion.rst +++ b/docs/source/scxml-jani-conversion.rst @@ -6,7 +6,7 @@ SCXML and JANI In CONVINCE, we expect developers to use Behavior Trees and SCXML to model the different parts of a robotic systems. -SCXML (Scope XML) is an XML format that describes a single state machine, and allows it to exchange information with other SCXML state machines using events. Each SCXML file defines its variables (datamodel), states, and transitions. +SCXML (State Chart XML) is an XML format that describes a single state machine, and allows it to exchange information with other SCXML state machines using events. Each SCXML file defines its variables (datamodel), states, and transitions. Using SCXML, the system can be modeled as a set of state machines, each one represented by an SCXML file, which are synchronized together using events. Operations are carried out when the execution of a state machine receives an event, enters a state, or exits a state. From c2d74c1d8b9ab092f84a9b861e71abc149c79b67 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 16:16:05 +0100 Subject: [PATCH 55/58] Fix default value of options Signed-off-by: Marco Lampacrescia --- src/as2fm/jani_generator/jani_entries/jani_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/as2fm/jani_generator/jani_entries/jani_helpers.py b/src/as2fm/jani_generator/jani_entries/jani_helpers.py index ec499782..ed4b4772 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_helpers.py +++ b/src/as2fm/jani_generator/jani_entries/jani_helpers.py @@ -44,7 +44,7 @@ def _generate_new_edge_for_random_assignments( def _expand_random_variables_in_edge( - jani_edge: JaniEdge, *, n_options: int = 100 + jani_edge: JaniEdge, *, n_options: int = 101 ) -> List[JaniEdge]: """ If there are random variables in the input JaniEdge, generate new edges to handle it. @@ -100,7 +100,7 @@ def _expand_random_variables_in_edge( return generated_edges -def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int = 100) -> None: +def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int = 101) -> 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(): From f7597829724bb29cc878438ccb6d5c6eb9c6c160 Mon Sep 17 00:00:00 2001 From: Marco Lampacrescia Date: Mon, 16 Dec 2024 16:23:56 +0100 Subject: [PATCH 56/58] Some more comments Signed-off-by: Marco Lampacrescia --- src/as2fm/jani_generator/jani_entries/jani_expression.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/as2fm/jani_generator/jani_entries/jani_expression.py b/src/as2fm/jani_generator/jani_entries/jani_expression.py index de98c59a..269d5569 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_expression.py +++ b/src/as2fm/jani_generator/jani_entries/jani_expression.py @@ -83,6 +83,7 @@ def __init__(self, expression: Union[SupportedExp, "JaniExpression", JaniValue]) self.operands = self._get_operands(expression) def _get_operands(self, expression_dict: dict) -> Dict[str, "JaniExpression"]: + """Generate the expressions operands from a raw dictionary, after validating it.""" assert self.op is not None, "Operator not set" if self.op in ("intersect", "distance"): # intersect: returns a value in [0.0, 1.0], indicating where on the robot trajectory @@ -222,6 +223,7 @@ def replace_event(self, replacement: Optional[str]) -> "JaniExpression": return self def is_valid(self) -> bool: + """Expression validity check.""" return self.identifier is not None or self.value is not None or self.op is not None def as_literal(self) -> Optional[JaniValue]: @@ -242,6 +244,7 @@ def as_operator(self) -> Optional[Tuple[str, Dict[str, "JaniExpression"]]]: return (self.op, self.operands) def as_dict(self) -> Union[str, int, float, bool, dict]: + """Convert the expression to a dictionary, ready to be converted to JSON.""" assert hasattr(self, "identifier"), f"Identifier not set for {self.__dict__}" if self.identifier is not None: return self.identifier @@ -278,6 +281,7 @@ def __init__(self, expression: dict): assert self.is_valid(), "Invalid arguments provided: expected args[0] <= args[1]." def is_valid(self): + """Distribution validity check.""" # All other checks are carried out in the constructor return all(isinstance(argument, (int, float)) for argument in self._args) and ( self._args[0] <= self._args[1] @@ -305,12 +309,15 @@ def as_operator(self) -> None: return None def get_dist_type(self) -> str: + """Return the distribution type set in the object.""" return self._distribution def get_dist_args(self) -> List[Union[int, float]]: + """Return the config. arguments of the distribution.""" return self._args def as_dict(self) -> Dict[str, Any]: + """Convert the distribution to a dictionary, ready to be converted to JSON.""" assert self.is_valid(), "Expected distribution to be valid." return {"distribution": self._distribution, "args": self._args} From a5542378fd93e3334820eba7a5d6a94f9c1b9d9b Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:40:15 +0100 Subject: [PATCH 57/58] Update src/as2fm/scxml_converter/bt_converter.py Signed-off-by: Christian Henkel <6976069+ct2034@users.noreply.github.com> --- src/as2fm/scxml_converter/bt_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/as2fm/scxml_converter/bt_converter.py b/src/as2fm/scxml_converter/bt_converter.py index 60e04d6a..d3e0baec 100644 --- a/src/as2fm/scxml_converter/bt_converter.py +++ b/src/as2fm/scxml_converter/bt_converter.py @@ -76,7 +76,7 @@ def get_blackboard_variables_from_models(models: List[ScxmlRoot]) -> Dict[str, s def generate_blackboard_scxml(bt_blackboard_vars: Dict[str, str]) -> ScxmlRoot: - """Generate an SCXML model that handles all BT related synchronization.""" + """Generate an SCXML model that handles all BT-related synchronization.""" assert len(bt_blackboard_vars) > 0, "Cannot generate BT Blackboard, no variables" # TODO: Append the name of the related BT, as in generate_bt_root_scxml scxml_model_name = "bt_blackboard_fsm" From f34e42edc8f23b358290904aad9f2c61b87065ac Mon Sep 17 00:00:00 2001 From: Christian Henkel <6976069+ct2034@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:40:24 +0100 Subject: [PATCH 58/58] Update src/as2fm/jani_generator/jani_entries/jani_assignment.py Signed-off-by: Christian Henkel <6976069+ct2034@users.noreply.github.com> --- src/as2fm/jani_generator/jani_entries/jani_assignment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/as2fm/jani_generator/jani_entries/jani_assignment.py b/src/as2fm/jani_generator/jani_entries/jani_assignment.py index 40bc94a3..4bd63998 100644 --- a/src/as2fm/jani_generator/jani_entries/jani_assignment.py +++ b/src/as2fm/jani_generator/jani_entries/jani_assignment.py @@ -45,6 +45,7 @@ def get_expression(self): return self._value def get_index(self) -> int: + """Returns the index, i.e. the number that defines the order of execution in Jani.""" return self._index def as_dict(self, constants: Dict[str, JaniConstant]):