diff --git a/src/XMLplotter.py b/src/XMLplotter.py new file mode 100644 index 0000000..c937b2b --- /dev/null +++ b/src/XMLplotter.py @@ -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("<>", 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("<>", 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() \ No newline at end of file diff --git a/src/pypdevs/tracer.py b/src/pypdevs/tracer.py index fa0213a..40573d7 100644 --- a/src/pypdevs/tracer.py +++ b/src/pypdevs/tracer.py @@ -33,13 +33,15 @@ class Tracers(object): :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) """ + loc = {} try: - exec("from pypdevs.tracers.%s import %s" % tracer[0:2]) + exec("from pypdevs.tracers.%s import %s" % (tracer[0], tracer[1]), {}, loc) except: - exec("from %s import %s" % tracer[0:2]) - self.tracers.append(eval("%s(%i, server, *%s)" % (tracer[1], - self.uid, - tracer[2]))) + exec("from %s import %s" % (tracer[0], tracer[1]), {}, loc) + self.tracers.append(loc[tracer[1]](self.uid, server, *tracer[2])) + # self.tracers.append(eval("%s(%i, server, *%s)" % (tracer[1], + # self.uid, + # tracer[2]))) self.tracers_init.append(tracer) self.uid += 1 self.tracers[-1].startTracer(recover) diff --git a/src/pypdevs/tracers/tracerBase.py b/src/pypdevs/tracers/tracerBase.py new file mode 100644 index 0000000..c06bbea --- /dev/null +++ b/src/pypdevs/tracers/tracerBase.py @@ -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 \ No newline at end of file diff --git a/src/pypdevs/tracers/tracerCell.py b/src/pypdevs/tracers/tracerCell.py index b13ecc0..f0544b8 100644 --- a/src/pypdevs/tracers/tracerCell.py +++ b/src/pypdevs/tracers/tracerCell.py @@ -13,11 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pypdevs.tracers.tracerBase import BaseTracer from pypdevs.util import runTraceAtController, toStr from pypdevs.activityVisualisation import visualizeMatrix import sys -class TracerCell(object): +class TracerCell(BaseTracer): """ 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 multifile: whether or not multiple files should be generated for each timestep """ + super(TracerCell, self).__init__(uid, server) if server.getName() == 0: self.filename = filename else: self.filename = None - self.server = server - self.uid = uid self.x_size = x_size self.y_size = y_size self.multifile = multifile diff --git a/src/pypdevs/tracers/tracerVCD.py b/src/pypdevs/tracers/tracerVCD.py index 35a8564..f6ade94 100644 --- a/src/pypdevs/tracers/tracerVCD.py +++ b/src/pypdevs/tracers/tracerVCD.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pypdevs.tracers.tracerBase import BaseTracer from pypdevs.util import runTraceAtController, toStr, DEVSException from math import floor @@ -36,7 +37,7 @@ class VCDRecord(object): # Set to None to make sure that it will be changed self.bit_size = None -class TracerVCD(object): +class TracerVCD(BaseTracer): """ 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 filename: file to save the trace to """ + super(TracerVCD, self).__init__(uid, server) if server.getName() == 0: self.filename = filename else: self.filename = None - self.server = server - self.uid = uid def startTracer(self, recover): """ @@ -65,9 +65,9 @@ class TracerVCD(object): # Nothing to do here as we aren't the controller return elif recover: - self.vcd_file = open(self.filename, 'a+') + self.vcd_file = open(self.filename, 'ab+') else: - self.vcd_file = open(self.filename, 'w') + self.vcd_file = open(self.filename, 'wb') self.vcd_var_list = [] self.vcd_prevtime = 0.0 self.vcdHeader() @@ -147,16 +147,16 @@ class TracerVCD(object): if vcd_state[i] == 'b': continue else: - raise DEVSException(("Port %s in model does not carry " + + raise DEVSException(("Port %s in model %s does not carry " + "a binary signal\n" + - "VCD exports require a binary signal," + - "not: ") % (port_name, model_name, vcd_state)) + "VCD exports require a binary signal, " + + "not: %s") % (port_name, model_name, vcd_state)) char = vcd_state[i] 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" + - "VCD exports require a binary signal," + - "not: ") % (port_name, model_name, vcd_state)) + "VCD exports require a binary signal, " + + "not: %s") % (port_name, model_name, vcd_state)) # Find the identifier of this wire for i in range(len(self.vcd_var_list)): if (self.vcd_var_list[i].model_name == model_name and diff --git a/src/pypdevs/tracers/tracerVerbose.py b/src/pypdevs/tracers/tracerVerbose.py index 1169b35..dc23dc7 100644 --- a/src/pypdevs/tracers/tracerVerbose.py +++ b/src/pypdevs/tracers/tracerVerbose.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pypdevs.tracers.tracerBase import BaseTracer from pypdevs.util import runTraceAtController import sys -class TracerVerbose(object): +class TracerVerbose(BaseTracer): """ A tracer for simple verbose output """ @@ -28,13 +29,12 @@ class TracerVerbose(object): :param server: the server to make remote calls on :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: self.filename = filename else: self.filename = None - self.server = server self.prevtime = (-1, -1) - self.uid = uid def startTracer(self, recover): """ diff --git a/src/pypdevs/tracers/tracerXML.py b/src/pypdevs/tracers/tracerXML.py index 738510a..f4e3d89 100644 --- a/src/pypdevs/tracers/tracerXML.py +++ b/src/pypdevs/tracers/tracerXML.py @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pypdevs.tracers.tracerBase import BaseTracer from pypdevs.util import runTraceAtController, toStr -import sys +import sys, re -class TracerXML(object): +class TracerXML(BaseTracer): """ A tracer for XML tracing output """ @@ -28,12 +29,11 @@ class TracerXML(object): :param server: the server to make remote calls on :param filename: file to save the trace to """ + super(TracerXML, self).__init__(uid, server) if server.getName() == 0: self.filename = filename else: self.filename = None - self.server = server - self.uid = uid def write_py23(self, string): try: @@ -103,7 +103,7 @@ class TracerXML(object): aDEVS.time_last, "'IN'", toStr(port_info), - toStr(aDEVS.state.toXML()), + toStr(TracerXML.toXML(aDEVS.state)), toStr(aDEVS.state)]) def traceExternal(self, aDEVS): @@ -125,7 +125,7 @@ class TracerXML(object): aDEVS.time_last, "'EX'", toStr(port_info), - toStr(aDEVS.state.toXML()), + toStr(TracerXML.toXML(aDEVS.state)), toStr(aDEVS.state)]) def traceConfluent(self, aDEVS): @@ -147,7 +147,7 @@ class TracerXML(object): aDEVS.time_last, "'EX'", toStr(port_info), - toStr(aDEVS.state.toXML()), + toStr(TracerXML.toXML(aDEVS.state)), toStr(aDEVS.state)]) port_info = "" for I in range(len(aDEVS.OPorts)): @@ -163,7 +163,7 @@ class TracerXML(object): aDEVS.time_last, "'IN'", toStr(port_info), - toStr(aDEVS.state.toXML()), + toStr(TracerXML.toXML(aDEVS.state)), toStr(aDEVS.state)]) def traceInit(self, aDEVS, t): @@ -180,5 +180,47 @@ class TracerXML(object): t, "'EX'", "''", - toStr(aDEVS.state.toXML()), + toStr(TracerXML.toXML(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 "%s%s%s" % ( + cat, name, type_, str(elem)) + else: + type_ = "Unknown" + value = TracerXML.toXML(elem) + return "%s%s%s" % ( + cat, name, type_, str(value)) + + if isinstance(state, (str, int, float)): + return "state%s%s" % (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 "stateMap%s" % res + elif isinstance(state, (list, tuple)): + res = "" + for ix, item in enumerate(state): + name = "item-%d" % ix + res += create_multi_attrib(name, item) + return "stateList%s" % 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))}) +