--- jupytext: formats: ipynb,md:myst text_representation: extension: .md format_name: myst format_version: 0.13 jupytext_version: 1.10.3 kernelspec: display_name: Python 3 language: python name: python3 --- # Notebook Client +++ # Demonstration +++ We start with a simple demonstration of a completed application. Here we launch both the server and the client on the same server. ```{code-cell} ipython3 %%javascript IPython.OutputArea.prototype._should_scroll = function(lines) { return false; } ``` ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot from super_hydro.clients import notebook notebook.run(network_server=True, run_server=False, tracer_particles=0) ``` ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot from super_hydro.clients import notebook notebook.run(model='gpe.BEC', network_server=False, tracer_particles=0, Nx=256//4, Ny=256//4, cylinder=True) ``` ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot from super_hydro.clients import notebook notebook.run(model='gpe.BEC', network_server=True, Nx=256//4, Ny=256//4, cylinder=True) ``` ## Initial Stages ```{code-cell} ipython3 import numpy as np import time from matplotlib import cm from super_hydro.clients import canvas_widget canvas_widget.display_js() canvas = canvas_widget.Canvas(width=521, height=100) display(canvas) tic = time.time() for n in range(10): canvas.rgba = cm.viridis(np.random.random((100, 100)), bytes=True) n/(time.time()-tic) ``` ```{code-cell} ipython3 canvas.width canvas.height = 512 ``` ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot from importlib import reload from super_hydro.physics import helpers;reload(helpers) from super_hydro.physics import gpe;reload(gpe) from super_hydro.physics import gpe2;reload(gpe2) from super_hydro.contexts import NoInterrupt from super_hydro.server import server; reload(server) from super_hydro.clients import notebook; reload(notebook); reload(notebook.widgets) #notebook.run(model='gpe.BEC', Nx=256//4, Ny=256//4, cylinder=True) #notebook.run(model='gpe2.SOC2', Nx=256//4, Ny=256//4) notebook.run(model='gpe.BECFlow', Nx=32, Ny=32) #notebook.run(run_server=False) ``` ```{code-cell} ipython3 from mmf_setup.set_path import hgroot import io import numpy as np from ipywidgets import FloatSlider, Image import PIL.Image A = (np.random.random((100, 100, 3))*255).astype('uint8') img = PIL.Image.fromarray(A) b = io.BytesIO() img.save(b, 'jpeg') from super_hydro import widgets as w img = w.ipywidgets.Image(value=b.getvalue()) img.layout.object_fit = 'scale_down' img.layout.width = "100%" img.layout.width = "100%" box = w.ipywidgets.Box([img])#, layout=dict(width='100%', height='100%')) w.VBox([ w.FloatSlider(), w.HBox([ box, w.FloatSlider(orientation='vertical'), ])]) ``` ```{code-cell} ipython3 tic = time.perf_counter() time.sleep(0.1) time.perf_counter() - tic ``` ```{code-cell} ipython3 app.run() ``` # Design +++ We base the notebook client on the [`IPyWidget`](https://ipywidgets.readthedocs.io/en/stable/) library. This now has support on [Google's CoLaboratory]() +++ This notebook provides a web-based client using matplotlib. ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot from importlib import reload from super_hydro.clients import notebook; reload(notebook); from super_hydro import widgets as w;reload(w) ``` ```{code-cell} ipython3 import time def draw1(d): IPython.display.display(PIL.Image.fromarray(d)) fig = plt.figure() img = plt.imshow(d) def draw2(d): img.set_data(d) IPython.display.display(fig) n = 0 tic = time.time() while True: draw2(d) n += 1 print("{}fps".format(n/(time.time()-tic))) IPython.display.clear_output(wait=True) ``` ```{code-cell} ipython3 import IPython.display import PIL.Image from io import StringIO #Use 'jpeg' instead of 'png' (~5 times faster) def showarray(a, fmt='jpeg'): f = StringIO() PIL.Image.fromarray(a[..., :3]).save(f, fmt) IPython.display.display(IPython.display.Image(data=f.getvalue())) showarray(d) ``` ```{code-cell} ipython3 d3 = d[..., :3] from PIL import Image Image.fromarray(d) ``` ```{code-cell} ipython3 d.shape ``` ```{code-cell} ipython3 import PIL PIL.__version__ ``` ```{code-cell} ipython3 slider.trait_names() ``` ```{code-cell} ipython3 import ipywidgets slider = ipywidgets.IntSlider(description="Hi") wid = ipywidgets.VBox([slider]) repr(wid) ``` ```{code-cell} ipython3 import traitlets class IntSlider(ipywidgets.IntSlider): name = traitlets.ObjectName().tag(sync=True) repr(IntSlider(name="a")) ``` ```{code-cell} ipython3 import ipywidgets all_widgets = [] for _w in ipywidgets.__dict__: w = getattr(ipywidgets, _w) if isinstance(w, type) and issubclass(w, ipywidgets.CoreWidget): all_widgets.append(_w) all_widgets ``` ```{code-cell} ipython3 ipywidgets.VBox.__bases__[0].__bases__ ``` ```{code-cell} ipython3 import ipywidgets from super_hydro import widgets as w; reload(w) repr(w.Text()) ``` ```{code-cell} ipython3 layout = w.VBox([ w.FloatLogSlider(name='cooling', base=10, min=-10, max=1, step=0.2, description='Cooling'), w.density]) ``` ```{code-cell} ipython3 layout.children ``` # Signals +++ We would like to enable the user to interrupt calculations with signals like SIGINT. A use case is starting a server in a background thread, then launching a client. This works generally if we use `mmfutils.contexts.NoInterrupt` and the client blocks, but if the client is driven by the javascript so that the cell does not block, then the handling of signals can be broken, specifically if the [`ipykernel`](https://github.com/ipython/ipykernel) package is used, as this resets the handlers between cells. The latest version of `mmfutils` deals with this by redefining the `pre_handler_hook` and `post_handler_hook` methods of the kernel. **References**: * [Signals broken in Python](https://bugs.python.org/issue13285) * [`cysignals`](https://cysignals.readthedocs.io/en/latest/pysignals.html): Might be a better option. * [IPyKernel issue](https://github.com/ipython/ipykernel/issues/328) +++ # Event Loop +++ ## No Event Loop +++ I have been having an issue with figuring out how to update the display with data from the server. The simplest solution is to simply get data from the server, display it, then wait. This, however, does not allow the user to update the controls. (In the following example, the moving the slider does not change the value seen in python.) ```{code-cell} ipython3 import ipywidgets import time from super_hydro.contexts import NoInterrupt frame = 0 _int = ipywidgets.IntSlider() _txt = ipywidgets.Label() _wid = ipywidgets.VBox([_int, _txt]) display(_wid) with NoInterrupt() as interrupted: while not interrupted: frame += 1 _txt.value = str(f"frame: {frame}, slider: {_int.value}") time.sleep(0.5) ``` ## Custom Event Loop +++ We can implement a custom event loop as long as we ensure that we call `kernel.do_one_iteration()` enough times. This will allow the widgets to work. ```{code-cell} ipython3 ip.kernel._poll_interval ``` ```{code-cell} ipython3 import IPython ip = IPython.get_ipython() import ipywidgets import time from mmf_setup.set_path import hgroot from super_hydro.contexts import NoInterrupt _int = ipywidgets.IntSlider() _txt = ipywidgets.Label() _wid = ipywidgets.VBox([_int, _txt]) display(_wid) with NoInterrupt() as interrupted: frame = 0 while not interrupted: frame += 1 _txt.value = str(f"frame: {frame}, slider: {_int.value}") for n in range(10): ip.kernel.do_one_iteration() ``` ## Browser Event Loop +++ Perhaps a better option is to allow the browser to trigger the updates when it is ready. This can be done in a couple of ways. The first is to use our own Canvas widget and register a callback and then use [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) to drive the updates. The second is to use the [Play](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#Play-(Animation)-widget) widget. +++ ### Canvas +++ With our Canvas widget, we can us [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) to drive the updates. This is probably the best solution as it saves energy if the browser tab is hidden. Here is the structure: 1. Use `requestAnimationFrame()` to send a message to python that the browser is ready for an update. 2. Wait until the browser performs an update. 3. Once the update is done, wait until the clock runs out (to limit the fps) then go to 1. +++ ### Play +++ One solution, is to use the `Play` widget, which sends update events as javascript messages. There are several issues with this: 1. All control of playback lies in the javascript. The python kernel does not block, so there is no way to interrupt from the kernel. 2. Infinite playback is not possible. 3. Sometimes javascript messages get lost (try making the interval very small). 4. Stop button and replay just change the counter. This is not ideal in terms of control. 5. Can't figure out how to autostart this. ```{code-cell} ipython3 import ipywidgets ipywidgets.Widget.close_all() import time from super_hydro.contexts import NoInterrupt frame = 0 _int = ipywidgets.IntSlider(description="interval") _txt = ipywidgets.Label() _play = ipywidgets.Play(interval=1) _wid = ipywidgets.VBox([_int, _txt, _play]) display(_wid) def update(change): global frame, _txt, _int frame += 1 _play.interval = _int.value _txt.value = str(f"frame: {frame}, slider: {_int.value}") _play.observe(update, names="value") ``` ## Threads +++ One option for full control is to have the display updates run in a separate thread. Then we can control this with buttons in the GUI, or allow the update to be driven by the server. ```{code-cell} ipython3 import time import threading import ipywidgets ipywidgets.Widget.close_all() from super_hydro.contexts import NoInterrupt _int = ipywidgets.IntSlider(value=100, description="interval") _running = ipywidgets.ToggleButton(value=True, icon='play', width="10px") _txt = ipywidgets.Label() _play = ipywidgets.HBox([_running]) _wid = ipywidgets.VBox([_int, _txt, _play]) display(_wid) def update(display): display.interval = _int.value _txt.value = str(f"frame: {display.frame}, slider: {_int.value}") class Display(object): def __init__(self, running, update): self.interval = 1 self.running = running self.update = update self.frame = 0 def run(self): while self.running.value: self.frame += 1 self.update(self) time.sleep(self.interval/1000) disp = Display(running=_running, update=update) thread = threading.Thread(target=disp.run) thread.start() ``` ```{code-cell} ipython3 import ipywidgets import numpy as np from matplotlib import cm import PIL.Image data = cm.viridis(np.random.random((32,32)), bytes=True) img = PIL.Image.fromarray(data) ipywidgets.Image(value=img._repr_png_(), format='png') ``` ```{code-cell} ipython3 %pylab inline from IPython.display import clear_output import PIL import time import ipywidgets as w from matplotlib import cm Nxy = (64*4, 64*4) data = np.random.seed(2) N = w.IntSlider(min=2, max=256, step=1) out = w.Output() msg = w.Text(value="Hi") display(w.VBox([N, out, msg])) with out: tic = time.time() for _n in range(100): data = np.random.random(Nxy) img = PIL.Image.fromarray(cm.viridis(data, bytes=True)) clear_output(wait=True) display(img) msg.value = f"fps={_n/(time.time()-tic)}" #i = w.Image(value=img._repr_png_(), format='png') ``` ```{code-cell} ipython3 l = w.Label(value="Hi") ``` ```{code-cell} ipython3 import ipywidgets ipywidgets.__version__ ``` ```{code-cell} ipython3 l = ipywidgets.Label(value="Hi") l.trait_names() import IPython, zmq IPython.__version__, zmq.__version__ ``` ```{code-cell} ipython3 import ipywidgets l = ipywidgets.Label(value="Hi") import trace, sys tracer = trace.Trace( #ignoredirs=[sys.prefix, sys.exec_prefix], trace=1, count=0) tracer.run('ipywidgets.Label(value="Hi")') ``` # Canvas +++ [Jupyter Canvas Widget](https://github.com/Who8MyLunch/Jupyter_Canvas_Widget) ```{code-cell} ipython3 import numpy as np N = 512 As = (np.random.random((10, N, N, 4))*255).astype(int) As[..., 3] = 255 ``` ```{code-cell} ipython3 from mmf_setup.set_path import hgroot import numpy as np import ipywidgets import IPython import jpy_canvas import time from super_hydro.contexts import NoInterrupt canvas = jpy_canvas.Canvas(data=As[0]) fps = ipywidgets.Label() display(ipywidgets.VBox([canvas, fps])) tic = time.time() frame = 0 with NoInterrupt() as interrupted: for A in As: if interrupted: break #A = np.random.random((N, N, 3)) canvas.data = A toc = time.time() frame += 1 fps.value = f"{frame/(toc-tic)}" ``` ```{code-cell} ipython3 %pylab inline from mmf_setup.set_path import hgroot import numpy as np import ipywidgets import IPython import fastcanvas import time N = 512 from super_hydro.contexts import NoInterrupt As = (np.random.random((10, N, N, 4)) * 255).astype('uint8') As[..., 3] = 255 canvas = fastcanvas.RawCanvas(data=As[0]) fps = ipywidgets.Label() display(ipywidgets.VBox([canvas, fps])) tic = time.time() frame = 0 with NoInterrupt() as interrupted: for A in As: if interrupted: break #A = np.random.random((N, N, 3)) canvas.data = A time.sleep(0.05) toc = time.time() frame += 1 fps.value = f"{frame/(toc-tic)}" print(time.time() - tic) ``` ```{code-cell} ipython3 import math cv2 = fastcanvas.RawCanvas() def gaussian(x, a, b, c, d=0): return a * math.exp(-(x - b)**2 / (2 * c**2)) + d height = 100 width = 600 gradient = np.zeros((height, width, 4), dtype='uint8') for x in range(gradient.shape[1]): r = int(gaussian(x, 158.8242, 201, 87.0739) + gaussian(x, 158.8242, 402, 87.0739)) g = int(gaussian(x, 129.9851, 157.7571, 108.0298) + gaussian(x, 200.6831, 399.4535, 143.6828)) b = int(gaussian(x, 231.3135, 206., 201.5447) + gaussian(x, 17.1017, 395.8819, 39.3148)) for y in range(gradient.shape[0]): gradient[y, x, :] = r, g, b, 255 cv2.data = gradient cv2 ``` # Density Widget ```{code-cell} ipython3 from density_widget import example example.HelloWorld() ``` # Custom Widgets +++ Here we build a custom widget. ```{code-cell} ipython3 %%html ``` ```{code-cell} ipython3 %%javascript var canvas = document.querySelector('canvas'); canvas.width = 200; canvas.height = 200; var c = canvas.getContext('2d'); c.fillRect(10, 10, 10, 10) ``` ```{code-cell} ipython3 import traitlets traitlets. ``` ```{code-cell} ipython3 from traitlets import Unicode, Bool, validate, TraitError, Instance, Int from ipywidgets import DOMWidget, register @register class Canvas(DOMWidget): _view_name = Unicode('CanvasView').tag(sync=True) _view_module = Unicode('canvas_widget').tag(sync=True) _view_module_version = Unicode('0.1.0').tag(sync=True) # Attributes width = Int(200, help="Width of canvas").tag(sync=True) height = Int(200, help="Height of canvas").tag(sync=True) ``` ```{code-cell} ipython3 %%javascript require.undef('canvas_widget'); define('canvas_widget', ["@jupyter-widgets/base"], function(widgets) { var CanvasView = widgets.DOMWidgetView.extend({ // Render the view. render: function() { this.canvas = document.createElement("canvas"); this.canvas.width = this.model.get('width'); this.canvas.height = this.model.get('height'); this.el.appendChild(this.canvas); // Python -> JavaScript update this.model.on('change:width', this.width_changed, this); this.model.on('change:height', this.height_changed, this); // JavaScript -> Python update //this.email_input.onchange = this.input_changed.bind(this); }, width_changed: function() { this.canvas.width = this.model.get('width'); }, height_changed: function() { this.canvas.height = this.model.get('height'); }, input_changed: function() { this.model.set('value', this.email_input.value); this.model.save_changes(); }, }); return { CanvasView: CanvasView }; }); ``` ```{code-cell} ipython3 Canvas() ``` ```{code-cell} ipython3 from traitlets import Unicode, Bool, validate, TraitError from ipywidgets import DOMWidget, register @register class Email(DOMWidget): _view_name = Unicode('EmailView').tag(sync=True) _view_module = Unicode('email_widget').tag(sync=True) _view_module_version = Unicode('0.1.0').tag(sync=True) # Attributes value = Unicode('example@example.com', help="The email value.").tag(sync=True) disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True) # Basic validator for the email value @validate('value') def _valid_value(self, proposal): if proposal['value'].count("@") != 1: raise TraitError('Invalid email value: it must contain an "@" character') if proposal['value'].count(".") == 0: raise TraitError('Invalid email value: it must contain at least one "." character') return proposal['value'] ``` ```{code-cell} ipython3 %%javascript require.undef('email_widget'); define('email_widget', ["@jupyter-widgets/base"], function(widgets) { var EmailView = widgets.DOMWidgetView.extend({ // Render the view. render: function() { this.email_input = document.createElement('input'); this.email_input.type = 'email'; this.email_input.value = this.model.get('value'); this.email_input.disabled = this.model.get('disabled'); this.el.appendChild(this.email_input); // Python -> JavaScript update this.model.on('change:value', this.value_changed, this); this.model.on('change:disabled', this.disabled_changed, this); // JavaScript -> Python update this.email_input.onchange = this.input_changed.bind(this); }, value_changed: function() { this.email_input.value = this.model.get('value'); }, disabled_changed: function() { this.email_input.disabled = this.model.get('disabled'); }, input_changed: function() { this.model.set('value', this.email_input.value); this.model.save_changes(); }, }); return { EmailView: EmailView }; }); ``` ```{code-cell} ipython3 Email() ``` # Logging ```{code-cell} ipython3 from mmf_setup.set_path import hgroot from importlib import reload import super_hydro.utils;reload(super_hydro.utils) l = super_hydro.utils.Logger() l.debug("Hi") ``` ```{code-cell} ipython3 ```