Ejemplo n.º 1
0
    def addtodoc(cls, mainviews, ctrl, doc) -> List[Div]:
        "creates the widget"
        dlg = SaveFileDialog(ctrl)
        div = Div(text = "", width = 0, height = 0)

        mainview = mainviews[0] if isinstance(mainviews, (list, tuple)) else mainviews
        figure   = mainview.getfigure()

        figure.tools = (
            figure.tools
            + [
                CustomAction(
                    action_tooltip = dlg.title,
                    callback       = CustomJS(
                        code = 'div.text = div.text + " ";',
                        args = dict(div = div)
                    )
                )
            ]
        )

        if isinstance(mainviews, (list, tuple)):
            for i in mainviews[1:]:
                i.getfigure().tools = i.getfigure().tools + [figure.tools[-1]]

        def _cb(attr, old, new):
            if new == " " and div.text == ' ':
                div.text = ""
                asyncio.create_task(cls._run(dlg, mainview, ctrl, doc))

        div.on_change("text", _cb)
        return [div]
Ejemplo n.º 2
0
class Fit1DVisualizer(interactive.PrimitiveVisualizer):
    """
    The generic class for interactive fitting of one or more 1D functions

    Attributes:
        reinit_panel: layout containing widgets that control the parameters
                      affecting the initialization of the (x,y) array(s)
        reinit_button: the button to reconstruct the (x,y) array(s)
        tabs: layout containing all the stuff required for an interactive 1D fit
        submit_button: the button signifying a successful end to the interactive session

        config: the Config object describing the parameters are their constraints
        widgets: a dict of (param_name, widget) elements that allow the properties
                 of the widgets to be set/accessed by the calling primitive. So far
                 I'm only including the widgets in the reinit_panel
        fits: list of InteractiveModel instances, one per (x,y) array
    """
    def __init__(self,
                 data_source,
                 fitting_parameters,
                 config,
                 reinit_params=None,
                 reinit_extras=None,
                 reinit_live=False,
                 order_param="order",
                 tab_name_fmt='{}',
                 xlabel='x',
                 ylabel='y',
                 domains=None,
                 function=None,
                 title=None,
                 **kwargs):
        """
        Parameters
        ----------
        data_source : array or function
            input data or the function to calculate the input data.  The input data
            should be [x, y] or [x, y, weights] or [[x, y], [x, y],.. or [[x, y, weights], [x, y, weights]..
            or, if a function, it accepts (config, extras) where extras is a dict of values based on reinit_extras
            and returns [[x, y], [x, y].. or [[x, y, weights], [x, y, weights], ...
        fitting_parameters : list of :class:`~geminidr.interactive.fit.fit1d.FittingParameters` or :class:`~geminidr.interactive.fit.fit1d.FittingParameters`
            Description of parameters to use for `fit_1d`
        config : Config instance describing primitive parameters and limitations
        reinit_params : list of str
            list of parameter names in config related to reinitializing fit arrays.  These cause the `data_source`
            function to be run to get the updated coordinates/weights.  Should not be passed if `data_source` is
            not a function.
        reinit_extras :
            Extra parameters to show on the left side that can affect the output of `data_source` but
            are not part of the primitive configuration.  Should not be passed if `data_source` is not a function.
        reinit_live :
            If False, supplies a button to call the `data_source` function and doesn't do so automatically when inputs
            are adjusted.  If `data_source` is known to be inexpensive, you can set this to `True`
        order_param : str
            Name of the parameter this primitive uses for `order`, to infer the min/max suggested values
        tab_name_fmt : str
            Format string for naming the tabs
        xlabel : str
            String label for X axis
        ylabel : str
            String label for Y axis
        domains : list
            List of domains for the inputs
        function : str
            ID of fit_1d function to use, if not a configuration option
        title : str
            Title for UI (Interactive <Title>)
        """
        super().__init__(config=config, title=title)

        # title_div = None
        # if title is not None:
        #     title_div = Div(text='<h2>%s</h2>' % title)

        # Make the widgets accessible from external code so we can update
        # their properties if the default setup isn't great
        self.widgets = {}

        # Make the panel with widgets to control the creation of (x, y) arrays

        # Function - either a dropdown or a label for the single option
        if 'function' in config._fields:
            fn = config.function
            fn_allowed = [k for k in config._fields['function'].allowed.keys()]

            # Dropdown for selecting fit_1D function
            self.function = Select(title="Fitting Function:",
                                   value=fn,
                                   options=fn_allowed)

            def fn_select_change(attr, old, new):
                def refit():
                    for fit in self.fits:
                        fit.set_function(new)
                        fit.perform_fit()

                self.do_later(refit)

            self.function.on_change('value', fn_select_change)
        else:
            if function is None:
                function = 'chebyshev'
            self.function = Div(text='Function: %s' % function)

        if reinit_params is not None or reinit_extras is not None:
            # Create left panel
            reinit_widgets = self.make_widgets_from_config(
                reinit_params, reinit_extras, reinit_live)

            # This should really go in the parent class, like submit_button
            if not reinit_live:
                self.reinit_button = bm.Button(label="Reconstruct points")
                self.reinit_button.on_click(self.reconstruct_points)
                self.make_modal(
                    self.reinit_button,
                    "<b>Recalculating Points</b><br/>This may take 20 seconds")
                reinit_widgets.append(self.reinit_button)

            self.reinit_panel = column(self.function, *reinit_widgets)
        else:
            # left panel with just the function selector (Chebyshev, etc.)
            self.reinit_panel = column(self.function)

        # Grab input coordinates or calculate if we were given a callable
        # TODO revisit the raging debate on `callable` for Python 3
        if callable(data_source):
            self.reconstruct_points_fn = data_source
            data = data_source(config, self.extras)
            # For this, we need to remap from
            # [[x1, y1, weights1], [x2, y2, weights2], ...]
            # to allx=[x1,x2..] ally=[y1,y2..] all_weights=[weights1,weights2..]
            allx = list()
            ally = list()
            all_weights = list()
            for dat in data:
                allx.append(dat[0])
                ally.append(dat[1])
                if len(dat) >= 3:
                    all_weights.append(dat[2])
            if len(all_weights) == 0:
                all_weights = None
        else:
            self.reconstruct_points_fn = None
            if reinit_params:
                raise ValueError(
                    "Saw reinit_params but data_source is not a callable")
            if reinit_extras:
                raise ValueError(
                    "Saw reinit_extras but data_source is not a callable")
            allx = data_source[0]
            ally = data_source[1]
            if len(data_source) >= 3:
                all_weights = data_source[2]
            else:
                all_weights = None

        # Some sanity checks now
        if isinstance(fitting_parameters, list):
            if not (len(fitting_parameters) == len(allx) == len(ally)):
                raise ValueError("Different numbers of models and coordinates")
            self.nfits = len(fitting_parameters)
        else:
            if allx.size != ally.size:
                raise ValueError("Different (x, y) array sizes")
            self.nfits = 1

        self.reinit_extras = [] if reinit_extras is None else reinit_extras

        kwargs.update({'xlabel': xlabel, 'ylabel': ylabel})
        if order_param and order_param in self.config._fields:
            field = self.config._fields[order_param]
            if hasattr(field, 'min') and field.min:
                kwargs['min_order'] = field.min
            else:
                kwargs['min_order'] = 1
            if hasattr(field, 'max') and field.max:
                kwargs['max_order'] = field.max
            else:
                kwargs['max_order'] = field.default * 2
        else:
            kwargs['min_order'] = 1
            kwargs['max_order'] = 10

        self.tabs = bm.Tabs(tabs=[], name="tabs")
        self.tabs.sizing_mode = 'scale_width'
        self.fits = []
        if self.nfits > 1:
            if domains is None:
                domains = [None] * len(fitting_parameters)
            if all_weights is None:
                all_weights = [None] * len(fitting_parameters)
            for i, (fitting_parms, domain, x, y, weights) in \
                    enumerate(zip(fitting_parameters, domains, allx, ally, all_weights), start=1):
                tui = Fit1DPanel(self, fitting_parms, domain, x, y, weights,
                                 **kwargs)
                tab = bm.Panel(child=tui.component,
                               title=tab_name_fmt.format(i))
                self.tabs.tabs.append(tab)
                self.fits.append(tui.fit)
        else:
            tui = Fit1DPanel(self, fitting_parameters[0], domains, allx[0],
                             ally[0], all_weights[0], **kwargs)
            tab = bm.Panel(child=tui.component, title=tab_name_fmt.format(1))
            self.tabs.tabs.append(tab)
            self.fits.append(tui.fit)

    def visualize(self, doc):
        """
        Start the bokeh document using this visualizer.

        This call is responsible for filling in the bokeh document with
        the user interface.

        Parameters
        ----------
        doc : :class:`~bokeh.document.Document`
            bokeh document to draw the UI in
        """
        super().visualize(doc)
        col = column(self.tabs, )
        col.sizing_mode = 'scale_width'
        layout = column(row(self.reinit_panel, col),
                        self.submit_button,
                        sizing_mode="stretch_width")
        doc.add_root(layout)

    def reconstruct_points(self):
        """
        Reconstruct the initial points to work with.

        This is expected to be expensive.  The core inputs
        are separated out in the UI as they are too slow to
        be interactive.  When a user is ready and submits
        updated core config parameters, this is what gets
        executed.  The configuration is updated with the
        new values form the user.  The UI is disabled and the
        expensive function is wrapped in the bokeh Tornado
        event look so the modal dialog can display.
        """
        if hasattr(self, 'reinit_button'):
            self.reinit_button.disabled = True

        def fn():
            """Top-level code to update the Config with the values from the widgets"""
            config_update = {k: v.value for k, v in self.widgets.items()}
            for extra in self.reinit_extras:
                del config_update[extra]
            for k, v in config_update.items():
                print(f'{k} = {v}')
            self.config.update(**config_update)

        self.do_later(fn)

        if self.reconstruct_points_fn is not None:

            def rfn():
                all_coords = self.reconstruct_points_fn(
                    self.config, self.extras)
                for fit, coords in zip(self.fits, all_coords):
                    if len(coords) > 2:
                        fit.weights = coords[2]
                    else:
                        fit.weights = None
                    fit.weights = fit.populate_bokeh_objects(coords[0],
                                                             coords[1],
                                                             fit.weights,
                                                             mask=None)
                    fit.perform_fit()
                if hasattr(self, 'reinit_button'):
                    self.reinit_button.disabled = False

            self.do_later(rfn)

    def results(self):
        """
        Get the results of the interactive fit.

        This gets the list of `~gempy.library.fitting.fit_1D` fits of
        the data to be used by the caller.

        Returns
        -------
        list of `~gempy.library.fitting.fit_1D`
        """
        return [fit.model.fit for fit in self.fits]