Source code for hyperspy_gui_anywidget.model

"""Model, component, and parameter widgets for HyperSpy using anywidget."""

import numpy as np
from link_traits import dlink, link

from hyperspy_gui_anywidget.custom_widgets import (
    ButtonWidget,
    CheckboxWidget,
    ContainerWidget,
    FloatSliderWidget,
    FloatTextWidget,
    LabelWidget,
)
from hyperspy_gui_anywidget.utils import (
    add_display_arg,
    enum2dropdown,
    labelme,
)


def _interactive_slider_bounds(obj, index=None):
    """Compute min, max, and step for an interactive parameter slider.

    Parameters
    ----------
    obj : hyperspy.component.Parameter
        The parameter to compute bounds for.
    index : int, optional
        For tuple-valued parameters, the element index.

    Returns
    -------
    dict
        Dictionary with keys ``"min"``, ``"max"``, and ``"step"``.
    """
    pad = 10.0
    _min, _max, step = None, None, None
    value = obj.value if index is None else obj.value[index]
    if obj.bmin is not None:
        _min = obj.bmin
    if obj.bmax is not None:
        _max = obj.bmax
    if _max is None and _min is not None:
        _max = value + pad
    if _min is None and _max is not None:
        _min = value - pad
    if _min is None and _max is None:
        if obj.component and obj is obj.component._position and obj._axes_manager is not None:
            axis = obj._axes_manager.signal_axes[-1]
            _min = axis.axis.min()
            _max = axis.axis.max()
            step = np.abs(axis.scale)
        else:
            _max = value + pad
            _min = value - pad
    if step is None:
        step = (_max - _min) * 0.001
    return {"min": _min, "max": _max, "step": step}


def _get_value_widget(obj, index=None):
    """Build a slider widget for a single parameter value.

    Parameters
    ----------
    obj : hyperspy.component.Parameter
        The parameter to create a widget for.
    index : int, optional
        For tuple-valued parameters, the element index.

    Returns
    -------
    dict
        ``{"widget": ContainerWidget, "wdict": {...}}``.
    """
    wdict = {}
    widget_bounds = _interactive_slider_bounds(obj, index=index)

    step = widget_bounds.get("step", 1.0)
    if step and step > 0:
        decimals = max(0, -int(np.floor(np.log10(abs(step)))) + 1)
    else:
        decimals = 4

    thismin = FloatTextWidget(value=round(widget_bounds["min"], decimals), description="min")
    thismax = FloatTextWidget(value=round(widget_bounds["max"], decimals), description="max")

    current_value = obj.value if index is None else obj.value[index]
    if index is None:
        current_name = obj.name
    else:
        current_name = "{}".format(index)

    widget = FloatSliderWidget(
        value=current_value,
        min=thismin.value,
        max=thismax.value,
        step=widget_bounds["step"],
        description=current_name,
        readout_format=".2f",
    )

    def on_min_change(change):
        if widget.max > change["new"]:
            widget.min = change["new"]
            widget.step = np.abs(widget.max - widget.min) * 0.001

    def on_max_change(change):
        if widget.min < change["new"]:
            widget.max = change["new"]
            widget.step = np.abs(widget.max - widget.min) * 0.001

    thismin.observe(on_min_change, names="value")
    thismax.observe(on_max_change, names="value")

    thismin._link = dlink((obj, "bmin"), (thismin, "value"))
    thismax._link = dlink((obj, "bmax"), (thismax, "value"))

    if index is not None:

        def _interactive_tuple_update(change):
            obj.value = obj.value[:index] + (change["new"],) + obj.value[index + 1 :]

        widget.observe(_interactive_tuple_update, names="value")
    else:
        link((obj, "value"), (widget, "value"))

    container = ContainerWidget(children=[thismin, widget, thismax], layout="horizontal")
    wdict["value"] = widget
    wdict["min"] = thismin
    wdict["max"] = thismax
    return {
        "widget": container,
        "wdict": wdict,
    }


[docs] @add_display_arg def get_parameter_widget(obj, **kwargs): """Build a widget for a HyperSpy component parameter. Parameters ---------- obj : hyperspy.component.Parameter The parameter to build a widget for. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ if obj._number_of_elements == 1: return _get_value_widget(obj) else: wdict = {} par_widgets = [] for i in range(obj._number_of_elements): thiswd = _get_value_widget(obj=obj, index=i) par_widgets.append(thiswd["widget"]) wdict["element{}".format(i)] = thiswd["wdict"] update = ButtonWidget( description="Update", tooltip="Unlike most other widgets, the multivalue parameter widgets do not update automatically when the value of the changes by other means. Use this button to update the values manually", ) def on_update_clicked(change): for i, value in enumerate(obj.value): element_wd = wdict["element{}".format(i)] minwidget = element_wd["min"] vwidget = element_wd["value"] maxwidget = element_wd["max"] if value < vwidget.min: minwidget.value = value elif value > vwidget.max: maxwidget.value = value vwidget.value = value update.observe(on_update_clicked, names="clicks") wdict["update_button"] = update container = ContainerWidget( children=[update] + par_widgets, layout="accordion", titles=[obj.name] ) return { "widget": container, "wdict": wdict, }
[docs] @add_display_arg def get_component_widget(obj, **kwargs): """Build a widget for a HyperSpy model component. Parameters ---------- obj : hyperspy.component.Component The component to build a widget for. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ wdict = {} active = CheckboxWidget(description="active", value=obj.active) wdict["active"] = active link((obj, "active"), (active, "value")) children = [active] for parameter in obj.parameters: pardict = parameter.gui(toolkit="anywidget", display=False)["anywidget"] wdict["parameter_{}".format(parameter.name)] = pardict["wdict"] children.append(pardict["widget"]) container = ContainerWidget(children=children, layout="vertical") return { "widget": container, "wdict": wdict, }
[docs] @add_display_arg def get_model_widget(obj, **kwargs): """Build a widget for a HyperSpy model. Parameters ---------- obj : hyperspy.model.BaseModel The model to build a widget for. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ children = [] wdict = {} for component in obj: idict = component.gui(display=False, toolkit="anywidget")["anywidget"] children.append(idict["widget"]) wdict["component_{}".format(component.name)] = idict["wdict"] container = ContainerWidget( children=children, layout="accordion", titles=[comp.name for comp in obj] ) return {"widget": container, "wdict": wdict}
[docs] @add_display_arg def get_eelscl_widget(obj, **kwargs): """Build a widget for an EELS core-loss edge component. Parameters ---------- obj : hyperspy.component.Component The EELS core-loss edge component. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ wdict = {} active = CheckboxWidget(description="active", value=obj.active) fine_structure = CheckboxWidget(description="Fine structure", value=obj.fine_structure_active) fs_smoothing = FloatSliderWidget( description="Fine structure smoothing", min=0, max=1, step=0.001, value=obj.fine_structure_smoothing, ) link((obj, "active"), (active, "value")) link((obj, "fine_structure_active"), (fine_structure, "value")) link((obj, "fine_structure_smoothing"), (fs_smoothing, "value")) children = [active, fine_structure, fs_smoothing] wdict["active"] = active wdict["fine_structure"] = fine_structure wdict["fs_smoothing"] = fs_smoothing for parameter in [obj.intensity, obj.effective_angle, obj.onset_energy]: pdict = parameter.gui(toolkit="anywidget", display=False)["anywidget"] children.append(pdict["widget"]) wdict["parameter_{}".format(parameter.name)] = pdict["wdict"] container = ContainerWidget(children=children, layout="vertical") return { "widget": container, "wdict": wdict, }
[docs] @add_display_arg def get_scalable_fixed_patter_widget(obj, **kwargs): """Build a widget for a ScalableFixedPattern component. Parameters ---------- obj : hyperspy.component.Component The ScalableFixedPattern component. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ wdict = {} active = CheckboxWidget(description="active", value=obj.active) wdict["active"] = active link((obj, "active"), (active, "value")) interpolate = CheckboxWidget(description="interpolate", value=obj.interpolate) wdict["interpolate"] = interpolate link((obj, "interpolate"), (interpolate, "value")) children = [active, interpolate] for parameter in obj.parameters: pardict = parameter.gui(toolkit="anywidget", display=False)["anywidget"] wdict["parameter_{}".format(parameter.name)] = pardict["wdict"] children.append(pardict["widget"]) container = ContainerWidget(children=children, layout="vertical") return { "widget": container, "wdict": wdict, }
[docs] @add_display_arg def fit_component_aw(obj, **kwargs): """Build a widget for fitting a single component in a selected range. Parameters ---------- obj : hyperspy.model.ComponentFit The component fit tool instance. **kwargs Passed through to the widget builder. Returns ------- dict or None ``{"widget": ContainerWidget, "wdict": {...}}`` when ``display=False``, otherwise ``None`` (widget displayed inline). """ wdict = {} only_current = CheckboxWidget(description="Only current") iterpath = enum2dropdown(obj.traits()["iterpath"]) def disable_iterpath(change): iterpath.disabled = change.new only_current.observe(disable_iterpath, names="value") wdict["only_current"] = only_current wdict["iterpath"] = iterpath help_text = LabelWidget( value="Click on the signal figure and drag to the right to select a range. Press `Fit` to fit the component in that range. If only current is unchecked the fit is performed in the whole dataset." ) wdict["help_text"] = help_text help_container = ContainerWidget(children=[help_text], layout="accordion", titles=["Help"]) link((obj, "only_current"), (only_current, "value")) link((obj, "iterpath"), (iterpath, "value")) fit = ButtonWidget(description="Fit", tooltip="Fit in the selected signal range") close = ButtonWidget( description="Close", tooltip="Close widget and remove span selector from the signal figure." ) wdict["close_button"] = close wdict["fit_button"] = fit def on_fit_clicked(change): obj._fit_fired() fit.observe(on_fit_clicked, names="clicks") box = ContainerWidget( children=[ only_current, labelme("Iterpath", iterpath), help_container, ContainerWidget(children=[fit, close], layout="horizontal"), ], layout="vertical", ) def on_close_clicked(change): obj.span_selector_switch(False) if hasattr(box, "close"): box.close() close.observe(on_close_clicked, names="clicks") return { "widget": box, "wdict": wdict, }