def test_get_transitions_from_subclass_instance_multi_transition_defs( self): class Test(crest.Entity): s1 = current = crest.State() s2 = crest.State() s3 = crest.State() @crest.transition(source=[s1, s3], target=s2) def t1(self): return True t2 = crest.Transition(source=s2, target=s3, guard=(lambda self: True)) t3 = crest.Transition(source=[s3, s2], target=s1, guard=(lambda self: False)) class SubTest(Test): pass instance = SubTest() transitions = crest.get_transitions(instance) self.assertCountEqual(transitions, [ getattr(instance, att) for att in ["t1___0", "t1___1", "t2", "t3___0", "t3___1"] ])
def test_get_transitions_from_subclass_definition_multi_transition_defs( self): class Test(crest.Entity): s1 = current = crest.State() s2 = crest.State() s3 = crest.State() @crest.transition(source=[s1, s3], target=s2) def t1(self): return True t2 = crest.Transition(source=s2, target=s3, guard=(lambda self: True)) t3 = crest.Transition(source=[s3, s2], target=s1, guard=(lambda self: False)) class SubTest(Test): pass klass = SubTest transitions = crest.get_transitions(klass) self.assertCountEqual( transitions, [klass.t1___0, klass.t1___1, klass.t2, klass.t3___0, klass.t3___1])
def test_get_transitions_from_subclass_instance_single_transition_defs( self): class Test(crest.Entity): s1 = current = crest.State() s2 = crest.State() s3 = crest.State() @crest.transition(source=s1, target=s2) def t1(self): return True t2 = crest.Transition(source=s2, target=s3, guard=(lambda self: True)) t3 = crest.Transition(source=s3, target=s1, guard=(lambda self: False)) class SubTest(Test): pass instance = SubTest() transitions = crest.get_transitions(instance) self.assertCountEqual(transitions, [instance.t1, instance.t2, instance.t3])
def test_transition_correctly_created(self): self.testclass.trans = crest.Transition(source="A", target="B", guard=(lambda self: True)) instance = self.instance() transitions = crest.get_transitions(instance) self.assertEqual(len(transitions), 1) self.assertEqual(self._count_transitions_from_to(transitions, instance.A, instance.B), 1)
def collect_transition_times_from_entity(self, entity=None): """ collect all transitions and their times """ if not entity: entity = self.entity logger.debug("Calculating transition times for entity: %s (%s)", entity._name, entity.__class__.__name__) dts = [] for name, trans in get_transitions(entity, as_dict=True).items(): if entity.current == trans.source: dt = self.get_transition_time(trans) dts.append((entity, trans, name, dt)) if dts: if logger.getEffectiveLevel() <= logging.DEBUG: logger.debug("times: ") logger.debug( str([(e._name, f"{t.source._name} -> {t.target._name} ({dt})", dt) for (e, t, name, dt) in dts])) else: if logger.getEffectiveLevel() <= logging.DEBUG: logger.debug("times: []") dts = list(filter(lambda t: t[3] is not None, dts)) # filter values with None as dt return dts
def gen_Entity(obj, name="", parent=None, **kwargs): islogical = issubclass(obj.__class__, Model.LogicalEntity) hide_behaviour = kwargs["interface_only"] or kwargs["no_behaviour"] or \ (islogical and kwargs["logical_interface_only"]) style = "dashed" if islogical else "solid" inputs = "" outputs = "" centre = "" body = "" """ Inputs """ for name_, input_ in Model.get_inputs(obj, as_dict=True).items(): inputs += "\n" + generate(input_, name_, obj, **kwargs) """ Centre """ if not hide_behaviour: for name_, state in Model.get_states(obj, as_dict=True).items(): if not name_ == CURRENT_IDENTIFIER: centre += "\n" + generate(state, name_, obj, **kwargs) if not hide_behaviour: for name_, local in Model.get_locals(obj, as_dict=True).items(): centre += "\n" + generate(local, name_, obj, **kwargs) """ Outputs """ for name_, output in Model.get_outputs(obj, as_dict=True).items(): outputs += "\n" + generate(output, name_, obj, **kwargs) """ Body """ if not hide_behaviour: if kwargs["transitions"]: for name_, trans in Model.get_transitions(obj, as_dict=True).items(): body += "\n" + "\n".join(generate(trans, name_, obj, **kwargs)) if not kwargs["interface_only"] and not (islogical and kwargs["logical_interface_only"]): for name_, entity in Model.get_entities(obj, as_dict=True).items(): if name_ != PARENT_IDENTIFIER: centre += "\n" + generate(entity, name_, obj, **kwargs) if not kwargs["interface_only"] and not (islogical and kwargs["logical_interface_only"]): for name_, influence in Model.get_influences(obj, as_dict=True).items(): body += "\n" + generate(influence, name_, obj, **kwargs) if not hide_behaviour: if kwargs["updates"]: for name_, update in Model.get_updates(obj, as_dict=True).items(): body += "\n" + "\n".join(generate(update, name_, obj, ** kwargs)) if not hide_behaviour: if kwargs["actions"]: for name_, action in Model.get_actions(obj, as_dict=True).items(): body += "\n" + "\n".join(generate(action, name_, obj, ** kwargs)) typename = obj.__class__.__name__ if isinstance( obj, CrestObject) else obj.__name__ return f"""
def select_transition_to_trigger(self, entity): transitions_from_current_state = [ t for t in crest.get_transitions(entity) if t.source is entity.current ] enabled_transitions = [ t for t in transitions_from_current_state if self._get_transition_guard_value(t) ] if len(enabled_transitions) == 1: # no choice return enabled_transitions[0] elif len(enabled_transitions) > 1: # this is the tricky part logger.debug( f"Multiple transitions enabled in entity {api.get_name(entity)}" ) transition = self._select_transition_according_to_execution_plan( entity) if transition is False: return random.choice(enabled_transitions) else: assert transition is not None and transition in enabled_transitions, \ f"The plan says to execute transition {api.get_name(transition)}, but it's not amongst the enabled ones." return transition else: # no transitions at all return None
def test_transitions_source_slash(self): self.testclass.trans = crest.Transition(source="/", target="A", guard=(lambda self: True)) instance = self.instance() transitions = crest.get_transitions(instance) self.assertEqual(len(transitions), 2) self.assertEqual(self._count_transitions_from_to(transitions, instance.B, instance.A), 1) self.assertEqual(self._count_transitions_from_to(transitions, instance.C, instance.A), 1)
def gen_Entity(obj, name="", parent=None, **kwargs): parentinfo = "" if parent is None else parent._name logger.debug( f"Adding entity '{name}' of type {type(obj)} with parent (id={id(parent)})" ) typename = obj.__class__.__name__ if isinstance( obj, CrestObject) else obj.__name__ node = { 'id': str(id(obj)), 'label': { 'label': f'{name} | {typename}', 'text': name }, 'children': [], 'ports': [], 'edges': [], 'cresttype': 'entity', 'width': 300, 'height': 200 } """ Inputs """ for name_, input_ in Model.get_inputs(obj, as_dict=True).items(): node["ports"].append(generate(input_, name_, obj, **kwargs)) """ Centre """ for name_, state in Model.get_states(obj, as_dict=True).items(): if not name_ == CURRENT_IDENTIFIER: node["children"].append(generate(state, name_, obj, **kwargs)) for name_, local in Model.get_locals(obj, as_dict=True).items(): node["children"].append(generate(local, name_, obj, **kwargs)) """ Outputs """ for name_, output in Model.get_outputs(obj, as_dict=True).items(): node["ports"].append(generate(output, name_, obj, **kwargs)) for name_, entity in Model.get_entities(obj, as_dict=True).items(): if name_ != PARENT_IDENTIFIER: node["children"].append(generate(entity, name_, obj, **kwargs)) for name_, trans in Model.get_transitions(obj, as_dict=True).items(): node["edges"].extend(generate(trans, name_, obj, **kwargs)) node["children"].append(generate_midpoint(trans, name_, obj, **kwargs)) for name_, influence in Model.get_influences(obj, as_dict=True).items(): node["edges"].extend(generate(influence, name_, obj, **kwargs)) for name_, update in Model.get_updates(obj, as_dict=True).items(): node["edges"].extend(generate(update, name_, obj, **kwargs)) pass for name_, action in Model.get_actions(obj, as_dict=True).items(): node["edges"].extend(generate(action, name_, obj, **kwargs)) return node
def select_transition_to_trigger(self, entity): """ This one operates randomly """ transitions_from_current_state = [ t for t in get_transitions(entity) if t.source is entity.current ] enabled_transitions = [ t for t in transitions_from_current_state if self._get_transition_guard_value(t) ] if len(enabled_transitions) >= 1: # by default, select one randomly return random.choice(enabled_transitions) else: return None
def calculate_entity_hook(self, entity): all_dts = [] logger.debug( f"Calculating behaviour change for entity {entity._name} ({entity.__class__.__name__})" ) for influence in get_influences(entity): if self.contains_if_condition(influence): inf_dts = self.get_condition_change_enablers(influence) if inf_dts is not None: all_dts.append(inf_dts) # updates = [up for up in get_updates(self.entity) if up.state == up._parent.current] for update in get_updates(entity): if update.state is update._parent.current: # only the currently active updates if self.contains_if_condition(update): up_dts = self.get_condition_change_enablers(update) if up_dts is not None: all_dts.append(up_dts) # TODO: check for transitions whether they can be done by time only for name, trans in get_transitions(entity, as_dict=True).items(): if entity.current is trans.source: trans_dts = self.get_transition_time(trans) if trans_dts is not None: all_dts.append(trans_dts) if logger.getEffectiveLevel() <= logging.DEBUG: if len(all_dts) == 0: logger.debug( "There were no change times in entity {entity._name} ({entity.__class__.__name__})." ) return [] min_dt_eps = min(all_dts, key=(lambda x: x[0])) # min_dt = get_minimum_dt_of_several(all_dts, self.timeunit) if min_dt_eps is not None: logger.debug( f"Minimum behaviour change time for entity {entity._name} ({entity.__class__.__name__}) is {min_dt_eps}" ) return [min_dt_eps] # return a list # min_dt = get_minimum_dt_of_several(all_dts, self.timeunit) # if min_dt is not None: # logger.debug(f"Minimum behaviour change time for entity {entity._name} ({entity.__class__.__name__}) is {min_dt}") # return [min_dt] # return a list else: # XXX This is the faster one that we run when we're not debugging !! return all_dts
def test_get_transitions_from_class_definition_single_transition_defs( self): class Test(crest.Entity): s1 = current = crest.State() s2 = crest.State() s3 = crest.State() @crest.transition(source=s1, target=s2) def t1(self): return True t2 = crest.Transition(source=s2, target=s3, guard=(lambda self: True)) t3 = crest.Transition(source=s3, target=s1, guard=(lambda self: False)) klass = Test transitions = crest.get_transitions(klass) self.assertCountEqual(transitions, [klass.t1, klass.t2, klass.t3])
def check_action_sanity(self): """Check that each action is properly named, has a transition and from the same entity and a target port that is in the "targets" of the entity. Also verifies the signature of the action function. """ for action in crest.get_all_actions(self.model): assert action._name is not None, f"There is an Action in {action._parent._name} ({action._parent.__class__.__name__}) whose name is 'None'" assert action._name != "", f"There is an Action in {action._parent._name} ({action._parent.__class__.__name__}) whose name is empty string" assert isinstance(action.transition, crest.Transition), f"Action {action._name}'s state is not a crest.Transition. It is: {action.transition} ({action.transition.__class__})" assert action.state in crest.get_transitions(action._parent), f"Action's transition {action.transition._name} ({action.transition}) is not in the transitions of entity {action._parent._name} ({action._parent})" assert isinstance(action.target, crest.Port), f"Action {action._name}'s target is not a crest.Port" assert action.target in api.get_targets(action._parent), f"Action's target {action.target._name} ({action.target}) is not in the targets of entity {action._parent._name} ({action._parent})" assert isinstance(action.function, (crestml.LearnedFunction, types.FunctionType)), f"Action {action._name}'s function needs to be of type types.FunctionType or crestdsl.ml.LearnedFunction" assert 'self' in inspect.signature(action.function).parameters, f"Action {action._name}'s function has no self parameter. entity: {action._parent._name} ({action._parent.__class__.__name__})" assert len(inspect.signature(action.function).parameters) == 1, f"An action should have only one one argument 'self'" for port in SH.get_read_ports_from_update(action.function, action): assert port in api.get_sources(action._parent), f"Action {action._name} seems to be reading a port {port._name} ({port}) which is not in the sources of its entity {action._parent._name} ({action._parent})"
def transition(self, entity): # logger.debug(f"transitions in entity {entity._name} ({entity.__class__.__name__})") transitions_from_current_state = [ t for t in model.get_transitions(entity) if t.source is entity.current ] enabled_transitions = [ t for t in transitions_from_current_state if self._get_transition_guard_value(t) ] state_before = SystemState(self.system).save() # backup the state states_after = [] for transition in enabled_transitions: state_before.apply() # reset to original state entity.current = transition.target # logger.info(f"Time: {self.global_time} | Firing transition <<{transition._name}>> in {entity._name} ({entity.__class__.__name__}) : {transition.source._name} -> {transition.target._name} | current global time: {self.global_time}") transition_updates = [ up for up in model.get_updates(transition._parent) if up.state is transition ] # FIXME: until we completely switched to only allowing actions... actions = [ a for a in model.get_actions(transition._parent) if a.transition is transition ] for act in actions + transition_updates: newval = self._get_action_function_value(act) if newval != act.target.value: act.target.value = newval state_after = SystemState(self.system).save() states_after.append((state_after, [transition])) # return the new states if there are any (this means that empty list means no transitions were fired) # logger.debug(f"finished transitions in entity {entity._name} ({entity.__class__.__name__}): Created {len(states_after)} new states!") return states_after
def _replacability_checks(entity, name, obj, existing): # TODO: clean me up ? These are lots of checks, maybe we need to make them look nice if isinstance(obj, crest.Entity): for update in crest.get_updates( entity ): # check if an update already writes to one of the entity's ports if update.target in crest.get_inputs(existing): raise AttributeError( f"Cannot reassign SubEntity '{name}' since one of its Input ports was used as target of Update '{get_name(update)}'." ) for influence in crest.get_influences( entity ): # check if an influence already reads from or writes to the entity's ports if influence.source in crest.get_outputs(existing): raise AttributeError( f"Cannot reassign SubEntity '{name}' since one of its Output ports was used as source of Influence '{get_name(influence)}'." ) if influence.target in crest.get_inputs(existing): raise AttributeError( f"Cannot reassign SubEntity '{name}' since one of its Input ports was used as target of Influence '{get_name(influence)}'." ) for action in crest.get_actions( entity ): # check if an action already writes to the entity's ports if action.target in crest.get_inputs(existing): raise AttributeError( f"Cannot reassign SubEntity '{name}' since one of its Input ports was used as target of Action '{get_name(action)}'." ) elif isinstance(obj, crest.Port): for update in crest.get_updates( entity ): # check if an update already writes to the port that we want to override if update.target == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} port '{name}' after it was used as target of Update '{get_name(update)}'." ) for influence in crest.get_influences( entity ): # check if an influence already reads from or writes to the port that we want to override if influence.source == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} port '{name}' after it was used as source of Influence '{get_name(influence)}'." ) if influence.target == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} port '{name}' after it was used as target of Influence '{get_name(influence)}'." ) for action in crest.get_actions( entity ): # check if an action already writes to the port that we want to override if action.target == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} port '{name}' after it was used as target of Action '{get_name(action)}'." ) elif isinstance(obj, crest.State): for update in crest.get_updates( entity ): # check if an update is already linked to the state that we want to override if update.state == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} '{name}' after it was used in Update '{get_name(update)}'." ) for transition in crest.get_transitions( entity ): # check if a transition already starts from or goes to the state that we want to override if transition.source == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} '{name}' after it was used as source of Transition '{get_name(transition)}'." ) if transition.target == existing: raise AttributeError( f"Cannot reassign {obj.__class__.__name__} '{name}' after it was used as target of Transition '{get_name(transition)}'." ) elif isinstance(obj, crest.Transition): # TODO: check here for any action uses the transition already # this is non-trivial however pass
def run_plan(self, execution_plan): """ Executes a plan. The execution plan is a heterogeneous list of the following items: - numeric values: (specify time advances) - pairs of (numeric, {entity: transition}-dict): advance a certain time and choose transition, everytime non-determinism is encountered in entity - {port: value} dicts (for setting of input ports) - {entity: transitions} dicts (for resolving conflicts) The list items will be iteratively consumed. If errors are observed they raise ValueError exceptions. If there is non-determinism, but no dict specifies how to resolve it, then we fall back to randomness. Here's a documented example of a plan: [ # advance 10 time units: 10, # set values: {entity.port : 33, entity.port2: -200}, # advance 20 time units and choose these transitions everytime there is a conflict in this period (20, {entity: entity.transition1, entity.subentity: entity.subentity.transition2} ), # advance 33 time units. # When you hit a conflict, check if the first element is an entity-state dict # if the entity is a key in the first element, then pop it and # use it to reolve the conflict (otherwise choose randomly) # then continue until anothoer conflict or end of advance 33, {entity: entity.transitionA}, {entity: entity.transitionB}, {entity: entity.transitionA}, # if you have two entities and you don't know which one will be conflicting first # (because they'll have conflicts at the same time) # you can put them both in a dict and duplicate the dict. # the first one will pop the first dict, the second one the second dict: 444, {entity.subentity1: entity.subentity2.transA, entity.subentity2: entity.subentity2.transB}, {entity.subentity1: entity.subentity2.transA, entity.subentity2: entity.subentity2.transB}, ] Parameters ---------- execution_plan: list The list of instructions that should be executed. Raises ------- ValueError In case there is something wrongly specified (e.g. take a transition that is not enabled, or so) """ # some setup self.execution_plan = execution_plan self._transition_selection = None while len(self.execution_plan) > 0: next_action = self.execution_plan.pop(0) logger.debug(f"Next action is {repr(next_action)}") if isinstance(next_action, numbers.Number) and not isinstance( next_action, bool): logger.info( f"Consuming command: advance {next_action} time units") self.advance(next_action) elif isinstance(next_action, tuple): assert isinstance( next_action[0], numbers.Number) and not isinstance( next_action, bool), "List entry have a numeric value first" assert isinstance( next_action[1], dict), "List entry must have a dict value second" # do some checks for entity, trans in next_action[1].items(): assert isinstance(entity, crest.Entity) assert isinstance(trans, crest.Transition) assert trans in crest.get_transitions(entity) self._transition_selection = next_action[1] self.advance(next_action[0]) self._transition_selection = None # reset it to None elif isinstance(next_action, dict): if not all([ isinstance(port, crest.Port) for port in next_action.keys() ]): raise ValueError( f"When consuming command I found a dict whose keys are not Port objects. Dict: \n{repr(next_action)}" ) as_strings = [ f"{api.get_name(port)}: {value}" for port, value in next_action.items() ] logger.info( f"Consuming command: setting port values { ', '.join(as_strings) }" ) self.set_values(next_action) else: raise ValueError( f"Don't know how to act for plan item:\n{repr(next_action)}." )