Skip to content

Commit

Permalink
Update uc2 models for paper and adjust random number generator (#77)
Browse files Browse the repository at this point in the history
Signed-off-by: Marco Lampacrescia <[email protected]>
  • Loading branch information
MarcoLm993 authored Jan 30, 2025
1 parent 144955d commit a53d243
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 44 deletions.
24 changes: 22 additions & 2 deletions docs/source/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ As in the case of ROS functionalities, BT ports need to be declared before being
.. code-block:: xml
<bt_declare_port_in key="my_string_port" type="string" />
<bt_declare_port_in key="start_value" type="int32">
<bt_declare_port_in key="start_value" type="int32" />
<bt_declare_port_out key="output_int" type="int32" />
Once declared, it is possible to reference to the port in multiple SCXML entries.

Expand All @@ -337,8 +338,27 @@ Or we can use `start_value` to define the initial value of a variable.
</data>
</datamodel>
Finally, we can store a specific value to the blackboard (only for output ports).

BT ports can also be linked to variables in the `BT Blackboard` by wrapping the variable name in curly braces in the BT XML file. However, this feature is not yet supported.
.. code-block:: xml
<state id="some_state">
<onentry>
...
<bt_set_output key="output_int" expr="new_value_expression" />
...
</onentry>
...
</state>
BT Ports can be declared either as input or output ports:

* input ports can refer to either fixed or mutable variables (i.e. blackboard variables)
* output ports on only refer to mutable variables

When a BT plugin declares an output port, this must be referenced to a `BT Blackboard` variable.
This is defined in the BT XML file, by providing a blackboard variable name wrapped by curly braces.


.. _main_xml_howto:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ def expand_expression(


def expand_distribution_expressions(
expression: JaniExpression, *, n_options: int = 101
expression: JaniExpression, *, n_options
) -> List[JaniExpression]:
"""
Traverse the expression and substitute each distribution with n expressions.
Expand Down Expand Up @@ -543,8 +543,7 @@ def expand_distribution_expressions(
dist_width = expression.get_dist_args()[1] - lower_bound
# Generate a (constant) JaniExpression for each possible outcome
return [
JaniExpression(lower_bound + (x * dist_width / (n_options - 1)))
for x in range(n_options)
JaniExpression(lower_bound + (x * dist_width / (n_options))) for x in range(n_options)
]
return [expression]

Expand Down
12 changes: 6 additions & 6 deletions src/as2fm/jani_generator/jani_entries/jani_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ def _generate_new_edge_for_random_assignments(
)


def _expand_random_variables_in_edge(
jani_edge: JaniEdge, *, n_options: int = 101
) -> List[JaniEdge]:
def _expand_random_variables_in_edge(jani_edge: JaniEdge, *, n_options: int) -> List[JaniEdge]:
"""
If there are random variables in the input JaniEdge, generate new edges to handle it.
Expand Down Expand Up @@ -100,17 +98,19 @@ def _expand_random_variables_in_edge(
return generated_edges


def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int = 101) -> None:
def expand_random_variables_in_jani_model(model: JaniModel, *, n_options: int) -> None:
"""Find all expression containing the 'distribution' expression and expand them."""
# Check that no global variable has a random value (not supported)
for g_var_name, g_var in model.get_variables().items():
assert (
len(expand_distribution_expressions(g_var.get_init_expr())) == 1
len(expand_distribution_expressions(g_var.get_init_expr(), n_options=100)) == 1
), f"Global variable {g_var_name} is init using a random value. This is unsupported."
for automaton in model.get_automata():
# Also for automaton, check variables initialization
for aut_var_name, aut_var in automaton.get_variables().items():
assert len(expand_distribution_expressions(aut_var.get_init_expr())) == 1, (
assert (
len(expand_distribution_expressions(aut_var.get_init_expr(), n_options=100)) == 1
), (
f"Variable {aut_var_name} in automaton {automaton.get_name()} is init using random "
f"values: init expr = '{aut_var.get_init_expr().as_dict()}'. This is unsupported."
)
Expand Down
24 changes: 24 additions & 0 deletions src/as2fm/jani_generator/scxml_helpers/scxml_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
from typing import Dict, List, Optional

from as2fm.jani_generator.ros_helpers.ros_timer import ROS_TIMER_RATE_EVENT_PREFIX
from as2fm.scxml_converter.scxml_entries.bt_utils import is_bt_response_event, is_bt_tick_event
from as2fm.scxml_converter.scxml_entries.ros_utils import (
is_action_request_event,
is_action_result_event,
is_action_thread_event,
is_srv_event,
)


class EventSender:
Expand Down Expand Up @@ -142,3 +149,20 @@ def get_events(self) -> Dict[str, Event]:
def add_event(self, event: Event):
assert event.name not in self._events, f"Event {event.name} must not be added twice."
self._events[event.name] = event


def is_event_synched(event_name: str) -> bool:
"""
Check if the event is synched, hence there should not be autogenerated self loops.
:param event_name: The name of the event to evaluate.
"""
# Action feedbacks are not considered synched, since a client might discard one or more of them
return (
is_bt_tick_event(event_name)
or is_bt_response_event(event_name)
or is_action_request_event(event_name)
or is_action_result_event(event_name)
or is_action_thread_event(event_name)
or is_srv_event(event_name)
)
5 changes: 4 additions & 1 deletion src/as2fm/jani_generator/scxml_helpers/scxml_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
get_variable_type,
is_variable_array,
)
from as2fm.jani_generator.scxml_helpers.scxml_event import Event, EventsHolder
from as2fm.jani_generator.scxml_helpers.scxml_event import Event, EventsHolder, is_event_synched
from as2fm.jani_generator.scxml_helpers.scxml_expression import (
ArrayInfo,
parse_ecmascript_to_jani_expression,
Expand Down Expand Up @@ -627,6 +627,9 @@ def add_unhandled_transitions(self, transitions_set: Set[str]):
if event_name in self._events_no_condition or len(event_name) == 0:
continue
guard_exp = self.get_guard_exp_for_prev_conditions(event_name)
# If the event was not handled in the state and is expected to be synched, skip it
if guard_exp is None and is_event_synched(event_name):
continue
edges, locations = _append_scxml_body_to_jani_automaton(
self.automaton,
self.events_holder,
Expand Down
10 changes: 10 additions & 0 deletions src/as2fm/scxml_converter/scxml_entries/bt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,21 @@ def generate_bt_tick_event(instance_id: str) -> str:
return f"bt_{instance_id}_tick"


def is_bt_tick_event(event_name: str) -> bool:
"""Check is the event is used for ticking a BT node."""
return re.match(r"^bt_.+_tick$", event_name) is not None


def generate_bt_response_event(instance_id: str) -> str:
"""Generate the BT response event name for a given BT node instance."""
return f"bt_{instance_id}_response"


def is_bt_response_event(event_name: str) -> bool:
"""Check if the event name is for BT node's responses(success, failure, running)."""
return re.match(r"^bt_.+_response$", event_name) is not None


def is_bt_event(event_name: str) -> bool:
"""Given an event name, returns whether it is related to a BT event or not."""
bt_events = [f"bt_{suffix}" for suffix in ["tick", "running", "success", "failure"]]
Expand Down
21 changes: 21 additions & 0 deletions src/as2fm/scxml_converter/scxml_entries/ros_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

"""Collection of SCXML utilities related to ROS functionalities."""

import re
from typing import Any, Dict, List, Tuple, Type

from as2fm.scxml_converter.scxml_entries import RosField, ScxmlBase
Expand Down Expand Up @@ -196,6 +197,11 @@ def generate_srv_server_response_event(service_name: str) -> str:
return f"srv_{sanitize_ros_interface_name(service_name)}_response"


def is_srv_event(event_name) -> str:
"""Check whether the event name matches the ROS service plain events pattern."""
return re.match(r"^srv_.+_(response|request|req_client).*$", event_name) is not None


def generate_action_goal_req_event(action_name: str, client_name: str) -> str:
"""Generate the name of the event that sends an action goal from a client to the server."""
return f"action_{sanitize_ros_interface_name(action_name)}_goal_req_client_{client_name}"
Expand Down Expand Up @@ -262,6 +268,21 @@ def generate_action_result_handle_event(action_name: str, automaton_name: str) -
)


def is_action_request_event(event_name: str) -> bool:
"""Check whether the event name matches the ROS action plain events pattern."""
return re.match(r"^action_.+_goal_.+$", event_name) is not None


def is_action_result_event(event_name: str) -> bool:
"""Check whether the event name matches the ROS action plain events pattern."""
return re.match(r"^action_.+_result.*$", event_name) is not None


def is_action_thread_event(event_name: str) -> bool:
"""Check whether the event name matches the ROS action plain events pattern."""
return re.match(r"^action_.+_thread_(start|free)$", event_name) is not None


class ScxmlRosDeclarationsContainer:
"""Object that contains a description of the ROS declarations in the SCXML root."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

<state id="init">
<transition target="next_goal">
<assign location="pose_x" expr="Math.floor(Math.random() * (n-1))" />
<assign location="pose_y" expr="Math.floor(Math.random() * (n-1))" />
<assign location="pose_x" expr="Math.floor(Math.random() * n)" />
<assign location="pose_y" expr="Math.floor(Math.random() * n)" />
<ros_topic_publish name="pose">
<field name="x" expr="pose_x" />
<field name="y" expr="pose_y" />
Expand All @@ -36,8 +36,8 @@

<state id="next_goal">
<transition target="running">
<assign location="goal_x" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_y" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_x" expr="Math.floor(Math.random() * n)" />
<assign location="goal_y" expr="Math.floor(Math.random() * n)" />
<ros_topic_publish name="goal">
<field name="x" expr="goal_x" />
<field name="y" expr="goal_y" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@

<state id="init">
<transition target="next_goal">
<assign location="curr_x" expr="Math.floor(Math.random() * (n-1))" />
<assign location="curr_y" expr="Math.floor(Math.random() * (n-1))" />
<assign location="curr_x" expr="Math.floor(Math.random() * n)" />
<assign location="curr_y" expr="Math.floor(Math.random() * n)" />
<bt_set_output key="curr_x" expr="curr_x" />
<bt_set_output key="curr_y" expr="curr_y" />
</transition>
</state>

<state id="next_goal">
<transition target="running">
<assign location="goal_x" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_y" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_x" expr="Math.floor(Math.random() * n)" />
<assign location="goal_y" expr="Math.floor(Math.random() * n)" />
<bt_set_output key="goal_x" expr="goal_x" />
<bt_set_output key="goal_y" expr="goal_y" />
</transition>
Expand All @@ -48,8 +48,8 @@
<onentry>
<if cond="curr_x == goal_x &amp;&amp; curr_y == goal_y">
<assign location="goal_count" expr="goal_count + 1" />
<assign location="goal_x" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_y" expr="Math.floor(Math.random() * (n-1))" />
<assign location="goal_x" expr="Math.floor(Math.random() * n)" />
<assign location="goal_y" expr="Math.floor(Math.random() * n)" />
<bt_set_output key="goal_x" expr="goal_x" />
<bt_set_output key="goal_y" expr="goal_y" />
</if>
Expand Down
42 changes: 37 additions & 5 deletions test/jani_generator/_test_data/uc2_assembly/Main/properties.jani
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"properties": [
{
"name": "can_execute_recovery_branch",
"name": "executes_recovery_branch_or_success",
"expression": {
"op": "filter",
"fun": "values",
"values": {
"op": "Pmin",
"exp": {
"comment": "((abort_time > 0) ⇒ (clock < abort_time + 5000)) U (recover_block_running > 0)",
"comment": "((abort_time > 0) ⇒ (clock < abort_time + 5000)) U (recover_block_running > 0 || tree_success)",
"op": "U",
"left": {
"op": "⇒",
Expand All @@ -28,9 +28,41 @@
}
},
"right": {
"op": ">",
"left": "topic_uc2__info__properties__recover_block_running_msg.ros_fields__data",
"right": 0
"op": "∨",
"left": {
"op": ">",
"left": "topic_uc2__info__properties__recover_block_running_msg.ros_fields__data",
"right": 0
},
"right": {
"op": "=",
"left": "bt_1000_response.status",
"right": 1,
"comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}"
}
}
}
},
"states": {
"op": "initial"
}
}
},
{
"name": "move_success",
"expression": {
"op": "filter",
"fun": "values",
"values": {
"op": "Pmin",
"exp": {
"comment": "The tree succeeds only in case MoveBlock succeeds, but currently not working, since after recovery the plugin MoveAction will FAIL anyway! This is a problem with the missing reset: when RecoverBlock runs, the MoveAction plugin shall be reset, preventing failure from happening...",
"op": "F",
"exp": {
"op": "=",
"left": "bt_1000_response.status",
"right": 1,
"comment": "Bt 1000 is always the root, Values = {1: SUCCESS, 2: FAILURE, 3: RUNNING}"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ros_action_server name="MoveBlock_action_srv" action_name="/uc2/skills/move_block" type="uc2_interfaces/MoveBlock" />
<!-- service server regarding block_down information -->
<ros_service_server name="get_block_status_srv" service_name="/uc2/world/get_block_status" type="uc2_interfaces/GetBlockStatus" />
<ros_service_server name="reset_block_status_srv" service_name="/uc2/world/reset_block_status" type="std_srvs/Empty" />
<ros_service_server name="reset_block_status_srv" service_name="/uc2/world/reset_block_status" type="std_srvs/Empty" />

<!-- listen time info and publish abort_time info -->
<ros_topic_subscriber name="clock_sub" topic="/uc2/info/clock" type="std_msgs/Int32" />
Expand Down Expand Up @@ -38,15 +38,15 @@
<!-- accept goal -->
<ros_action_accept_goal name="MoveBlock_action_srv" goal_id="goal_id" />
<!-- compute result -->
<assign location="block_down" expr="block_down + 1" />
<if cond="(block_down % 2) == 0">
<assign location="block_down" expr="Math.floor(Math.random() * 2)" />
<if cond="block_down == 0">
<!-- then success -->
<ros_action_succeed name="MoveBlock_action_srv" goal_id="goal_id" />
<else/>
<!-- else abort -->
<!-- publish abort time -->
<ros_topic_publish name="moveblock_abort_time_pub">
<field name="data" expr="time" />
<field name="data" expr="time" />
</ros_topic_publish>
<ros_action_aborted name="MoveBlock_action_srv" goal_id="goal_id" />
</if>
Expand Down
4 changes: 2 additions & 2 deletions test/jani_generator/test_systemtest_scxml_to_jani.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def test_uc2_assembly(self):
self._test_with_main(
os.path.join("uc2_assembly", "Main"),
model_xml="main.xml",
property_name="can_execute_recovery_branch",
property_name="executes_recovery_branch_or_success",
success=True,
)

Expand All @@ -436,7 +436,7 @@ def test_uc2_assembly_with_bug(self):
self._test_with_main(
os.path.join("uc2_assembly", "Main"),
model_xml="main_bug.xml",
property_name="can_execute_recovery_branch",
property_name="executes_recovery_branch_or_success",
success=False,
)

Expand Down
Loading

0 comments on commit a53d243

Please sign in to comment.