Unialized tracers + added a visualizer for XML traces

This commit is contained in:
rparedis 2023-02-10 16:27:10 +01:00
parent 325880f46e
commit a2a6928b0a
7 changed files with 457 additions and 31 deletions

286
src/XMLplotter.py Normal file
View file

@ -0,0 +1,286 @@
"""
This visual DEVS plotter is based on Bill Song's DEVS Visual Modeling and Simulation Environment,
however, it has been ported to the PythonPDEVS logic.
See Also:
`http://msdl.uantwerpen.be/people/bill/devsenv/summerpresentation.pdf`_
"""
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog as fd
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import xml.etree.ElementTree as ET
class Window:
def __init__(self):
self.root = tk.Tk()
self.filename = fd.askopenfilename(parent=self.root, title="Open an XML trace file",
initialdir="/", filetypes=[("XML files", "*.xml")])
if not self.filename:
self.root.quit()
self.root.title("DEVS Plotting Environment - %s" % self.filename)
self.frame = ttk.Frame(self.root, padding=10)
self.frame.pack(fill=tk.BOTH, expand=True)
self.toolbar = ttk.Frame(self.frame)
self.toolbar.pack(side=tk.TOP, fill=tk.X)
self.container = ttk.Frame(self.frame)
self.container.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.trees = ttk.Frame(self.container)
self.trees.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.time = 0.0
self.button_first = ttk.Button(self.toolbar, text="<<", command=self.to_first)
self.button_first.pack(side=tk.LEFT)
self.button_prev = ttk.Button(self.toolbar, text="<", command=self.to_prev)
self.button_prev.pack(side=tk.LEFT)
self.button_next = ttk.Button(self.toolbar, text=">", command=self.to_next)
self.button_next.pack(side=tk.LEFT)
self.button_last = ttk.Button(self.toolbar, text=">>", command=self.to_last)
self.button_last.pack(side=tk.LEFT)
# sep = ttk.Separator(self.toolbar)
# sep.pack(side=tk.LEFT)
lbl_window = ttk.Label(self.toolbar, text=" Window Size: ")
lbl_window.pack(side=tk.LEFT)
self.window_size = ttk.Spinbox(self.toolbar, from_=0, to=50)
self.window_size.set(10)
self.window_size.pack(side=tk.LEFT)
self.mtree = ttk.Treeview(self.trees, selectmode="browse", columns=["path"], displaycolumns=[])
self.mtree.heading('#0', text="Select a Model:", anchor=tk.W)
self.mtree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.mtree.bind("<<TreeviewSelect>>", self.select_in_mtree)
self.stree = ttk.Treeview(self.trees, selectmode="browse", columns=["path"], displaycolumns=[])
self.stree.heading('#0', text="Plottable Attributes:", anchor=tk.W)
self.stree.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.stree.pack_forget()
self.stree.bind("<<TreeviewSelect>>", self.select_in_stree)
# load in the model
self.trace = {}
self.parse_trace_file()
self._build_model_mtree()
self.figure = plt.figure(dpi=100)
# self.figure.tight_layout()
self.axis = self.figure.add_subplot(111)
self.axis.set_xlabel("time")
self.axis.set_ylim((0, 1))
self.canvas = FigureCanvasTkAgg(self.figure, master=self.container)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.__cursor, = self.axis.plot([0, 0], [0, 0], '--', c='b', alpha=0.7)
self.__line, = self.axis.plot([], [], c='r')
self.__dots, = self.axis.plot([], [], 'o', c='g')
self.__ani = animation.FuncAnimation(self.figure, lambda _: self.update(), interval=100)
self.active_model = ""
self.active_state = ""
self.output = tk.Text(self.frame, height=7)
self.output.pack(side=tk.BOTTOM, fill=tk.X, expand=True)
self.output.pack_forget()
self.root.mainloop()
def to_first(self):
if self.active_model != "" and self.active_state != "":
self.time = 0
def to_prev(self):
if self.active_model != "" and self.active_state != "":
for ev in reversed(self.trace[self.active_model]):
if ev["time"] < self.time:
self.time = ev["time"]
break
def to_next(self):
if self.active_model != "" and self.active_state != "":
for ev in self.trace[self.active_model]:
if ev["time"] > self.time:
self.time = ev["time"]
break
def to_last(self):
if self.active_model != "" and self.active_state != "":
self.time = self.trace[self.active_model][-1]["time"]
def get_window(self):
return int(self.window_size.get())
def parse_trace_file(self):
tree = ET.parse(self.filename)
root = tree.getroot()
for item in root.findall('event'):
model = item.find("model").text
data = {}
data["time"] = float(item.find("time").text)
data["kind"] = item.find("kind").text
data["state"] = self._parse_attributes(item.find("state"))
if data["kind"] == "IN":
port = item.find("port")
data["port"] = {
"name": port.get("name"),
"category": port.get("category"), # I or O (in or out)
"message": port.find("message").text
}
self.trace.setdefault(model, []).append(data)
def _parse_attributes(self, node):
res = {}
for attr in node.findall('attribute'):
name = attr.find("name").text
valueN = attr.find("value")
if len(valueN.findall("attribute")) > 0:
res[name] = self._parse_attributes(valueN)
else:
res[name] = valueN.text
return res
def _build_model_mtree(self):
ix = 0
tree_ids = {}
for model in self.trace:
lst = model.split(".")
for mix in range(len(lst)):
parent = ".".join(lst[:mix])
path = ".".join(lst[:mix + 1])
if path not in tree_ids:
self.mtree.insert(tree_ids.get(parent, ''), tk.END, ix, text=lst[mix], open=True, values=[path])
tree_ids[path] = ix
ix += 1
def _build_model_stree(self, model):
self.stree.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
if len(self.stree.get_children()) > 0:
self.stree.delete(self.stree.get_children())
event_list = self.trace[model]
state = {}
for evt in event_list:
state.update(evt["state"])
self._build_stree(state)
def _build_stree(self, state, pid='', parent=""):
uid = pid
if pid == '':
uid = 0
uid += 1
for s, v in state.items():
path = s
if parent != "":
path = parent + "." + path
self.stree.insert(pid, tk.END, uid, text=s, open=True, values=[path])
if isinstance(v, dict):
self._build_stree(v, uid, path)
uid += len(v)
else:
uid += 1
def update(self):
if self.active_model != "" and self.active_state != "":
self.create_plot_for_active_model_state()
self.output.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.output.delete("1.0", tk.END)
tam = self.trace[self.active_model]
for ix, ev in enumerate(tam):
if ev["time"] == self.time:
state = ev["state"]
for p in self.active_state.split("."):
state = state[p]
self.output.insert(tk.END, "TIME: %.4f\nSTATE: %s\n" % (self.time, str(state)))
if ev["kind"] == "IN":
self.output.insert(tk.END, 'Internal Transition:\n Port: %s\n Output: %s\n Time Next: %.4f' %
(ev["port"]["name"], ev["port"]["message"], tam[ix + 1]["time"] if ix + 1 < len(tam) else "N/A"))
else:
self.output.insert(tk.END, 'External Transition')
break
else:
self.clear_plot()
def select_in_mtree(self, event):
tree = event.widget
selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0]
if len(selection) == 1:
self.active_model = selection[0]
self._build_model_stree(self.active_model)
else:
self.active_model = ""
self.stree.pack_forget()
self.active_state = ""
def select_in_stree(self, event):
tree = event.widget
selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0]
if len(selection) == 1:
self.active_state = selection[0]
def clear_plot(self):
self.__line.set_data([], [])
self.__dots.set_data([], [])
self.axis.set_title("")
self.axis.set_xlim((0, 1))
self.axis.set_ylim((-0.5, 0.5))
self.axis.set_yticks([])
self.axis.set_yticklabels([])
def create_plot_for_active_model_state(self):
event_list = self.trace[self.active_model]
path = self.active_state.split(".")
in_times = []
in_evts = []
states = []
for ev in event_list:
state = ev["state"]
for p in path:
state = state[p]
states.append(state)
if ev["kind"] == "IN":
in_times.append(ev["time"])
in_evts.append(state)
state_sets = list(sorted(set(states)))
times = [x["time"] for x in event_list]
values = [state_sets.index(x) for x in states]
ts, vs = [], []
for time in times:
ts.append(time)
ts.append(time)
for val in values:
vs.append(val)
vs.append(val)
ts.pop(0)
vs.pop()
self.axis.set_title("%s: %s" % (self.active_model, self.active_state))
mid = self.time
ws = self.get_window()
lower = max(times[0], mid - ws/2)
upper = lower + ws
self.axis.set_xlim((lower, upper))
self.axis.set_ylim((-0.5, len(state_sets) - 0.5))
self.axis.set_yticks(range(len(state_sets)))
self.axis.set_yticklabels(state_sets)
self.__cursor.set_data([mid, mid], [-0.5, len(state_sets) - 0.5])
self.__line.set_data(ts, vs)
self.__dots.set_data(in_times, [state_sets.index(x) for x in in_evts])
if __name__ == '__main__':
Window()

View file

@ -33,13 +33,15 @@ class Tracers(object):
:param server: the server object to be able to make remote calls :param server: the server object to be able to make remote calls
:param recover: whether or not this is a recovered registration (used during checkpointing) :param recover: whether or not this is a recovered registration (used during checkpointing)
""" """
loc = {}
try: try:
exec("from pypdevs.tracers.%s import %s" % tracer[0:2]) exec("from pypdevs.tracers.%s import %s" % (tracer[0], tracer[1]), {}, loc)
except: except:
exec("from %s import %s" % tracer[0:2]) exec("from %s import %s" % (tracer[0], tracer[1]), {}, loc)
self.tracers.append(eval("%s(%i, server, *%s)" % (tracer[1], self.tracers.append(loc[tracer[1]](self.uid, server, *tracer[2]))
self.uid, # self.tracers.append(eval("%s(%i, server, *%s)" % (tracer[1],
tracer[2]))) # self.uid,
# tracer[2])))
self.tracers_init.append(tracer) self.tracers_init.append(tracer)
self.uid += 1 self.uid += 1
self.tracers[-1].startTracer(recover) self.tracers[-1].startTracer(recover)

View file

@ -0,0 +1,96 @@
# Copyright 2014 Modelling, Simulation and Design Lab (MSDL) at
# McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class BaseTracer:
"""
The baseclass for the tracers, allows for inheritance.
"""
def __init__(self, uid, server):
"""
Constructor
:param uid: the UID of this tracer
:param server: the server to make remote calls on
"""
self.uid = uid
self.server = server
def startTracer(self, recover):
"""
Starts up the tracer
:param recover: whether or not this is a recovery call (so whether or not the file should be appended to)
"""
pass
def stopTracer(self):
"""
Stop the tracer
"""
pass
def trace(self, time, text):
"""
Actual tracing function
:param time: time at which this trace happened
:param text: the text that was traced
"""
pass
def traceInternal(self, aDEVS):
"""
Tracing done for the internal transition function
:param aDEVS: the model that transitioned
"""
pass
def traceConfluent(self, aDEVS):
"""
Tracing done for the confluent transition function
:param aDEVS: the model that transitioned
"""
pass
def traceExternal(self, aDEVS):
"""
Tracing done for the external transition function
:param aDEVS: the model that transitioned
"""
pass
def traceInit(self, aDEVS, t):
"""
Tracing done for the initialisation
:param aDEVS: the model that was initialised
:param t: time at which it should be traced
"""
pass
def traceUser(self, time, aDEVS, variable, value):
"""
Tracing done for a user change
:param aDEVS: the model that was initialised
:param time: time at which it should be traced
:param variable: the variable that was changed
:param value: the new value for the variable
"""
pass

View file

@ -13,11 +13,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pypdevs.tracers.tracerBase import BaseTracer
from pypdevs.util import runTraceAtController, toStr from pypdevs.util import runTraceAtController, toStr
from pypdevs.activityVisualisation import visualizeMatrix from pypdevs.activityVisualisation import visualizeMatrix
import sys import sys
class TracerCell(object): class TracerCell(BaseTracer):
""" """
A tracer for Cell-DEVS style tracing output A tracer for Cell-DEVS style tracing output
""" """
@ -32,12 +33,11 @@ class TracerCell(object):
:param y_size: the y size of the grid :param y_size: the y size of the grid
:param multifile: whether or not multiple files should be generated for each timestep :param multifile: whether or not multiple files should be generated for each timestep
""" """
super(TracerCell, self).__init__(uid, server)
if server.getName() == 0: if server.getName() == 0:
self.filename = filename self.filename = filename
else: else:
self.filename = None self.filename = None
self.server = server
self.uid = uid
self.x_size = x_size self.x_size = x_size
self.y_size = y_size self.y_size = y_size
self.multifile = multifile self.multifile = multifile

View file

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pypdevs.tracers.tracerBase import BaseTracer
from pypdevs.util import runTraceAtController, toStr, DEVSException from pypdevs.util import runTraceAtController, toStr, DEVSException
from math import floor from math import floor
@ -36,7 +37,7 @@ class VCDRecord(object):
# Set to None to make sure that it will be changed # Set to None to make sure that it will be changed
self.bit_size = None self.bit_size = None
class TracerVCD(object): class TracerVCD(BaseTracer):
""" """
A tracer for VCD output. Should only be used for binary signals! A tracer for VCD output. Should only be used for binary signals!
""" """
@ -48,12 +49,11 @@ class TracerVCD(object):
:param server: the server to make remote requests on :param server: the server to make remote requests on
:param filename: file to save the trace to :param filename: file to save the trace to
""" """
super(TracerVCD, self).__init__(uid, server)
if server.getName() == 0: if server.getName() == 0:
self.filename = filename self.filename = filename
else: else:
self.filename = None self.filename = None
self.server = server
self.uid = uid
def startTracer(self, recover): def startTracer(self, recover):
""" """
@ -65,9 +65,9 @@ class TracerVCD(object):
# Nothing to do here as we aren't the controller # Nothing to do here as we aren't the controller
return return
elif recover: elif recover:
self.vcd_file = open(self.filename, 'a+') self.vcd_file = open(self.filename, 'ab+')
else: else:
self.vcd_file = open(self.filename, 'w') self.vcd_file = open(self.filename, 'wb')
self.vcd_var_list = [] self.vcd_var_list = []
self.vcd_prevtime = 0.0 self.vcd_prevtime = 0.0
self.vcdHeader() self.vcdHeader()
@ -147,16 +147,16 @@ class TracerVCD(object):
if vcd_state[i] == 'b': if vcd_state[i] == 'b':
continue continue
else: else:
raise DEVSException(("Port %s in model does not carry " + raise DEVSException(("Port %s in model %s does not carry " +
"a binary signal\n" + "a binary signal\n" +
"VCD exports require a binary signal," + "VCD exports require a binary signal, " +
"not: ") % (port_name, model_name, vcd_state)) "not: %s") % (port_name, model_name, vcd_state))
char = vcd_state[i] char = vcd_state[i]
if char not in ["0", "1", "E", "x"]: if char not in ["0", "1", "E", "x"]:
raise DEVSException(("Port %s in model does not carry " + raise DEVSException(("Port %s in model %s does not carry " +
"a binary signal\n" + "a binary signal\n" +
"VCD exports require a binary signal," + "VCD exports require a binary signal, " +
"not: ") % (port_name, model_name, vcd_state)) "not: %s") % (port_name, model_name, vcd_state))
# Find the identifier of this wire # Find the identifier of this wire
for i in range(len(self.vcd_var_list)): for i in range(len(self.vcd_var_list)):
if (self.vcd_var_list[i].model_name == model_name and if (self.vcd_var_list[i].model_name == model_name and

View file

@ -13,10 +13,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pypdevs.tracers.tracerBase import BaseTracer
from pypdevs.util import runTraceAtController from pypdevs.util import runTraceAtController
import sys import sys
class TracerVerbose(object): class TracerVerbose(BaseTracer):
""" """
A tracer for simple verbose output A tracer for simple verbose output
""" """
@ -28,13 +29,12 @@ class TracerVerbose(object):
:param server: the server to make remote calls on :param server: the server to make remote calls on
:param filename: file to save the trace to, can be None for output to stdout :param filename: file to save the trace to, can be None for output to stdout
""" """
super(TracerVerbose, self).__init__(uid, server)
if server.getName() == 0: if server.getName() == 0:
self.filename = filename self.filename = filename
else: else:
self.filename = None self.filename = None
self.server = server
self.prevtime = (-1, -1) self.prevtime = (-1, -1)
self.uid = uid
def startTracer(self, recover): def startTracer(self, recover):
""" """

View file

@ -13,10 +13,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from pypdevs.tracers.tracerBase import BaseTracer
from pypdevs.util import runTraceAtController, toStr from pypdevs.util import runTraceAtController, toStr
import sys import sys, re
class TracerXML(object): class TracerXML(BaseTracer):
""" """
A tracer for XML tracing output A tracer for XML tracing output
""" """
@ -28,12 +29,11 @@ class TracerXML(object):
:param server: the server to make remote calls on :param server: the server to make remote calls on
:param filename: file to save the trace to :param filename: file to save the trace to
""" """
super(TracerXML, self).__init__(uid, server)
if server.getName() == 0: if server.getName() == 0:
self.filename = filename self.filename = filename
else: else:
self.filename = None self.filename = None
self.server = server
self.uid = uid
def write_py23(self, string): def write_py23(self, string):
try: try:
@ -103,7 +103,7 @@ class TracerXML(object):
aDEVS.time_last, aDEVS.time_last,
"'IN'", "'IN'",
toStr(port_info), toStr(port_info),
toStr(aDEVS.state.toXML()), toStr(TracerXML.toXML(aDEVS.state)),
toStr(aDEVS.state)]) toStr(aDEVS.state)])
def traceExternal(self, aDEVS): def traceExternal(self, aDEVS):
@ -125,7 +125,7 @@ class TracerXML(object):
aDEVS.time_last, aDEVS.time_last,
"'EX'", "'EX'",
toStr(port_info), toStr(port_info),
toStr(aDEVS.state.toXML()), toStr(TracerXML.toXML(aDEVS.state)),
toStr(aDEVS.state)]) toStr(aDEVS.state)])
def traceConfluent(self, aDEVS): def traceConfluent(self, aDEVS):
@ -147,7 +147,7 @@ class TracerXML(object):
aDEVS.time_last, aDEVS.time_last,
"'EX'", "'EX'",
toStr(port_info), toStr(port_info),
toStr(aDEVS.state.toXML()), toStr(TracerXML.toXML(aDEVS.state)),
toStr(aDEVS.state)]) toStr(aDEVS.state)])
port_info = "" port_info = ""
for I in range(len(aDEVS.OPorts)): for I in range(len(aDEVS.OPorts)):
@ -163,7 +163,7 @@ class TracerXML(object):
aDEVS.time_last, aDEVS.time_last,
"'IN'", "'IN'",
toStr(port_info), toStr(port_info),
toStr(aDEVS.state.toXML()), toStr(TracerXML.toXML(aDEVS.state)),
toStr(aDEVS.state)]) toStr(aDEVS.state)])
def traceInit(self, aDEVS, t): def traceInit(self, aDEVS, t):
@ -180,5 +180,47 @@ class TracerXML(object):
t, t,
"'EX'", "'EX'",
"''", "''",
toStr(aDEVS.state.toXML()), toStr(TracerXML.toXML(aDEVS.state)),
toStr(aDEVS.state)]) toStr(aDEVS.state)])
@staticmethod
def toXML(state):
primitives = {
int: "Integer",
float: "Float",
str: "String"
}
def create_multi_attrib(name, elem):
cat = "C"
if type(elem) in primitives:
cat = "P"
type_ = primitives[type(elem)]
return "<attribute category=\"%s\"><name>%s</name><type>%s</type><value>%s</value></attribute>" % (
cat, name, type_, str(elem))
else:
type_ = "Unknown"
value = TracerXML.toXML(elem)
return "<attribute category=\"%s\"><name>%s</name><type>%s</type><value>%s</value></attribute>" % (
cat, name, type_, str(value))
if isinstance(state, (str, int, float)):
return "<attribute category=\"P\"><name>state</name><type>%s</type><value>%s</value></attribute>" % (primitives[type(state)], str(state))
elif isinstance(state, dict):
res = ""
for k, v in state.items():
name = re.sub("[^a-zA-Z0-9_]", "", k)
res += create_multi_attrib(name, v)
return "<attribute category=\"C\"><name>state</name><type>Map</type><value>%s</value></attribute>" % res
elif isinstance(state, (list, tuple)):
res = ""
for ix, item in enumerate(state):
name = "item-%d" % ix
res += create_multi_attrib(name, item)
return "<attribute category=\"C\"><name>state</name><type>List</type><value>%s</value></attribute>" % res
elif hasattr(state, "toXML"):
return state.toXML()
elif hasattr(state, "__str__"):
return TracerXML.toXML(str(state))
return TracerXML.toXML({k: getattr(state, k) for k in dir(state) if not k.startswith("_") and not callable(getattr(state, k))})