woods_pysem.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import functools
  2. import random
  3. import math
  4. from state.devstate import DevState
  5. from bootstrap.scd import bootstrap_scd
  6. from framework.conformance import Conformance, render_conformance_check_result
  7. from concrete_syntax.textual_od import parser, renderer
  8. from concrete_syntax.common import indent
  9. from concrete_syntax.plantuml import renderer as plantuml
  10. from util import prompt
  11. from transformation.cloner import clone_od
  12. from api.od import ODAPI
  13. from examples.semantics.operational.simulator import Simulator, RandomDecisionMaker, InteractiveDecisionMaker, filter_valid_actions
  14. state = DevState()
  15. # Load meta-meta-model
  16. scd_mmm = bootstrap_scd(state)
  17. # Design meta-model
  18. woods_mm_cs = """
  19. Animal:Class {
  20. abstract = True;
  21. }
  22. Bear:Class
  23. :Inheritance (Bear -> Animal)
  24. Man:Class {
  25. lower_cardinality = 1;
  26. upper_cardinality = 2;
  27. constraint = `get_value(get_slot(this, "weight")) > 20`;
  28. }
  29. :Inheritance (Man -> Animal)
  30. Man_weight:AttributeLink (Man -> Integer) {
  31. name = "weight";
  32. optional = False;
  33. }
  34. afraidOf:Association (Man -> Animal) {
  35. source_upper_cardinality = 6;
  36. target_lower_cardinality = 1;
  37. }
  38. """
  39. woods_mm = parser.parse_od(
  40. state,
  41. m_text=woods_mm_cs,
  42. mm=scd_mmm)
  43. conf = Conformance(state, woods_mm, scd_mmm)
  44. print("MM ...", render_conformance_check_result(conf.check_nominal()))
  45. # Runtime meta-model
  46. woods_rt_mm_cs = woods_mm_cs + """
  47. AnimalState:Class {
  48. abstract = True;
  49. }
  50. AnimalState_dead:AttributeLink (AnimalState -> Boolean) {
  51. name = "dead";
  52. optional = False;
  53. }
  54. of:Association (AnimalState -> Animal) {
  55. source_lower_cardinality = 1;
  56. source_upper_cardinality = 1;
  57. target_lower_cardinality = 1;
  58. target_upper_cardinality = 1;
  59. }
  60. BearState:Class {
  61. constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Bear"`;
  62. }
  63. :Inheritance (BearState -> AnimalState)
  64. BearState_hunger:AttributeLink (BearState -> Integer) {
  65. name = "hunger";
  66. optional = False;
  67. constraint = ```
  68. val = get_value(get_target(this))
  69. val >= 0 and val <= 100
  70. ```;
  71. }
  72. ManState:Class {
  73. constraint = `get_type_name(get_target(get_outgoing(this, "of")[0])) == "Man"`;
  74. }
  75. :Inheritance (ManState -> AnimalState)
  76. attacking:Association (AnimalState -> ManState) {
  77. # Animal can only attack one Man at a time
  78. target_upper_cardinality = 1;
  79. # Man can only be attacked by one Animal at a time
  80. source_upper_cardinality = 1;
  81. constraint = ```
  82. attacker = get_source(this)
  83. if get_type_name(attacker) == "BearState":
  84. # only BearState has 'hunger' attribute
  85. hunger = get_value(get_slot(attacker, "hunger"))
  86. else:
  87. hunger = 100 # Man can always attack
  88. attacker_dead = get_value(get_slot(attacker, "dead"))
  89. attacked_state = get_target(this)
  90. attacked_dead = get_value(get_slot(attacked_state, "dead"))
  91. (
  92. hunger >= 50
  93. and not attacker_dead # cannot attack while dead
  94. and not attacked_dead # cannot attack whoever is dead
  95. )
  96. ```;
  97. }
  98. attacking_starttime:AttributeLink (attacking -> Integer) {
  99. name = "starttime";
  100. optional = False;
  101. constraint = ```
  102. val = get_value(get_target(this))
  103. _, clock = get_all_instances("Clock")[0]
  104. current_time = get_slot_value(clock, "time")
  105. val >= 0 and val <= current_time
  106. ```;
  107. }
  108. # Just a clock singleton for keeping the time
  109. Clock:Class {
  110. lower_cardinality = 1;
  111. upper_cardinality = 1;
  112. }
  113. Clock_time:AttributeLink (Clock -> Integer) {
  114. name = "time";
  115. optional = False;
  116. constraint = `get_value(get_target(this)) >= 0`;
  117. }
  118. """
  119. woods_rt_mm = parser.parse_od(
  120. state,
  121. m_text=woods_rt_mm_cs,
  122. mm=scd_mmm)
  123. conf = Conformance(state, woods_rt_mm, scd_mmm)
  124. print("RT-MM ...", render_conformance_check_result(conf.check_nominal()))
  125. # print("--------------")
  126. # print(indent(
  127. # renderer.render_od(state,
  128. # m_id=woods_rt_mm,
  129. # mm_id=scd_mmm),
  130. # 4))
  131. # print("--------------")
  132. # Our design model - the part that doesn't change
  133. woods_m_cs = """
  134. george:Man {
  135. weight = 80;
  136. }
  137. bill:Man {
  138. weight = 70;
  139. }
  140. teddy:Bear
  141. mrBrown:Bear
  142. # george is afraid of both bears
  143. :afraidOf (george -> teddy)
  144. :afraidOf (george -> mrBrown)
  145. # the men are afraid of each other
  146. :afraidOf (bill -> george)
  147. :afraidOf (george -> bill)
  148. """
  149. woods_m = parser.parse_od(
  150. state,
  151. m_text=woods_m_cs,
  152. mm=woods_mm)
  153. conf = Conformance(state, woods_m, woods_mm)
  154. print("M ...", render_conformance_check_result(conf.check_nominal()))
  155. # Our runtime model - the part that changes with every execution step
  156. woods_rt_initial_m_cs = woods_m_cs + """
  157. georgeState:ManState {
  158. dead = False;
  159. }
  160. :of (georgeState -> george)
  161. billState:ManState {
  162. dead = False;
  163. }
  164. :of (billState -> bill)
  165. teddyState:BearState {
  166. dead = False;
  167. hunger = 40;
  168. }
  169. :of (teddyState -> teddy)
  170. mrBrownState:BearState {
  171. dead = False;
  172. hunger = 80;
  173. }
  174. :of (mrBrownState -> mrBrown)
  175. clock:Clock {
  176. time = 0;
  177. }
  178. """
  179. woods_rt_m = parser.parse_od(
  180. state,
  181. m_text=woods_rt_initial_m_cs,
  182. mm=woods_rt_mm)
  183. conf = Conformance(state, woods_rt_m, woods_rt_mm)
  184. print("RT-M ...", render_conformance_check_result(conf.check_nominal()))
  185. # Helpers
  186. def state_of(od, animal):
  187. return od.get_source(od.get_incoming(animal, "of")[0])
  188. def animal_of(od, state):
  189. return od.get_target(od.get_outgoing(state, "of")[0])
  190. def get_time(od):
  191. _, clock = od.get_all_instances("Clock")[0]
  192. return clock, od.get_slot_value(clock, "time")
  193. def advance_time(od):
  194. msgs = []
  195. clock, old_time = get_time(od)
  196. new_time = old_time + 1
  197. od.set_slot_value(clock, "time", new_time)
  198. for _, attacking_link in od.get_all_instances("attacking"):
  199. man_state = od.get_target(attacking_link)
  200. animal_state = od.get_source(attacking_link)
  201. if od.get_type_name(animal_state) == "BearState":
  202. od.set_slot_value(animal_state, "hunger", max(od.get_slot_value(animal_state, "hunger") - 50, 0))
  203. od.set_slot_value(man_state, "dead", True)
  204. od.delete(attacking_link)
  205. msgs.append(f"{od.get_name(animal_of(od, animal_state))} kills {od.get_name(animal_of(od, man_state))}.")
  206. for _, bear_state in od.get_all_instances("BearState"):
  207. if od.get_slot_value(bear_state, "dead"):
  208. continue # bear already dead
  209. old_hunger = od.get_slot_value(bear_state, "hunger")
  210. new_hunger = min(old_hunger + 10, 100)
  211. od.set_slot_value(bear_state, "hunger", new_hunger)
  212. bear = od.get_target(od.get_outgoing(bear_state, "of")[0])
  213. bear_name = od.get_name(bear)
  214. if new_hunger == 100:
  215. od.set_slot_value(bear_state, "dead", True)
  216. msgs.append(f"Bear {bear_name} dies of hunger.")
  217. else:
  218. msgs.append(f"Bear {bear_name}'s hunger level is now {new_hunger}.")
  219. return msgs
  220. # we must use the names of the objects as parameters, because when cloning, the IDs of objects change!
  221. def attack(od, animal_name: str, man_name: str):
  222. msgs = []
  223. animal = od.get(animal_name)
  224. man = od.get(man_name)
  225. animal_state = state_of(od, animal)
  226. man_state = state_of(od, man)
  227. attack_link = od.create_link(None, # auto-generate link name
  228. "attacking", animal_state, man_state)
  229. _, clock = od.get_all_instances("Clock")[0]
  230. current_time = od.get_slot_value(clock, "time")
  231. od.set_slot_value(attack_link, "starttime", current_time)
  232. msgs.append(f"{animal_name} is now attacking {man_name}")
  233. return msgs
  234. def get_all_actions(od):
  235. def _get_actions(od):
  236. # can always advance time:
  237. yield ("advance time", advance_time)
  238. # who can attack whom?
  239. for _, afraid_link in od.get_all_instances("afraidOf"):
  240. man = od.get_source(afraid_link)
  241. animal = od.get_target(afraid_link)
  242. animal_name = od.get_name(animal)
  243. man_name = od.get_name(man)
  244. man_state = state_of(od, man)
  245. animal_state = state_of(od, animal)
  246. descr = f"{animal_name} ({od.get_type_name(animal)}) attacks {man_name} ({od.get_type_name(man)})"
  247. yield (descr, functools.partial(attack, animal_name=animal_name, man_name=man_name))
  248. # Copy model before modifying it
  249. def exec_pure(action, od):
  250. cloned_rt_m = clone_od(state, od.m, od.mm)
  251. new_od = ODAPI(state, cloned_rt_m, od.mm)
  252. msgs = action(new_od)
  253. return (new_od, msgs)
  254. for descr, action in _get_actions(od):
  255. yield (descr, functools.partial(exec_pure, action, od))
  256. def get_valid_actions(od):
  257. return filter_valid_actions(get_all_actions(od))
  258. def render_woods(od):
  259. txt = ""
  260. _, time = get_time(od)
  261. txt += f"T = {time}.\n"
  262. txt += "Bears:\n"
  263. def render_attacking(animal_state):
  264. attacking = od.get_outgoing(animal_state, "attacking")
  265. if len(attacking) == 1:
  266. whom_state = od.get_target(attacking[0])
  267. whom_name = od.get_name(animal_of(od, whom_state))
  268. return f" attacking {whom_name}"
  269. else:
  270. return ""
  271. def render_dead(animal_state):
  272. return 'dead' if od.get_slot_value(animal_state, 'dead') else 'alive'
  273. for _, bear_state in od.get_all_instances("BearState"):
  274. bear = animal_of(od, bear_state)
  275. hunger = od.get_slot_value(bear_state, "hunger")
  276. txt += f" 🐻 {od.get_name(bear)} (hunger: {hunger}, {render_dead(bear_state)}) {render_attacking(bear_state)}\n"
  277. txt += "Men:\n"
  278. for _, man_state in od.get_all_instances("ManState"):
  279. man = animal_of(od, man_state)
  280. attacked_by = od.get_incoming(man_state, "attacking")
  281. if len(attacked_by) == 1:
  282. whom_state = od.get_source(attacked_by[0])
  283. whom_name = od.get_name(animal_of(od, whom_state))
  284. being_attacked = f" being attacked by {whom_name}"
  285. else:
  286. being_attacked = ""
  287. txt += f" 👨 {od.get_name(man)} ({render_dead(man_state)}) {render_attacking(man_state)}{being_attacked}\n"
  288. return txt
  289. def termination_condition(od):
  290. _, time = get_time(od)
  291. if time >= 10:
  292. return "Took too long"
  293. # End simulation when 2 animals are dead
  294. who_is_dead = []
  295. for _, animal_state in od.get_all_instances("AnimalState"):
  296. if od.get_slot_value(animal_state, "dead"):
  297. animal_name = od.get_name(animal_of(od, animal_state))
  298. who_is_dead.append(animal_name)
  299. if len(who_is_dead) >= 2:
  300. return f"{' and '.join(who_is_dead)} are dead"
  301. sim = Simulator(
  302. action_generator=get_valid_actions,
  303. # action_generator=get_actions,
  304. decision_maker=RandomDecisionMaker(seed=0),
  305. # decision_maker=InteractiveDecisionMaker(),
  306. termination_condition=termination_condition,
  307. check_conformance=True,
  308. verbose=True,
  309. renderer=render_woods,
  310. )
  311. od = ODAPI(state, woods_rt_m, woods_rt_mm)
  312. print()
  313. sim.run(od)