Bokeh-serve client tutorial
For this tutorial, we will create a simple model which generates an image from sampled from a uniform distribution between the two scalar input variables. This model will be served using the lume-epics server and a bokeh client will be used to display simple sliders for controlling the image, and the image output.
Note:
The code for this example can be found in lume-epics/examples
Set up conda environment
$ conda create -n lume-epics-demo python=3.7
$ conda activate lume-epics-demo
Install lume-model and lume-epics from the conda-forge
:
$ conda install numpy lume-model lume-epics -c conda-forge
Create model
Create a new file named model.py
. Set up out model:
import numpy as np
from lume_model.variables import ScalarInputVariable, ImageOutputVariable
from lume_model.models import BaseModel
from lume_model.utils import save_variables
Next, define the demo model. Here, we define the input and output variables as keyword arguments. In order for the evaluate method to execute correctly, these passed variables must be dictionaries of variables with corresponding types and names. These could also be defined as class attributes.
class DemoModel(BaseModel):
def __init__(self, input_variables=None, output_variables=None):
self.input_variables = input_variables
self.output_variables = output_variables
def evaluate(self, input_variables):
self.output_variables["output1"].value = np.random.uniform(
self.input_variables["input1"].value, # lower dist bound
self.input_variables["input2"].value, # upper dist bound
(50,50)
)
return list(self.output_variables.values())
Now, we create a YAML file my_variables.yml
describing our input and output variables using the following format:
input_variables:
input1:
name: input1
type: scalar
default: 1
range: [0, 256]
input2:
name: input2
type: scalar
default: 2.0
range: [0, 256]
output_variables:
output1:
name: output1
type: image
x_label: "value1"
y_label: "value2"
axis_units: ["mm", "mm"]
x_min: 0
x_max: 10
y_min: 0
y_max: 10
Create server
First, we create a YAML file my_epics_config.yml
describing our EPICS configuration. The variable input1
will be served using Channel access using the pvname test:input1
. The variables input2
and output1
will be served using pvAccess.
input_variables:
input1:
pvname: test:input1
protocol: ca
input2:
pvname: test:input2
protocol: pva
output_variables:
output1:
pvname: test:output1
protocol: pva
Create a new file named server.py
. Import the DemoModel
, load the variables, and configure the server. Due to the multiprocess spawning of the application, the server must run inside the main conditional of the python script.
from examples.model import DemoModel
from lume_epics.epics_server import Server
from lume_model.utils import variables_from_yaml
from lume_epics.utils import config_from_yaml
# Server must run in main
if __name__ == "__main__":
with open("my_variables.yml", "r") as f:
input_variables, output_variables = variables_from_yaml(f)
with open("my_epics_config.yml", "r") as f:
epics_config = config_from_yaml(f)
# pass the input + output variable to initialize the classs
model_kwargs = {
"input_variables": input_variables,
"output_variables": output_variables
}
server = Server(
DemoModel,
epics_config,
model_kwargs=model_kwargs
)
# monitor = False does not loop in main thread
server.start(monitor=True)
Set up the client
Create a new file named client.py
. Add the following imports:
from bokeh.io import curdoc
from bokeh import palettes
from bokeh.layouts import column, row
from bokeh.models import LinearColorMapper
from lume_epics.client.controller import Controller
from lume_model.utils import variables_from_yaml
from lume_epics.utils import config_from_yaml
from lume_epics.client.widgets.plots import ImagePlot
from lume_epics.client.widgets.controls import build_sliders
from lume_epics.client.controller import Controller
Set up the Controller
for interfacing with EPICS process variables:
Load variables and epics configuraiton:
# load variables
with open("my_variables.yml", "r") as f:
input_variables, output_variables = variables_from_yaml(f)
# load epics config
with open("my_epics_config.yml", "r") as f:
epics_config = config_from_yaml(f)
controller = Controller(epics_config)
Prepare sliders:
# use all input variables for slider
# prepare as list for rendering
input_variables = list(input_variables.values())
# build sliders
sliders = build_sliders(input_variables, controller)
Setup the image output variable:
output_variables = list(output_variables.values())
# create image plot
image_plot = ImagePlot(output_variables, controller)
# build plot using a bokeh color map
pal = palettes.viridis(256)
color_mapper = LinearColorMapper(palette=pal, low=0, high=256)
image_plot.build_plot(color_mapper=color_mapper)
The image plot will require a callback to continually update the plot to display the lates process variables. Here we define the callback function:
# Set up image update callback
def image_update_callback():
image_plot.update()
Render the application using bokeh curdoc()
function. The image_plot object's plot
attribute must be used in formatting:
curdoc().title = "Demo App"
curdoc().add_root(
row(
column(sliders, width=350), column(image_plot.plot)
)
)
curdoc().add_periodic_callback(image_update_callback, 250)
Run demo
Now, open two terminal windows and navigate to the directory with your demo files. Activate the lume-epics-demo
environment. In the first, execute the command:
$ python example/server.py
In the second, serve the bokeh client using the command:
$ bokeh serve --show example/client.py
A browser window will display the user interface. Both the client and server may be terminated with keyboard interrupt (Ctrl+C
).