Refactor 'port' demo to include starting point for assignment

This commit is contained in:
Joeri Exelmans 2024-10-30 01:02:11 +01:00
parent 72c78c664f
commit 86610139d2
7 changed files with 300 additions and 16 deletions

View file

@ -0,0 +1,144 @@
import functools
from concrete_syntax.common import indent
from examples.semantics.operational.port.helpers import design_to_state, state_to_design, get_time
from examples.semantics.operational.simulator import make_actions_pure, filter_valid_actions
def precondition_can_move_from(od, from_state):
# TO IMPLEMENT
# Function should return True if a ship can move out of 'from_state'
return False
def precondition_can_move_to(od, to_state):
# TO IMPLEMENT
# Function should return True if a ship can move into 'to_state'
return False
def precondition_all_successors_moved(od, conn):
# TO IMPLEMENT
# A move (or skip) can only be made along a connection after all subsequent connections have already made their move (or were skipped).
return True
def precondition_workers_available(od, workerset):
# TO IMPLEMENT
# A worker in a WorkerSet can only be allocated to a berth, if the number of 'isOperating'-links is smaller than the number of workers in the WorkerSet.
return True
def precondition_berth_unserved(od, berth):
# TO IMPLEMENT
# A worker can only be allocated to a berth, if the berth contains an 'unserved' ship.
return True
def action_skip(od, conn_name):
# SERVES AS AN EXAMPLE - NO NEED TO EDIT THIS FUNCTION
conn = od.get(conn_name)
conn_state = design_to_state(od, conn)
od.set_slot_value(conn_state, "moved", True)
return [f"skip {conn_name}"]
def action_move(od, conn_name):
action_skip(od, conn_name) # flag the connection as 'moved'
conn = od.get(conn_name)
from_place = od.get_source(conn)
to_place = od.get_target(conn)
from_state = design_to_state(od, from_place) # beware: Generator does not have State
to_state = design_to_state(od, to_place)
# TO IMPLEMENT:
# - move a ship along the connection
return [f"unimplemented! nothing changed!"]
def action_serve_berth(od, workerset_name, berth_name):
# TO IMPLEMENT:
# - A worker starts operating a berth
return [f"unimplemented! nothing changed!"]
def action_advance_time(od):
_, clock = od.get_all_instances("Clock")[0]
time = od.get_slot_value(clock, "time")
new_time = time + 1
od.set_slot_value(clock, "time", new_time)
# TO IMPLEMENT:
# - all 'moved'-attributes need to be reset (to False)
# - if there is a worker operating a Berth, then:
# (1) the Berth's status becomes 'served'
# (2) the worker is no longer operating the Berth
return [f"time is now {new_time}"]
# This function is called to discover the possible steps that can be made.
# It should not be necessary to edit this function
def get_actions(od):
actions = {}
# Add move-actions (or skip-actions)
for conn_name, conn in od.get_all_instances("connection"):
already_moved = od.get_slot_value(design_to_state(od, conn), "moved")
if already_moved or not precondition_all_successors_moved(od, conn):
# a move was already made along this connection in the current time-step
continue
from_place = od.get_source(conn)
to_place = od.get_target(conn)
from_name = od.get_name(from_place)
to_name = od.get_name(to_place)
from_state = design_to_state(od, from_place)
to_state = design_to_state(od, to_place)
if (precondition_can_move_from(od, from_state)
and precondition_can_move_to(od, to_state)):
actions[f"move {conn_name} ({from_name} -> {to_name})"] = functools.partial(action_move, conn_name=conn_name)
else:
actions[f"skip {from_name} -> {to_name}"] = functools.partial(action_skip, conn_name=conn_name)
# Add actions to assign workers
for _, workerset in od.get_all_instances("WorkerSet"):
if not precondition_workers_available(od, workerset):
continue
for lnk in od.get_outgoing(workerset, "canOperate"):
berth = od.get_target(lnk)
if precondition_berth_unserved(od, berth):
berth_name = od.get_name(berth)
workerset_name = od.get_name(workerset)
actions[f"{workerset_name} operates {berth_name}"] = functools.partial(action_serve_berth, workerset_name=workerset_name, berth_name=berth_name)
# Only when no other action can be performed, can time advance
if len(actions) == 0:
actions["advance time"] = action_advance_time
# This wrapper turns our actions into pure functions: they will clone the model before modifying it. This is useful if we ever want to rollback an action.
return make_actions_pure(actions.items(), od)
# Called every time the runtime state changes.
# When this function returns a string, the simulation ends.
# The string should represent the reason for ending the simulation.
# When this function returns None, the simulation continues.
def termination_condition(od):
# TO IMPLEMENT: terminate simulation when the place 'served' contains 2 ships.
if len(od.get_all_instances("Place")) > 5:
return "More places than I can count :("

View file

@ -0,0 +1,18 @@
# Some helper functions
def get_num_ships(od, place):
place_state = design_to_state(od, place)
return od.get_slot_value(place_state, "numShips")
def design_to_state(od, design):
incoming = od.get_incoming(design, "of")
if len(incoming) == 1:
# not all design-objects have a state
return od.get_source(incoming[0])
def state_to_design(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")

View file

@ -0,0 +1,293 @@
# Design meta-model
port_mm_cs = """
Source:Class {
abstract = True;
}
Sink:Class {
abstract = True;
}
Place:Class
:Inheritance (Place -> Source)
:Inheritance (Place -> Sink)
connection:Association (Source -> Sink)
CapacityConstraint:Class
CapacityConstraint_shipCapacity:AttributeLink (CapacityConstraint -> Integer) {
name = "shipCapacity";
optional = False;
# cannot have negative capacity:
constraint = `get_value(get_target(this)) >= 0`; # non-negative
}
# Capacity
capacityOf:Association (CapacityConstraint -> Place) {
# must say something about at least one Place, otherwise what is the point of the constraint?
target_lower_cardinality = 1;
}
Berth:Class
:Inheritance (Berth -> Place)
# Set of workers
WorkerSet:Class
WorkerSet_numWorkers:AttributeLink (WorkerSet -> Integer) {
name = "numWorkers";
optional = False;
constraint = `get_value(get_target(this)) >= 0`; # non-negative
}
canOperate:Association (WorkerSet -> Berth) {
target_lower_cardinality = 1;
}
Generator:Class
:Inheritance (Generator -> Source)
# Those classes to which we want to attach a runtime state object
Stateful:Class {
abstract = True;
}
:Inheritance (Place -> Stateful)
:Inheritance (WorkerSet -> Stateful)
:Inheritance (Berth -> Stateful)
:Inheritance (connection -> Stateful)
""";
# Runtime meta-model
port_rt_mm_cs = port_mm_cs + """
State:Class
of:Association (State -> Stateful) {
source_lower_cardinality = 1;
source_upper_cardinality = 1;
target_lower_cardinality = 1;
target_upper_cardinality = 1;
}
PlaceState:Class
:Inheritance (PlaceState -> State)
PlaceState_numShips:AttributeLink (PlaceState -> Integer) {
# number of ships currently in the place
name = "numShips";
optional = False;
constraint = `get_value(get_target(this)) >= 0`; # non-negative
}
shipCapacities:GlobalConstraint {
constraint = ```
errors = []
for _, constr in get_all_instances("CapacityConstraint"):
cap = get_slot_value(constr, "shipCapacity")
total = 0
place_names = [] # for debugging
for lnk in get_outgoing(constr, "capacityOf"):
place = get_target(lnk)
place_names.append(get_name(place))
place_state = get_source(get_incoming(place, "of")[0])
total += get_slot_value(place_state, "numShips")
if total > cap:
errors.append(f"The number of ships in places {','.join(place_names)} ({total}) exceeds the capacity ({cap}) of CapacityConstraint {get_name(constr)}.")
errors
```;
}
BerthState:Class {
# status == empty <=> numShips == 0
constraint = `(get_slot_value(this, "numShips") == 0) == (get_slot_value(this, "status") == "empty")`;
}
:Inheritance (BerthState -> PlaceState)
BerthState_status:AttributeLink (BerthState -> String) {
name = "status";
optional = False;
constraint = `(
get_value(get_target(this)) in { "empty", "unserved", "served" }
)`;
}
WorkerSetState:Class
:Inheritance (WorkerSetState -> State)
isOperating:Association (WorkerSetState -> Berth) {
constraint = ```
errors = []
# get status of Berth
berth = get_target(this)
berth_state = get_source(get_incoming(berth, "of")[0])
status = get_slot_value(berth_state, "status")
if status != "unserved":
errors.append(f"Cannot operate {get_name(berth)} because there is no unserved ship there.")
# only operate Berts that we can operate
workerset = get_target(get_outgoing(get_source(this), "of")[0])
can_operate = [get_target(lnk) for lnk in get_outgoing(workerset, "canOperate")]
if berth not in can_operate:
errors.append(f"Cannot operate {get_name(berth)}.")
errors
```;
}
operatingCapacities:GlobalConstraint {
constraint = ```
errors = []
for _, workersetstate in get_all_instances("WorkerSetState"):
workerset = get_target(get_outgoing(workersetstate, "of")[0])
num_operating = len(get_outgoing(workersetstate, "isOperating"))
num_workers = get_slot_value(workerset, "numWorkers")
if num_operating > num_workers:
errors.append(f"WorkerSet {get_name(workerset)} is operating more berths ({num_operating}) than there are workers ({num_workers})")
errors
```;
}
ConnectionState:Class
:Inheritance (ConnectionState -> State)
ConnectionState_moved:AttributeLink (ConnectionState -> Boolean) {
name = "moved";
optional = False;
constraint = ```
result = True
all_successors_moved = True
moved = get_value(get_target(this))
conn_state = get_source(this)
conn = get_target(get_outgoing(conn_state, "of")[0])
tgt_place = get_target(conn)
next_conns = get_outgoing(tgt_place, "connection")
for next_conn in next_conns:
next_conn_state = get_source(get_incoming(next_conn, "of")[0])
if not get_slot_value(next_conn_state, "moved"):
all_successors_moved = False
if moved and not all_successors_moved:
result = f"Connection {get_name(conn)} played before its turn."
result
```;
}
Clock:Class {
lower_cardinality = 1;
upper_cardinality = 1;
}
Clock_time:AttributeLink (Clock -> Integer) {
name = "time";
optional = False;
constraint = `get_value(get_target(this)) >= 0`;
}
"""
# Design model: the part that doesn't change
port_m_cs = """
gen:Generator
# newly arrive ships collect here
waiting:Place
c1:connection (gen -> waiting)
inboundPassage:Place
c2:connection (waiting -> inboundPassage)
outboundPassage:Place
# inboundPassage and outboundPassage cannot have more than 3 ships total
passageCap:CapacityConstraint {
shipCapacity = 3;
}
:capacityOf (passageCap -> inboundPassage)
:capacityOf (passageCap -> outboundPassage)
# Berth 1
inboundBerth1:Place
berth1:Berth
outboundBerth1:Place
inboundBerth1Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (inboundBerth1Cap -> inboundBerth1)
outboundBerth1Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (outboundBerth1Cap -> outboundBerth1)
berth1Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (berth1Cap -> berth1)
c3:connection (inboundBerth1 -> berth1)
c4:connection (berth1 -> outboundBerth1)
# Berth 2
inboundBerth2:Place
berth2:Berth
outboundBerth2:Place
inboundBerth2Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (inboundBerth2Cap -> inboundBerth2)
outboundBerth2Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (outboundBerth2Cap -> outboundBerth2)
berth2Cap:CapacityConstraint { shipCapacity = 1; }
:capacityOf (berth2Cap -> berth2)
c5:connection (inboundBerth2 -> berth2)
c6:connection (berth2 -> outboundBerth2)
# can either go to Berth 1 or Berth 2
c7:connection (inboundPassage -> inboundBerth1)
c8:connection (inboundPassage -> inboundBerth2)
c9:connection (outboundBerth1 -> outboundPassage)
c10:connection (outboundBerth2 -> outboundPassage)
# ships that have been served are counted here
served:Place
c11:connection (outboundPassage -> served)
workers:WorkerSet {
numWorkers = 1;
}
:canOperate (workers -> berth1)
:canOperate (workers -> berth2)
"""
# Initial runtime model: the part that changes (every execution step)
port_rt_m_cs = port_m_cs + """
clock:Clock {
time = 0;
}
waitingState:PlaceState { numShips = 2; } :of (waitingState -> waiting)
inboundPassageState:PlaceState { numShips = 0; } :of (inboundPassageState -> inboundPassage)
outboundPassageState:PlaceState { numShips = 0; } :of (outboundPassageState -> outboundPassage)
inboundBerth1State:PlaceState { numShips = 0; } :of (inboundBerth1State -> inboundBerth1)
outboundBerth1State:PlaceState { numShips = 0; } :of (outboundBerth1State -> outboundBerth1)
inboundBerth2State:PlaceState { numShips = 0; } :of (inboundBerth2State -> inboundBerth2)
outboundBerth2State:PlaceState { numShips = 0; } :of (outboundBerth2State -> outboundBerth2)
berth1State:BerthState { status = "empty"; numShips = 0; } :of (berth1State -> berth1)
berth2State:BerthState { status = "empty"; numShips = 0; } :of (berth2State -> berth2)
servedState:PlaceState { numShips = 0; } :of (servedState -> served)
workersState:WorkerSetState :of (workersState -> workers)
c1S:ConnectionState { moved = False; } :of (c1S -> c1)
c2S:ConnectionState { moved = False; } :of (c2S -> c2)
c3S:ConnectionState { moved = False; } :of (c3S -> c3)
c4S:ConnectionState { moved = False; } :of (c4S -> c4)
c5S:ConnectionState { moved = False; } :of (c5S -> c5)
c6S:ConnectionState { moved = False; } :of (c6S -> c6)
c7S:ConnectionState { moved = False; } :of (c7S -> c7)
c8S:ConnectionState { moved = False; } :of (c8S -> c8)
c9S:ConnectionState { moved = False; } :of (c9S -> c9)
c10S:ConnectionState { moved = False; } :of (c10S -> c10)
c11S:ConnectionState { moved = False; } :of (c11S -> c11)
"""

View file

@ -0,0 +1,73 @@
from examples.semantics.operational.port.helpers import design_to_state, state_to_design, get_time, get_num_ships
def render_port_graphviz(od):
txt = ""
def render_place(place):
name = od.get_name(place)
return f'"{name}" [ label = "{name}\\n ships = {get_num_ships(od, place)}", style = filled, fillcolor = lightblue ]\n'
for _, cap in od.get_all_instances("CapacityConstraint", include_subtypes=False):
name = od.get_name(cap)
capacity = od.get_slot_value(cap, "shipCapacity")
txt += f'subgraph cluster_{name} {{\n label = "{name}\\n capacity = {capacity}";\n'
for lnk in od.get_outgoing(cap, "capacityOf"):
place = od.get_target(lnk)
txt += f' {render_place(place)}'
txt += f'}}\n'
for _, place_state in od.get_all_instances("PlaceState", include_subtypes=False):
place = state_to_design(od, place_state)
if len(od.get_incoming(place, "capacityOf")) == 0:
txt += render_place(place)
for _, berth_state in od.get_all_instances("BerthState", include_subtypes=False):
berth = state_to_design(od, berth_state)
name = od.get_name(berth)
txt += f'"{name}" [ label = "{name}\\n numShips = {get_num_ships(od, berth)}\\n status = {od.get_slot_value(berth_state, "status")}", fillcolor = yellow, style = filled]\n'
for _, gen in od.get_all_instances("Generator", include_subtypes=False):
txt += f'"{od.get_name(gen)}" [ label = "+", shape = diamond, fillcolor = green, fontsize = 30, style = filled ]\n'
for _, blackhole in od.get_all_instances("BlackHole", include_subtypes=False):
txt += f'"{od.get_name(blackhole)}" [ label = "-", shape = diamond, fillcolor = red, fontsize = 30, style = filled ]\n'
for _, conn in od.get_all_instances("connection"):
src = od.get_source(conn)
tgt = od.get_target(conn)
moved = od.get_slot_value(design_to_state(od, conn), "moved")
src_name = od.get_name(src)
tgt_name = od.get_name(tgt)
txt += f"{src_name} -> {tgt_name} [color=deepskyblue3, penwidth={1 if moved else 2}];\n"
for _, workers in od.get_all_instances("WorkerSet"):
already_have = []
name = od.get_name(workers)
num_workers = od.get_slot_value(workers, "numWorkers")
txt += f'{name} [label="{num_workers} worker(s)", shape=parallelogram, fillcolor=chocolate, style=filled];\n'
for lnk in od.get_outgoing(design_to_state(od, workers), "isOperating"):
berth = od.get_target(lnk)
already_have.append(berth)
txt += f"{name} -> {od.get_name(berth)} [arrowhead=none, color=chocolate];\n"
for lnk in od.get_outgoing(workers, "canOperate"):
berth = od.get_target(lnk)
if berth not in already_have:
txt += f"{name} -> {od.get_name(berth)} [style=dotted, arrowhead=none, color=chocolate];\n"
graphviz = f"digraph {{\n{indent(txt, 2)}}}"
return "https://dreampuf.github.io/GraphvizOnline/#"+urllib.parse.quote(graphviz)
def render_port_textual(od):
txt = ""
for _, place_state in od.get_all_instances("PlaceState", include_subtypes=False):
place = state_to_design(od, place_state)
name = od.get_name(place)
txt += f'place "{name}" {"🚢"*get_num_ships(od, place)}\n'
for _, berth_state in od.get_all_instances("BerthState", include_subtypes=False):
berth = state_to_design(od, berth_state)
name = od.get_name(berth)
txt += f'berth "{name}" {"🚢"*get_num_ships(od, berth)} {od.get_slot_value(berth_state, "status")}\n'
return txt

View file

@ -0,0 +1,56 @@
import urllib.parse
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
from concrete_syntax.plantuml.renderer import render_object_diagram, render_class_diagram
from api.od import ODAPI
from examples.semantics.operational.simulator import Simulator, RandomDecisionMaker, InteractiveDecisionMaker
from examples.semantics.operational.port import models
from examples.semantics.operational.port.helpers import design_to_state, state_to_design, get_time
from examples.semantics.operational.port.renderer import render_port_textual, render_port_graphviz
# from examples.semantics.operational.port.joeris_solution import termination_condition, get_actions
from examples.semantics.operational.port.assignment import termination_condition, get_actions
state = DevState()
scd_mmm = bootstrap_scd(state) # Load meta-meta-model
### Load (meta-)models ###
def parse_and_check(m_cs: str, mm, descr: str):
m = parser.parse_od(
state,
m_text=m_cs,
mm=mm)
conf = Conformance(state, m, mm)
print(descr, "...", render_conformance_check_result(conf.check_nominal()))
return m
port_mm = parse_and_check(models.port_mm_cs, scd_mmm, "MM")
port_m = parse_and_check(models.port_m_cs, port_mm, "M")
port_rt_mm = parse_and_check(models.port_rt_mm_cs, scd_mmm, "RT-MM")
port_rt_m = parse_and_check(models.port_rt_m_cs, port_rt_mm, "RT-M")
print()
# print(render_class_diagram(state, port_rt_mm))
### Simulate ###
sim = Simulator(
action_generator=get_actions,
# decision_maker=RandomDecisionMaker(seed=2),
decision_maker=InteractiveDecisionMaker(),
termination_condition=termination_condition,
check_conformance=True,
verbose=True,
renderer=render_port_textual,
# renderer=render_port_graphviz,
)
od = ODAPI(state, port_rt_m, port_rt_mm)
sim.run(od)