This commit is contained in:
2021-05-08 13:55:56 +02:00
parent 97a34ca997
commit 2492edda48
7 changed files with 256 additions and 172 deletions

31
animation.py Normal file
View File

@ -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()

191
caplot.py
View File

@ -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)

94
plot0d.py Normal file
View File

@ -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()

15
plot1d.py Normal file
View File

@ -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)

13
plot2d.py Normal file
View File

@ -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)

6
plots.py Normal file
View File

@ -0,0 +1,6 @@
from plot0d import Plot0D
from plot1d import Plot1D
from plot2d import Plot2D

78
sources.py Normal file
View File

@ -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)