Skip to content

Commit

Permalink
Merge branch 'transition-priority'
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Jun 25, 2018
2 parents 059d481 + ee61298 commit 3152562
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 60 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

Unreleased
----------

- (Added) Priority can be set for transitions (using *low*, *high* or any integer in yaml). transitions
are selected according to their priorities (still following eventless and inner-first/source state semantics).


1.2.2 (2018-06-21)
------------------

Expand Down
3 changes: 3 additions & 0 deletions docs/execution.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ We decide to follow Rhapsody and to raise an error (in fact, a :py:exc:`~sismic.
nondeterminism occur during the execution. Notice that this only concerns multiple transitions in the same
composite state, not in parallel states.

.. note:: Sismic allows to define priorities on transitions to address nondeterminism: transitions with
higher priorities will be selected first for execution, ignoring transitions with lower priorities.

When multiple transitions are triggered from within distinct parallel states, the situation is even more intricate.
According to the Rhapsody implementation:

Expand Down
10 changes: 9 additions & 1 deletion docs/format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,15 @@ Instead, it **must** either define an event or define a guard to determine when
Notice that such a transition does not trigger the *on entry* and *on exit* of its state, and can thus be used
to model an *internal action*.


Priorities can be set for transitions using the *priority* property. By default, all transitions
have a priority of 0. A priority can be any integer, or *low* (equivalent to -1) or *high*
(equivalent to 1).

.. note:: Transition priorities can be used to simulate default transitions or to fix non-determinism
when multiple transitions from a single state can be triggered at the same time. Notice that
transition priorities are considered **after** the default semantics of Sismic (eventless
transitions first, and inner-first/source state first).


Statechart examples
*******************
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ but can be easily tuned to other semantics.
Sismic statecharts provides full support for the majority of the UML 2 statechart concepts:

- simple states, composite states, orthogonal (parallel) states, initial and final states, shallow and deep history states
- state transitions, guarded transitions, automatic (eventless) transitions, internal transitions
- state transitions, guarded transitions, automatic (eventless) transitions, internal transitions and transition priorities
- statechart variables and their initialisation
- state entry and exit actions, transition actions
- internal and external parametrized events
Expand Down
4 changes: 2 additions & 2 deletions sismic/code/evaluator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import abc
from typing import Any, Optional, Iterable, List, Mapping, cast
from typing import Any, Optional, Iterable, List, Mapping

from ..model import ActionStateMixin, Statechart, StateMixin, Transition, Event
from ..model import Statechart, StateMixin, Transition, Event
from ..exceptions import CodeEvaluationError

__all__ = ['Evaluator']
Expand Down
2 changes: 0 additions & 2 deletions sismic/code/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,6 @@ def evaluate_preconditions(self, obj, event: Optional[Event]=None) -> Iterator[s
:param event: an optional *Event* instance, in the case of a transition
:return: list of unsatisfied conditions
"""
state_name = obj.source if isinstance(obj, Transition) else obj.name

additional_context = {'event': event} if isinstance(obj, Transition) else {} # type: Dict[str, Any]
additional_context.update({
'received': self._received,
Expand Down
111 changes: 63 additions & 48 deletions sismic/interpreter/default.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import deque
from itertools import combinations, groupby
from collections import deque, defaultdict
from itertools import combinations
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Union, cast, Tuple

try:
Expand All @@ -21,6 +21,17 @@
__all__ = ['Interpreter']


def sorted_groupby(iterable, key=None, reverse=False):
"""
Return pairs (label, group) grouped and sorted by label = key(item).
"""
groups = defaultdict(list)
for value in iterable:
groups[key(value)].append(value)
sort_key = lambda e: e[0]
return sorted(groups.items(), key=sort_key, reverse=reverse)


class Interpreter:
"""
A discrete interpreter that executes a statechart according to a semantic close to SCXML.
Expand Down Expand Up @@ -227,7 +238,7 @@ def execute_once(self) -> Optional[MacroStep]:
"""
# Compute steps
computed_steps = self._compute_steps()

if computed_steps is None:
# No step (no transition, no event). However, check properties
self._check_properties(None)
Expand Down Expand Up @@ -349,69 +360,73 @@ def _select_transitions(self, event: Event, states: List[str], *,
eventless_first=True, inner_first=True) -> List[Transition]:
"""
Select and return the transitions that are triggered, based on given event
(or None if no event can be consumed) and given list of states.
(or None if no event can be consumed) and given list of states.
By default, this function prioritizes eventless transitions and follows
inner-first/source state semantics.
inner-first/source state semantics.
:param event: event to consider, possibly None.
:param states: state names to consider.
:param states: state names to consider.
:param eventless_first: True to prioritize eventless transitions.
:param inner_first: True to follow inner-first/source state semantics.
:return: list of triggered transitions.
"""
"""
selected_transitions = [] # type: List[Transition]

# List of (event_order, depth_order, transition)
considered_transitions = [] # type: List[Tuple[int, int, Transition]]

# Use a cache for state depth (could avoid some costly computations)
considered_transitions = [] # type: List[Transition]
_state_depth_cache = dict() # type: Dict[str, int]

# Select triggerable (based on event) transitions for considered states
for transition in self._statechart.transitions:
if transition.source in states:
if transition.event is None or transition.event == getattr(event, 'name', None):
# Compute order based on event
event_order = int(transition.event is None) # event = 0, no event = 1

# Compute order based on depth
if transition.source not in _state_depth_cache:
_state_depth_cache[transition.source] = self._statechart.depth_for(transition.source)
depth_order = _state_depth_cache[transition.source] # less nested first

# Change order according to the semantics
event_order = -event_order if eventless_first else event_order
depth_order = -depth_order if inner_first else depth_order

considered_transitions.append((event_order, depth_order, transition))

# Order transitions based on event and depth orderings
considered_transitions.sort(key=lambda t: (t[0], t[1]))

_state_depth_cache[transition.source] = self._statechart.depth_for(transition.source)

considered_transitions.append(transition)

# Which states should be selected to satisfy depth ordering?
ignored_state_selector = self._statechart.ancestors_for if inner_first else self._statechart.descendants_for
ignored_states = set() # type: Set[str]

# Select transitions according to the semantics
for eventless, transitions in groupby(considered_transitions, lambda t: t[0]): # event order

# Group and sort transitions based on the event
eventless_first_order = lambda t: t.event is not None
for has_event, transitions in sorted_groupby(considered_transitions, key=eventless_first_order, reverse=not eventless_first):
# Event shouldn't be exposed to guards if we're processing eventless transition
exposed_event = None if eventless else event
exposed_event = event if has_event else None

# If there are selected transitions (from previous group), ignore new ones
if len(selected_transitions) > 0:
break

# Remember that transitions are sorted based on event/eventless and the source state depth
for *_, transition in transitions:
if transition.source not in ignored_states:
if transition.guard is None or self._evaluator.evaluate_guard(transition, exposed_event):
# Add descendants/ancestors to ignored states
for state in ignored_state_selector(transition.source):
ignored_states.add(state)
# Add transition to the list of selected ones
selected_transitions.append(transition)

# Group and sort transitions based on the source state depth
depth_order = lambda t: _state_depth_cache[t.source]
for _, transitions in sorted_groupby(transitions, key=depth_order, reverse=inner_first):
# Group and sort transitions based on the source state
state_order = lambda t: t.source # we just want states to be grouped here
for source, transitions in sorted_groupby(transitions, key=state_order):
# Do not considered ignored states
if source in ignored_states:
continue

has_found_transitions = False
# Group and sort transitions based on their priority
priority_order = lambda t: t.priority
for _, transitions in sorted_groupby(transitions, key=priority_order, reverse=True):
for transition in transitions:
if transition.guard is None or self._evaluator.evaluate_guard(transition, exposed_event):
# Add transition to the list of selected ones
selected_transitions.append(transition)
has_found_transitions = True

# Ignore ancestors/descendants w.r.t. inner-first/source state
if has_found_transitions:
for state in ignored_state_selector(source):
ignored_states.add(state)
# Also ignore current state, as we found transitions in a higher priority class
ignored_states.add(source)
break

return selected_transitions

def _sort_transitions(self, transitions: List[Transition]) -> List[Transition]:
Expand Down Expand Up @@ -467,15 +482,15 @@ def _sort_transitions(self, transitions: List[Transition]) -> List[Transition]:
def _compute_steps(self) -> Optional[List[MicroStep]]:
"""
Compute and returns the next steps based on current configuration
and event queues.
and event queues.
:return A (possibly None) list of steps.
"""
# Initialization
if not self._initialized:
self._initialized = True
return [MicroStep(entered_states=[self._statechart.root])]

# Select transitions
event = self._select_event(consume=False)
transitions = self._select_transitions(event, states=self._configuration)
Expand All @@ -488,13 +503,13 @@ def _compute_steps(self) -> Optional[List[MicroStep]]:
else:
# Empty step, so that event is eventually consumed
return [MicroStep(event=event)]

# Compute transitions order
transitions = self._sort_transitions(transitions)

# Should the step consume an event?
event = None if transitions[0].event is None else event

return self._create_steps(event, transitions)


Expand Down
17 changes: 16 additions & 1 deletion sismic/io/datadict.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,20 @@ def _import_transition_from_dict(state_name: str, transition_d: Mapping[str, Any
event = transition_d.get('event', None)
guard = transition_d.get('guard', None)
action = transition_d.get('action', None)
priority = transition_d.get('priority', None)

if priority == 'low':
priority = Transition.LOW_PRIORITY
elif priority == 'high':
priority = Transition.HIGH_PRIORITY

transition = Transition(
state_name,
transition_d.get('target', None),
event.strip() if event else None,
guard.strip() if guard else None,
action.strip() if action else None,
priority,
)

# Preconditions, postconditions and invariants
Expand Down Expand Up @@ -218,6 +225,14 @@ def _export_state_to_dict(statechart: Statechart, state_name: str, ordered=True)
transition_data['target'] = transition.target
if transition.action:
transition_data['action'] = transition.action
if transition.priority != Transition.DEFAULT_PRIORITY:
if transition.priority == Transition.LOW_PRIORITY:
priority = 'low'
elif transition.priority == Transition.HIGH_PRIORITY:
priority = 'high'
else:
priority = transition.priority
transition_data['priority'] = priority

preconditions = getattr(transition, 'preconditions', [])
postconditions = getattr(transition, 'postconditions', [])
Expand All @@ -243,4 +258,4 @@ def _export_state_to_dict(statechart: Statechart, state_name: str, ordered=True)
elif isinstance(state, OrthogonalState):
data['parallel states'] = children_data

return data
return data
8 changes: 6 additions & 2 deletions sismic/io/plantuml.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def export_state(self, name: str) -> None:
if len(transitions) > 0:
for transition in transitions:
text = []
if transition.priority != Transition.DEFAULT_PRIORITY:
text.append('{}:'.format(transition.priority))
if transition.event:
text.append('**{}** '.format(transition.event))
if transition.guard:
Expand Down Expand Up @@ -193,6 +195,8 @@ def export_transition(self, transition: Transition) -> None:
target_name = self.state_id(target.name)

text = []
if transition.priority != Transition.DEFAULT_PRIORITY:
text.append('{}:'.format(transition.priority))
if transition.event:
text.append(transition.event + ' ')
if transition.guard:
Expand All @@ -211,8 +215,8 @@ def export_transition(self, transition: Transition) -> None:
for cond in transition.postconditions:
text.append('post: {}\n'.format(cond))

format = '{source} {arrow} {target} : {text}' if len(text) > 0 else '{source} {arrow} {target}'
self.output(format.format(
_format = '{source} {arrow} {target} : {text}' if len(text) > 0 else '{source} {arrow} {target}'
self.output(_format.format(
source=self.state_id(transition.source),
arrow=self.arrow(transition.source, transition.target),
target=target_name,
Expand Down
1 change: 1 addition & 0 deletions sismic/io/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SCHEMA:
schema.Optional('guard'): schema.Use(str),
schema.Optional('action'): schema.Use(str),
schema.Optional('contract'): [contract],
schema.Optional('priority'): schema.Or(schema.Use(int), 'high', 'low'),
}

state = dict() # type: ignore
Expand Down
16 changes: 14 additions & 2 deletions sismic/model/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,21 @@ class Transition(ContractMixin):
:param event: event name (if any)
:param guard: condition as code (if any)
:param action: action as code (if any)
:param priority: priority (default to 0)
"""

def __init__(self, source: str, target: str=None, event: str=None, guard: str=None, action: str=None) -> None:
LOW_PRIORITY = -1
DEFAULT_PRIORITY = 0
HIGH_PRIORITY = 1

def __init__(self, source: str, target: str=None, event: str=None, guard: str=None, action: str=None, priority=None) -> None:
ContractMixin.__init__(self)
self._source = source
self._target = target
self.event = event
self.guard = guard
self.action = action
self.priority = 0 if priority is None else priority

@property
def source(self):
Expand Down Expand Up @@ -340,7 +346,13 @@ def __repr__(self):
return 'Transition({!r}, {!r}, event={!r})'.format(self.source, self.target, self.event)

def __str__(self):
return '{} -> {} [{}] -> {}'.format(self.source, self.event, self.guard, self.target if self.target else '')
return '{} -> {}{} [{}] -> {}'.format(
self.source,
'' if self.priority == 0 else '{}:'.format(self.priority),
self.event,
self.guard,
self.target if self.target else ''
)

def __hash__(self):
return hash(self.source)
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ def internal_statechart():
return import_from_yaml(filepath='tests/yaml/internal.yaml')


@pytest.fixture(params=['actions', 'composite', 'deep_history', 'final', 'infinite', 'internal',
@pytest.fixture
def priority_statechart():
return import_from_yaml(filepath='tests/yaml/priority.yaml')


@pytest.fixture(params=['actions', 'composite', 'deep_history', 'final', 'infinite', 'internal', 'priority',
'nested_parallel', 'nondeterministic', 'parallel', 'simple', 'timer'])
def example_from_tests(request):
return import_from_yaml(filepath=os.path.join('tests', 'yaml', request.param + '.yaml'))
Expand Down

0 comments on commit 3152562

Please sign in to comment.