Updates to XML plotter - using pandas for more efficiency

This commit is contained in:
rparedis 2024-01-12 16:49:26 +01:00
parent 5982ba62ba
commit 30064b7101
3 changed files with 246 additions and 124 deletions

View file

@ -11,21 +11,54 @@ from tkinter import ttk
from tkinter import filedialog as fd from tkinter import filedialog as fd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, ArrowStyle
import matplotlib.animation as animation import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import pandas as pd
import dataclasses
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
def is_float(val):
try:
float(val)
except ValueError:
return False
else:
return True
class Window: class Window:
def __init__(self): def __init__(self):
self.root = tk.Tk() self.root = tk.Tk()
self.filename = fd.askopenfilename(parent=self.root, title="Open an XML trace file", # self.filename = fd.askopenfilename(parent=self.root, title="Open an XML trace file",
initialdir="/", filetypes=[("XML files", "*.xml")]) # initialdir=r"C:\Users\randy\AppData\Roaming\JetBrains\PyCharm2023.3\scratches",
if not self.filename: # filetypes=[("XML files", "*.xml")])
self.root.quit() # 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 = ttk.Frame(self.root, padding=10)
self.frame.pack(fill=tk.BOTH, expand=True) self.frame.pack(fill=tk.BOTH, expand=True)
@ -38,7 +71,6 @@ class Window:
self.trees = ttk.Frame(self.container) self.trees = ttk.Frame(self.container)
self.trees.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 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 = ttk.Button(self.toolbar, text="<<", command=self.to_first)
self.button_first.pack(side=tk.LEFT) self.button_first.pack(side=tk.LEFT)
self.button_prev = ttk.Button(self.toolbar, text="<", command=self.to_prev) self.button_prev = ttk.Button(self.toolbar, text="<", command=self.to_prev)
@ -66,13 +98,8 @@ class Window:
self.stree.pack_forget() self.stree.pack_forget()
self.stree.bind("<<TreeviewSelect>>", self.select_in_stree) 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 = plt.figure(dpi=100)
# self.figure.tight_layout() self.figure.tight_layout()
self.axis = self.figure.add_subplot(111) self.axis = self.figure.add_subplot(111)
self.axis.set_xlabel("time") self.axis.set_xlabel("time")
self.axis.set_ylim((0, 1)) self.axis.set_ylim((0, 1))
@ -80,142 +107,172 @@ class Window:
self.canvas.draw() self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 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.__cursor, = self.axis.plot([0, 0], [0, 0], '--', c='b', alpha=0.7)
self.__line, = self.axis.plot([], [], c='r') self.__line, = self.axis.plot([], [], '-o', c='g', mec='r', fillstyle='none')
self.__dots, = self.axis.plot([], [], 'o', c='g') 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.__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 = tk.Text(self.frame, height=7)
self.output.pack(side=tk.BOTTOM, fill=tk.X, expand=True) self.output.pack(side=tk.BOTTOM, fill=tk.X, expand=True)
self.output.pack_forget() self.output.pack_forget()
self.root.mainloop()
def to_first(self): def to_first(self):
if self.active_model != "" and self.active_state != "": if self.active_model != "" and self.active_state != "":
self.time = 0 self.time = 0.0
def to_prev(self): def to_prev(self):
if self.active_model != "" and self.active_state != "": if self.active_model != "" and self.active_state != "":
for ev in reversed(self.trace[self.active_model]): event_list = self.trace_state[(self.trace_state["model"] == self.active_model) &
if ev["time"] < self.time: (self.trace_state["path"] == self.active_state)]
self.time = ev["time"] earlier = event_list[event_list["time"] < self.time]
break if len(earlier) > 0:
self.time = earlier.iloc[-1]["time"]
def to_next(self): def to_next(self):
if self.active_model != "" and self.active_state != "": if self.active_model != "" and self.active_state != "":
for ev in self.trace[self.active_model]: event_list = self.trace_state[(self.trace_state["model"] == self.active_model) &
if ev["time"] > self.time: (self.trace_state["path"] == self.active_state)]
self.time = ev["time"] later = event_list[event_list["time"] > self.time]
break if len(later) > 0:
self.time = later.iloc[0]["time"]
def to_last(self): def to_last(self):
if self.active_model != "" and self.active_state != "": 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): def get_window(self):
return int(self.window_size.get()) 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): def parse_trace_file(self):
tree = ET.parse(self.filename) tree = ET.parse(self.filename)
root = tree.getroot() root = tree.getroot()
for item in root.findall('event'): for item in root.findall('event'):
model = item.find("model").text model = item.find("model").text
data = {} attrs = self._flatten_dict(self._parse_attributes(item.find("state")))
data["time"] = float(item.find("time").text) time = float(item.find("time").text)
data["kind"] = item.find("kind").text 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) 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): def _parse_attributes(self, node):
res = {} res = {}
for attr in node.findall('attribute'): for attr in node.findall('attribute'):
name = attr.find("name").text name = attr.find("name").text
valueN = attr.find("value") valueN = attr.find("value")
typ = attr.find("type").text
if len(valueN.findall("attribute")) > 0: if len(valueN.findall("attribute")) > 0:
res[name] = self._parse_attributes(valueN) res[name] = self._parse_attributes(valueN)
else:
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: else:
res[name] = valueN.text res[name] = valueN.text
return res return res
def _build_model_mtree(self): def _build_tree(self, paths, tree):
ix = 0 ix = 0
tree_ids = {} tree_ids = {}
for model in self.trace: for model in paths:
lst = model.split(".") lst = model.split(".")
for mix in range(len(lst)): for mix in range(len(lst)):
parent = ".".join(lst[:mix]) parent = ".".join(lst[:mix])
path = ".".join(lst[:mix + 1]) path = ".".join(lst[:mix + 1])
if path not in tree_ids: 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 tree_ids[path] = ix
ix += 1 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): def update(self):
if self.active_model != "" and self.active_state != "": if self.active_model != "" and self.active_state != "":
self.create_plot_for_active_model_state() self.create_plot_for_active_model_state()
self.output.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) self.output.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.output.delete("1.0", tk.END) self.output.delete("1.0", tk.END)
tam = self.trace[self.active_model] event_list = self.trace_state[(self.trace_state["model"] == self.active_model) &
for ix, ev in enumerate(tam): (self.trace_state["path"] == self.active_state) &
if ev["time"] == self.time: (self.trace_state["time"] == self.time)]
state = ev["state"] next_evts = self.trace_state[(self.trace_state["model"] == self.active_model) &
for p in self.active_state.split("."): (self.trace_state["path"] == self.active_state) &
state = state[p] (self.trace_state["time"] > self.time)]
self.output.insert(tk.END, "TIME: %.4f\nSTATE: %s\n" % (self.time, str(state))) if len(next_evts) > 0:
if ev["kind"] == "IN": next_time = next_evts.iloc[0]["time"]
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: 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') self.output.insert(tk.END, 'External Transition')
break 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: else:
self.clear_plot() self.clear_plot()
def select_in_mtree(self, event): def select_in_mtree(self, event):
self.clear_plot()
tree = event.widget tree = event.widget
selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0] selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0]
if len(selection) == 1: if len(selection) == 1:
self.active_model = selection[0] 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: else:
self.active_model = "" self.active_model = ""
self.stree.pack_forget() self.stree.pack_forget()
@ -225,11 +282,28 @@ class Window:
tree = event.widget tree = event.widget
selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0] selection = [tree.item(item)["values"][0] for item in tree.selection() if len(tree.get_children(item)) == 0]
if len(selection) == 1: if len(selection) == 1:
self.clear_plot()
self.active_state = selection[0] 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): def clear_plot(self):
self.__line.set_data([], []) 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_title("")
self.axis.set_xlim((0, 1)) self.axis.set_xlim((0, 1))
self.axis.set_ylim((-0.5, 0.5)) self.axis.set_ylim((-0.5, 0.5))
@ -237,48 +311,78 @@ class Window:
self.axis.set_yticklabels([]) self.axis.set_yticklabels([])
def create_plot_for_active_model_state(self): 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)) 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 mid = self.time
ws = self.get_window() ws = self.get_window()
lower = max(times[0], mid - ws/2) lower = max(mid - ws / 2, 0.0)
upper = lower + ws upper = lower + ws
self.axis.set_xlim((lower, upper))
self.axis.set_ylim((-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_yticks(range(len(state_sets)))
self.axis.set_yticklabels(state_sets) self.axis.set_yticklabels(state_sets)
else:
self.axis.set_yticks([min_, max_])
self.axis.set_yticklabels([min_, max_])
self.__cursor.set_data([mid, mid], [-0.5, len(state_sets) - 0.5]) 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.__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()

View file

@ -186,9 +186,23 @@ class BaseDEVS(object):
Get the full model name, including the path from the root Get the full model name, including the path from the root
:returns: string -- the fully qualified name of the model :returns: string -- the fully qualified name of the model
:raises: AttributeError -- when the model is not fully
initialized for simulation
""" """
return self.full_name 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): class AtomicDEVS(BaseDEVS):
""" """
Abstract base class for all atomic-DEVS descriptive classes. Abstract base class for all atomic-DEVS descriptive classes.
@ -835,6 +849,9 @@ class Port(object):
self.is_input = is_input self.is_input = is_input
self.z_functions = {} self.z_functions = {}
def __repr__(self):
return "%s (%s)" % (self.type(), self.getPortFullName())
def getPortName(self): def getPortName(self):
""" """
Returns the name of the port Returns the name of the port
@ -849,7 +866,7 @@ class Port(object):
:returns: fully qualified name of the port :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): def type(self):
""" """

View file

@ -188,7 +188,8 @@ class TracerXML(BaseTracer):
primitives = { primitives = {
int: "Integer", int: "Integer",
float: "Float", float: "Float",
str: "String" str: "String",
bool: "Boolean"
} }
def create_multi_attrib(name, elem): def create_multi_attrib(name, elem):
@ -204,7 +205,7 @@ class TracerXML(BaseTracer):
return "<attribute category=\"%s\"><name>%s</name><type>%s</type><value>%s</value></attribute>" % ( return "<attribute category=\"%s\"><name>%s</name><type>%s</type><value>%s</value></attribute>" % (
cat, name, type_, str(value)) cat, name, type_, str(value))
if isinstance(state, (str, int, float)): if isinstance(state, (str, int, float, bool)):
return "<attribute category=\"P\"><name>state</name><type>%s</type><value>%s</value></attribute>" % (primitives[type(state)], str(state)) return "<attribute category=\"P\"><name>state</name><type>%s</type><value>%s</value></attribute>" % (primitives[type(state)], str(state))
elif isinstance(state, dict): elif isinstance(state, dict):
res = "" res = ""