Converting State Graphs to Charts
The JavaScript Object Notation, JSON, is a textual interface format for (nested) objects (records), arrays (lists), strings, and numbers. The pState Core receives state graph with nodes and connections from the front end in JSON. From the state graph, first the state hierarchy is constructed. If there are no errors, the transitions of a chart are constructed.
VERSION USING CHILDREN FIELD
The state hierarchy is built by
- creating the Root state from the
root
node, - repeatedly creating states for all the children of the nodes visited so far,
- classifying the created states as Basic, Xor, And,
- parsing all node labels and setting the corresponding fields of the states.
As expressions in state labels may contain state tests, these can only be checked after the state hierarchy is built, hence all state labels are parsed as the last step. For nodes of xor
type, a Basic or Xor state is created, depending on whether the node has children. For and
nodes, an And state is created. Following checks are performed on the nodes while building the state hierarchy:
- A state with id
root
must be defined among the nodes. - All nodes in
children
fields must be defined. - The relation among nodes given by the
children
field must not be cyclic. - There cannot be orphans, i.e. nodes that are not descendants of
root
. - The type of each node must be present and must be one
init
,prob
,cond
,xor
,and
. init
,prob
,cond
nodes can be children only ofxor
nodes.- Basic and And states must have Xor parents.
The last rule implies that Root must be an Xor state, if there are any children.
# VERSION USING CHILDREN FIELD
# TODO buildStates uses the 'children' field of 'nodes', buildTransitions uses the 'parent' field;
# revise buildStates to use only parent? Would make finding orphans simpler.
from fractions import Fraction
from typing import NamedTuple, MutableSet, Mapping, Text, Sequence, MutableMapping, Set, List
from pstate.core.core import skip, State, Error, ChartMessages, ChartException, Conditional, Alternative, Transition
from pstate.core.parser import parseStateLabel, parseCondConnectionLabel, parseProbConnectionLabel, \
parseFirstConnectionLabel
from pstate.server.chart import StateProxy, StateType, ChartProxy, ConnectionProxy
from pstate.server.json_proxy import AnyJson
class StatesManifest(NamedTuple):
root: State
Basic: MutableSet[State]
Xor: MutableSet[State]
And: MutableSet[State]
directory: Mapping[Text, State]
def buildStates(nodes: Mapping[Text, StateProxy]) -> StatesManifest:
# 1. create the root state from the `root` node
if 'root' not in nodes: raise Error('No root node defined') # can't continue
root = State(id='root', parent=None)
msgs = []
# 2. repeatedly create states for all the children of the nodes visited so far;
# classify the created states as Basic, Xor, And
Basic: MutableSet[State] = set()
Xor: MutableSet[State] = set()
And: MutableSet[State] = set()
chartstate, pseudostate = {}, set() # map id to State, set of id
visit: MutableSet[State] = {root} # set of states to visit
while len(visit) > 0:
state = visit.pop() # pick an arbitrary state
if state.id in chartstate: # already visited
raise Error('Child relation is cyclic', state.id) # can't continue
chartstate[state.id] = state
node = nodes[state.id] # node is the node corresponding to state
if node.children is not None:
for child_id in node.children or ():
child_proxy = nodes.get(child_id)
if child_proxy is None:
msgs.append(Error('Missing child definition', child_id))
continue
child_type = child_proxy.type
if child_type in (StateType.AND, StateType.XOR):
state.children.add(State(child_id, state))
elif child_type in (StateType.INIT, StateType.PROB, StateType.CHOICE):
pseudostate.add(child_id)
if node.type not in (StateType.XOR, StateType.ROOT):
msgs.append(Error('Parent must be Xor state', child_id))
else:
msgs.append(Error('State type must be one of "xor", "and", "init", "prob", "choice"', child_id))
visit |= state.children
if node.type == StateType.AND:
if state.parent in Xor:
And.add(state)
else:
msgs.append(Error('And state must have Xor parent', state.id))
elif node.type == StateType.XOR:
if len(state.children) == 0: # node type is xor, no children
if state.parent in Xor:
Basic.add(state)
else:
msgs.append(Error('Basic state must have Xor parent', state.id))
else:
Xor.add(state)
elif node.type == StateType.ROOT:
# TODO is this the appropriate way to handle the root? How should any empty chart be parsed?
Xor.add(state)
# 3. parsing all state labels and setting the corresponding fields of the states
for node_id in nodes:
if node_id in chartstate:
label = nodes[node_id].label or ''
try:
parseStateLabel(label, chartstate[node_id])
except ChartException as m:
msgs.extend(m.msgs)
elif node_id not in pseudostate:
msgs.append(Error('Orphan node', node_id))
if len(msgs) > 0:
raise ChartMessages(*msgs)
return StatesManifest(root, Basic, Xor, And, chartstate)
VERSION USING PARENT FIELD
The state hierarchy is built by
- picking an
xor
node that has not been visited so far and creating a corresponding Basic state, - starting with that node, following the
parent
, creating corresponding states, setting thechildren
field of those, until theroot
state or a visited state is reached. If a visited state is reached, check if that is on the path of states visited, to detect a cycle, and turn it into an Xor state if it was Basic - parsing all node labels and setting the corresponding fields of the states.
As expressions in state labels may contain state tests, these state tests can only be resolved after the state hierarchy is built, hence all state labels are parsed as the last step. For nodes of xor
type, initially a Basic state is create. If that state is later found to have children, it is converted to an Xor state. For and
nodes, an And state is created. Following checks are performed on the nodes while building the state hierarchy:
- A state with id
root
must be defined among the nodes. - Each nodes must have a
parent
fields and thatparent
node must be defined. - The relation among nodes given by the
parent
field must not be cyclic. - Each node must have a
type
field that must be oneinit
,prob
,cond
,xor
,and
. - Nodes of
init
,prob
,cond
type must havexor
parents.
Additionally, Basic and And states must have Xor parents in the resulting chart. This implies that the root state must be an Xor state, if the chart is not empty.
# def buildStates(nodes: 'map id to node') -> 'root, Basic, Xor, And, chartstate':
# """modifies msg"""
# # 1. create the root state from the `root` node
# if 'root' not in nodes: raise Error("No 'root' node defined") # can't continue
# root = State('root', None) # id is 'root', no parent
# # 2. repeatedly create states for all the children of the nodes visited so far;
# # classify the created states as Basic, Xor, And
# Basic, Xor, And = set(), set(), set()
# chartstate = {} # map id to State
# visit = {root} # set of states to visit
# for nid in nodes:
# if 'parent' in nodes[nid] and nodes[nid]['parent'] in nodes:
# pid = nodes[nid]['parent']
# if pid in nodes:
# if 'type' in nodes[cid]:
# tp = nodes[cid]['type']
# if tp in ('xor', 'and'):
# s = State(nid, s)
# else: msg.append(Error("State type must be one of 'xor', 'and', 'init', 'prob', 'cond'", nid))
# else: msg.append(Error('Missing type', nid))
# else: msg.append(Error("Missing 'parent' definition"))
# else: msg.append(Error("Missing 'parent' field", nid))
# while len(visit) > 0:
# s = visit.pop() # pick an arbirary state
# if s.id in chartstate: # already visited
# raise Error('Child relation is cyclic', s.id) # can't continue
# chartstate[s.id], n = s, nodes[s.id] # n is the node corresponding to s
# if 'children' in n:
# for cid in n['children']: # child id
# if cid in nodes:
# if 'type' in nodes[cid]:
# ctp = nodes[cid]['type'] # child type
# if ctp in ('and', 'xor'): s.children.add(State(cid, s))
# elif ctp in ('init', 'prob', 'cond'):
# pseudostate.add(cid)
# if n['type'] != 'xor':
# msg.append(Error('Parent must be Xor state', cid))
# else: msg.append(Error('State type must be one of "xor", "and", \
# "init", "prob", "cond"', cid))
# else: msg.append(Error('Missing type', cid))
# else: msg.append(Error('Missing child definition', cid))
# if n['type'] == 'and':
# if s.parent in Xor: And.add(s)
# else: msg.append(Error('And state must have Xor parent', s.id))
# elif len(s.children) == 0: # node type is xor, no children
# if s.parent in Xor: Basic.add(s)
# else: msg.append(Error('Basic state must have Xor parent', s.id))
# else: Xor.add(s)
# visit.update(s.children)
# # 3. parsing all state labels and setting the corresponding fields of the states
# for nid in nodes:
# if nid in chartstate:
# label = nodes[nid]['label'] if 'label' in nodes[nid] else ''
# try: parseStateLabel(label, chartstate[nid])
# except Exception as m: msg.extend(m.args)
# elif nid not in pseudostate: msg.append(Error('Orphan node', nid))
# return root, Basic, Xor, And, chartstate
Following checks are performed:
- source and target of every connection is defined
- root is not source or target of any connection
init
,prob
,cond
states have at least one outgoing connectioninit
states cannot have incoming connections- connections leaving an
init
state can only go to a sibling - connections leaving an
init
state can only go to proper states - connections leaving a proper, P, C state can only go to the parent, a sibling, a child
- connections leaving a P state can only go to C or proper states
- connections leaving a C state can only go to proper states
The auxiliary dictionary outgoing
, mapping all states to the while doing so, construct incoming, outgoing to allow above check
NodeId = Text
ConnectionId = Text
def buildTransitions(nodes: Mapping[Text, StateProxy], connections: Mapping[Text, ConnectionProxy], states_manifest: StatesManifest) -> List[Transition]:
msgs = [] # collect error messages
outgoing: MutableMapping[NodeId, MutableSet[ConnectionId]] = {} # maps node id (of proper or pseudo state) to outgoing connection
incoming = set() # set of all nodes with incoming connections
def buildCondChoices(source_id: NodeId) -> Set[Conditional]: # uses nodes, chartstate, outgoing
if nodes[source_id].type == StateType.CHOICE:
if len(outgoing[source_id]) <= 0:
raise Error('Ⓒ must have outgoing connection', source_id)
choices = set()
for conn_id in outgoing[source_id]:
conn = connections[conn_id]
guard, body = parseCondConnectionLabel(conn.label or '', conn_id)
target = states_manifest.directory[conn.target]
choices.add(Conditional(guard, body, target))
else:
choices = {Conditional(True, skip, states_manifest.directory[source_id])}
return choices
def buildProbAlternatives(source_id: NodeId) -> Sequence[Alternative]: # uses nodes, outgoing
if nodes[source_id].type == 'prob':
if len(outgoing[source_id]) <= 0:
raise Error('Ⓟ must have outgoing connection', source_id)
alternatives = []
for conn_id in outgoing[source_id]:
conn = connections[conn_id]
prob, body = parseProbConnectionLabel(conn.label, conn_id)
choices = buildCondChoices(conn.target)
alternatives.append(Alternative(prob, body, choices))
else:
alternatives = [Alternative(Fraction(1), skip, buildCondChoices(source_id))]
return alternatives
for conn_id, conn in connections.items(): # check if source and target are defined
if conn.source is None or conn.target is None:
msgs.append(Error('"source" and "target" are required', conn_id))
continue
outgoing.setdefault(conn.source, set()).add(conn_id)
incoming.add(conn.target)
if conn.source not in nodes:
msgs.append(Error('Source "%s" undefined' % conn.source, conn_id))
continue
elif conn.target not in nodes:
msgs.append(Error('Target "%s" undefined' % conn.target, conn_id))
continue
if conn.source == 'root':
msgs.append(Error('Source cannot be root', conn_id))
continue
elif conn.target == 'root':
msgs.append(Error('Target cannot be root', conn_id))
continue
source = nodes[conn.source]; target = nodes[conn.target]
if target.type == StateType.INIT:
msgs.append(Error('Must not target •', conn_id))
if source.type == StateType.INIT:
if source.parent != target.parent:
msgs.append(Error('Target must be a sibling of its source', conn_id))
if target.type not in (StateType.AND, StateType.XOR):
msgs.append(Error('Target must be a proper state', conn_id))
else: # source is proper, P, C state
if source.parent != target.parent and source.parent != conn.target and conn.source != target.parent:
msgs.append(Error('Must go to sibling, parent, or child of its source', conn_id))
if source.type == StateType.PROB and target.type not in (StateType.CHOICE, StateType.XOR, StateType.AND):
msgs.append(Error('Target must be one of Ⓒ, Basic, And, Xor', conn_id))
elif source.type == StateType.CHOICE and target.type not in (StateType.XOR, StateType.AND):
msgs.append(Error('Target must be one of Basic, And, Xor', conn_id))
transitions = [] # builds transitions and ...
for conn_id, conn in connections.items(): # ... checks that P, C nodes have at least one outgoing connection
try:
source = nodes[conn.source]
if source.type in (StateType.XOR, StateType.AND):
event, guard, cost, body = parseFirstConnectionLabel(conn.label or '', conn_id)
alts = buildProbAlternatives(conn.target) # set
transitions.append(Transition(conn_id, states_manifest.directory[conn.source], event, guard, cost, body, alts))
elif source.type == StateType.INIT:
p = states_manifest.directory[source.parent]
guard, body = parseCondConnectionLabel(conn.label or '', conn_id)
p.init.add(Conditional(guard, body, states_manifest.directory[conn.target]))
except ChartException as m:
msgs.extend(m.msgs)
# check P, C nodes and states with siblings for incoming connection
for n in set(nodes.keys()).difference(incoming): # all nodes without incoming connection
node = nodes[n]
if node.type == StateType.PROB:
msgs.append(Error('Ⓟ must have incoming connection', n))
elif node.type == StateType.CHOICE:
msgs.append(Error('Ⓒ must have incoming connection', n))
elif node.type in (StateType.XOR, StateType.AND): # states that are not sole child should ...
# ... have an incoming connection, except root, which does not have
parent_node = nodes.get(node.parent) # None if root
if n != 'root' and parent_node.type == StateType.XOR and len(parent_node.children) > 1:
msgs.append(Error('Must have incoming connection', n))
if len(msgs) > 0: raise ChartMessages(*msgs)
return transitions
Validating a state graph and generating intermediate code proceeds in three steps:
- building the state hierarchy from the nodes,
- building the transitions from the connections,
- generating intermediate code.
The implementation below additionally:
- creates an auxiliary dictionary
chartstates
mapping ids to states, - checks that
nodes
andconnections
are defined instategraph
, - continues with the next step only if the previous step did not result in errors.
class ChartManifest(NamedTuple):
root: State
Basic: MutableSet[State]
Xor: MutableSet[State]
And: MutableSet[State]
transitions: Sequence[Transition]
def inflate(stategraph: MutableMapping[Text, AnyJson]) -> ChartManifest:
chart_proxy = ChartProxy(stategraph)
nodes = chart_proxy.states
state_manifest = buildStates(nodes)
connections = chart_proxy.connections
transitions = buildTransitions(nodes, connections, state_manifest)
return ChartManifest(*state_manifest[:4], transitions=transitions)
# from pstate import *
# pChart = PChartWidget()
# %%pstate_compile pChart
{
"states": {
"a": {
"type": "xor",
"id": "a",
"children": {},
"name": "A",
"label": "Some code",
"position": {
"x": 10,
"y": 30
},
"dimensions": {
"width": 100,
"height": 75
}
},
"b": {
"type": "xor",
"id": "b",
"children": {},
"name": "B",
"label": "Some other code",
"position": {
"x": 200,
"y": 300
},
"dimensions": {
"width": 100,
"height": 75
}
},
"c": {
"type": "init",
"id": "c",
"position": {
"x": 200,
"y": 100
},
"radius": 10
},
"d": {
"type": "prob",
"id": "d",
"position": {
"x": 200,
"y": 200
},
"radius": 20
},
"e": {
"type": "choice",
"id": "e",
"position": {
"x": 300,
"y": 200
},
"radius": 40
}
},
"connections": {
"t_1": {
"id": "t_1",
"source": "a",
"target": "b",
"label": "event"
},
"t_2": {
"id": "t_2",
"source": "d",
"target": "a",
"label": "@0.9"
},
"t_3": {
"id": "t_3",
"source": "d",
"target": "b",
"label": "@0.1"
},
"t_4": {
"id": "t_4",
"source": "c",
"target": "a",
"comment": "Initial state is a"
},
"t_5": {
"id": "t_5",
"source": "b",
"target": "d"
}
}
}
# pChart.value
# from IPython.display import display
# import ipywidgets as widgets
# int_range = widgets.IntSlider()
# display(int_range)
# def on_value_change(change):
# print(change['new'])
# int_range.observe(on_value_change, names='value')
# p = pChart('chartfile'); p ; q = pView(p, 'T') ; q ; p.compileC('T', 'Cfile')
"""
# VERSION CREATING PSEUDO CHART STATES
def inflate(rawchart):
global Root, Basic, XOR, AND, Init, Prob, Cond, chartstate, incoming, outgoing, Trans
msg = [] # for collecting messages
# 1. traverse all raw states to form tree with id, parent, children fields,
# starting with "root"; determine state Root and sets Basic, XOR, AND
rawstates = rawchart.get('states', set()) # raw states, empty set if not defined
if 'root' not in rawstates: return error('No "root" state defined') # can't continue
Root = State(None, 'root') # no parent, id "root"
Basic, XOR, AND, Init, Prob, Cond = set(), set(), set(), set(), set(), set()
chartstate = {} # maps id of raw state to chart state
incoming, outgoing = {}, {} # maps chart state to incoming, outgoing connection ids
visit = {Root} # set of chart states to visit
while len(visit) > 0:
cs = visit.pop() # pick an arbirary chart state
if cs.id in chartstate:
return error('Child relation is cyclic', ('state', cs.id)) # can't continue
chartstate[cs.id] = cs
incoming[cs], outgoing[cs] = set(), set()
rs = rawstates[cs.id] # rs is the corresponding raw state
rt = rs['type'] if 'type' in rs else None # raw type, None if not defined
rc = rs['children'] if 'children' in rs else set() # raw children, empty set if not defined
if rt == 'and':
if cs.parent in XOR: AND.add(cs)
else: msg.append(error('AND state must have XOR parent', ('state', cs.id)))
elif rt == 'xor':
if len(rc) == 0:
if cs. parent in XOR: Basic.add(cs)
else: msg.append(error('Basic state must have XOR parent', ('state', cs.id)))
else: XOR.add(cs)
elif rt == 'init':
if cs.parent in XOR: Init.add(cs)
else: msg.append(error('Parent of • must be XOR state', ('state', cs.id)))
elif rt == 'prob':
if cs.parent in XOR: Prob.add(cs)
else: msg.append(error('Parent of Ⓟ ℗ must be XOR state', ('state', cs.id)))
elif rt == 'cond':
if cs.parent in XOR: Cond.add(cs)
else: msg.append(error('Parent of Ⓒ © must be XOR state', ('state', cs.id)))
else: msg.append(error('State type must be one of "xor", "and", "init", "prob", "cond"',
('state', cs.id)))
cs.children = set()
for r in rc:
if r in rawstates: cs.children.add(State(cs, r))
else: msg.append(error('Missing child definition', ('state', r)))
visit.update(cs.children)
if Root not in XOR:
msg.append(error('Root must be XOR state', ('state', Root.id)))
for rs in rawstates.keys() - chartstate.keys():
msg.append(error('Orphan state', ('state', rs)))
# 2. traverse all chart states to parse the label, setting the name, variables/decls,
# event, inv, cost fields of each state
visit = {Root} # set of chart states to visit
while len(visit) > 0:
cs = visit.pop()
rs = rawstates.get(cs.id, {})
# try:
parseStateLabel(rs.get("label", ""), cs)
# except Exception as m: msg.extend(m.args)
visit.update(cs.children)
# 3. traverse all connections and build maps incoming, outgoing:
# - incoming maps every chart state to its set of incoming connection ids
# - outgoing maps every chart state to its set of outgoing connection ids
connections = rawchart.get("connections", set()) # connections or empty set if not defined
for cid in connections:
rc = connections[cid]
if 'source' in rc:
rs = rc['source']
if rs in chartstate:
outgoing[chartstate[rs]].add(cid)
else: msg.append(error('Source not a state', ('connection', cid)))
else: msg.append(error('"source" undefined', ('connection', cid)))
if 'target' in rc:
rt = rc["target"]
if rt in chartstate: incoming[chartstate[rt]].add(cid)
else: msg.append(error('Target not a state', ('connection', cid)))
else: msg.append(error('"target" undefined', ('connection', cid)))
# 4. check that
# - init states have no incoming connection
# - P, C states have at least one incoming connection
# - init, P, C states have at least one outgoing connection
for cs in Init:
for cid in incoming[cs]:
msg.append(error('Must not target •', ('connection', cid)))
for cs in Prob | Cond:
if len(incoming[cs]) == 0:
msg.append(error('Must have incoming connection', ('state', cs.id)))
for cs in Init | Prob | Cond:
if len(outgoing[cs]) == 0:
msg.append(error('Must have outgoing connection', ('state', cs.id)))
# 4. NEW check that
# - Root is not source or target of any connection
# - init, P, C states have at least one outgoing connection
# - init states cannot have incoming connections
# - connections leaving an init can only go to a sibling
# - connections leaving an init state can only go to proper states
# - connections leaving a P state can only go to C or proper statea
# - connections leaving a C state can only go to proper statea
# - connections leaving a proper, P, C state can only go to the parent, a sibling, a child
if len(msg) > 0: return msg
# 5. construct a transition for every connection leaving a proper state
Trans = [] # set of all transitions
for s0 in Basic | XOR | AND: # s0 ranges over proper chart states
for cid1 in outgoing[s0]: # cid1 ranges over ids of connections leaving s0
# try: # first create transition, parse and check label
tr = Transition(s0); c1 = connections[cid1]
lab = c1['label'] if 'label' in c1 else ''
tr.kind, tr.guard, tr.cost, tr.body = parseFirstConnectionLabel(lab, s0, cid1)
s1 = chartstate[c1['target']] # target is P, C, proper state
msg.extend(checkWellScoped(s0, s1, cid1))
if s1 in Prob: # add probabilistic alternatives to tr.target
alternatives = [] # set of all alternatives
for cid2 in outgoing[s1]:
c2 = connections[cid2]
# check no event/time, no guard, no cost
lab = c2['label'] if 'label' in c2 else ''
prob, body = parseProbConnectionLabel(lab, s0, cid2)
s2 = chartstate[c2["target"]]
msg.extend(checkWellScoped(s1, s2, cid2))
alternatives.append([prob, body, s2])
else: alternatives = [[1, skip, s1]]
msg.extend(checkProbsSumToOne(alternatives, s1))
tr.target = alternatives
for alt in alternatives:
s2 = alt[2]
if s2 in Cond:
conditionals = []
for cid3 in outgoing[s2]:
c3 = connections[cid3]
lab = c3['label'] if 'label' in c3 else ''
# CHECK IF s0 IS THE APPROPRIATE SCOPE(yes?)
guard, body = parseCondConnectionLabel(lab, s0, cid3)
# check that target is proper state and is or has same parent as source
s3 = chartstate[c3["target"]]
if s3 not in Basic | AND | XOR:
msg.append(error('Target must be one of: Basic, AND, OR'), cid3)
msg.extend(checkWellScoped(s2, s3, cid3))
conditionals.append([guard, body, s3])
else:
if s2 in Prob:
msg.append(error('Target must be one of: Basic, AND, OR, Ⓒ'), \
('connection', cid2))
conditionals = [[True, skip, s2]]
msg.extend(checkCompleteDisjoint(conditionals, s2))
alt[2] = conditionals
Trans.append(tr)
# except Exception as m: msg.extend(m.args)
# 6. set default of XOR states
for i0 in Init:
if i0.parent not in XOR: msg.append(error('Init must be child of XOR state', ))
conditionals = set()
for cid in outgoing[i0]:
c = connections[cid]
# parse label, check no event/time, no probability, no cost
lab = c['label'] if 'label' in c else ''
# CHECK IF i0.parent IS THE APPROPRIATE SCOPE
guard, body = parseCondConnectionLabel(lab, i0.parent, cid)
# check that target is proper state and is child of parent
i1 = chartstate[c["target"]]
if i1 not in Basic | AND | XOR:
msg.append(error('Target must be Basic, AND, OR state'), cid)
if i0.parent != i1.parent:
msg.append(error('Invalid target'), cid)
conditionals.add((guard, body, i1))
checkCompleteDisjoint(conditionals, i0.id)
i0.parent.default = conditionals
"""
Alternative Implementations
"""
# VERSION NOT CREATING PSEUDO CHART STATES
def inflate(rawchart):
global Root, Basic, XOR, AND, Init, Prob, Cond, chartstate, incoming, outgoing, Trans
msg = [] # for collecting messages
# 1. traverse all raw states to form tree with id, parent, children fields,
# starting with "root"; determine state Root and sets Basic, XOR, AND
rawstates = rawchart['states'] if 'states' in rawchart else set() # raw states, empty set if not defined
if 'root' not in rawstates: return Error('No "root" state defined') # can't continue
Root = State(None, 'root') # no parent, id 'root'
Basic, XOR, AND = set(), set(), set() # sets of chart states
Init, Prob, Cond = set(), set(), set() # sets of state ids
chartstate = {} # maps state id to chart state
incoming, outgoing = {}, {} # maps state id to connection ids
visit = {Root} # set of chart states to visit
while len(visit) > 0:
cs = visit.pop() # pick an arbirary chart state
if cs.id in chartstate:
return Error('Child relation is cyclic', ('state', cs.id)) # can't continue
chartstate[cs.id] = cs
incoming[cs.id], outgoing[cs.id] = set(), set()
rs = rawstates[cs.id] # rs is the corresponding raw state
rc = rs['children'] if 'children' in rs else set() # raw children, empty set if not defined
if len(rc) == 0:
if cs.parent in XOR: Basic.add(cs)
else: msg.append(Error('Basic state must have XOR parent', ('state', cs.id)))
else: XOR.add(cs)
cs.children = set()
for r in rc:
if r in rawstates:
rs = rawstates[r]
rt = rs['type'] if 'type' in rs else None
if rt in ('and', 'xor'):
cs.children.add(State(cs, r))
if rt == 'and' and cs not in XOR:
msg.append(Error('AND state must have XOR parent', ('state', cs.id)))
elif rt in ('init', 'prob', 'cond'):
if cs not in XOR:
msg.append(Error('Must have XOR parent', ('state', cs.id)))
if rt == 'and':
if cs in XOR: AND.add(cs)
else: msg.append(Error('AND state must have XOR parent', ('state', cs.id)))
elif rt == 'xor':
elif rt == 'init':
if cs.parent in XOR: Init.add(cs)
else: msg.append(Error('Parent of • must be XOR state', ('state', cs.id)))
elif rt == 'prob':
if cs.parent in XOR: Prob.add(cs)
else: msg.append(Error('Parent of Ⓟ ℗ must be XOR state', ('state', cs.id)))
elif rt == 'cond':
if cs.parent in XOR: Cond.add(cs)
else: msg.append(Error('Parent of Ⓒ © must be XOR state', ('state', cs.id)))
else: msg.append(Error('State type must be one of "xor", "and", "init", "prob", "cond"',
('state', cs.id)))
cs.children.add(State(cs, r))
# else: msg.append(Error('Missing child definition', ('state', r)))
visit.update(cs.children)
if Root not in XOR:
msg.append(Error('Root must be XOR state', ('state', Root.id)))
for rs in rawstates.keys() - chartstate.keys():
msg.append(Error('Orphan state', ('state', rs)))
# 2. traverse all chart states to parse the label, setting the name, variables/decls,
# event, inv, cost fields of each state
visit = {Root} # set of chart states to visit
while len(visit) > 0:
cs = visit.pop()
rs = rawstates.get(cs.id, {})
# try:
parseStateLabel(rs.get("label", ""), cs)
# except Exception as m: msg.extend(m.args)
visit.update(cs.children)
# 3. traverse all connections and build maps incoming, outgoing:
# - incoming maps every chart state to its set of incoming connection ids
# - outgoing maps every chart state to its set of outgoing connection ids
connections = rawchart.get("connections", set()) # connections or empty set if not defined
for cid in connections:
rc = connections[cid]
if 'source' in rc:
rs = rc['source']
if rs in chartstate:
outgoing[chartstate[rs]].add(cid)
else: msg.append(Error('Source not a state', ('connection', cid)))
else: msg.append(Error('"source" undefined', ('connection', cid)))
if 'target' in rc:
rt = rc["target"]
if rt in chartstate: incoming[chartstate[rt]].add(cid)
else: msg.append(Error('Target not a state', ('connection', cid)))
else: msg.append(Error('"target" undefined', ('connection', cid)))
# 4. check that
# - init states have no incoming connection
# - P, C states have at least one incoming connection
# - init, P, C states have at least one outgoing connection
for cs in Init:
for cid in incoming[cs]:
msg.append(Error('Must not target •', ('connection', cid)))
for cs in Prob | Cond:
if len(incoming[cs]) == 0:
msg.append(Error('Must have incoming connection', ('state', cs.id)))
for cs in Init | Prob | Cond:
if len(outgoing[cs]) == 0:
msg.append(Error('Must have outgoing connection', ('state', cs.id)))
# 4. NEW check that
# - Root is not source or target of any connection
# - init, P, C states have at least one outgoing connection
# - init states cannot have incoming connections
# - connections leaving an init can only go to a sibling
# - connections leaving an init state can only go to proper states
# - connections leaving a P state can only go to C or proper states
# - connections leaving a C state can only go to proper statea
# - connections leaving a proper, P, C state can only go to the parent, a sibling, a child
# - check for undefined pseudostates, orphan pseudostates
# while doing so, construct incoming, outgoing to allow above check
if len(msg) > 0: return msg
# 5. construct a transition for every connection leaving a proper state
Trans = [] # set of all transitions
for s0 in Basic | XOR | AND: # s0 ranges over proper chart states
for cid1 in outgoing[s0]: # cid1 ranges over ids of connections leaving s0
# try: # first create transition, parse and check label
tr = Transition(s0); c1 = connections[cid1]
lab = c1['label'] if 'label' in c1 else ''
tr.kind, tr.guard, tr.cost, tr.body = parseFirstConnectionLabel(lab, s0, cid1)
s1 = chartstate[c1['target']] # target is P, C, proper state
msg.extend(checkWellScoped(s0, s1, cid1))
if s1 in Prob: # add probabilistic alternatives to tr.target
alternatives = [] # set of all alternatives
for cid2 in outgoing[s1]:
c2 = connections[cid2]
# check no event/time, no guard, no cost
lab = c2['label'] if 'label' in c2 else ''
prob, body = parseProbConnectionLabel(lab, s0, cid2)
s2 = chartstate[c2["target"]]
msg.extend(checkWellScoped(s1, s2, cid2))
alternatives.append([prob, body, s2])
else: alternatives = [[1, skip, s1]]
msg.extend(checkProbsSumToOne(alternatives, s1))
tr.target = alternatives
for alt in alternatives:
s2 = alt[2]
if s2 in Cond:
conditionals = []
for cid3 in outgoing[s2]:
c3 = connections[cid3]
lab = c3['label'] if 'label' in c3 else ''
# CHECK IF s0 IS THE APPROPRIATE SCOPE(yes?)
guard, body = parseCondConnectionLabel(lab, s0, cid3)
# check that target is proper state and is or has same parent as source
s3 = chartstate[c3["target"]]
if s3 not in Basic | AND | XOR:
msg.append(Error('Target must be one of: Basic, AND, OR'), cid3)
msg.extend(checkWellScoped(s2, s3, cid3))
conditionals.append([guard, body, s3])
else:
if s2 in Prob:
msg.append(Error('Target must be one of: Basic, AND, OR, Ⓒ'), \
('connection', cid2))
conditionals = [[True, skip, s2]]
msg.extend(checkCompleteDisjoint(conditionals, s2))
alt[2] = conditionals
Trans.append(tr)
# except Exception as m: msg.extend(m.args)
# 6. set default of XOR states
for i0 in Init:
if i0.parent not in XOR: msg.append(Error('Init must be child of XOR state', ))
conditionals = set()
for cid in outgoing[i0]:
c = connections[cid]
# parse label, check no event/time, no probability, no cost
lab = c['label'] if 'label' in c else ''
# CHECK IF i0.parent IS THE APPROPRIATE SCOPE
guard, body = parseCondConnectionLabel(lab, i0.parent, cid)
# check that target is proper state and is child of parent
i1 = chartstate[c["target"]]
if i1 not in Basic | AND | XOR:
msg.append(Error('Target must be Basic, AND, OR state'), cid)
if i0.parent != i1.parent:
msg.append(Error('Invalid target'), cid)
conditionals.add((guard, body, i1))
checkCompleteDisjoint(conditionals, i0.id)
i0.parent.default = conditionals
"""