diff --git a/cacher.py b/cacher.py new file mode 100644 index 0000000..8f0365d --- /dev/null +++ b/cacher.py @@ -0,0 +1,38 @@ +from collections import deque +from time import time + + +class Cacher: + + def __init__(self): + self.caches = {} + + def add(self, name, size=100): + self.caches[name] = cache = Cache(size=size) + return cache + + + +class Cache: + + def __init__(self, size=100): + self.times = deque(maxlen=size) + self.values = deque(maxlen=size) + + def append_callback(self, value=None, **kwargs): + self.append(value) + + def append(self, value): + self.times.append(time()) + self.values.append(value) + + @property + def xy(self): + return self.times, self.values + + + +cacher = Cacher() + + + diff --git a/kabuki.py b/kabuki.py new file mode 100644 index 0000000..6586fbe --- /dev/null +++ b/kabuki.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +from random import choice + +from bokeh.plotting import curdoc +from bokeh.layouts import column + +from cacher import cacher +from plot0d import Plot0D +from plot2d import Plot2D +from sources import Camera, Source + + +name0 = "MTEST:RAND0" + +src0 = Source(name0) + +cache0 = cacher.add(name0, size=100) +cache0.append(src0.get()) + +src0.add_callback(cache0.append_callback) + + + +name2 = "MTEST:CHAN-IMAGE:FPICTURE" + +src2 = Camera(name2) + +cache2 = cacher.add(name2, size=1) +cache2.append(src2.get()) + +src2.add_callback(cache2.append_callback) + + + +p0 = Plot0D() +p2 = Plot2D() + +def update(): + p0.set(*cache0.xy) + p2.set(cache2.values[0]) + + +doc = curdoc() +doc.title = choice("👺👹") + " kabuki" +doc.add_periodic_callback(update, 1000) + +plt = column( + p0.fig, + p2.fig +) + +doc.add_root(plt) + + + diff --git a/plot0d.py b/plot0d.py new file mode 100644 index 0000000..0362fb0 --- /dev/null +++ b/plot0d.py @@ -0,0 +1,74 @@ +import numpy as np + +from bokeh.plotting import figure +from bokeh.layouts import row +from bokeh.models import ColumnDataSource + + +class Plot0D: + + def __init__(self): + self.timeseries = timeseries = TimeSeries() + self.histo = histo = Histo() + self.fig = row(timeseries.fig, histo.fig) + + def set(self, times, values): + self.timeseries.set(times, values) + self.histo.set(values) + + + +class TimeSeries: + + def __init__(self): + data = { + "x": [], + "y": [] + } + + self.source = source = ColumnDataSource(data=data) + + self.fig = fig = figure() + fig.step(x="x", y="y", source=source, mode="after") + fig.circle(x="x", y="y", source=source) + + + def set(self, times, values): + data = { + "x": times, + "y": values + } + + self.source.data.update(data) + + + +class Histo: + + def __init__(self): + data = { + "left": [], + "right": [], + "top": [] + } + + self.source = source = ColumnDataSource(data) + + self.fig = fig = figure() + fig.quad(left="left", right="right", top="top", bottom=0, source=source) + + + def set(self, values): + data = hist(values) + self.source.data.update(data) + + + +def hist(values, bins="auto"): + counts, edges = np.histogram(values, bins="auto") + left = edges[:-1] + right = edges[1:] + return dict(left=left, right=right, top=counts) + + + diff --git a/plot2d.py b/plot2d.py new file mode 100644 index 0000000..f4adef9 --- /dev/null +++ b/plot2d.py @@ -0,0 +1,24 @@ +import numpy as np + +from bokeh.plotting import figure +from bokeh.models import ColumnDataSource + + +class Plot2D: + + def __init__(self, *args, **kwargs): + data = { + "image": [np.zeros((1, 1))] + } + + self.source = source = ColumnDataSource(data=data) + + self.fig = fig = figure() + fig.image(source=source, x=0, y=0, dw=1, dh=1, palette="Spectral11") + + + def set(self, image): + self.source.data.update(image=[image]) + + + diff --git a/sources.py b/sources.py new file mode 100644 index 0000000..75ebaef --- /dev/null +++ b/sources.py @@ -0,0 +1,81 @@ +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" + ) + self.pvs.image.auto_monitor = True + + 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() + width = int(round(width)) + height = int(round(height)) + shape = (height, width) + 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) + + +