diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb new file mode 100644 index 0000000..6e245eb --- /dev/null +++ b/sans/CreateConvertPlotExample.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data manpulation widgets toy examples" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from example_widgets import ProcessWidget, PlotWidget, fake_load, setup_code_hiding, allowed_dimensions, filepath_converter\n", + "import scipp as sc\n", + "import functools\n", + "\n", + "# Workaround for the fact that you can't raise directly in tertiary expressions.\n", + "def raise_(ex):\n", + " raise ex\n", + "\n", + "setup_code_hiding()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_widget = PlotWidget(globals())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Loading\n", + "Currently creates some example data rather than loading a file. The created file is a histogrammed time of flight data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_creation = ProcessWidget(globals(), fake_load, 'Load', {'filepath': filepath_converter}, {'filepath': 'run number or file name'})\n", + "data_creation.subscribe(plot_widget)\n", + "data_creation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data conversion\n", + "This converts one physical dimension to another the current conversions support in scipp.neutron are:\n", + "* tof -> dspacing, wavelength, E\n", + "* d-spacing -> tof\n", + "* wavelength -> Q, tof\n", + "* E -> tof\n", + "* Q -> wavelength" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "dim_converter = lambda dim : dim if dim in allowed_dimensions else raise_(ValueError(\n", + " f'{dim} not an allowed dimension. Supported dimensions are {list(dimesion_to_unit.keys())}'))\n", + "data_converter = lambda name : globals()[name] if (name in globals()) else raise_(ValueError(\n", + " f'{name} does not exist in notebook.'))\n", + "data_options = lambda : [\n", + " key for key, item in globals().items()\n", + " if isinstance(item, (sc.DataArray, sc.Dataset, sc.Variable))\n", + " ]\n", + "\n", + "data_conversion = ProcessWidget(globals(), \n", + " sc.neutron.convert, 'Convert', \n", + " {'data': data_converter,\n", + " 'from' : dim_converter, 'to': dim_converter},\n", + " options={'from' : allowed_dimensions,\n", + " 'to': allowed_dimensions,\n", + " 'data': data_options})\n", + "data_conversion.subscribe(plot_widget)\n", + "data_conversion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Can also specify just some inputs of a function graphically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Workaround because python doesn't like shadowing of from\n", + "kwargs = {'from':'tof'}\n", + "convert_tof = functools.partial(sc.neutron.convert, **kwargs)\n", + "data_conversion_tof = ProcessWidget(globals(), \n", + " convert_tof, 'Convert from tof', \n", + " {'data': data_converter,\n", + " 'to': dim_converter}, \n", + " options={'to': allowed_dimensions,\n", + " 'data': data_options})\n", + "data_conversion_tof.subscribe(plot_widget)\n", + "data_conversion_tof" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data plotting\n", + "Plots the data aquired by evaluating the expression entered." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "plot_widget" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/sans/example_widgets.py b/sans/example_widgets.py new file mode 100644 index 0000000..698a9cd --- /dev/null +++ b/sans/example_widgets.py @@ -0,0 +1,236 @@ +import ipywidgets as w +import scipp as sc +from scipp.plot import plot +import numpy as np +import IPython.display as disp +import os + +allowed_dimensions = ['tof', 'd-spacing', 'wavelength', 'E', 'Q'] + + +def filepath_converter(filename): + if filename.isdigit(): + # specified by run number + filename = 'LARMOR' + filename + + # We will probably want to be a bit cleverer in how we hande file + # finding and directory specifying. In particular browsing to files + # or directories may be a requirment. + directory = '/path/to/data/directory' + filepath = os.path.join(directory, filename) + # Commenting out as would always throw when loading fake files. + # if not os.path.exists(filepath): + # raise ValueError(f'File {}') + return filepath + + +class ProcessWidget(w.Box): + def __init__(self, + scope, + callable, + name, + inputs, + descriptions={}, + options={}): + super().__init__() + self.scope = scope + self.callable = callable + + self.input_widgets = [] + self.inputs = inputs + self.setup_input_widgets(descriptions, options) + + self.output = w.Text(placeholder='output name', + value='', + continuous_update=False) + + self.button = w.Button(description=name) + self.button.on_click(self._on_button_click) + + self.children = [ + w.HBox(self.input_widgets + [self.output, self.button]) + ] + + self.subscribers = [] + + def setup_input_widgets(self, descriptions, options): + for name in self.inputs.keys(): + placeholder = descriptions[name] if name in descriptions else name + option = options[name] if name in options else [] + option = option() if callable(option) else option + self.input_widgets.append( + w.Combobox(placeholder=placeholder, + continuous_update=False, + options=option)) + + def subscribe(self, observer): + self.subscribers.append(observer) + + def notify(self): + for observer in self.subscribers: + observer.update() + + def _on_button_click(self, b): + self.process() + self.notify() + + def _retrive_kwargs(self): + kwargs = { + name: converter(item.value) + for name, converter, item in zip( + self.inputs.keys(), self.inputs.values(), self.input_widgets) + } + return kwargs + + def process(self): + try: + kwargs = self._retrive_kwargs() + except ValueError as e: + print(f'Invalid inputs: {e}') + return + + if self.output.value: + output_name = self.output.value + else: + print(f'Invalid inputs: No output name specified') + return + + self.scope[output_name] = self.callable(**kwargs) + + +class PlotWidget(w.Box): + def __init__(self, scope): + super().__init__() + self.scope = scope + options = [ + key for key, item in globals().items() + if isinstance(item, (sc.DataArray, sc.Dataset)) + ] + self._data_selector = w.Combobox(placeholder='Data to plot', + options=options) + self._button = w.Button(description='Plot') + self._button.on_click(self._on_button_clicked) + self.plot_options = w.Output() + self.update_button = w.Button(description='Manual Update') + self.update_button.on_click(self.update) + self.output = w.Output(width='100%', height='100%') + self.children = [ + w.VBox([ + w.HBox([self.plot_options, self.update_button]), + w.HBox([self._data_selector, self._button]), self.output + ]) + ] + self.update() + + def _repr_html_(self, input_scope=None): + import inspect + # Is there a better way to get the scope? The `7` is hard-coded for the + # current IPython stack when calling _repr_html_ so this is bound to break. + scope = input_scope if input_scope else inspect.stack()[7][0].f_globals + from IPython import get_ipython + ipython = get_ipython() + out = '' + for category in ['Variable', 'DataArray', 'Dataset']: + names = ipython.magic(f"who_ls {category}") + out += f"
{category}s:"\ + f"({len(names)})" + for name in names: + html = sc.table_html.make_html(eval(name, scope)) + out += f"
"\ + f"{name}{html}
" + out += "
" + from IPython.core.display import display, HTML + display(HTML(out)) + + def _on_button_clicked(self, b): + self.output.clear_output() + with self.output: + disp.display(plot(eval(self._data_selector.value, self.scope))) + + def update(self, b=None): + options = [ + key for key, item in self.scope.items() + if isinstance(item, (sc.DataArray, sc.Dataset, sc.Variable)) + ] + self._data_selector.options = options + self.plot_options.clear_output() + with self.plot_options: + self._repr_html_(self.scope) + + +def fake_load(filepath): + dim = 'tof' + num_spectra = 10 + return sc.Dataset( + { + 'sample': + sc.Variable(['spectrum', dim], + values=np.random.rand(num_spectra, 10), + variances=0.1 * np.random.rand(num_spectra, 10)), + 'background': + sc.Variable(['spectrum', dim], + values=np.arange(0.0, num_spectra, 0.1).reshape( + num_spectra, 10), + variances=0.1 * np.random.rand(num_spectra, 10)) + }, + coords={ + dim: + sc.Variable([dim], values=np.arange(11.0), unit=sc.units.us), + 'spectrum': + sc.Variable(['spectrum'], + values=np.arange(num_spectra), + unit=sc.units.one), + 'source-position': + sc.Variable(value=np.array([1., 1., 10.]), + dtype=sc.dtype.vector_3_float64, + unit=sc.units.m), + 'sample-position': + sc.Variable(value=np.array([1., 1., 60.]), + dtype=sc.dtype.vector_3_float64, + unit=sc.units.m), + 'position': + sc.Variable(['spectrum'], + values=np.arange(3 * num_spectra).reshape( + num_spectra, 3), + unit=sc.units.m, + dtype=sc.dtype.vector_3_float64) + }) + + +#Method to hide code blocks taken from +#https://stackoverflow.com/questions/27934885/how-to-hide-code-from-cells-in-ipython-notebook-visualized-with-nbviewer +javascript_functions = {False: "hide()", True: "show()"} +button_descriptions = {False: "Show code", True: "Hide code"} + + +def toggle_code(state): + """ + Toggles the JavaScript show()/hide() function on the div.input element. + """ + + output_string = "" + output_args = (javascript_functions[state], ) + output = output_string.format(*output_args) + + disp.display(disp.HTML(output)) + + +def button_action(value): + """ + Calls the toggle_code function and updates the button description. + """ + + state = value.new + + toggle_code(state) + + value.owner.description = button_descriptions[state] + + +def setup_code_hiding(): + state = False + toggle_code(state) + + button = w.ToggleButton(state, description=button_descriptions[state]) + button.observe(button_action, "value") + return button