Skip to content

Commit

Permalink
Merge pull request #109 from scipp/binedges-widget
Browse files Browse the repository at this point in the history
Binedges widget
  • Loading branch information
nvaytet authored Sep 30, 2024
2 parents 43e2cdb + f06ab10 commit cd09315
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 9 deletions.
26 changes: 21 additions & 5 deletions src/ess/reduce/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,30 @@ class MultiFilenameParameter(Parameter[tuple[str, ...]]):
class BinEdgesParameter(Parameter[sc.Variable]):
"""Widget for entering bin edges."""

# dim and unit displayed in widget to provide context of numbers
dim: str
unit: str

def __init__(self, t: type[T], dim: str, unit: str):
start: float | None = None
stop: float | None = None
nbins: int = 1
unit: str | None = "undefined" # If "undefined", the unit is deduced from the dim
log: bool = False

def __init__(
self,
t: type[T],
dim: str,
start: float | None = None,
stop: float | None = None,
nbins: int = 1,
unit: str | None = "undefined",
log: bool = False,
):
self.dim = dim
self.start = start
self.stop = stop
self.nbins = nbins
self.unit = unit
super().__init__(name=str(t), description=t.__doc__, default=None)
self.log = log
super().__init__(name=key_name(t), description=t.__doc__, default=None)


@dataclass
Expand Down
14 changes: 11 additions & 3 deletions src/ess/reduce/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from ._config import default_layout, default_style

from ._binedges_widget import BinEdgesWidget
from ._linspace_widget import LinspaceWidget
from ._vector_widget import VectorWidget
from ._bounds_widget import BoundsWidget
Expand Down Expand Up @@ -156,9 +157,15 @@ def param_with_bounds_widget(param: ParamWithBounds):

@create_parameter_widget.register(BinEdgesParameter)
def bin_edges_parameter_widget(param: BinEdgesParameter):
dim = param.dim
unit = param.unit
return LinspaceWidget(dim, unit)
return BinEdgesWidget(
name=param.name,
dim=param.dim,
start=param.start,
stop=param.stop,
nbins=param.nbins,
unit=param.unit,
log=param.log,
)


@create_parameter_widget.register(VectorParameter)
Expand All @@ -167,6 +174,7 @@ def vector_parameter_widget(param: VectorParameter):


__all__ = [
'BinEdgesWidget',
'BoundsWidget',
'EssWidget',
'LinspaceWidget',
Expand Down
72 changes: 72 additions & 0 deletions src/ess/reduce/widgets/_binedges_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
import ipywidgets as ipw
import scipp as sc

UNITS_LIBRARY = {
"wavelength": {"options": ("angstrom", "nm")},
"Q": {"options": ("1/angstrom", "1/nm")},
"Qx": {"options": ("1/angstrom", "1/nm")},
"Qy": {"options": ("1/angstrom", "1/nm")},
"tof": {"options": ("s", "ms", "us", "ns"), "selected": "us"},
"dspacing": {"options": ("angstrom", "nm")},
"energy_transfer": {"options": ("meV",)},
"theta": {"options": ("rad", "deg")},
"two_theta": {"options": ("rad", "deg")},
"phi": {"options": ("rad", "deg")},
"time": {"options": ("s", "ms", "us", "ns")},
"temperature": {"options": ("K", "C", "F")},
}


class BinEdgesWidget(ipw.HBox, ipw.ValueWidget):
def __init__(
self,
name: str,
dim: str,
start: float | None = None,
stop: float | None = None,
nbins: int = 1,
unit: str | None = "undefined",
log: bool = False,
):
super().__init__()
style = {
"layout": {"width": "100px"},
"style": {"description_width": "initial"},
}
units = UNITS_LIBRARY[dim] if unit == "undefined" else unit
if isinstance(units, str):
units = {"options": (units,)}
if not isinstance(units, dict):
units = {"options": units}
self.fields = {
"dim": ipw.Label(str(dim)),
"unit": ipw.Dropdown(
options=units["options"],
value=units.get("selected", units["options"][0]),
layout={"width": "initial"},
),
"start": ipw.FloatText(description='start:', value=start, **style),
"stop": ipw.FloatText(description='stop:', value=stop, **style),
"nbins": ipw.BoundedIntText(
description='nbins:', value=nbins, min=1, max=int(1e9), **style
), # Note that a max value is required as it defaults to 100 without it.
"spacing": ipw.Dropdown(
options=['linear', 'log'],
value='log' if log else 'linear',
layout={"width": "initial"},
),
}
self.children = [ipw.Label(f"{name}:")] + list(self.fields.values())[1:]

@property
def value(self) -> sc.Variable:
func = sc.geomspace if self.fields["spacing"].value == "log" else sc.linspace
return func(
dim=self.fields["dim"].value,
start=self.fields["start"].value,
stop=self.fields["stop"].value,
num=self.fields["nbins"].value + 1,
unit=self.fields["unit"].value,
)
114 changes: 113 additions & 1 deletion tests/widget_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from typing import Any, NewType

import sciline as sl
import scipp as sc
from ipywidgets import FloatText, IntText

from ess.reduce.parameter import Parameter, parameter_registry
from ess.reduce.parameter import BinEdgesParameter, Parameter, parameter_registry
from ess.reduce.ui import WorkflowWidget, workflow_widget
from ess.reduce.widgets import OptionalWidget, SwitchWidget, create_parameter_widget
from ess.reduce.workflow import register_workflow, workflow_registry
Expand Down Expand Up @@ -290,3 +291,114 @@ def test_workflow_selection() -> None:
SwitchableInt,
SwitchableFloat,
}


WavelengthBins = NewType('WavelengthBins', sc.Variable)
WavelengthBinsWithUnit = NewType('WavelengthBinsWithUnit', sc.Variable)
QBins = NewType('QBins', sc.Variable)
TemperatureBinsWithUnits = NewType('TemperatureBinsWithUnits', sc.Variable)


parameter_registry[WavelengthBins] = BinEdgesParameter(WavelengthBins, dim='wavelength')
parameter_registry[WavelengthBinsWithUnit] = BinEdgesParameter(
WavelengthBinsWithUnit, dim='wavelength', unit='m'
)
parameter_registry[QBins] = BinEdgesParameter(
QBins, dim='Q', start=0.01, stop=0.6, nbins=150
)
parameter_registry[TemperatureBinsWithUnits] = BinEdgesParameter(
TemperatureBinsWithUnits, dim='temperature', unit=('Kelvin', 'Celsius', 'Degrees')
)


def wavelength_bins_print_provider(bins: WavelengthBins) -> str:
return str(bins)


def wavelength_bins_with_unit_print_provider(bins: WavelengthBinsWithUnit) -> str:
return str(bins)


def q_bins_print_provider(bins: QBins) -> str:
return str(bins)


def temperature_bins_print_provider(bins: TemperatureBinsWithUnits) -> str:
return str(bins)


def test_bin_edges_widget_simple() -> None:
widget = _ready_widget(
providers=[wavelength_bins_print_provider], output_selections=[str]
)
param_widget = _get_param_widget(widget, WavelengthBins)
assert sc.identical(
param_widget.value,
sc.linspace(dim='wavelength', start=0.0, stop=0.0, num=2, unit='angstrom'),
)
assert set(param_widget.fields['unit'].options) == {'angstrom', 'nm'}
assert param_widget.fields['nbins'].value == 1
assert param_widget.fields['spacing'].value == 'linear'
# Modify the values
param_widget.fields['start'].value = 1.0
param_widget.fields['stop'].value = 200.0
param_widget.fields['nbins'].value = 10
assert sc.identical(
param_widget.value,
sc.linspace(dim='wavelength', start=1.0, stop=200.0, num=11, unit='angstrom'),
)


def test_bin_edges_widget_override_unit() -> None:
widget = _ready_widget(
providers=[wavelength_bins_with_unit_print_provider], output_selections=[str]
)
param_widget = _get_param_widget(widget, WavelengthBinsWithUnit)
assert sc.identical(
param_widget.value,
sc.linspace(dim='wavelength', start=0.0, stop=0.0, num=2, unit='m'),
)
assert set(param_widget.fields['unit'].options) == {'m'}
assert param_widget.fields['nbins'].value == 1


def test_bin_edges_widget_override_unit_multiple() -> None:
widget = _ready_widget(
providers=[temperature_bins_print_provider], output_selections=[str]
)
param_widget = _get_param_widget(widget, TemperatureBinsWithUnits)
assert sc.identical(
param_widget.value,
sc.linspace(dim='temperature', start=0.0, stop=0.0, num=2, unit='Kelvin'),
)
assert set(param_widget.fields['unit'].options) == {'Kelvin', 'Celsius', 'Degrees'}


def test_bin_edges_widget_log_spacing() -> None:
widget = _ready_widget(
providers=[wavelength_bins_print_provider], output_selections=[str]
)
param_widget = _get_param_widget(widget, WavelengthBins)
param_widget.fields['spacing'].value = 'log'
param_widget.fields['start'].value = 1.0e3
param_widget.fields['stop'].value = 1.0e5
param_widget.fields['nbins'].value = 7
assert sc.identical(
param_widget.value,
sc.geomspace(dim='wavelength', start=1.0e3, stop=1.0e5, num=8, unit='angstrom'),
)
assert param_widget.fields['spacing'].value == 'log'


def test_bin_edges_widget_with_default_values() -> None:
widget = _ready_widget(providers=[q_bins_print_provider], output_selections=[str])
param_widget = _get_param_widget(widget, QBins)
assert sc.identical(
param_widget.value,
sc.linspace(dim='Q', start=0.01, stop=0.6, num=151, unit='1/angstrom'),
)
assert set(param_widget.fields['unit'].options) == {'1/angstrom', '1/nm'}
assert param_widget.fields['start'].value == 0.01
assert param_widget.fields['stop'].value == 0.6
assert param_widget.fields['nbins'].value == 150
assert param_widget.fields['spacing'].value == 'linear'

0 comments on commit cd09315

Please sign in to comment.