From 136580789ad9cf2994f6654aa971129706dfdf3c Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Tue, 29 Sep 2020 14:40:25 +0100 Subject: [PATCH 01/10] Added graphical reduction toy example --- sans/graphical_reduction.ipynb | 158 ++++++++++++++++++++++++++++++ sans/graphical_reduction.py | 169 +++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 sans/graphical_reduction.ipynb create mode 100644 sans/graphical_reduction.py diff --git a/sans/graphical_reduction.ipynb b/sans/graphical_reduction.ipynb new file mode 100644 index 0000000..fad75db --- /dev/null +++ b/sans/graphical_reduction.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import scipp as sc\n", + "from scipp.plot import plot\n", + "import numpy as np\n", + "import dataconfig # run make_config.py to create this\n", + "from graphical_reduction import q1d, load_data, run_reduction \n", + "import ipywidgets as w\n", + "import IPython.display as disp\n", + "import warnings\n", + "from convert_helpers import ConvertWidget, TransformationWidget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class ReductionWidget(w.Box):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.sample = w.Text(description='Sample', value='49338')\n", + " self.sample_trans = w.Text(description='Sample trans', value='49339')\n", + " self.background = w.Text(description='Background', value='49334')\n", + " self.background_trans = w.Text(description='Backg... trans', value='49335')\n", + " self.load_button = w.Button(description='Load')\n", + " self.load_button.on_click(self._on_load_button_clicked)\n", + " self._button = w.Button(description='Process')\n", + " self._button.on_click(self._on_process_button_clicked)\n", + " self.output = w.Output(width='100%', height='100%')\n", + " self.children = [w.VBox([w.HBox([self.sample, self.load_button]), w.HBox([self.sample_trans, self._button]), self.background, self.background_trans, self.output])]\n", + " \n", + " def _on_process_button_clicked(self, b):\n", + " self.output.clear_output()\n", + " sample_run_number = self.sample.value\n", + " sample_transmission_run_number = self.sample_trans.value\n", + " background_run_number = self.background.value\n", + " background_transmission_run_number = self.background_trans.value\n", + " \n", + " with self.output:\n", + " run_reduction(sample_run_number, sample_transmission_run_number, background_transmission_run_number,\n", + " background_run_number, loaded_data, path, moderator_file, direct_beam_file,\n", + " l_collimation, r1, r2, dr)\n", + " \n", + " \n", + " def _on_load_button_clicked(self, b):\n", + " global loaded_data\n", + " self.output.clear_output()\n", + " run_list = [self.sample.value, self.sample_trans.value, self.background.value, self.background_trans.value]\n", + " with self.output:\n", + " load_data(run_list, loaded_data, path)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class PlotWidget(w.Box):\n", + " def __init__(self):\n", + " super().__init__()\n", + " options = [key for key, item in globals().items() if isinstance(item, (sc.DataArray, sc.Dataset))]\n", + " self._data_selector = w.Combobox(placeholder='Data to plot', options=options)\n", + " self._button = w.Button(description='Plot')\n", + " self._button.on_click(self._on_button_clicked)\n", + " self.output = w.Output(width='100%', height='100%')\n", + " self.children = [w.VBox([w.HBox([self._data_selector, self._button]), self.output])]\n", + " \n", + " def _on_button_clicked(self, b):\n", + " self.output.clear_output()\n", + " data_dict = {key: globals()[key] for key in \"\".join(self._data_selector.value.split()).split(',')}\n", + " ds = sc.Dataset(data_dict)\n", + " with self.output:\n", + " plot(ds)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set reduction settings.\n", + "path = dataconfig.data_root\n", + "direct_beam_file = 'DirectBeam_20feb_full_v3.dat'\n", + "moderator_file = 'ModeratorStdDev_TS2_SANS_LETexptl_07Aug2015.txt'\n", + "l_collimation = sc.Variable(value=5.0, unit=sc.units.m)\n", + "r2 = sc.Variable(value=4.0824829046386295/1000, unit=sc.units.m) # sample aperture radius\n", + "r1 = sc.Variable(value=14.433756729740645/1000, unit=sc.units.m) # source aperture radius\n", + "dr = sc.Variable(value=8.0/1000, unit=sc.units.m) # virtual ring width on detector\n", + "wavelength_bins = sc.Variable(\n", + " dims=['wavelength'],\n", + " unit=sc.units.angstrom,\n", + " values=np.geomspace(0.9, 13.5, num=110))\n", + "loaded_data = {}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "disp.display(ReductionWidget())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "disp.display(PlotWidget())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/graphical_reduction.py b/sans/graphical_reduction.py new file mode 100644 index 0000000..388ba90 --- /dev/null +++ b/sans/graphical_reduction.py @@ -0,0 +1,169 @@ +# Functions used in graphical reduction demo. Lifted from reduction.ipynb +import scipp as sc +import numpy as np +from contrib import to_bin_centers, to_bin_edges, map_to_bins + +def load_larmor(run_number): + return sc.neutron.load(filename=f'{path}/LARMOR000{run_number}.nxs') + +def load_rkh(filename): + return sc.neutron.load( + filename=filename, + mantid_alg='LoadRKH', + mantid_args={'FirstColumnValue':'Wavelength'}) + +def apply_masks(data): + tof = data.coords['tof'] + data.masks['bins'] = sc.less(tof['tof',1:], 1500.0 * sc.units.us) | \ + (sc.greater(tof['tof',:-1], 17500.0 * sc.units.us) & \ + sc.less(tof['tof',1:], 19000.0 * sc.units.us)) + pos = sc.neutron.position(data) + x = sc.geometry.x(pos) + y = sc.geometry.y(pos) + data.masks['beam-stop'] = sc.less(sc.sqrt(x*x+y*y), 0.045 * sc.units.m) + data.masks['tube-ends'] = sc.greater(sc.abs(x), 0.36 * sc.units.m) # roughly all det IDs listed in original + #MaskDetectorsInShape(Workspace=maskWs, ShapeXML=self.maskingPlaneXML) # irrelevant tiny wedge? + + +def background_mean(data, dim, begin, end): + coord = data.coords[dim] + assert (coord.unit == begin.unit) and (coord.unit == end.unit) + i = np.searchsorted(coord, begin.value) + j = np.searchsorted(coord, end.value) + 1 + return data - sc.mean(data[dim, i:j], dim) + + +def transmission_fraction(incident_beam, transmission, wavelength_bins): + # Approximation based on equations in CalculateTransmission documentation + # TODO proper implementation of mantid.CalculateTransmission + return (transmission / transmission) * (incident_beam / incident_beam) + #CalculateTransmission(SampleRunWorkspace=transWsTmp, + # DirectRunWorkspace=transWsTmp, + # OutputWorkspace=outWsName, + # IncidentBeamMonitor=1, + # TransmissionMonitor=4, RebinParams='0.9,-0.025,13.5', + # FitMethod='Polynomial', + # PolynomialOrder=3, OutputUnfittedData=True) + +def extract_monitor_background(data, begin, end, wavelength_bins): + background = background_mean(data, 'tof', begin, end) + del background.coords['sample-position'] # ensure unit conversion treats this a monitor + background = sc.neutron.convert(background, 'tof', 'wavelength') + background = sc.rebin(background, 'wavelength', wavelength_bins) + return background + + +def setup_transmission(data, wavelength_bins): + incident_beam = extract_monitor_background(data['spectrum', 0], 40000.0*sc.units.us, 99000.0*sc.units.us, wavelength_bins) + transmission = extract_monitor_background(data['spectrum', 3], 88000.0*sc.units.us, 98000.0*sc.units.us, wavelength_bins) + return transmission_fraction(incident_beam, transmission, wavelength_bins) + + +def solid_angle(data): + # TODO proper solid angle + # [0.0117188,0.0075,0.0075] bounding box size + pixel_size = 0.0075 * sc.units.m + pixel_length = 0.0117188 * sc.units.m + L2 = sc.neutron.l2(data) + return (pixel_size * pixel_length) / (L2 * L2) + + +def q_resolution(lam_edges, moderator, d, l_collimation, r1, r2, dr): + moderator = sc.rebin(moderator, 'wavelength', lam_edges) + + d_lam = lam_edges['wavelength', 1:] - lam_edges['wavelength', :-1] # bin widths + lam = 0.5 * (lam_edges['wavelength', 1:] + lam_edges['wavelength', :-1]) # bin centres + + l2 = sc.neutron.l2(d) + theta = sc.neutron.scattering_angle(d) + inv_l3 = (l_collimation + l2) / (l_collimation * l2) + + # Terms in Mildner and Carpenter equation. + # See https://docs.mantidproject.org/nightly/algorithms/TOFSANSResolutionByPixel-v1.html + a1 = (r1/l_collimation)*(r1/l_collimation) * 3.0 + a2 = (r2*inv_l3)*(r2*inv_l3) * 3.0 + a3 = (dr/l2) * (dr/l2) + q_sq = 4.0 * np.pi * sc.sin(theta) * sc.reciprocal(lam) # keep in wav dim to prevent broadcast + q_sq *= q_sq + + tof = moderator.data.copy() + tof.variances = None # shortcoming of Mantid or Mantid converter? + tof.rename_dims({'wavelength':'tof'}) # TODO overly restrictive check in convert (rename) + tof.unit = sc.units.us + mod = sc.Dataset(coords={ + 'tof':tof, + 'position':sample.coords['position'], + 'source_position':sample.coords['source_position'], + 'sample_position':sample.coords['sample_position']}) + s = sc.neutron.convert(mod, 'tof', 'wavelength').coords['wavelength'] + + std_dev_lam_sq = (d_lam * d_lam)/12 + s * s + std_dev_lam_sq *= sc.reciprocal(lam * lam) + f = (4 * np.pi * np.pi) * sc.reciprocal(12 * lam * lam) + + return sc.DataArray(f * (a1 + a2 + a3) + (q_sq * std_dev_lam_sq), + coords={'wavelength':lam, 'spectrum':d.coords['spectrum'].copy()}) + + +def q1d(data, transmission, l_collimation, r1, r2, dr, wavelength_bins, direct_beam_file_path, moderator_file_path, wavelength_bands=None): + transmission = setup_transmission(transmission, wavelength_bins) + data = data.copy() + apply_masks(data) + data = sc.neutron.convert(data, 'tof', 'wavelength', out=data) + data = sc.rebin(data, 'wavelength', wavelength_bins) + + monitor = data.coords['monitor1'].value + monitor = background_mean(monitor, 'tof', 40000.0*sc.units.us, 99000.0*sc.units.us) + monitor = sc.neutron.convert(monitor, 'tof', 'wavelength', out=monitor) + monitor = sc.rebin(monitor, 'wavelength', wavelength_bins) + + # this factor seems to be a fudge factor. Explanation pending. + data *= 100.0 / 176.71458676442586 + + # Setup direct beam and normalise to monitor. I.e. adjust for efficiency of detector across the wavelengths. + direct_beam = load_rkh(filename=direct_beam_file_path) + # This would work assuming that there is a least one wavelength point per bin + #direct_beam = sc.groupby(direct_beam, 'wavelength', bins=monitor.coords['wavelength']).mean('wavelength') + direct_beam = map_to_bins(direct_beam, 'wavelength', monitor.coords['wavelength']) + direct_beam = monitor * transmission * direct_beam + + # Estimate qresolution function + moderator = load_rkh(filename=moderator_file_path) + to_bin_edges(moderator, 'wavelength') + + q_bins = sc.Variable( + dims=['Q'], + unit=sc.units.one/sc.units.angstrom, + values=np.geomspace(0.008, 0.6, num=55)) + + d = sc.Dataset({'data':data, 'norm':solid_angle(data)*direct_beam}) + #dq_sq = q_resolution(d.coords['wavelength'], moderator, d, l_collimation, r1, r2, dr) + to_bin_centers(d, 'wavelength') + d = sc.neutron.convert(d, 'wavelength', 'Q', out=d) # TODO no gravity yet + + + if wavelength_bands is None: + d = sc.histogram(d, q_bins) + d = sc.sum(d, 'spectrum') + I = d['data']/d['norm'] + else: + # Cut range into number of requested bands + n_band = int(wavelength_bands) + n_bin = len(wavelength_bins.values)-1 + bounds = np.arange(n_bin)[::n_bin//n_band] + bounds[-1] = n_bin + slices = [slice(i, j) for i,j in zip(bounds[:-1],bounds[1:])] + bands = None + # Reduce by wavelength slice + for s in slices: + band = sc.histogram(d['Q', s].copy(), q_bins) # TODO fix scipp to avoid need for copy + band = sc.sum(band, 'spectrum') + bands = sc.concatenate(bands, band, 'wavelength') if bands is not None else band + # Add coord for wavelength edges of bands + bands.coords['wavelength'] = sc.Variable( + dims=['wavelength'], + unit=sc.units.angstrom, + values=np.take(wavelength_bins.values, bounds)) + I = bands['data']/bands['norm'] + + return I From 99dbf00f760d935506721e88f9d93f8095f9d169 Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Thu, 1 Oct 2020 16:34:30 +0100 Subject: [PATCH 02/10] Added dataless example --- sans/CreateConvertPlotExample.ipynb | 129 ++++++++++++++++++++ sans/convert_helpers.py | 179 ++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 sans/CreateConvertPlotExample.ipynb create mode 100644 sans/convert_helpers.py diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb new file mode 100644 index 0000000..0cd0720 --- /dev/null +++ b/sans/CreateConvertPlotExample.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data manpulation widgets toy examples\n", + "Currently to get this to work need to update the \\_repr_html\\_() method of the scip module to the following so that an optional scope can be passed in. This seemed like the nicest way to control the scope from within the widgets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _repr_html_(input_scope=None):\n", + " import inspect\n", + " # Is there a better way to get the scope? The `7` is hard-coded for the\n", + " # current IPython stack when calling _repr_html_ so this is bound to break.\n", + " scope = input_scope if input_scope else inspect.stack()[7][0].f_globals\n", + " from IPython import get_ipython\n", + " ipython = get_ipython()\n", + " out = ''\n", + " for category in ['Variable', 'DataArray', 'Dataset']:\n", + " names = ipython.magic(f\"who_ls {category}\")\n", + " out += f\"
{category}s:\"\\\n", + " f\"({len(names)})\"\n", + " for name in names:\n", + " html = make_html(eval(name, scope))\n", + " out += f\"
\"\\\n", + " f\"{name}{html}
\"\n", + " out += \"
\"\n", + " from IPython.core.display import display, HTML\n", + " display(HTML(out))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from convert_helpers import ConvertWidget, DataCreationWidget, PlotWidget\n", + "import IPython.display as disp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data creation\n", + "Simple widget to create some pesudo real histogram data with sample, background and positional data. Currently only supports creating time of flight data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_creation = DataCreationWidget(globals())\n", + "disp.display(data_creation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data conversion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "data_conversion = ConvertWidget(globals())\n", + "disp.display(data_conversion)" + ] + }, + { + "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 = PlotWidget(globals())\n", + "data_creation.subscribe(plot_widget)\n", + "data_conversion.subscribe(plot_widget)\n", + "disp.display(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/convert_helpers.py b/sans/convert_helpers.py new file mode 100644 index 0000000..adf9f8a --- /dev/null +++ b/sans/convert_helpers.py @@ -0,0 +1,179 @@ +# Helpers for adding convert in a graphical format +import ipywidgets as w +import scipp as sc +from scipp.plot import plot +import numpy as np +import IPython.display as disp + + +# Cheat dict to list allowed conversions. Should ideally get this from C++ code +allowed_conversions = { + 'tof': ('dspacing', 'wavelength', 'E'), + 'd-spacing': ('tof', ), + 'wavelength': ('Q', 'tof'), + 'E': ('tof', ), + 'Q': ('wavelength', ), + '': tuple() +} + +dimesion_to_unit = { + 'tof': sc.units.us, + 'd-spacing': sc.units.angstrom, + 'wavelength': sc.units.angstrom, + 'E': sc.units.meV, + 'Q': sc.units.one/sc.units.angstrom +} + + +class ConvertWidget(w.Box): + def __init__(self, scope): + super().__init__() + self.scope = scope + self.input = w.Text(placeholder='Input', value='', continuous_update=False) + self.convert_from = w.Combobox(placeholder='from', disabled=False, continuous_update=False) + self.convert_to = w.Combobox(placeholder='to', options=[], disabled=False, continuous_update=False) + self.output = w.Text(placeholder='Output', value='', continuous_update=False) + self.button = w.Button(description='convert') + self.button.on_click(self._on_convert) + self.input.observe(self._on_input_changed) + self.convert_from.observe(self._on_from_changed) + self.convert_to.observe(self._on_to_changed) + self.children = [ + w.HBox([ + self.input, self.convert_from, self.convert_to, self.output, + self.button + ]) + ] + + self.subscribers = [] + + def subscribe(self, observer): + self.subscribers.append(observer) + + def notify(self): + for observer in self.subscribers: + observer.update() + + def _on_convert(self, b): + output_name = self.output.value + input_name = self.input.value + self.scope[output_name] = sc.neutron.convert(self.scope[input_name], self.convert_from.value, self.convert_to.value) + self.notify() + + def _on_input_changed(self, change_dict): + if change_dict['name'] == 'value': + try: + input = self.scope[change_dict['new']] + self.convert_from.options = [key for key in allowed_conversions.keys() if key in input.coords] + except KeyError: + print(f'{change_dict["owner"].value} does not exist.') + self.convert_from.disabled = False + self.update_output() + + def _on_from_changed(self, change_dict): + if change_dict['name'] == 'value': + allowed_dimensions = change_dict['owner'].options if change_dict['owner'].options else allowed_conversions.keys() + if change_dict['new'] not in allowed_dimensions: + print(f"{change_dict['new']} not a recognised conversion dimension. Dimensions in data are {allowed_dimensions}") + return + self.convert_to.options = allowed_conversions[change_dict['new']] + self.update_output() + + def _on_to_changed(self, change_dict): + if change_dict['name'] == 'value': + allowed_dimensions = change_dict['owner'].options if change_dict['owner'].options else allowed_conversions.keys() + if change_dict['new'] not in allowed_dimensions: + print(f"{change_dict['new']} not a recognised conversion dimension. Dimensions supported are {allowed_dimensions}") + return + self.update_output() + + def update_output(self, changes=None): + if not self.output.value and self.convert_to.value and self.input.value: + self.output.value = self.input.value + '_' + self.convert_to.value + + +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()#HTML() + with self.plot_options: + sc._repr_html_(self.scope) + 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])] + + 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): + self.plot_options.clear_output() + with self.plot_options: + sc._repr_html_(self.scope) + + +class DataCreationWidget(w.Box): + def __init__(self, scope): + super().__init__() + self.name = w.Text(placeholder='name') + self.dims = w.Combobox(placeholder='dims', + options=('tof',), ensure_option=False) + self.num_spectra = w.Text(placeholder='num spectra', value='') + self.button = w.Button(description='create') + self.button.on_click(self._create_data) + self.scope = scope + self.children = [ + w.HBox([self.name, self.dims, self.num_spectra, self.button]) + ] + self.subscribers = [] + + def subscribe(self, observer): + self.subscribers.append(observer) + + def notify(self): + for observer in self.subscribers: + observer.update() + + def _create_data(self, b): + dim = self.dims.value + if dim not in self.dims.options: + print(f'Please enter a valid data dimension currently supported dimensions are {self.dims.options}') + return + num_spectra = int(self.num_spectra.value) + output_name = self.name.value + self.scope[output_name] = 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=dimesion_to_unit[dim]), + '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) + }) + self.notify() From 4ab1f96cb43e4379dee49f20b68837f6a30e258d Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Fri, 2 Oct 2020 15:17:35 +0100 Subject: [PATCH 03/10] Tidied up sans example --- sans/convert_helpers.py | 184 +++++++++++++++++++++++++++------ sans/graphical_reduction.ipynb | 91 ++-------------- sans/graphical_reduction.py | 38 +++++++ 3 files changed, 201 insertions(+), 112 deletions(-) diff --git a/sans/convert_helpers.py b/sans/convert_helpers.py index adf9f8a..20a3247 100644 --- a/sans/convert_helpers.py +++ b/sans/convert_helpers.py @@ -4,7 +4,7 @@ from scipp.plot import plot import numpy as np import IPython.display as disp - +from graphical_reduction import q1d, run_reduction, load_and_return # Cheat dict to list allowed conversions. Should ideally get this from C++ code allowed_conversions = { @@ -21,7 +21,7 @@ 'd-spacing': sc.units.angstrom, 'wavelength': sc.units.angstrom, 'E': sc.units.meV, - 'Q': sc.units.one/sc.units.angstrom + 'Q': sc.units.one / sc.units.angstrom } @@ -29,10 +29,19 @@ class ConvertWidget(w.Box): def __init__(self, scope): super().__init__() self.scope = scope - self.input = w.Text(placeholder='Input', value='', continuous_update=False) - self.convert_from = w.Combobox(placeholder='from', disabled=False, continuous_update=False) - self.convert_to = w.Combobox(placeholder='to', options=[], disabled=False, continuous_update=False) - self.output = w.Text(placeholder='Output', value='', continuous_update=False) + self.input = w.Text(placeholder='Input', + value='', + continuous_update=False) + self.convert_from = w.Combobox(placeholder='from', + disabled=False, + continuous_update=False) + self.convert_to = w.Combobox(placeholder='to', + options=[], + disabled=False, + continuous_update=False) + self.output = w.Text(placeholder='Output', + value='', + continuous_update=False) self.button = w.Button(description='convert') self.button.on_click(self._on_convert) self.input.observe(self._on_input_changed) @@ -40,7 +49,7 @@ def __init__(self, scope): self.convert_to.observe(self._on_to_changed) self.children = [ w.HBox([ - self.input, self.convert_from, self.convert_to, self.output, + self.input, self.convert_from, self.convert_to, self.output, self.button ]) ] @@ -57,14 +66,19 @@ def notify(self): def _on_convert(self, b): output_name = self.output.value input_name = self.input.value - self.scope[output_name] = sc.neutron.convert(self.scope[input_name], self.convert_from.value, self.convert_to.value) + self.scope[output_name] = sc.neutron.convert(self.scope[input_name], + self.convert_from.value, + self.convert_to.value) self.notify() def _on_input_changed(self, change_dict): if change_dict['name'] == 'value': try: input = self.scope[change_dict['new']] - self.convert_from.options = [key for key in allowed_conversions.keys() if key in input.coords] + self.convert_from.options = [ + key for key in allowed_conversions.keys() + if key in input.coords + ] except KeyError: print(f'{change_dict["owner"].value} does not exist.') self.convert_from.disabled = False @@ -72,18 +86,24 @@ def _on_input_changed(self, change_dict): def _on_from_changed(self, change_dict): if change_dict['name'] == 'value': - allowed_dimensions = change_dict['owner'].options if change_dict['owner'].options else allowed_conversions.keys() + allowed_dimensions = change_dict['owner'].options if change_dict[ + 'owner'].options else allowed_conversions.keys() if change_dict['new'] not in allowed_dimensions: - print(f"{change_dict['new']} not a recognised conversion dimension. Dimensions in data are {allowed_dimensions}") + print( + f"{change_dict['new']} not a recognised conversion dimension. Dimensions in data are {allowed_dimensions}" + ) return self.convert_to.options = allowed_conversions[change_dict['new']] self.update_output() def _on_to_changed(self, change_dict): if change_dict['name'] == 'value': - allowed_dimensions = change_dict['owner'].options if change_dict['owner'].options else allowed_conversions.keys() + allowed_dimensions = change_dict['owner'].options if change_dict[ + 'owner'].options else allowed_conversions.keys() if change_dict['new'] not in allowed_dimensions: - print(f"{change_dict['new']} not a recognised conversion dimension. Dimensions supported are {allowed_dimensions}") + print( + f"{change_dict['new']} not a recognised conversion dimension. Dimensions supported are {allowed_dimensions}" + ) return self.update_output() @@ -96,24 +116,37 @@ 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) + 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()#HTML() - with self.plot_options: - sc._repr_html_(self.scope) + self.plot_options = w.Output() #HTML() 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.children = [ + w.VBox([ + w.HBox([self.plot_options, self.update_button]), + w.HBox([self._data_selector, self._button]), self.output + ]) + ] + self.update() + 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: sc._repr_html_(self.scope) @@ -124,7 +157,8 @@ def __init__(self, scope): super().__init__() self.name = w.Text(placeholder='name') self.dims = w.Combobox(placeholder='dims', - options=('tof',), ensure_option=False) + options=('tof', ), + ensure_option=False) self.num_spectra = w.Text(placeholder='num spectra', value='') self.button = w.Button(description='create') self.button.on_click(self._create_data) @@ -144,7 +178,9 @@ def notify(self): def _create_data(self, b): dim = self.dims.value if dim not in self.dims.options: - print(f'Please enter a valid data dimension currently supported dimensions are {self.dims.options}') + print( + f'Please enter a valid data dimension currently supported dimensions are {self.dims.options}' + ) return num_spectra = int(self.num_spectra.value) output_name = self.name.value @@ -162,18 +198,104 @@ def _create_data(self, b): }, coords={ dim: - sc.Variable([dim], values=np.arange(11.0), unit=dimesion_to_unit[dim]), + sc.Variable([dim], + values=np.arange(11.0), + unit=dimesion_to_unit[dim]), '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) + '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) }) self.notify() + + +class ReductionWidget(w.Box): + def __init__(self, scope): + super().__init__() + self.sample = w.Text(description='Sample', value='49338') + self.sample_trans = w.Text(description='Sample trans', value='49339') + self.background = w.Text(description='Background', value='49334') + self.background_trans = w.Text(description='Backg... trans', + value='49335') + self.load_button = w.Button(description='Load') + self.load_button.on_click(self._on_load_button_clicked) + self._button = w.Button(description='Process') + self._button.on_click(self._on_process_button_clicked) + self.output = w.Output(width='100%', height='100%') + self.children = [ + w.VBox([ + w.HBox([self.sample, self.load_button]), + w.HBox([self.sample_trans, self._button]), self.background, + self.background_trans, self.output + ]) + ] + self.scope = scope + self.subscribers = [] + + def _on_process_button_clicked(self, b): + self._on_load_button_clicked(None) + sample = self.scope[self.data_name(self.sample.value)] + sample_trans = self.scope[self.data_name(self.sample_trans.value)] + background = self.scope[self.data_name(self.background.value)] + background_trans = self.scope[self.data_name(self.background_trans.value)] + moderator_file_path = f'{self.scope["path"]}/{self.scope["moderator_file"]}' + direct_beam_file_path = f'{self.scope["path"]}/{self.scope["direct_beam_file"]}' + l_collimation = self.scope['l_collimation'] + r1 = self.scope['r1'] + r2 = self.scope['r2'] + dr = self.scope['dr'] + wavelength_bins = self.scope['wavelength_bins'] + + with self.output: + reduced, sample_q1d, background_q1d = run_reduction( + sample, sample_trans, background, background_trans, + moderator_file_path, direct_beam_file_path, l_collimation, r1, + r2, dr, wavelength_bins) + + #Need to think up a better naming scheme for reduced data + reduced_name = f'reduced_sans{self.sample.value}' + sample_name = f'sample_sans{self.sample.value}' + background_name = f'background{self.background.value}' + + self.scope[reduced_name] = reduced + self.scope[sample_name] = sample_q1d + self.scope[background_name] = background_q1d + self.notify() + + def _on_load_button_clicked(self, b): + self.output.clear_output() + run_list = [ + self.sample.value, self.sample_trans.value, self.background.value, + self.background_trans.value + ] + run_list = [item for item in run_list if self.data_name(item) not in self.scope.keys()] + with self.output: + for run in run_list: + data = load_and_return(run, self.scope['path']) + name = self.data_name(run) + self.scope[name] = data + self.notify() + + def data_name(self, run): + return self.scope['instrument'] + str(run) + + def subscribe(self, observer): + self.subscribers.append(observer) + + def notify(self): + for observer in self.subscribers: + observer.update() diff --git a/sans/graphical_reduction.ipynb b/sans/graphical_reduction.ipynb index fad75db..9f1a357 100644 --- a/sans/graphical_reduction.ipynb +++ b/sans/graphical_reduction.ipynb @@ -10,11 +10,9 @@ "from scipp.plot import plot\n", "import numpy as np\n", "import dataconfig # run make_config.py to create this\n", - "from graphical_reduction import q1d, load_data, run_reduction \n", "import ipywidgets as w\n", "import IPython.display as disp\n", - "import warnings\n", - "from convert_helpers import ConvertWidget, TransformationWidget" + "from convert_helpers import PlotWidget, ReductionWidget" ] }, { @@ -23,74 +21,10 @@ "metadata": {}, "outputs": [], "source": [ - "class ReductionWidget(w.Box):\n", - " def __init__(self):\n", - " super().__init__()\n", - " self.sample = w.Text(description='Sample', value='49338')\n", - " self.sample_trans = w.Text(description='Sample trans', value='49339')\n", - " self.background = w.Text(description='Background', value='49334')\n", - " self.background_trans = w.Text(description='Backg... trans', value='49335')\n", - " self.load_button = w.Button(description='Load')\n", - " self.load_button.on_click(self._on_load_button_clicked)\n", - " self._button = w.Button(description='Process')\n", - " self._button.on_click(self._on_process_button_clicked)\n", - " self.output = w.Output(width='100%', height='100%')\n", - " self.children = [w.VBox([w.HBox([self.sample, self.load_button]), w.HBox([self.sample_trans, self._button]), self.background, self.background_trans, self.output])]\n", - " \n", - " def _on_process_button_clicked(self, b):\n", - " self.output.clear_output()\n", - " sample_run_number = self.sample.value\n", - " sample_transmission_run_number = self.sample_trans.value\n", - " background_run_number = self.background.value\n", - " background_transmission_run_number = self.background_trans.value\n", - " \n", - " with self.output:\n", - " run_reduction(sample_run_number, sample_transmission_run_number, background_transmission_run_number,\n", - " background_run_number, loaded_data, path, moderator_file, direct_beam_file,\n", - " l_collimation, r1, r2, dr)\n", - " \n", - " \n", - " def _on_load_button_clicked(self, b):\n", - " global loaded_data\n", - " self.output.clear_output()\n", - " run_list = [self.sample.value, self.sample_trans.value, self.background.value, self.background_trans.value]\n", - " with self.output:\n", - " load_data(run_list, loaded_data, path)\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class PlotWidget(w.Box):\n", - " def __init__(self):\n", - " super().__init__()\n", - " options = [key for key, item in globals().items() if isinstance(item, (sc.DataArray, sc.Dataset))]\n", - " self._data_selector = w.Combobox(placeholder='Data to plot', options=options)\n", - " self._button = w.Button(description='Plot')\n", - " self._button.on_click(self._on_button_clicked)\n", - " self.output = w.Output(width='100%', height='100%')\n", - " self.children = [w.VBox([w.HBox([self._data_selector, self._button]), self.output])]\n", - " \n", - " def _on_button_clicked(self, b):\n", - " self.output.clear_output()\n", - " data_dict = {key: globals()[key] for key in \"\".join(self._data_selector.value.split()).split(',')}\n", - " ds = sc.Dataset(data_dict)\n", - " with self.output:\n", - " plot(ds)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set reduction settings.\n", + "# Set reduction settings. This is currently quite a brittle implimentation where we require certain variables to exist\n", + "# in the global namespace in order for the reduction to work.\n", "path = dataconfig.data_root\n", + "instrument = 'LARMOR'\n", "direct_beam_file = 'DirectBeam_20feb_full_v3.dat'\n", "moderator_file = 'ModeratorStdDev_TS2_SANS_LETexptl_07Aug2015.txt'\n", "l_collimation = sc.Variable(value=5.0, unit=sc.units.m)\n", @@ -100,8 +34,7 @@ "wavelength_bins = sc.Variable(\n", " dims=['wavelength'],\n", " unit=sc.units.angstrom,\n", - " values=np.geomspace(0.9, 13.5, num=110))\n", - "loaded_data = {}" + " values=np.geomspace(0.9, 13.5, num=110))" ] }, { @@ -112,7 +45,8 @@ }, "outputs": [], "source": [ - "disp.display(ReductionWidget())" + "reduction_widget = ReductionWidget(globals())\n", + "disp.display(reduction_widget)" ] }, { @@ -123,15 +57,10 @@ }, "outputs": [], "source": [ - "disp.display(PlotWidget())" + "plot_widget = PlotWidget(globals())\n", + "reduction_widget.subscribe(plot_widget)\n", + "disp.display(plot_widget)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/sans/graphical_reduction.py b/sans/graphical_reduction.py index 388ba90..006e47e 100644 --- a/sans/graphical_reduction.py +++ b/sans/graphical_reduction.py @@ -2,6 +2,44 @@ import scipp as sc import numpy as np from contrib import to_bin_centers, to_bin_edges, map_to_bins +import ipywidgets as w + +def load_and_return(run, path): + print(f'Loading data for run {run}:') + return sc.neutron.load(filename=f'{path}/LARMOR000{run}.nxs') + +def run_reduction(sample, sample_trans, background, + background_trans, moderator_file_path, direct_beam_file_path, + l_collimation, r1, r2, dr, wavelength_bins): + + dtype = sample.coords['position'].dtype + sample_pos_offset = sc.Variable(value=[0.0, 0.0, 0.30530], unit=sc.units.m, dtype=dtype) + bench_pos_offset = sc.Variable(value=[0.0, 0.001, 0.0], unit=sc.units.m, dtype=dtype) + for item in [sample, sample_trans, background, background_trans]: + item.coords['sample-position'] += sample_pos_offset + item.coords['position'] += bench_pos_offset + + print('Reducing sample data:') + sample_q1d = q1d(data=sample, transmission=sample_trans, + l_collimation=l_collimation, r1=r1, r2=r2, dr=dr, wavelength_bins=wavelength_bins, + direct_beam_file_path=direct_beam_file_path, moderator_file_path=moderator_file_path, + wavelength_bands=None) + + print('Reducing background data:') + background_q1d = q1d(data=background, transmission=background_trans, + l_collimation=l_collimation, r1=r1, r2=r2, dr=dr, wavelength_bins=wavelength_bins, + direct_beam_file_path=direct_beam_file_path, moderator_file_path=moderator_file_path, + wavelength_bands=None) + + reduced = sample_q1d - background_q1d + + # reduced.coords['Transmission'] = sc.Variable( + # value=f'{sample_transmission_run_number}_trans_sample_0.9_13.5_unfitted') + # reduced.coords['TransmissionCan'] = sc.Variable( + # value=f'{background_transmission_run_number}_trans_can_0.9_13.5_unfitted') + + print('Finished Reduction') + return reduced, sample_q1d, background_q1d def load_larmor(run_number): return sc.neutron.load(filename=f'{path}/LARMOR000{run_number}.nxs') From 87ac205ecba53dc4a2b0fc5c3f7db114cdc27b8e Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Tue, 6 Oct 2020 07:45:55 +0200 Subject: [PATCH 04/10] Avoid need to patch scipp for widget examples --- sans/convert_helpers.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/sans/convert_helpers.py b/sans/convert_helpers.py index 20a3247..7cc7d47 100644 --- a/sans/convert_helpers.py +++ b/sans/convert_helpers.py @@ -136,6 +136,27 @@ def __init__(self, scope): ] 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: @@ -149,7 +170,7 @@ def update(self, b=None): self._data_selector.options = options self.plot_options.clear_output() with self.plot_options: - sc._repr_html_(self.scope) + self._repr_html_(self.scope) class DataCreationWidget(w.Box): From 2a6e59ef8788c1562db4f3669c9e10c3a3a30e79 Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Tue, 6 Oct 2020 14:38:22 +0100 Subject: [PATCH 05/10] Shifted to generic widgets --- sans/CreateConvertPlotExample.ipynb | 193 ++++++++++++++++----- sans/convert_helpers.py | 250 +++++++++++++--------------- 2 files changed, 261 insertions(+), 182 deletions(-) diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 0cd0720..92aee22 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -4,63 +4,131 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Data manpulation widgets toy examples\n", - "Currently to get this to work need to update the \\_repr_html\\_() method of the scip module to the following so that an optional scope can be passed in. This seemed like the nicest way to control the scope from within the widgets." + "## Data manpulation widgets toy examples" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4388b50f6ba24d51b37595888b0e8e7a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "ToggleButton(value=False, description='Show code')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "def _repr_html_(input_scope=None):\n", - " import inspect\n", - " # Is there a better way to get the scope? The `7` is hard-coded for the\n", - " # current IPython stack when calling _repr_html_ so this is bound to break.\n", - " scope = input_scope if input_scope else inspect.stack()[7][0].f_globals\n", - " from IPython import get_ipython\n", - " ipython = get_ipython()\n", - " out = ''\n", - " for category in ['Variable', 'DataArray', 'Dataset']:\n", - " names = ipython.magic(f\"who_ls {category}\")\n", - " out += f\"
{category}s:\"\\\n", - " f\"({len(names)})\"\n", - " for name in names:\n", - " html = make_html(eval(name, scope))\n", - " out += f\"
\"\\\n", - " f\"{name}{html}
\"\n", - " out += \"
\"\n", - " from IPython.core.display import display, HTML\n", - " display(HTML(out))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from convert_helpers import ConvertWidget, DataCreationWidget, PlotWidget\n", - "import IPython.display as disp" + "from convert_helpers import TransformWidget, PlotWidget, fake_load, LoadWidget\n", + "import scipp as sc\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, HTML\n", + "import functools\n", + "\n", + "javascript_functions = {False: \"hide()\", True: \"show()\"}\n", + "button_descriptions = {False: \"Show code\", True: \"Hide code\"}\n", + "\n", + "def toggle_code(state):\n", + "\n", + " \"\"\"\n", + " Toggles the JavaScript show()/hide() function on the div.input element.\n", + " \"\"\"\n", + "\n", + " output_string = \"\"\n", + " output_args = (javascript_functions[state],)\n", + " output = output_string.format(*output_args)\n", + "\n", + " display(HTML(output))\n", + "\n", + "\n", + "def button_action(value):\n", + "\n", + " \"\"\"\n", + " Calls the toggle_code function and updates the button description.\n", + " \"\"\"\n", + "\n", + " state = value.new\n", + "\n", + " toggle_code(state)\n", + "\n", + " value.owner.description = button_descriptions[state]\n", + "\n", + "\n", + "state = False\n", + "toggle_code(state)\n", + "\n", + "button = widgets.ToggleButton(state, description = button_descriptions[state])\n", + "button.observe(button_action, \"value\")\n", + "\n", + "button" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Data creation\n", - "Simple widget to create some pesudo real histogram data with sample, background and positional data. Currently only supports creating time of flight data." + "### Data Loading\n", + "Currently creates some example data rather than loading anything." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aab5b27941a74ff2913442aa6adf8069", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "LoadWidget(children=(HBox(children=(Text(value='', placeholder='run number'), Button(description='Load', style…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "data_creation = DataCreationWidget(globals())\n", - "disp.display(data_creation)" + "data_creation = LoadWidget(globals(), fake_load, \n", + " 'path/to/directory/', \n", + " 'run number', lambda run: 'LARMOR' + run)\n", + "data_creation" ] }, { @@ -72,14 +140,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "48120446980b4f768fb73b28e4011441", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "TransformWidget(children=(HBox(children=(Text(value='', continuous_update=False, placeholder='data'), Text(val…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "data_conversion = ConvertWidget(globals())\n", - "disp.display(data_conversion)" + "data_conversion = TransformWidget(globals(), \n", + " sc.neutron.convert, 'Convert', \n", + " {'data': lambda name : globals()[name],\n", + " 'from' : str, 'to': str})\n", + "data_conversion" ] }, { @@ -92,16 +178,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3a6fc5358834479fb47587b871bec4d5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "PlotWidget(children=(VBox(children=(HBox(children=(Output(), Button(description='Manual Update', style=ButtonS…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plot_widget = PlotWidget(globals())\n", "data_creation.subscribe(plot_widget)\n", "data_conversion.subscribe(plot_widget)\n", - "disp.display(plot_widget)" + "plot_widget" ] } ], diff --git a/sans/convert_helpers.py b/sans/convert_helpers.py index 7cc7d47..2f10ad9 100644 --- a/sans/convert_helpers.py +++ b/sans/convert_helpers.py @@ -5,17 +5,9 @@ import numpy as np import IPython.display as disp from graphical_reduction import q1d, run_reduction, load_and_return +import os # Cheat dict to list allowed conversions. Should ideally get this from C++ code -allowed_conversions = { - 'tof': ('dspacing', 'wavelength', 'E'), - 'd-spacing': ('tof', ), - 'wavelength': ('Q', 'tof'), - 'E': ('tof', ), - 'Q': ('wavelength', ), - '': tuple() -} - dimesion_to_unit = { 'tof': sc.units.us, 'd-spacing': sc.units.angstrom, @@ -24,38 +16,36 @@ 'Q': sc.units.one / sc.units.angstrom } - -class ConvertWidget(w.Box): - def __init__(self, scope): +class TransformWidget(w.Box): + def __init__(self, scope, callable, name, inputs): super().__init__() self.scope = scope - self.input = w.Text(placeholder='Input', - value='', - continuous_update=False) - self.convert_from = w.Combobox(placeholder='from', - disabled=False, - continuous_update=False) - self.convert_to = w.Combobox(placeholder='to', - options=[], - disabled=False, - continuous_update=False) + self.input_widgets = [] + self.input_converters = [] + self.callback = callable + + self.setup_inputs(inputs) + self.output = w.Text(placeholder='Output', + value='', + continuous_update=False) + self.output = w.Text(placeholder='Output', value='', continuous_update=False) - self.button = w.Button(description='convert') - self.button.on_click(self._on_convert) - self.input.observe(self._on_input_changed) - self.convert_from.observe(self._on_from_changed) - self.convert_to.observe(self._on_to_changed) + self.button = w.Button(description=name) + self.button.on_click(self._on_button_click) self.children = [ - w.HBox([ - self.input, self.convert_from, self.convert_to, self.output, - self.button - ]) + w.HBox(self.input_widgets + [self.output, self.button]) ] self.subscribers = [] + def setup_inputs(self, inputs): + for name, converter in inputs.items(): + self.input_widgets.append( + w.Text(placeholder=name, continuous_update=False)) + self.input_converters.append(converter) + def subscribe(self, observer): self.subscribers.append(observer) @@ -63,54 +53,16 @@ def notify(self): for observer in self.subscribers: observer.update() - def _on_convert(self, b): + def _on_button_click(self, b): + kwargs = { + item.placeholder: converter(item.value) + for item, converter in zip(self.input_widgets, + self.input_converters) + } output_name = self.output.value - input_name = self.input.value - self.scope[output_name] = sc.neutron.convert(self.scope[input_name], - self.convert_from.value, - self.convert_to.value) + self.scope[output_name] = self.callback(**kwargs) self.notify() - def _on_input_changed(self, change_dict): - if change_dict['name'] == 'value': - try: - input = self.scope[change_dict['new']] - self.convert_from.options = [ - key for key in allowed_conversions.keys() - if key in input.coords - ] - except KeyError: - print(f'{change_dict["owner"].value} does not exist.') - self.convert_from.disabled = False - self.update_output() - - def _on_from_changed(self, change_dict): - if change_dict['name'] == 'value': - allowed_dimensions = change_dict['owner'].options if change_dict[ - 'owner'].options else allowed_conversions.keys() - if change_dict['new'] not in allowed_dimensions: - print( - f"{change_dict['new']} not a recognised conversion dimension. Dimensions in data are {allowed_dimensions}" - ) - return - self.convert_to.options = allowed_conversions[change_dict['new']] - self.update_output() - - def _on_to_changed(self, change_dict): - if change_dict['name'] == 'value': - allowed_dimensions = change_dict['owner'].options if change_dict[ - 'owner'].options else allowed_conversions.keys() - if change_dict['new'] not in allowed_dimensions: - print( - f"{change_dict['new']} not a recognised conversion dimension. Dimensions supported are {allowed_dimensions}" - ) - return - self.update_output() - - def update_output(self, changes=None): - if not self.output.value and self.convert_to.value and self.input.value: - self.output.value = self.input.value + '_' + self.convert_to.value - class PlotWidget(w.Box): def __init__(self, scope): @@ -156,7 +108,6 @@ def _repr_html_(self, input_scope=None): from IPython.core.display import display, HTML display(HTML(out)) - def _on_button_clicked(self, b): self.output.clear_output() with self.output: @@ -173,22 +124,82 @@ def update(self, b=None): self._repr_html_(self.scope) -class DataCreationWidget(w.Box): - def __init__(self, scope): +def fake_load(filename): + 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=dimesion_to_unit[dim]), + '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) + }) + + +class LoadWidget(w.Box): + def __init__( + self, + scope, + load_callable, + directory, + filename_descriptor, + filename_converter, + inputs = {} + ): super().__init__() - self.name = w.Text(placeholder='name') - self.dims = w.Combobox(placeholder='dims', - options=('tof', ), - ensure_option=False) - self.num_spectra = w.Text(placeholder='num spectra', value='') - self.button = w.Button(description='create') - self.button.on_click(self._create_data) + self.directory = directory + self.filename = w.Text(placeholder=filename_descriptor) + self.filename_converter = filename_converter + + self.input_widgets = [] + self.input_converters = [] + self.callback = load_callable + + self.setup_inputs(inputs) + + self.button = w.Button(description='Load') + self.button.on_click(self._on_button_clicked) self.scope = scope self.children = [ - w.HBox([self.name, self.dims, self.num_spectra, self.button]) + w.HBox([self.filename] + self.input_widgets + [self.button]) ] self.subscribers = [] + def setup_inputs(self, inputs): + for name, converter in inputs.items(): + self.input_widgets.append( + w.Text(placeholder=name, continuous_update=False)) + self.input_converters.append(converter) + def subscribe(self, observer): self.subscribers.append(observer) @@ -196,54 +207,17 @@ def notify(self): for observer in self.subscribers: observer.update() - def _create_data(self, b): - dim = self.dims.value - if dim not in self.dims.options: - print( - f'Please enter a valid data dimension currently supported dimensions are {self.dims.options}' - ) - return - num_spectra = int(self.num_spectra.value) - output_name = self.name.value - self.scope[output_name] = 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=dimesion_to_unit[dim]), - '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) - }) + def _on_button_clicked(self, b): + kwargs = { + item.placeholder: converter(item.value) + for item, converter in zip(self.input_widgets, + self.input_converters) + } + filename = self.filename_converter(self.filename.value) + filepath = os.path.join(self.directory, filename) + self.scope[filename] = self.callback(filepath, **kwargs) self.notify() - class ReductionWidget(w.Box): def __init__(self, scope): super().__init__() @@ -272,7 +246,8 @@ def _on_process_button_clicked(self, b): sample = self.scope[self.data_name(self.sample.value)] sample_trans = self.scope[self.data_name(self.sample_trans.value)] background = self.scope[self.data_name(self.background.value)] - background_trans = self.scope[self.data_name(self.background_trans.value)] + background_trans = self.scope[self.data_name( + self.background_trans.value)] moderator_file_path = f'{self.scope["path"]}/{self.scope["moderator_file"]}' direct_beam_file_path = f'{self.scope["path"]}/{self.scope["direct_beam_file"]}' l_collimation = self.scope['l_collimation'] @@ -286,7 +261,7 @@ def _on_process_button_clicked(self, b): sample, sample_trans, background, background_trans, moderator_file_path, direct_beam_file_path, l_collimation, r1, r2, dr, wavelength_bins) - + #Need to think up a better naming scheme for reduced data reduced_name = f'reduced_sans{self.sample.value}' sample_name = f'sample_sans{self.sample.value}' @@ -303,7 +278,10 @@ def _on_load_button_clicked(self, b): self.sample.value, self.sample_trans.value, self.background.value, self.background_trans.value ] - run_list = [item for item in run_list if self.data_name(item) not in self.scope.keys()] + run_list = [ + item for item in run_list + if self.data_name(item) not in self.scope.keys() + ] with self.output: for run in run_list: data = load_and_return(run, self.scope['path']) From 2f4ab6b5ab49ede31ac81ef771c7fba3a7404846 Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Tue, 6 Oct 2020 15:28:38 +0100 Subject: [PATCH 06/10] Removed SANS reduction example --- sans/CreateConvertPlotExample.ipynb | 152 ++----------- ...{convert_helpers.py => example_widgets.py} | 215 +++++++----------- sans/graphical_reduction.ipynb | 87 ------- sans/graphical_reduction.py | 207 ----------------- 4 files changed, 100 insertions(+), 561 deletions(-) rename sans/{convert_helpers.py => example_widgets.py} (57%) delete mode 100644 sans/graphical_reduction.ipynb delete mode 100644 sans/graphical_reduction.py diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 92aee22..29fe763 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -9,91 +9,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4388b50f6ba24d51b37595888b0e8e7a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "ToggleButton(value=False, description='Show code')" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "from convert_helpers import TransformWidget, PlotWidget, fake_load, LoadWidget\n", + "from example_widgets import TransformWidget, PlotWidget, fake_load, LoadWidget, setup_code_hiding\n", "import scipp as sc\n", - "import ipywidgets as widgets\n", - "from IPython.display import display, HTML\n", - "import functools\n", "\n", - "javascript_functions = {False: \"hide()\", True: \"show()\"}\n", - "button_descriptions = {False: \"Show code\", True: \"Hide code\"}\n", - "\n", - "def toggle_code(state):\n", - "\n", - " \"\"\"\n", - " Toggles the JavaScript show()/hide() function on the div.input element.\n", - " \"\"\"\n", - "\n", - " output_string = \"\"\n", - " output_args = (javascript_functions[state],)\n", - " output = output_string.format(*output_args)\n", - "\n", - " display(HTML(output))\n", - "\n", - "\n", - "def button_action(value):\n", - "\n", - " \"\"\"\n", - " Calls the toggle_code function and updates the button description.\n", - " \"\"\"\n", - "\n", - " state = value.new\n", - "\n", - " toggle_code(state)\n", - "\n", - " value.owner.description = button_descriptions[state]\n", - "\n", - "\n", - "state = False\n", - "toggle_code(state)\n", - "\n", - "button = widgets.ToggleButton(state, description = button_descriptions[state])\n", - "button.observe(button_action, \"value\")\n", - "\n", - "button" + "setup_code_hiding()" ] }, { @@ -101,29 +24,14 @@ "metadata": {}, "source": [ "### Data Loading\n", - "Currently creates some example data rather than loading anything." + "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": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aab5b27941a74ff2913442aa6adf8069", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "LoadWidget(children=(HBox(children=(Text(value='', placeholder='run number'), Button(description='Load', style…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "data_creation = LoadWidget(globals(), fake_load, \n", " 'path/to/directory/', \n", @@ -135,31 +43,22 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Data conversion" + "### 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": 3, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "48120446980b4f768fb73b28e4011441", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "TransformWidget(children=(HBox(children=(Text(value='', continuous_update=False, placeholder='data'), Text(val…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "data_conversion = TransformWidget(globals(), \n", " sc.neutron.convert, 'Convert', \n", @@ -178,26 +77,11 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3a6fc5358834479fb47587b871bec4d5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "PlotWidget(children=(VBox(children=(HBox(children=(Output(), Button(description='Manual Update', style=ButtonS…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plot_widget = PlotWidget(globals())\n", "data_creation.subscribe(plot_widget)\n", diff --git a/sans/convert_helpers.py b/sans/example_widgets.py similarity index 57% rename from sans/convert_helpers.py rename to sans/example_widgets.py index 2f10ad9..72fdab6 100644 --- a/sans/convert_helpers.py +++ b/sans/example_widgets.py @@ -1,13 +1,11 @@ -# Helpers for adding convert in a graphical format import ipywidgets as w import scipp as sc from scipp.plot import plot import numpy as np import IPython.display as disp -from graphical_reduction import q1d, run_reduction, load_and_return import os -# Cheat dict to list allowed conversions. Should ideally get this from C++ code + dimesion_to_unit = { 'tof': sc.units.us, 'd-spacing': sc.units.angstrom, @@ -16,26 +14,22 @@ 'Q': sc.units.one / sc.units.angstrom } -class TransformWidget(w.Box): + +class ProcessWidget(w.Box): def __init__(self, scope, callable, name, inputs): super().__init__() self.scope = scope + self.callable = callable self.input_widgets = [] self.input_converters = [] - self.callback = callable self.setup_inputs(inputs) - self.output = w.Text(placeholder='Output', - value='', - continuous_update=False) - self.output = w.Text(placeholder='Output', - 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]) + w.HBox(self.input_widgets + [self.button]) ] self.subscribers = [] @@ -54,13 +48,62 @@ def notify(self): observer.update() def _on_button_click(self, b): + self.process() + self.notify() + + def process(self): + pass + + +class TransformWidget(ProcessWidget): + def __init__(self, scope, callable, name, inputs): + super().__init__(scope, callable, name, inputs) + self.output = w.Text(placeholder='Output', + value='', + continuous_update=False) + + self.children = [ + w.HBox(self.input_widgets + [self.output, self.button]) + ] + + def process(self): kwargs = { item.placeholder: converter(item.value) for item, converter in zip(self.input_widgets, self.input_converters) } output_name = self.output.value - self.scope[output_name] = self.callback(**kwargs) + self.scope[output_name] = self.callable(**kwargs) + + +class LoadWidget(ProcessWidget): + def __init__( + self, + scope, + load_callable, + directory, + filename_descriptor, + filename_converter, + inputs = {} + ): + super().__init__(scope, load_callable, 'Load', inputs) + self.directory = directory + self.filename = w.Text(placeholder=filename_descriptor) + self.filename_converter = filename_converter + + self.children = [ + w.HBox([self.filename] + self.input_widgets + [self.button]) + ] + + def process(self): + kwargs = { + item.placeholder: converter(item.value) + for item, converter in zip(self.input_widgets, + self.input_converters) + } + filename = self.filename_converter(self.filename.value) + filepath = os.path.join(self.directory, filename) + self.scope[filename] = self.callable(filepath, **kwargs) self.notify() @@ -76,7 +119,7 @@ def __init__(self, scope): options=options) self._button = w.Button(description='Plot') self._button.on_click(self._on_button_clicked) - self.plot_options = w.Output() #HTML() + 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%') @@ -165,136 +208,42 @@ def fake_load(filename): }) -class LoadWidget(w.Box): - def __init__( - self, - scope, - load_callable, - directory, - filename_descriptor, - filename_converter, - inputs = {} - ): - super().__init__() - self.directory = directory - self.filename = w.Text(placeholder=filename_descriptor) - self.filename_converter = filename_converter +#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"} - self.input_widgets = [] - self.input_converters = [] - self.callback = load_callable - self.setup_inputs(inputs) +def toggle_code(state): - self.button = w.Button(description='Load') - self.button.on_click(self._on_button_clicked) - self.scope = scope - self.children = [ - w.HBox([self.filename] + self.input_widgets + [self.button]) - ] - self.subscribers = [] + """ + Toggles the JavaScript show()/hide() function on the div.input element. + """ - def setup_inputs(self, inputs): - for name, converter in inputs.items(): - self.input_widgets.append( - w.Text(placeholder=name, continuous_update=False)) - self.input_converters.append(converter) + output_string = "" + output_args = (javascript_functions[state],) + output = output_string.format(*output_args) - def subscribe(self, observer): - self.subscribers.append(observer) + disp.display(disp.HTML(output)) - def notify(self): - for observer in self.subscribers: - observer.update() - def _on_button_clicked(self, b): - kwargs = { - item.placeholder: converter(item.value) - for item, converter in zip(self.input_widgets, - self.input_converters) - } - filename = self.filename_converter(self.filename.value) - filepath = os.path.join(self.directory, filename) - self.scope[filename] = self.callback(filepath, **kwargs) - self.notify() +def button_action(value): -class ReductionWidget(w.Box): - def __init__(self, scope): - super().__init__() - self.sample = w.Text(description='Sample', value='49338') - self.sample_trans = w.Text(description='Sample trans', value='49339') - self.background = w.Text(description='Background', value='49334') - self.background_trans = w.Text(description='Backg... trans', - value='49335') - self.load_button = w.Button(description='Load') - self.load_button.on_click(self._on_load_button_clicked) - self._button = w.Button(description='Process') - self._button.on_click(self._on_process_button_clicked) - self.output = w.Output(width='100%', height='100%') - self.children = [ - w.VBox([ - w.HBox([self.sample, self.load_button]), - w.HBox([self.sample_trans, self._button]), self.background, - self.background_trans, self.output - ]) - ] - self.scope = scope - self.subscribers = [] + """ + Calls the toggle_code function and updates the button description. + """ - def _on_process_button_clicked(self, b): - self._on_load_button_clicked(None) - sample = self.scope[self.data_name(self.sample.value)] - sample_trans = self.scope[self.data_name(self.sample_trans.value)] - background = self.scope[self.data_name(self.background.value)] - background_trans = self.scope[self.data_name( - self.background_trans.value)] - moderator_file_path = f'{self.scope["path"]}/{self.scope["moderator_file"]}' - direct_beam_file_path = f'{self.scope["path"]}/{self.scope["direct_beam_file"]}' - l_collimation = self.scope['l_collimation'] - r1 = self.scope['r1'] - r2 = self.scope['r2'] - dr = self.scope['dr'] - wavelength_bins = self.scope['wavelength_bins'] + state = value.new - with self.output: - reduced, sample_q1d, background_q1d = run_reduction( - sample, sample_trans, background, background_trans, - moderator_file_path, direct_beam_file_path, l_collimation, r1, - r2, dr, wavelength_bins) - - #Need to think up a better naming scheme for reduced data - reduced_name = f'reduced_sans{self.sample.value}' - sample_name = f'sample_sans{self.sample.value}' - background_name = f'background{self.background.value}' - - self.scope[reduced_name] = reduced - self.scope[sample_name] = sample_q1d - self.scope[background_name] = background_q1d - self.notify() + toggle_code(state) - def _on_load_button_clicked(self, b): - self.output.clear_output() - run_list = [ - self.sample.value, self.sample_trans.value, self.background.value, - self.background_trans.value - ] - run_list = [ - item for item in run_list - if self.data_name(item) not in self.scope.keys() - ] - with self.output: - for run in run_list: - data = load_and_return(run, self.scope['path']) - name = self.data_name(run) - self.scope[name] = data - self.notify() + value.owner.description = button_descriptions[state] - def data_name(self, run): - return self.scope['instrument'] + str(run) - def subscribe(self, observer): - self.subscribers.append(observer) +def setup_code_hiding(): + state = False + toggle_code(state) - def notify(self): - for observer in self.subscribers: - observer.update() + button = w.ToggleButton(state, description = button_descriptions[state]) + button.observe(button_action, "value") + return button diff --git a/sans/graphical_reduction.ipynb b/sans/graphical_reduction.ipynb deleted file mode 100644 index 9f1a357..0000000 --- a/sans/graphical_reduction.ipynb +++ /dev/null @@ -1,87 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import scipp as sc\n", - "from scipp.plot import plot\n", - "import numpy as np\n", - "import dataconfig # run make_config.py to create this\n", - "import ipywidgets as w\n", - "import IPython.display as disp\n", - "from convert_helpers import PlotWidget, ReductionWidget" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set reduction settings. This is currently quite a brittle implimentation where we require certain variables to exist\n", - "# in the global namespace in order for the reduction to work.\n", - "path = dataconfig.data_root\n", - "instrument = 'LARMOR'\n", - "direct_beam_file = 'DirectBeam_20feb_full_v3.dat'\n", - "moderator_file = 'ModeratorStdDev_TS2_SANS_LETexptl_07Aug2015.txt'\n", - "l_collimation = sc.Variable(value=5.0, unit=sc.units.m)\n", - "r2 = sc.Variable(value=4.0824829046386295/1000, unit=sc.units.m) # sample aperture radius\n", - "r1 = sc.Variable(value=14.433756729740645/1000, unit=sc.units.m) # source aperture radius\n", - "dr = sc.Variable(value=8.0/1000, unit=sc.units.m) # virtual ring width on detector\n", - "wavelength_bins = sc.Variable(\n", - " dims=['wavelength'],\n", - " unit=sc.units.angstrom,\n", - " values=np.geomspace(0.9, 13.5, num=110))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "reduction_widget = ReductionWidget(globals())\n", - "disp.display(reduction_widget)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "plot_widget = PlotWidget(globals())\n", - "reduction_widget.subscribe(plot_widget)\n", - "disp.display(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/graphical_reduction.py b/sans/graphical_reduction.py deleted file mode 100644 index 006e47e..0000000 --- a/sans/graphical_reduction.py +++ /dev/null @@ -1,207 +0,0 @@ -# Functions used in graphical reduction demo. Lifted from reduction.ipynb -import scipp as sc -import numpy as np -from contrib import to_bin_centers, to_bin_edges, map_to_bins -import ipywidgets as w - -def load_and_return(run, path): - print(f'Loading data for run {run}:') - return sc.neutron.load(filename=f'{path}/LARMOR000{run}.nxs') - -def run_reduction(sample, sample_trans, background, - background_trans, moderator_file_path, direct_beam_file_path, - l_collimation, r1, r2, dr, wavelength_bins): - - dtype = sample.coords['position'].dtype - sample_pos_offset = sc.Variable(value=[0.0, 0.0, 0.30530], unit=sc.units.m, dtype=dtype) - bench_pos_offset = sc.Variable(value=[0.0, 0.001, 0.0], unit=sc.units.m, dtype=dtype) - for item in [sample, sample_trans, background, background_trans]: - item.coords['sample-position'] += sample_pos_offset - item.coords['position'] += bench_pos_offset - - print('Reducing sample data:') - sample_q1d = q1d(data=sample, transmission=sample_trans, - l_collimation=l_collimation, r1=r1, r2=r2, dr=dr, wavelength_bins=wavelength_bins, - direct_beam_file_path=direct_beam_file_path, moderator_file_path=moderator_file_path, - wavelength_bands=None) - - print('Reducing background data:') - background_q1d = q1d(data=background, transmission=background_trans, - l_collimation=l_collimation, r1=r1, r2=r2, dr=dr, wavelength_bins=wavelength_bins, - direct_beam_file_path=direct_beam_file_path, moderator_file_path=moderator_file_path, - wavelength_bands=None) - - reduced = sample_q1d - background_q1d - - # reduced.coords['Transmission'] = sc.Variable( - # value=f'{sample_transmission_run_number}_trans_sample_0.9_13.5_unfitted') - # reduced.coords['TransmissionCan'] = sc.Variable( - # value=f'{background_transmission_run_number}_trans_can_0.9_13.5_unfitted') - - print('Finished Reduction') - return reduced, sample_q1d, background_q1d - -def load_larmor(run_number): - return sc.neutron.load(filename=f'{path}/LARMOR000{run_number}.nxs') - -def load_rkh(filename): - return sc.neutron.load( - filename=filename, - mantid_alg='LoadRKH', - mantid_args={'FirstColumnValue':'Wavelength'}) - -def apply_masks(data): - tof = data.coords['tof'] - data.masks['bins'] = sc.less(tof['tof',1:], 1500.0 * sc.units.us) | \ - (sc.greater(tof['tof',:-1], 17500.0 * sc.units.us) & \ - sc.less(tof['tof',1:], 19000.0 * sc.units.us)) - pos = sc.neutron.position(data) - x = sc.geometry.x(pos) - y = sc.geometry.y(pos) - data.masks['beam-stop'] = sc.less(sc.sqrt(x*x+y*y), 0.045 * sc.units.m) - data.masks['tube-ends'] = sc.greater(sc.abs(x), 0.36 * sc.units.m) # roughly all det IDs listed in original - #MaskDetectorsInShape(Workspace=maskWs, ShapeXML=self.maskingPlaneXML) # irrelevant tiny wedge? - - -def background_mean(data, dim, begin, end): - coord = data.coords[dim] - assert (coord.unit == begin.unit) and (coord.unit == end.unit) - i = np.searchsorted(coord, begin.value) - j = np.searchsorted(coord, end.value) + 1 - return data - sc.mean(data[dim, i:j], dim) - - -def transmission_fraction(incident_beam, transmission, wavelength_bins): - # Approximation based on equations in CalculateTransmission documentation - # TODO proper implementation of mantid.CalculateTransmission - return (transmission / transmission) * (incident_beam / incident_beam) - #CalculateTransmission(SampleRunWorkspace=transWsTmp, - # DirectRunWorkspace=transWsTmp, - # OutputWorkspace=outWsName, - # IncidentBeamMonitor=1, - # TransmissionMonitor=4, RebinParams='0.9,-0.025,13.5', - # FitMethod='Polynomial', - # PolynomialOrder=3, OutputUnfittedData=True) - -def extract_monitor_background(data, begin, end, wavelength_bins): - background = background_mean(data, 'tof', begin, end) - del background.coords['sample-position'] # ensure unit conversion treats this a monitor - background = sc.neutron.convert(background, 'tof', 'wavelength') - background = sc.rebin(background, 'wavelength', wavelength_bins) - return background - - -def setup_transmission(data, wavelength_bins): - incident_beam = extract_monitor_background(data['spectrum', 0], 40000.0*sc.units.us, 99000.0*sc.units.us, wavelength_bins) - transmission = extract_monitor_background(data['spectrum', 3], 88000.0*sc.units.us, 98000.0*sc.units.us, wavelength_bins) - return transmission_fraction(incident_beam, transmission, wavelength_bins) - - -def solid_angle(data): - # TODO proper solid angle - # [0.0117188,0.0075,0.0075] bounding box size - pixel_size = 0.0075 * sc.units.m - pixel_length = 0.0117188 * sc.units.m - L2 = sc.neutron.l2(data) - return (pixel_size * pixel_length) / (L2 * L2) - - -def q_resolution(lam_edges, moderator, d, l_collimation, r1, r2, dr): - moderator = sc.rebin(moderator, 'wavelength', lam_edges) - - d_lam = lam_edges['wavelength', 1:] - lam_edges['wavelength', :-1] # bin widths - lam = 0.5 * (lam_edges['wavelength', 1:] + lam_edges['wavelength', :-1]) # bin centres - - l2 = sc.neutron.l2(d) - theta = sc.neutron.scattering_angle(d) - inv_l3 = (l_collimation + l2) / (l_collimation * l2) - - # Terms in Mildner and Carpenter equation. - # See https://docs.mantidproject.org/nightly/algorithms/TOFSANSResolutionByPixel-v1.html - a1 = (r1/l_collimation)*(r1/l_collimation) * 3.0 - a2 = (r2*inv_l3)*(r2*inv_l3) * 3.0 - a3 = (dr/l2) * (dr/l2) - q_sq = 4.0 * np.pi * sc.sin(theta) * sc.reciprocal(lam) # keep in wav dim to prevent broadcast - q_sq *= q_sq - - tof = moderator.data.copy() - tof.variances = None # shortcoming of Mantid or Mantid converter? - tof.rename_dims({'wavelength':'tof'}) # TODO overly restrictive check in convert (rename) - tof.unit = sc.units.us - mod = sc.Dataset(coords={ - 'tof':tof, - 'position':sample.coords['position'], - 'source_position':sample.coords['source_position'], - 'sample_position':sample.coords['sample_position']}) - s = sc.neutron.convert(mod, 'tof', 'wavelength').coords['wavelength'] - - std_dev_lam_sq = (d_lam * d_lam)/12 + s * s - std_dev_lam_sq *= sc.reciprocal(lam * lam) - f = (4 * np.pi * np.pi) * sc.reciprocal(12 * lam * lam) - - return sc.DataArray(f * (a1 + a2 + a3) + (q_sq * std_dev_lam_sq), - coords={'wavelength':lam, 'spectrum':d.coords['spectrum'].copy()}) - - -def q1d(data, transmission, l_collimation, r1, r2, dr, wavelength_bins, direct_beam_file_path, moderator_file_path, wavelength_bands=None): - transmission = setup_transmission(transmission, wavelength_bins) - data = data.copy() - apply_masks(data) - data = sc.neutron.convert(data, 'tof', 'wavelength', out=data) - data = sc.rebin(data, 'wavelength', wavelength_bins) - - monitor = data.coords['monitor1'].value - monitor = background_mean(monitor, 'tof', 40000.0*sc.units.us, 99000.0*sc.units.us) - monitor = sc.neutron.convert(monitor, 'tof', 'wavelength', out=monitor) - monitor = sc.rebin(monitor, 'wavelength', wavelength_bins) - - # this factor seems to be a fudge factor. Explanation pending. - data *= 100.0 / 176.71458676442586 - - # Setup direct beam and normalise to monitor. I.e. adjust for efficiency of detector across the wavelengths. - direct_beam = load_rkh(filename=direct_beam_file_path) - # This would work assuming that there is a least one wavelength point per bin - #direct_beam = sc.groupby(direct_beam, 'wavelength', bins=monitor.coords['wavelength']).mean('wavelength') - direct_beam = map_to_bins(direct_beam, 'wavelength', monitor.coords['wavelength']) - direct_beam = monitor * transmission * direct_beam - - # Estimate qresolution function - moderator = load_rkh(filename=moderator_file_path) - to_bin_edges(moderator, 'wavelength') - - q_bins = sc.Variable( - dims=['Q'], - unit=sc.units.one/sc.units.angstrom, - values=np.geomspace(0.008, 0.6, num=55)) - - d = sc.Dataset({'data':data, 'norm':solid_angle(data)*direct_beam}) - #dq_sq = q_resolution(d.coords['wavelength'], moderator, d, l_collimation, r1, r2, dr) - to_bin_centers(d, 'wavelength') - d = sc.neutron.convert(d, 'wavelength', 'Q', out=d) # TODO no gravity yet - - - if wavelength_bands is None: - d = sc.histogram(d, q_bins) - d = sc.sum(d, 'spectrum') - I = d['data']/d['norm'] - else: - # Cut range into number of requested bands - n_band = int(wavelength_bands) - n_bin = len(wavelength_bins.values)-1 - bounds = np.arange(n_bin)[::n_bin//n_band] - bounds[-1] = n_bin - slices = [slice(i, j) for i,j in zip(bounds[:-1],bounds[1:])] - bands = None - # Reduce by wavelength slice - for s in slices: - band = sc.histogram(d['Q', s].copy(), q_bins) # TODO fix scipp to avoid need for copy - band = sc.sum(band, 'spectrum') - bands = sc.concatenate(bands, band, 'wavelength') if bands is not None else band - # Add coord for wavelength edges of bands - bands.coords['wavelength'] = sc.Variable( - dims=['wavelength'], - unit=sc.units.angstrom, - values=np.take(wavelength_bins.values, bounds)) - I = bands['data']/bands['norm'] - - return I From 7e061fa6bb90eb85ffad7c5368d4c2651d1f1f3e Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Tue, 6 Oct 2020 16:00:55 +0100 Subject: [PATCH 07/10] Added example using functools --- sans/CreateConvertPlotExample.ipynb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 29fe763..90a7b46 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -15,6 +15,7 @@ "source": [ "from example_widgets import TransformWidget, PlotWidget, fake_load, LoadWidget, setup_code_hiding\n", "import scipp as sc\n", + "import functools\n", "\n", "setup_code_hiding()" ] @@ -67,6 +68,29 @@ "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 pythn doesn't like shadowing of from\n", + "kwargs = {'from':'tof'}\n", + "convert_tof = functools.partial(sc.neutron.convert, **kwargs)\n", + "data_conversion_tof = TransformWidget(globals(), \n", + " convert_tof, 'Convert from tof', \n", + " {'data': lambda name : globals()[name],\n", + " 'to': str})\n", + "data_conversion_tof" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -86,6 +110,7 @@ "plot_widget = PlotWidget(globals())\n", "data_creation.subscribe(plot_widget)\n", "data_conversion.subscribe(plot_widget)\n", + "data_conversion_tof.subscribe(plot_widget)\n", "plot_widget" ] } From 2f5bb1149cb2781d69ef0e7460c82f042b5ad139 Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Wed, 7 Oct 2020 08:43:29 +0100 Subject: [PATCH 08/10] Added validators This updates ProcessWidget so that the conversion functions provided for input cells can optionally do a validation check and throw a ValueError if this fails. This will then be handled as a user input error by the widget. An alternative route to pass in optional validators would be to have a seperate dict sharing keys with the input dict which specifies options to use. I went for this method over that because conversion and validation are often closely linked. --- sans/CreateConvertPlotExample.ipynb | 41 +++++++++++++++++++++-------- sans/example_widgets.py | 24 ++++++++++------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 90a7b46..374f8e8 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -10,16 +10,31 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "from example_widgets import TransformWidget, PlotWidget, fake_load, LoadWidget, setup_code_hiding\n", + "from example_widgets import TransformWidget, PlotWidget, fake_load, LoadWidget, setup_code_hiding, dimesion_to_unit\n", "import scipp as sc\n", "import functools\n", "\n", + "# Workaround for the fact that you can't raise directly in lambda's\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": {}, @@ -37,6 +52,7 @@ "data_creation = LoadWidget(globals(), fake_load, \n", " 'path/to/directory/', \n", " 'run number', lambda run: 'LARMOR' + run)\n", + "data_creation.subscribe(plot_widget)\n", "data_creation" ] }, @@ -61,10 +77,16 @@ }, "outputs": [], "source": [ + "dim_converter = lambda dim : dim if dim in dimesion_to_unit.keys() 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", + "\n", "data_conversion = TransformWidget(globals(), \n", " sc.neutron.convert, 'Convert', \n", - " {'data': lambda name : globals()[name],\n", - " 'from' : str, 'to': str})\n", + " {'data': data_converter,\n", + " 'from' : dim_converter, 'to': dim_converter})\n", + "data_conversion.subscribe(plot_widget)\n", "data_conversion" ] }, @@ -81,13 +103,14 @@ "metadata": {}, "outputs": [], "source": [ - "# Workaround because pythn doesn't like shadowing of from\n", + "# 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 = TransformWidget(globals(), \n", " convert_tof, 'Convert from tof', \n", - " {'data': lambda name : globals()[name],\n", - " 'to': str})\n", + " {'data': data_converter,\n", + " 'to': dim_converter})\n", + "data_conversion_tof.subscribe(plot_widget)\n", "data_conversion_tof" ] }, @@ -107,10 +130,6 @@ }, "outputs": [], "source": [ - "plot_widget = PlotWidget(globals())\n", - "data_creation.subscribe(plot_widget)\n", - "data_conversion.subscribe(plot_widget)\n", - "data_conversion_tof.subscribe(plot_widget)\n", "plot_widget" ] } diff --git a/sans/example_widgets.py b/sans/example_widgets.py index 72fdab6..b1496bb 100644 --- a/sans/example_widgets.py +++ b/sans/example_widgets.py @@ -51,6 +51,18 @@ def _on_button_click(self, b): self.process() self.notify() + def _retrive_kwargs(self): + try: + kwargs = { + item.placeholder: converter(item.value) + for item, converter in zip(self.input_widgets, + self.input_converters) + } + except ValueError as e: + print(f'Invalid inputs: {e}') + return + return kwargs + def process(self): pass @@ -67,11 +79,7 @@ def __init__(self, scope, callable, name, inputs): ] def process(self): - kwargs = { - item.placeholder: converter(item.value) - for item, converter in zip(self.input_widgets, - self.input_converters) - } + kwargs = self._retrive_kwargs() output_name = self.output.value self.scope[output_name] = self.callable(**kwargs) @@ -96,11 +104,7 @@ def __init__( ] def process(self): - kwargs = { - item.placeholder: converter(item.value) - for item, converter in zip(self.input_widgets, - self.input_converters) - } + kwargs = self._retrive_kwargs() filename = self.filename_converter(self.filename.value) filepath = os.path.join(self.directory, filename) self.scope[filename] = self.callable(filepath, **kwargs) From e3cfa30585fb76bf9cd75a914e5b3d7e06215952 Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Wed, 7 Oct 2020 11:44:45 +0100 Subject: [PATCH 09/10] Harmonised loading and processing --- sans/CreateConvertPlotExample.ipynb | 19 +++-- sans/example_widgets.py | 109 +++++++++++----------------- 2 files changed, 55 insertions(+), 73 deletions(-) diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 374f8e8..4b8a290 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -15,7 +15,7 @@ }, "outputs": [], "source": [ - "from example_widgets import TransformWidget, PlotWidget, fake_load, LoadWidget, setup_code_hiding, dimesion_to_unit\n", + "from example_widgets import ProcessWidget, PlotWidget, fake_load, setup_code_hiding, dimesion_to_unit, filepath_converter\n", "import scipp as sc\n", "import functools\n", "\n", @@ -49,9 +49,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_creation = LoadWidget(globals(), fake_load, \n", - " 'path/to/directory/', \n", - " 'run number', lambda run: 'LARMOR' + run)\n", + "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" ] @@ -82,7 +80,7 @@ "data_converter = lambda name : globals()[name] if (name in globals()) else raise_(ValueError(\n", " f'{name} does not exist in notebook.'))\n", "\n", - "data_conversion = TransformWidget(globals(), \n", + "data_conversion = ProcessWidget(globals(), \n", " sc.neutron.convert, 'Convert', \n", " {'data': data_converter,\n", " 'from' : dim_converter, 'to': dim_converter})\n", @@ -106,7 +104,7 @@ "# 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 = TransformWidget(globals(), \n", + "data_conversion_tof = ProcessWidget(globals(), \n", " convert_tof, 'Convert from tof', \n", " {'data': data_converter,\n", " 'to': dim_converter})\n", @@ -114,6 +112,15 @@ "data_conversion_tof" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d = (sc.Dataset(),sc.Dataset())" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/sans/example_widgets.py b/sans/example_widgets.py index b1496bb..ae446b8 100644 --- a/sans/example_widgets.py +++ b/sans/example_widgets.py @@ -5,7 +5,6 @@ import IPython.display as disp import os - dimesion_to_unit = { 'tof': sc.units.us, 'd-spacing': sc.units.angstrom, @@ -15,30 +14,50 @@ } +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): + def __init__(self, scope, callable, name, inputs, descriptions={}): super().__init__() self.scope = scope self.callable = callable + self.input_widgets = [] - self.input_converters = [] + self.inputs = inputs + self.setup_input_widgets(descriptions) - self.setup_inputs(inputs) + self.output = w.Text(placeholder='Output', + 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.button]) + w.HBox(self.input_widgets + [self.output, self.button]) ] self.subscribers = [] - def setup_inputs(self, inputs): - for name, converter in inputs.items(): + def setup_input_widgets(self, descriptions): + for name in self.inputs.keys(): + placeholder = descriptions[name] if name in descriptions else name self.input_widgets.append( - w.Text(placeholder=name, continuous_update=False)) - self.input_converters.append(converter) + w.Text(placeholder=placeholder, continuous_update=False)) def subscribe(self, observer): self.subscribers.append(observer) @@ -52,65 +71,23 @@ def _on_button_click(self, b): 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 = { - item.placeholder: converter(item.value) - for item, converter in zip(self.input_widgets, - self.input_converters) - } + kwargs = self._retrive_kwargs() except ValueError as e: print(f'Invalid inputs: {e}') return - return kwargs - - def process(self): - pass - - -class TransformWidget(ProcessWidget): - def __init__(self, scope, callable, name, inputs): - super().__init__(scope, callable, name, inputs) - self.output = w.Text(placeholder='Output', - value='', - continuous_update=False) - - self.children = [ - w.HBox(self.input_widgets + [self.output, self.button]) - ] - - def process(self): - kwargs = self._retrive_kwargs() output_name = self.output.value self.scope[output_name] = self.callable(**kwargs) -class LoadWidget(ProcessWidget): - def __init__( - self, - scope, - load_callable, - directory, - filename_descriptor, - filename_converter, - inputs = {} - ): - super().__init__(scope, load_callable, 'Load', inputs) - self.directory = directory - self.filename = w.Text(placeholder=filename_descriptor) - self.filename_converter = filename_converter - - self.children = [ - w.HBox([self.filename] + self.input_widgets + [self.button]) - ] - - def process(self): - kwargs = self._retrive_kwargs() - filename = self.filename_converter(self.filename.value) - filepath = os.path.join(self.directory, filename) - self.scope[filename] = self.callable(filepath, **kwargs) - self.notify() - - class PlotWidget(w.Box): def __init__(self, scope): super().__init__() @@ -171,7 +148,7 @@ def update(self, b=None): self._repr_html_(self.scope) -def fake_load(filename): +def fake_load(filepath): dim = 'tof' num_spectra = 10 return sc.Dataset( @@ -215,24 +192,22 @@ def fake_load(filename): #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"} +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) + 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. """ @@ -248,6 +223,6 @@ def setup_code_hiding(): state = False toggle_code(state) - button = w.ToggleButton(state, description = button_descriptions[state]) + button = w.ToggleButton(state, description=button_descriptions[state]) button.observe(button_action, "value") return button From b94cb2d4db9173fd959c44be5b794b6f4c7ab7ce Mon Sep 17 00:00:00 2001 From: Matthew-Andrew Date: Wed, 7 Oct 2020 12:10:32 +0100 Subject: [PATCH 10/10] Added autocomplete options. Added ability to optionally specify autocomplete values for cells. This is demonstrated for the data inputs and dimension values in the conversion widgets. Note however that without linking up some observer you have to re-run the cell to get the latest data to properly appear as options. In the long term if may be worth having a notifier control object which all widgets which create scipp objects register with and can then subscribe to if they wish to know when scipp objects are created by have not done so currently. --- sans/CreateConvertPlotExample.ipynb | 30 ++++++++++---------- sans/example_widgets.py | 44 +++++++++++++++++------------ 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/sans/CreateConvertPlotExample.ipynb b/sans/CreateConvertPlotExample.ipynb index 4b8a290..6e245eb 100644 --- a/sans/CreateConvertPlotExample.ipynb +++ b/sans/CreateConvertPlotExample.ipynb @@ -15,11 +15,11 @@ }, "outputs": [], "source": [ - "from example_widgets import ProcessWidget, PlotWidget, fake_load, setup_code_hiding, dimesion_to_unit, filepath_converter\n", + "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 lambda's\n", + "# Workaround for the fact that you can't raise directly in tertiary expressions.\n", "def raise_(ex):\n", " raise ex\n", "\n", @@ -71,19 +71,26 @@ "cell_type": "code", "execution_count": null, "metadata": { - "scrolled": false + "scrolled": true }, "outputs": [], "source": [ - "dim_converter = lambda dim : dim if dim in dimesion_to_unit.keys() else raise_(ValueError(\n", + "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", + " '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" ] @@ -107,20 +114,13 @@ "data_conversion_tof = ProcessWidget(globals(), \n", " convert_tof, 'Convert from tof', \n", " {'data': data_converter,\n", - " 'to': dim_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": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "d = (sc.Dataset(),sc.Dataset())" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/sans/example_widgets.py b/sans/example_widgets.py index ae446b8..698a9cd 100644 --- a/sans/example_widgets.py +++ b/sans/example_widgets.py @@ -5,13 +5,7 @@ import IPython.display as disp import os -dimesion_to_unit = { - 'tof': sc.units.us, - 'd-spacing': sc.units.angstrom, - 'wavelength': sc.units.angstrom, - 'E': sc.units.meV, - 'Q': sc.units.one / sc.units.angstrom -} +allowed_dimensions = ['tof', 'd-spacing', 'wavelength', 'E', 'Q'] def filepath_converter(filename): @@ -31,16 +25,22 @@ def filepath_converter(filename): class ProcessWidget(w.Box): - def __init__(self, scope, callable, name, inputs, descriptions={}): + 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) + self.setup_input_widgets(descriptions, options) - self.output = w.Text(placeholder='Output', + self.output = w.Text(placeholder='output name', value='', continuous_update=False) @@ -53,11 +53,15 @@ def __init__(self, scope, callable, name, inputs, descriptions={}): self.subscribers = [] - def setup_input_widgets(self, descriptions): + 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.Text(placeholder=placeholder, continuous_update=False)) + w.Combobox(placeholder=placeholder, + continuous_update=False, + options=option)) def subscribe(self, observer): self.subscribers.append(observer) @@ -73,8 +77,8 @@ def _on_button_click(self, b): def _retrive_kwargs(self): kwargs = { name: converter(item.value) - for name, converter, item in zip(self.inputs.keys( - ), self.inputs.values(), self.input_widgets) + for name, converter, item in zip( + self.inputs.keys(), self.inputs.values(), self.input_widgets) } return kwargs @@ -84,7 +88,13 @@ def process(self): except ValueError as e: print(f'Invalid inputs: {e}') return - output_name = self.output.value + + 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) @@ -165,9 +175,7 @@ def fake_load(filepath): }, coords={ dim: - sc.Variable([dim], - values=np.arange(11.0), - unit=dimesion_to_unit[dim]), + sc.Variable([dim], values=np.arange(11.0), unit=sc.units.us), 'spectrum': sc.Variable(['spectrum'], values=np.arange(num_spectra),