add examples

This commit is contained in:
Joeri Exelmans 2024-11-13 10:07:16 +01:00
parent 8504ba52f6
commit 42757ddc4f
35 changed files with 1104 additions and 609 deletions

58
examples/woods/common.py Normal file
View file

@ -0,0 +1,58 @@
# Helpers
def state_of(od, animal):
return od.get_source(od.get_incoming(animal, "of")[0])
def animal_of(od, state):
return od.get_target(od.get_outgoing(state, "of")[0])
def get_time(od):
_, clock = od.get_all_instances("Clock")[0]
return clock, od.get_slot_value(clock, "time")
# Render our run-time state to a string
def render_woods(od):
txt = ""
_, time = get_time(od)
txt += f"T = {time}.\n"
txt += "Bears:\n"
def render_attacking(animal_state):
attacking = od.get_outgoing(animal_state, "attacking")
if len(attacking) == 1:
whom_state = od.get_target(attacking[0])
whom_name = od.get_name(animal_of(od, whom_state))
return f" attacking {whom_name}"
else:
return ""
def render_dead(animal_state):
return 'dead' if od.get_slot_value(animal_state, 'dead') else 'alive'
for _, bear_state in od.get_all_instances("BearState"):
bear = animal_of(od, bear_state)
hunger = od.get_slot_value(bear_state, "hunger")
txt += f" 🐻 {od.get_name(bear)} (hunger: {hunger}, {render_dead(bear_state)}) {render_attacking(bear_state)}\n"
txt += "Men:\n"
for _, man_state in od.get_all_instances("ManState"):
man = animal_of(od, man_state)
attacked_by = od.get_incoming(man_state, "attacking")
if len(attacked_by) == 1:
whom_state = od.get_source(attacked_by[0])
whom_name = od.get_name(animal_of(od, whom_state))
being_attacked = f" being attacked by {whom_name}"
else:
being_attacked = ""
txt += f" 👨 {od.get_name(man)} ({render_dead(man_state)}) {render_attacking(man_state)}{being_attacked}\n"
return txt
# When should simulation stop?
def termination_condition(od):
_, time = get_time(od)
if time >= 10:
return "Took too long"
# End simulation when 2 animals are dead
who_is_dead = []
for _, animal_state in od.get_all_instances("AnimalState"):
if od.get_slot_value(animal_state, "dead"):
animal_name = od.get_name(animal_of(od, animal_state))
who_is_dead.append(animal_name)
if len(who_is_dead) >= 2:
return f"{' and '.join(who_is_dead)} are dead"

158
examples/woods/models.py Normal file
View file

@ -0,0 +1,158 @@
# Design meta-model
woods_mm_cs = """
Animal:Class {
abstract = True;
}
Bear:Class
:Inheritance (Bear -> Animal)
Man:Class {
lower_cardinality = 1;
upper_cardinality = 2;
constraint = `get_value(get_slot(this, "weight")) > 20`;
}
:Inheritance (Man -> Animal)
Man_weight:AttributeLink (Man -> Integer) {
name = "weight";
optional = False;
}
afraidOf:Association (Man -> Animal) {
source_upper_cardinality = 6;
target_lower_cardinality = 1;
}
"""
# Runtime meta-model
woods_rt_mm_cs = woods_mm_cs + """
AnimalState:Class {
abstract = True;
}
AnimalState_dead:AttributeLink (AnimalState -> Boolean) {
name = "dead";
optional = False;
}
of:Association (AnimalState -> Animal) {
source_lower_cardinality = 1;
source_upper_cardinality = 1;
target_lower_cardinality = 1;
target_upper_cardinality = 1;
}
BearState:Class {
constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Bear"`;
}
:Inheritance (BearState -> AnimalState)
BearState_hunger:AttributeLink (BearState -> Integer) {
name = "hunger";
optional = False;
constraint = ```
val = get_value(get_target(this))
val >= 0 and val <= 100
```;
}
ManState:Class {
constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Man"`;
}
:Inheritance (ManState -> AnimalState)
attacking:Association (AnimalState -> ManState) {
# Animal can only attack one Man at a time
target_upper_cardinality = 1;
# Man can only be attacked by one Animal at a time
source_upper_cardinality = 1;
constraint = ```
attacker = get_source(this)
if get_type_name(attacker) == "BearState":
# only BearState has 'hunger' attribute
hunger = get_value(get_slot(attacker, "hunger"))
else:
hunger = 100 # Man can always attack
attacker_dead = get_value(get_slot(attacker, "dead"))
attacked_state = get_target(this)
attacked_dead = get_value(get_slot(attacked_state, "dead"))
(
hunger >= 50
and not attacker_dead # cannot attack while dead
and not attacked_dead # cannot attack whoever is dead
)
```;
}
attacking_starttime:AttributeLink (attacking -> Integer) {
name = "starttime";
optional = False;
constraint = ```
val = get_value(get_target(this))
_, clock = get_all_instances("Clock")[0]
current_time = get_slot_value(clock, "time")
val >= 0 and val <= current_time
```;
}
# Just a clock singleton for keeping the time
Clock:Class {
lower_cardinality = 1;
upper_cardinality = 1;
}
Clock_time:AttributeLink (Clock -> Integer) {
name = "time";
optional = False;
constraint = `get_value(get_target(this)) >= 0`;
}
"""
# Our design model - the part that doesn't change
woods_m_cs = """
george:Man {
weight = 80;
}
bill:Man {
weight = 70;
}
teddy:Bear
mrBrown:Bear
# george is afraid of both bears
:afraidOf (george -> teddy)
:afraidOf (george -> mrBrown)
# the men are afraid of each other
:afraidOf (bill -> george)
:afraidOf (george -> bill)
"""
# Our runtime model - the part that changes with every execution step
woods_rt_initial_m_cs = woods_m_cs + """
georgeState:ManState {
dead = False;
}
:of (georgeState -> george)
billState:ManState {
dead = False;
}
:of (billState -> bill)
teddyState:BearState {
dead = False;
hunger = 40;
}
:of (teddyState -> teddy)
mrBrownState:BearState {
dead = False;
hunger = 80;
}
:of (mrBrownState -> mrBrown)
clock:Clock {
time = 0;
}
"""

View file

@ -0,0 +1,75 @@
### Operational Semantics - coded in Python ###
import functools
from examples.semantics.operational.simulator import make_actions_pure, filter_valid_actions
from examples.woods.common import *
# Action: Time advances, whoever is being attacked dies, bears become hungrier
def action_advance_time(od):
msgs = []
clock, old_time = get_time(od)
new_time = old_time + 1
od.set_slot_value(clock, "time", new_time)
for _, attacking_link in od.get_all_instances("attacking"):
man_state = od.get_target(attacking_link)
animal_state = od.get_source(attacking_link)
if od.get_type_name(animal_state) == "BearState":
od.set_slot_value(animal_state, "hunger", max(od.get_slot_value(animal_state, "hunger") - 50, 0))
od.set_slot_value(man_state, "dead", True)
od.delete(attacking_link)
msgs.append(f"{od.get_name(animal_of(od, animal_state))} kills {od.get_name(animal_of(od, man_state))}.")
for _, bear_state in od.get_all_instances("BearState"):
if od.get_slot_value(bear_state, "dead"):
continue # bear already dead
old_hunger = od.get_slot_value(bear_state, "hunger")
new_hunger = min(old_hunger + 10, 100)
od.set_slot_value(bear_state, "hunger", new_hunger)
bear = od.get_target(od.get_outgoing(bear_state, "of")[0])
bear_name = od.get_name(bear)
if new_hunger == 100:
od.set_slot_value(bear_state, "dead", True)
msgs.append(f"Bear {bear_name} dies of hunger.")
else:
msgs.append(f"Bear {bear_name}'s hunger level is now {new_hunger}.")
return msgs
# Action: Animal attacks Man
# Note: We must use the names of the objects as parameters, because when cloning, the IDs of objects change!
def action_attack(od, animal_name: str, man_name: str):
msgs = []
animal = od.get(animal_name)
man = od.get(man_name)
animal_state = state_of(od, animal)
man_state = state_of(od, man)
attack_link = od.create_link(None, # auto-generate link name
"attacking", animal_state, man_state)
_, clock = od.get_all_instances("Clock")[0]
current_time = od.get_slot_value(clock, "time")
od.set_slot_value(attack_link, "starttime", current_time)
msgs.append(f"{animal_name} is now attacking {man_name}")
return msgs
# Get all actions that can be performed (including those that bring us to a non-conforming state)
def get_all_actions(od):
def _generate_actions(od):
# can always advance time:
yield ("advance time", action_advance_time)
# if A is afraid of B, then B can attack A:
for _, afraid_link in od.get_all_instances("afraidOf"):
man = od.get_source(afraid_link)
animal = od.get_target(afraid_link)
animal_name = od.get_name(animal)
man_name = od.get_name(man)
man_state = state_of(od, man)
animal_state = state_of(od, animal)
descr = f"{animal_name} ({od.get_type_name(animal)}) attacks {man_name} ({od.get_type_name(man)})"
yield (descr, functools.partial(action_attack, animal_name=animal_name, man_name=man_name))
return make_actions_pure(_generate_actions(od), od)
# Only get those actions that bring us to a conforming state
def get_valid_actions(od):
return filter_valid_actions(get_all_actions(od))

View file

@ -0,0 +1,25 @@
### Operational Semantics - defined by rule-based model transformation ###
from transformation.rule import Rule, RuleMatcherRewriter, PriorityActionGenerator
from transformation.ramify import ramify
from util import loader
import os
THIS_DIR = os.path.dirname(__file__)
get_filename = lambda rule_name, kind: f"{THIS_DIR}/rules/r_{rule_name}_{kind}.od"
def get_action_generator(state, rt_mm):
rt_mm_ramified = ramify(state, rt_mm)
matcher_rewriter = RuleMatcherRewriter(state, rt_mm, rt_mm_ramified)
rules0_dict = loader.load_rules(state, get_filename, rt_mm_ramified, ["hungry_bear_dies"])
rules1_dict = loader.load_rules(state, get_filename, rt_mm_ramified, ["advance_time", "attack"])
generator = PriorityActionGenerator(matcher_rewriter, [
rules0_dict, # highest priority
rules1_dict, # lowest priority
])
return generator

View file

@ -0,0 +1,4 @@
clock:RAM_Clock {
RAM_time = `True`;
}

View file

@ -0,0 +1,27 @@
clock:RAM_Clock {
RAM_time = `get_value(this) + 1`;
}
# Advance time has a bunch of side-effects that we cannot easily model using NAC/LHS/RHS-kind of rules,
# so we just do it in code:
:GlobalCondition {
condition = ```
for _, attacking_link in get_all_instances("attacking"):
man_state = get_target(attacking_link)
animal_state = get_source(attacking_link)
if get_type_name(animal_state) == "BearState":
# Bear hunger decreases
set_slot_value(animal_state, "hunger", max(get_slot_value(animal_state, "hunger") - 50, 0))
set_slot_value(man_state, "dead", True)
delete(attacking_link)
# Bear hunger increases
for _, bear_state in get_all_instances("BearState"):
if get_slot_value(bear_state, "dead"):
continue # bear already dead
old_hunger = get_slot_value(bear_state, "hunger")
new_hunger = min(old_hunger + 10, 100)
set_slot_value(bear_state, "hunger", new_hunger)
```;
}

View file

@ -0,0 +1,18 @@
# Some man is afraid of some animal:
man:RAM_Man
animal:RAM_Animal
manAfraidOfAnimal:RAM_afraidOf (man -> animal)
# Both man and animal have an associated state:
manState:RAM_ManState
man2State:RAM_of (manState -> man)
animalState:RAM_AnimalState
animal2State:RAM_of (animalState -> animal)

View file

@ -0,0 +1,7 @@
# Cannot attack if already attacking
manState:RAM_ManState
animalState:RAM_AnimalState
:RAM_attacking(animalState -> manState)

View file

@ -0,0 +1,7 @@
# Bear won't attack unless hungry
animalState:RAM_AnimalState {
condition = ```
get_type_name(this) == "BearState" and get_slot_value(this, "hunger") < 50
```;
}

View file

@ -0,0 +1,5 @@
# If dead, cannot be attacked
manState:RAM_ManState {
RAM_dead = `get_value(this)`;
}

View file

@ -0,0 +1,5 @@
# If dead, cannot attack
animalState:RAM_AnimalState {
RAM_dead = `get_value(this)`;
}

View file

@ -0,0 +1,7 @@
# Not already attacking someone else:
animalState:RAM_AnimalState
other:RAM_ManState
:RAM_attacking(animalState -> other)

View file

@ -0,0 +1,7 @@
# Not already being attacked by someone else:
manState:RAM_ManState
other:RAM_AnimalState
:RAM_attacking(other -> manState)

View file

@ -0,0 +1,28 @@
# Our entire LHS (don't delete anything)
# Some man is afraid of some animal:
man:RAM_Man
animal:RAM_Animal
manAfraidOfAnimal:RAM_afraidOf (man -> animal)
# Both man and animal have an associated state:
manState:RAM_ManState
man2State:RAM_of (manState -> man)
animalState:RAM_AnimalState
animal2State:RAM_of (animalState -> animal)
# Animal attacks man:
:RAM_attacking(animalState -> manState) {
RAM_starttime = `get_slot_value(get_all_instances("Clock")[0][1], "time")`;
}

View file

@ -0,0 +1,8 @@
bearState:RAM_BearState {
RAM_hunger = ```
get_value(this) == 100
```;
RAM_dead = ```
not get_value(this)
```;
}

View file

@ -0,0 +1,4 @@
bearState:RAM_BearState {
RAM_hunger = `get_value(this)`; # unchanged
RAM_dead = `True`;
}

View file

@ -0,0 +1,42 @@
from state.devstate import DevState
from bootstrap.scd import bootstrap_scd
from framework.conformance import Conformance, render_conformance_check_result
from concrete_syntax.textual_od import parser, renderer
from concrete_syntax.plantuml import renderer as plantuml
from api.od import ODAPI
from examples.semantics.operational.simulator import Simulator, RandomDecisionMaker, InteractiveDecisionMaker
from examples.woods import models, opsem_python, opsem_rulebased
from examples.woods.common import termination_condition, render_woods
from util import loader
state = DevState()
scd_mmm = bootstrap_scd(state) # Load meta-meta-model
### Load (meta-)models ###
woods_mm = loader.parse_and_check(state, models.woods_mm_cs, scd_mmm, "MM")
woods_rt_mm = loader.parse_and_check(state, models.woods_rt_mm_cs, scd_mmm, "RT-MM")
woods_m = loader.parse_and_check(state, models.woods_m_cs, woods_mm, "M")
woods_rt_m = loader.parse_and_check(state, models.woods_rt_initial_m_cs, woods_rt_mm, "RT-M")
print()
rulebased_action_generator = opsem_rulebased.get_action_generator(state, woods_rt_mm)
sim = Simulator(
# action_generator=opsem_python.get_valid_actions,
# action_generator=opsem_python.get_all_actions,
action_generator=rulebased_action_generator,
# decision_maker=RandomDecisionMaker(seed=3),
decision_maker=InteractiveDecisionMaker(),
termination_condition=termination_condition,
check_conformance=True,
verbose=True,
renderer=render_woods,
)
od = ODAPI(state, woods_rt_m, woods_rt_mm)
sim.run(od)