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]
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]