From 2492edda487055b81799f390aa9980fa6d33ad81 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sat, 8 May 2021 13:55:56 +0200 Subject: [PATCH] refactor --- animation.py | 31 +++++++++ caplot.py | 191 +++++---------------------------------------------- plot0d.py | 94 +++++++++++++++++++++++++ plot1d.py | 15 ++++ plot2d.py | 13 ++++ plots.py | 6 ++ sources.py | 78 +++++++++++++++++++++ 7 files changed, 256 insertions(+), 172 deletions(-) create mode 100644 animation.py create mode 100644 plot0d.py create mode 100644 plot1d.py create mode 100644 plot2d.py create mode 100644 plots.py create mode 100644 sources.py diff --git a/animation.py b/animation.py new file mode 100644 index 0000000..5709ed5 --- /dev/null +++ b/animation.py @@ -0,0 +1,31 @@ +from matplotlib import pyplot as plt + + +# expect from source: +# - get() -> value +# - name +# - add_callback(cb) +# - disconnect() + +# plot_func takes result of source.get() +# returns Plot object + +# Plot object has set(val) + + +class Animation: + + def __init__(self, source, plot_func): + value = source.get() + self.plot = plot_func(value) + plt.suptitle(source.name) + source.add_callback(self.update) + plt.show() + source.disconnect() + + def update(self, value=None, **kwargs): + self.plot.set(value) + plt.draw() + + + diff --git a/caplot.py b/caplot.py index c6a9cf4..c6f9230 100755 --- a/caplot.py +++ b/caplot.py @@ -11,193 +11,40 @@ parser.add_argument("pvname") clargs = parser.parse_args() -import numpy as np -from collections import deque -from time import sleep -from functools import wraps -from types import SimpleNamespace -from matplotlib import pyplot as plt -from epics import PV +from animation import Animation +from plots import Plot0D, Plot1D, Plot2D +from sources import Camera, Source +def decide_src_fun(pvname): + pvname = pvname.upper() + if pvname.endswith(":FPICTURE"): + print("Camera", pvname) + src = Camera(pvname) + fun = Plot2D + return src, fun - -class PVCollection: - - def __init__(self, name, **kwargs): - self.name = name -# self.pvnames = SimpleNamespace(**kwargs) - pvs = {k: PV(v) for k, v in kwargs.items()} - for pv in pvs.values(): - pv.wait_for_connection() - self.pvs = SimpleNamespace(**pvs) - - def __str__(self): - return self.name - - - -class Camera(PVCollection): - - def __init__(self, pvname): - base = pvname.rsplit(":", 1)[0] - super().__init__( - base, - image = pvname, - width = base + ":WIDTH", - height = base + ":HEIGHT" - ) - - def get(self): - data = self.pvs.image.get() - data.shape = self.get_shape() - return data - - def get_shape(self): - width = self.pvs.width.get() - height = self.pvs.height.get() - shape = (width, height) - return shape - - def add_callback(self, callback, **kwargs): - @wraps(callback) - def wrapper(*args, value=None, **kwargs): - value.shape = self.get_shape() - return callback(*args, value=value, **kwargs) - return self.pvs.image.add_callback(callback=wrapper, with_ctrlvars=False, **kwargs) - - def disconnect(self): - for pv in self.pvs.__dict__.values(): #TODO - pv.disconnect() - - - -class Source(PV): - - def __init__(self, pvname, *args, **kwargs): - self.name = pvname - super().__init__(pvname, *args, **kwargs) - self.wait_for_connection() - - - -class Animation: - - def __init__(self, source, plot_func): - value = source.get() - self.plot = plot_func(value) - plt.suptitle(source.name) - source.add_callback(self.update) - plt.show() - source.disconnect() - - def update(self, value=None, **kwargs): - self.plot.set(value) - plt.draw() - - - -class PlotScalar: - - def __init__(self, *args, **kwargs): - self.fig, axs = plt.subplots(1, 2) - self.ax_time, self.ax_hist = ax_time, ax_hist = axs - - length = 100 -# init = [0] + [np.nan] * (length-2) + [0] -# self.cache = cache = deque(init, maxlen=length) - self.cache = cache = deque(maxlen=length) - self.x = x = range(length) - -# lines = ax_time.plot(x, cache) - lines = ax_time.plot([0], [0]) - self.plot_time = lines[0] #TODO: wtf? - -# plot_hist = ax_hist.bar(np.arange(0, 1, 100), [0] * 100) -# self.plot_hist = plot_hist - - - def set(self, value): - cache = self.cache - cache.append(value) - - x = range(len(cache)) - self.plot_time.set_data(x, cache) - nums = np.nan_to_num(cache) -# print(nums) - if min(nums) != max(nums): - self.ax_time.set_ylim(min(nums), max(nums)) - - self.ax_time.relim() - self.ax_time.autoscale_view() - - - nums = np.array(cache) - nums = nums[np.isfinite(nums)] - ys, xs = np.histogram(nums, bins="auto") -# print(ys) -# for rect, x, y in zip(self.plot_hist, xs, ys): -# rect.set_x(x) -# rect.set_height(y) - width = (xs[1:] - xs[:-1]).mean() - try: - self.plot_hist.remove() - color = self.plot_hist.patches[0].get_facecolor() - except: - color = None - - plot_hist = self.ax_hist.bar(xs[:-1], ys, color=color,align="edge", width=width) - self.plot_hist = plot_hist - - n = len(xs) - self.ax_hist.set_title(f"#bins: {n}") - - self.ax_hist.relim() - self.ax_hist.autoscale_view() - - -class Plot1D: - - def __init__(self, *args, **kwargs): - lines = plt.plot(*args, **kwargs) - self.plot = lines[0] #TODO: wtf? - - def set(self, value): - self.plot.set_ydata(value) - - -class Plot2D: - - def __init__(self, *args, **kwargs): - self.plot = plt.imshow(*args, **kwargs) - - def set(self, value): - self.plot.set_data(value) - - - -pvname = clargs.pvname -if pvname.endswith(":FPICTURE"): - print("Camera", pvname) - src = Camera(pvname) - fun = Plot2D -else: print("Source", pvname) src = Source(pvname) print(src.nelm) if src.nelm == 0: raise SystemExit(f"{src}: {src.value}") + if src.nelm == 1: print("Scalar") - fun = PlotScalar + fun = Plot0D else: - print("1D") + print("Curve") fun = Plot1D + return src, fun + + + +pvname = clargs.pvname +src, fun = decide_src_fun(pvname) Animation(src, fun) - - diff --git a/plot0d.py b/plot0d.py new file mode 100644 index 0000000..21ee664 --- /dev/null +++ b/plot0d.py @@ -0,0 +1,94 @@ +from collections import deque +import numpy as np +from matplotlib import pyplot as plt + + + +class Cache: + + def __init__(self, size=100): + self.data = deque(maxlen=size) + + def append(self, value): + self.data.append(value) + + @property + def x(self): + length = len(self.data) + return np.arange(length) + + @property + def y(self): + return np.array(self.data) + + @property + def xy(self): + return self.x, self.y + + + +def hist(values, bins="auto"): + ys, xs = np.histogram(values, bins="auto") + width = (xs[1:] - xs[:-1]).mean() + xs = xs[:-1] # remove closing edge + length = len(xs) + return xs, ys, width, length + + + + +class Plot0D: + + def __init__(self, *args, **kwargs): + self.fig, axs = plt.subplots(1, 2) + self.ax_time, self.ax_hist = ax_time, ax_hist = axs + + self.cache = Cache() + + lines = ax_time.plot([0], [0]) + self.plot_time = lines[0] #TODO: wtf? + + self.color = None # this will store the color after the first hist + + + + def set(self, value): + cache = self.cache + cache.append(value) + x, y = cache.xy + + + self.plot_time.set_data(x, y) + + ax_time = self.ax_time + + ymin, ymax = min(y), max(y) + if ymin != ymax: + ax_time.set_ylim(ymin, ymax) + + ax_time.relim() + ax_time.autoscale_view() + + + ax_hist = self.ax_hist + + try: + self.plot_hist.remove() + except: + pass + + xs, ys, width, length = hist(y) + + plot_hist = ax_hist.bar(xs, ys, align="edge", width=width, color=self.color) + self.plot_hist = plot_hist + + self.color = plot_hist.patches[0].get_facecolor() + + ax_hist.set_title(f"#bins: {length}") + + ax_hist.relim() + ax_hist.autoscale_view() + + + + diff --git a/plot1d.py b/plot1d.py new file mode 100644 index 0000000..3263587 --- /dev/null +++ b/plot1d.py @@ -0,0 +1,15 @@ +from matplotlib import pyplot as plt + + +class Plot1D: + + def __init__(self, *args, **kwargs): + lines = plt.plot(*args, **kwargs) + assert len(lines) == 1 + self.plot = lines[0] #TODO: wtf? + + def set(self, value): + self.plot.set_ydata(value) + + + diff --git a/plot2d.py b/plot2d.py new file mode 100644 index 0000000..94acbfe --- /dev/null +++ b/plot2d.py @@ -0,0 +1,13 @@ +from matplotlib import pyplot as plt + + +class Plot2D: + + def __init__(self, *args, **kwargs): + self.plot = plt.imshow(*args, **kwargs) + + def set(self, value): + self.plot.set_data(value) + + + diff --git a/plots.py b/plots.py new file mode 100644 index 0000000..e3649ca --- /dev/null +++ b/plots.py @@ -0,0 +1,6 @@ + +from plot0d import Plot0D +from plot1d import Plot1D +from plot2d import Plot2D + + diff --git a/sources.py b/sources.py new file mode 100644 index 0000000..60a1814 --- /dev/null +++ b/sources.py @@ -0,0 +1,78 @@ +from functools import wraps +from types import SimpleNamespace +from epics import PV + + +class Source(PV): + """ + PV, but waits for connection on creation + """ + + def __init__(self, pvname, *args, **kwargs): + self.name = pvname + super().__init__(pvname, *args, **kwargs) + self.wait_for_connection() + + + +class PVCollection: + """ + Creates all PVs, then waits for connection + Disconnects all PVs + """ + + def __init__(self, name, **kwargs): + self.name = name +# self.pvnames = SimpleNamespace(**kwargs) + self.pv_dict = pv_dict = {k: PV(v) for k, v in kwargs.items()} + for pv in pv_dict.values(): + pv.wait_for_connection() + self.pvs = SimpleNamespace(**pv_dict) + + def __str__(self): + return self.name + + def disconnect(self): + for pv in self.pv_dict.values(): #TODO is it better to use: pv_dict -> pvs.__dict__ + pv.disconnect() + + + +class Camera(PVCollection): + """ + Expects: pvname = "NAME:FPICTURE" + Also creates NAME:WIDTH and NAME:HEIGHT + """ + + #TODO: could also accept base as argument, and also append ":FPICTURE" + + def __init__(self, pvname): + assert pvname.endswith(":FPICTURE") + base = pvname.rsplit(":", 1)[0] + super().__init__( + base, + image = pvname, + width = base + ":WIDTH", + height = base + ":HEIGHT" + ) + + def get(self): + data = self.pvs.image.get() + data.shape = self.get_shape() + return data + + def get_shape(self): + width = self.pvs.width.get() + height = self.pvs.height.get() + shape = (width, height) + return shape + + def add_callback(self, callback, **kwargs): + @wraps(callback) + def wrapper(*args, value=None, **kwargs): + value.shape = self.get_shape() + return callback(*args, value=value, **kwargs) + return self.pvs.image.add_callback(callback=wrapper, with_ctrlvars=False, **kwargs) + + +