diff --git a/src/XMLplotter.py b/src/XMLplotter.py index c937b2b..a1e5017 100644 --- a/src/XMLplotter.py +++ b/src/XMLplotter.py @@ -11,21 +11,54 @@ from tkinter import ttk from tkinter import filedialog as fd import matplotlib.pyplot as plt +from matplotlib.patches import FancyArrowPatch, ArrowStyle import matplotlib.animation as animation from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import numpy as np +import pandas as pd + +import dataclasses import xml.etree.ElementTree as ET + +def is_float(val): + try: + float(val) + except ValueError: + return False + else: + return True + + 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.filename = fd.askopenfilename(parent=self.root, title="Open an XML trace file", + # initialdir=r"C:\Users\randy\AppData\Roaming\JetBrains\PyCharm2023.3\scratches", + # filetypes=[("XML files", "*.xml")]) + # if not self.filename: + # self.root.quit() - self.root.title("DEVS Plotting Environment - %s" % self.filename) + self.filename = r"C:\Users\randy\AppData\Roaming\JetBrains\PyCharm2023.3\scratches\test.xml" + + self.time = 0.0 + self.active_model = "" + self.active_state = "" + + # load in the model + self.trace_state = pd.DataFrame(columns=['time', 'model', 'kind', 'path', 'value']) + self.parse_trace_file() + + self.make_gui() + self._build_tree(pd.unique(self.trace_state["model"]), self.mtree) + + self.update() + self.root.mainloop() + + def make_gui(self): + self.root.title("DEVS XML Plotting Environment - %s" % self.filename) self.frame = ttk.Frame(self.root, padding=10) self.frame.pack(fill=tk.BOTH, expand=True) @@ -38,7 +71,6 @@ class Window: 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) @@ -66,13 +98,8 @@ class Window: 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.figure.tight_layout() self.axis = self.figure.add_subplot(111) self.axis.set_xlabel("time") self.axis.set_ylim((0, 1)) @@ -80,142 +107,172 @@ class Window: 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.__line, = self.axis.plot([], [], '-o', c='g', mec='r', fillstyle='none') + self.__idots, = self.axis.plot([], [], 'o', c='g') + self.__edots, = self.axis.plot([], [], 'o', c='r') + self.__arrows = [] 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 + self.time = 0.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 + event_list = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state)] + earlier = event_list[event_list["time"] < self.time] + if len(earlier) > 0: + self.time = earlier.iloc[-1]["time"] 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 + event_list = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state)] + later = event_list[event_list["time"] > self.time] + if len(later) > 0: + self.time = later.iloc[0]["time"] def to_last(self): if self.active_model != "" and self.active_state != "": - self.time = self.trace[self.active_model][-1]["time"] + event_list = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state)] + if len(event_list) > 0: + self.time = event_list.iloc[-1]["time"] def get_window(self): return int(self.window_size.get()) + def _flatten_dict(self, data): + res = {} + for k, v in data.items(): + if isinstance(v, dict): + dct = self._flatten_dict(v) + for kk, vv in dct.items(): + res[k + "." + kk] = vv + else: + res[k] = v + return res + 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 - } + attrs = self._flatten_dict(self._parse_attributes(item.find("state"))) + time = float(item.find("time").text) + kind = item.find("kind").text - self.trace.setdefault(model, []).append(data) + rows = [] + for key, v in attrs.items(): + rows.append([time, model, kind, key, v]) + self.trace_state = pd.concat([self.trace_state, pd.DataFrame(rows, columns=self.trace_state.columns)], + ignore_index=True) + self.trace_state = self.trace_state.sort_values(by="time") def _parse_attributes(self, node): res = {} for attr in node.findall('attribute'): name = attr.find("name").text valueN = attr.find("value") + typ = attr.find("type").text if len(valueN.findall("attribute")) > 0: res[name] = self._parse_attributes(valueN) else: - res[name] = valueN.text + if attr.attrib["category"] == "P": + if typ == "Integer": + res[name] = int(valueN.text) + elif typ == "Float": + res[name] = float(valueN.text) + elif typ == "Boolean": + res[name] = valueN.text == "True" + else: # String + res[name] = valueN.text + else: + res[name] = valueN.text return res - def _build_model_mtree(self): + def _build_tree(self, paths, tree): ix = 0 tree_ids = {} - for model in self.trace: + for model in paths: 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.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 + event_list = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state) & + (self.trace_state["time"] == self.time)] + next_evts = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state) & + (self.trace_state["time"] > self.time)] + if len(next_evts) > 0: + next_time = next_evts.iloc[0]["time"] + else: + next_time = "N/A" + + for eidx, event in event_list.iterrows(): + self.output.insert(tk.END, "TIME: %.4f\nSTATE: %s\n" % (self.time, str(event["value"]))) + if event["kind"] == "IN": + self.output.insert(tk.END, 'Internal Transition:\n') + # self.output.insert(tk.END, " Port: %s\n" % ) + self.output.insert(tk.END, " Time Next: %s" % next_time) + elif event["kind"] == "EX": + self.output.insert(tk.END, 'External Transition') + else: + self.output.insert(tk.END, 'Undefined Transition') + + # 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")) + # elif ev["kind"] == "EX": + # if "port" in ev: + # self.output.insert(tk.END, + # 'External Transition:\n Port: %s\n Input: %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') + # else: + # self.output.insert(tk.END, 'Undefined Transition') + # break else: self.clear_plot() def select_in_mtree(self, event): + self.clear_plot() 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) + 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()) + self._build_tree(pd.unique(self.trace_state[self.trace_state["model"] == self.active_model]["path"]), + self.stree) else: self.active_model = "" self.stree.pack_forget() @@ -225,11 +282,28 @@ class Window: 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.clear_plot() self.active_state = selection[0] + def create_arrow(self, x, y, dx, dy): + if dy < 0: + style = "angle,angleA=45,angleB=-45,rad=15" + elif dy > 0: + style = "angle,angleA=-45,angleB=45,rad=15" + else: + style = "arc,angleA=135,angleB=45,armA=20,armB=20,rad=15" + return FancyArrowPatch((x, y), (x + dx, y + dy), + connectionstyle=style, + shrinkA=1, shrinkB=1, zorder=10, color='black', + arrowstyle=ArrowStyle.CurveFilledB(head_width=3, head_length=5)) + def clear_plot(self): self.__line.set_data([], []) - self.__dots.set_data([], []) + self.__idots.set_data([], []) + self.__edots.set_data([], []) + for a in self.__arrows: + a.remove() + self.__arrows.clear() self.axis.set_title("") self.axis.set_xlim((0, 1)) self.axis.set_ylim((-0.5, 0.5)) @@ -237,48 +311,78 @@ class Window: 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)) + event_list = self.trace_state[(self.trace_state["model"] == self.active_model) & + (self.trace_state["path"] == self.active_state)] + mid = self.time ws = self.get_window() - lower = max(times[0], mid - ws/2) + lower = max(mid - ws / 2, 0.0) 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]) + event_list_lowest = lower + event_list_lower = event_list[event_list["time"] < lower] + if len(event_list_lower) > 0: + event_list_lowest = event_list_lower.iloc[-1]["time"] + event_list_highest = upper + event_list_upper = event_list[event_list["time"] > upper] + if len(event_list_upper) > 0: + event_list_highest = event_list_upper.iloc[0]["time"] + event_list = event_list[event_list["time"].between(event_list_lowest, event_list_highest)] + + times = event_list["time"] + lower = max(times.min(), lower) + upper = min(times.max(), upper) + + in_times = event_list[event_list["kind"] == "IN"]["time"].to_numpy() + in_evts = event_list[event_list["kind"] == "IN"]["value"].to_numpy() + out_times = event_list[event_list["kind"] == "EX"]["time"].to_numpy() + out_evts = event_list[event_list["kind"] == "EX"]["value"].to_numpy() + + ts = times.repeat(3).iloc[2:-2].to_numpy() + vs = event_list["value"].iloc[:-1].repeat(2).to_numpy() + vs = np.insert(vs, [x for x in range(2, len(vs), 2)], np.nan) + times = times.to_numpy() + state_sets = np.sort(event_list["value"].unique(), kind='mergesort') + min_ = np.nanmin(vs) + max_ = np.nanmax(vs) + + if len(times) < 20: + self.axis.set_xticks(times) + else: + self.axis.set_xticks([times.min(), times.max()]) + if len(state_sets) < 20: + if np.all(np.vectorize(is_float, otypes=[bool])(state_sets)): + self.axis.set_yticks(state_sets) + self.axis.set_yticklabels(state_sets) + else: + self.axis.set_yticks(range(len(state_sets))) + self.axis.set_yticklabels(state_sets) + else: + self.axis.set_yticks([min_, max_]) + self.axis.set_yticklabels([min_, max_]) + + self.axis.set_xlim((lower, upper)) + self.axis.set_ylim((min_ - 0.5, max_ + 0.5)) + + self.__cursor.set_data([mid, mid], [min_ - 0.5, max_ + 0.5]) self.__line.set_data(ts, vs) - self.__dots.set_data(in_times, [state_sets.index(x) for x in in_evts]) + self.__idots.set_data(in_times, in_evts) + self.__edots.set_data(out_times, out_evts) + + for i in range(len(ts) // 3): + ix = i * 3 + 1 + iy = (i + 1) * 3 + if i >= len(self.__arrows): + arrow = self.create_arrow(ts[ix], vs[ix], 0, vs[iy] - vs[ix]) + self.__arrows.append(arrow) + self.axis.add_patch(arrow) + else: + self.__arrows[i].set_positions((ts[ix], vs[ix]), (ts[iy], vs[iy])) + + while len(self.__arrows) > (len(ts) // 3): + self.__arrows.pop().remove() diff --git a/src/pypdevs/DEVS.py b/src/pypdevs/DEVS.py index 282c37b..47cbd50 100644 --- a/src/pypdevs/DEVS.py +++ b/src/pypdevs/DEVS.py @@ -186,9 +186,23 @@ class BaseDEVS(object): Get the full model name, including the path from the root :returns: string -- the fully qualified name of the model + + :raises: AttributeError -- when the model is not fully + initialized for simulation """ return self.full_name + def getModelFullNameRec(self): + """ + Get the full model name, including the path from the root, + using recursion. + + :returns: string -- the fully qualified name of the model + """ + if self.parent is None: + return self.getModelName() + return self.parent.getModelFullNameRec() + "." + self.getModelName() + class AtomicDEVS(BaseDEVS): """ Abstract base class for all atomic-DEVS descriptive classes. @@ -835,6 +849,9 @@ class Port(object): self.is_input = is_input self.z_functions = {} + def __repr__(self): + return "%s (%s)" % (self.type(), self.getPortFullName()) + def getPortName(self): """ Returns the name of the port @@ -849,7 +866,7 @@ class Port(object): :returns: fully qualified name of the port """ - return "%s.%s" % (self.host_DEVS.getModelFullName(), self.getPortName()) + return "%s.%s" % (self.host_DEVS.getModelFullNameRec(), self.getPortName()) def type(self): """ diff --git a/src/pypdevs/tracers/tracerXML.py b/src/pypdevs/tracers/tracerXML.py index f4e3d89..dc5c5fd 100644 --- a/src/pypdevs/tracers/tracerXML.py +++ b/src/pypdevs/tracers/tracerXML.py @@ -188,7 +188,8 @@ class TracerXML(BaseTracer): primitives = { int: "Integer", float: "Float", - str: "String" + str: "String", + bool: "Boolean" } def create_multi_attrib(name, elem): @@ -204,7 +205,7 @@ class TracerXML(BaseTracer): return "%s%s%s" % ( cat, name, type_, str(value)) - if isinstance(state, (str, int, float)): + if isinstance(state, (str, int, float, bool)): return "state%s%s" % (primitives[type(state)], str(state)) elif isinstance(state, dict): res = ""