Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion slicops/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def cfg():
vue_port=(8008, pykern.pkasyncio.cfg_port, "port of Vue dev server"),
),
package_path=(
tuple(["slicops"]),
("slicops",),
tuple,
"Names of root packages that should be checked for codes and resources. Order is important, the first package with a matching code/resource will be used.",
),
Expand Down
21 changes: 17 additions & 4 deletions slicops/ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import pykern.fconf
import pykern.pkresource
import pykern.util
import slicops.config
import slicops.field
import slicops.ui_layout

Expand All @@ -32,10 +33,19 @@ def _check_raw(got):

step = "yaml"
try:
r = pykern.fconf.parse_all(
path or pykern.pkresource.file_path("sliclet"),
glob=f"{name}*",
)
n = f"sliclet/{name}.yaml"
r = pykern.fconf.Parser(
[
(
path.join(n)
if path
else pykern.pkresource.file_path(
n,
packages=slicops.config.cfg().package_path,
)
)
]
).result
_check_raw(r)
step = "fields"
self.fields = self.__parse(r[step], PKDict(), slicops.field.prototypes())
Expand Down Expand Up @@ -165,6 +175,9 @@ def _update(old, new):
def group_get(self, field, group, attr=None):
return self.__ctx.fields[field].group_get(group, attr)

def multi_get(self, fields):
return PKDict((k, self.__field(k).value_get()) for k in fields)

def multi_set(self, *args):
def _args():
if len(args) > 1:
Expand Down
2 changes: 1 addition & 1 deletion slicops/package_data/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
vue/
vue
ng-build/
29 changes: 25 additions & 4 deletions slicops/package_data/sliclet/screen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ fields:
Gaussian: gaussian
"Super Gaussian": super_gaussian
value: gaussian
images_to_average:
prototype: Enum
constraints:
choices:
- 1
- 2
- 3
- 4
- 5
ui:
label: Images to average
value: 1
plot:
prototype: Dict
ui:
Expand Down Expand Up @@ -56,6 +68,11 @@ fields:
ui:
css_kind: danger
label: Stop
save_to_file:
prototype: Button
ui:
css_kind: outline-info
label: Save to File
target_in_button:
prototype: Button
ui:
Expand All @@ -69,11 +86,12 @@ fields:

ui_layout:
- cols:
- css: col-sm-3
- css: col-lg-3
rows:
- beam_path
- camera
- pv
- images_to_average
- cell_group:
- start_button
- stop_button
Expand All @@ -82,13 +100,16 @@ ui_layout:
- target_in_button
- target_out_button
- target_status
- css: col-sm-9 col-xxl-7
- css: col-lg-9 col-xxl-7
rows:
- plot
- cols:
- css: col-sm-3
- css: col-lg-3
rows:
- curve_fit_method
- css: col-sm-3
- css: col-lg-3
rows:
- color_map
- css: col-md-1
rows:
- save_to_file
66 changes: 41 additions & 25 deletions slicops/pkcli/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,56 +24,67 @@ def ui_api(self, tcp_port=None, prod=False):
"""
from pykern import pkconfig, pkresource
from pykern.api import server
from slicops import config, ui_api, quest
from tornado import web
from slicops import config, quest, sliclet, ui_api

def _tcp_port():
return (
PKDict(tcp_port=pkconfig.parse_positive_int(tcp_port))
if tcp_port
else PKDict()
)
from tornado import web

def _uri_map(config):
if prod:
return [
(
# very specific so we control the name space
r"^/(assets/[^/.]+\.(?:css|js)|favicon.png|index.html|)$",
web.StaticFileHandler,
PKDict(
path=str(pkresource.file_path("vue")),
default_filename="index.html",
),
),
]
def _dev_uri_map(config):
return [
(
# send any non-api call to the proxy
rf"^(?!{config.api_uri}).*",
ProxyHandler,
_ProxyHandler,
PKDict(
proxy_url=f"http://localhost:{config.vue_port}",
),
),
]

def _prod_uri_map(config):
d = PKDict(
# TODO(robnagler) package_path
path=str(pkresource.file_path("vue")),
default_filename="index.html",
)
return [
# NOTE: StaticFileHandler requires match returns a group
(
# very specific so we control the name space
r"^/(assets/[^/.]+\.(?:css|js)|favicon.png|index.html|)$",
web.StaticFileHandler,
d,
),
(
# vue index.html is returned for sliclet URLs
rf"^/($|(?:{'|'.join(sliclet.names())})(?:$|/.*))",
_VueIndexHandler,
d,
),
]

def _tcp_port():
return (
PKDict(tcp_port=pkconfig.parse_positive_int(tcp_port))
if tcp_port
else PKDict()
)

c = config.cfg().ui_api.copy()
server.start(
attr_classes=quest.attr_classes(),
api_classes=ui_api.api_classes(),
http_config=c.pkupdate(
PKDict(
uri_map=_uri_map(c),
uri_map=_prod_uri_map(c) if prod else _dev_uri_map(c),
**_tcp_port(),
)
),
)


class ProxyHandler(tornado.web.RequestHandler):
class _ProxyHandler(tornado.web.RequestHandler):
def initialize(self, proxy_url, **kwargs):
super(ProxyHandler, self).initialize(**kwargs)
super().initialize(**kwargs)
self.http_client = tornado.httpclient.AsyncHTTPClient()
self.proxy_url = proxy_url

Expand All @@ -82,3 +93,8 @@ async def get(self):
self.set_status(r.code)
self.set_header("Content-Type", r.headers["Content-Type"])
self.write(r.body)


class _VueIndexHandler(tornado.web.StaticFileHandler):
def get_absolute_path(self, root, path, *args, **kwargs):
return super().get_absolute_path(root, self.default_filename)
105 changes: 105 additions & 0 deletions slicops/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,115 @@

from pykern.pkcollections import PKDict
from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp
import h5py
import numpy
import pykern.pkio
import scipy.optimize


class ImageSet:
"""Fits images, possibly averaging.

Can take arbitrary meta data, e.g. pv and it will be written by
`save_file`.

Args:
meta (PKDict): images_to_average, camera, curve_fit_method, pv

"""

def __init__(self, meta):
self.meta = meta
self._frames = []
self._timestamps = []
self._prev = None

def add_frame(self, frame, timestamp):
"""Add and update fit

Args:
frame (ndarray): new image
timestamp (datetime): time of frame
Returns:
PKDict: frame and fit or None if not enough frames
"""

def _mean():
if self.meta.images_to_average == 1:
return self._frames[-1]
return numpy.mean(self._frames, axis=0)

self._frames.append(frame)
self._timestamps.append(timestamp)
if len(self._frames) != self.meta.images_to_average:
return None
self._prev = PKDict(
fit=fit_image(_mean(), self.meta.curve_fit_method),
frames=self._frames,
timestamps=self._timestamps,
)
self._frames = []
self._timestamps = []
return self._prev.fit

def save_file(self, dir_path):
# TODO(robnagler) the naming is a bit goofy, possibly frames/{images,timestamps} and analysis.
"""Creates a hdf5 file with the structure::
/image Group
/frames Dataset {images_to_average, ysize, xsize}
/mean Dataset {ysize, xsize}
/timestamps Dataset {images_to_average}
/x Group
/fit Dataset {xsize}
/profile Dataset {xsize}
/y Group
/fit Dataset {ysize}
/profile Dataset {ysize}
/meta Group (camera, curve_fit_method, images_to_average, etc.)

Args:
dir_path (py.path): directory
"""

def _image_dim(meta_group, dim):
g = meta_group.create_group(dim)
f = self._prev.fit[dim]
g.create_dataset("profile", data=f.lineout)
if not f.fit.results:
return
g.attrs.update(f.fit.results)
g.create_dataset("fit", data=f.fit.fit_line)

def _meta(h5_file):
g = h5_file.create_group("meta")
g.attrs.update(self.meta)
g.create_dataset("frames", data=self._prev.frames)
g.create_dataset(
"timestamps",
data=[d.timestamp() for d in self._prev.timestamps],
)

def _path():
# TODO(robnagler) centralize timestamp format
t = self._prev.timestamps[-1]
rv = dir_path.join(
t.strftime("%Y-%m"),
f"{t.strftime('%Y%m%dT%H%M%SZ')}-{self.meta.camera}.h5",
)
rv.dirpath().ensure(dir=True)
return rv

def _writer(path):
with h5py.File(path, "w") as f:
_meta(f)
g = f.create_group("image")
g.create_dataset("mean", data=self._prev.fit.raw_pixels)
_image_dim(g, "x")
_image_dim(g, "y")

pykern.pkio.atomic_write(_path(), writer=_writer)


def fit_image(image, method):
"""Attemp an analytical fit for the sum along the x and y dimensions

Expand Down
Loading