def get_panels(self, parent): """ Create and return the list of wx.Panels for your plug-in. Define the plug-in perspective. Panels should inherit from DefaultPanel defined below, or should present the same interface. They must define "window_caption" and "window_name". :param parent: parent window :return: list of panels """ # # Save a reference to the parent self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100, 200)) self.invariant_panel = InvariantPanel(parent=self.frame) self.frame.set_panel(self.invariant_panel) self._frame_set_helper() self.invariant_panel.set_manager(manager=self) self.perspective.append(self.invariant_panel.window_name) # Create reader when fitting panel are created self.state_reader = reader(self.set_state) # append that reader to list of available reader loader = Loader() loader.associate_file_reader(".inv", self.state_reader) # loader.associate_file_reader(".svs", self.state_reader) # Return the list of panels return [self.invariant_panel]
def create_1d_panel(self, data, group_id): """ """ # Create a new plot panel if none was available if issubclass(data.__class__, Data1D): from Plotter1D import ModelPanel1D ## get the data representation label of the data to plot ## when even the user select "change scale" xtransform = data.xtransform ytransform = data.ytransform ## create a plotpanel for 1D Data win = MDIFrame(self.parent, None, 'None', (100, 200)) new_panel = ModelPanel1D(win, -1, xtransform=xtransform, ytransform=ytransform, style=wx.RAISED_BORDER) win.set_panel(new_panel) win.Show(False) new_panel.frame = win #win.Show(True) return new_panel msg = "1D Panel of group ID %s could not be created" % str(group_id) raise ValueError, msg
def get_panels(self, parent): """ Define the GUI panels """ self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100,200)) self.data_id = IQ_DATA_LABEL self.corfunc_panel = CorfuncPanel(parent=self.frame) self.frame.set_panel(self.corfunc_panel) self.corfunc_panel.set_manager(self) self._frame_set_helper() self.perspective.append(self.corfunc_panel.window_name) l = Loader() l.associate_file_reader('.cor', self.state_reader) return [self.corfunc_panel]
def create_2d_panel(self, data, group_id): """ """ if issubclass(data.__class__, Data2D): ##Create a new plotpanel for 2D data from Plotter2D import ModelPanel2D scale = data.scale win = MDIFrame(self.parent, None, 'None', (200, 150)) win.Show(False) new_panel = ModelPanel2D(win, id=-1, data2d=data, scale=scale, style=wx.RAISED_BORDER) win.set_panel(new_panel) new_panel.frame = win return new_panel msg = "2D Panel of group ID %s could not be created" % str(group_id) raise ValueError, msg
def get_panels(self, parent): """ Create and return a list of panel objects """ from inversion_panel import InversionControl self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100, 200)) self.control_panel = InversionControl(self.frame, -1, style=wx.RAISED_BORDER) self.frame.set_panel(self.control_panel) self._frame_set_helper() self.control_panel.set_manager(self) self.control_panel.nfunc = self.nfunc self.control_panel.d_max = self.max_length self.control_panel.alpha = self.alpha self.perspective = [] self.perspective.append(self.control_panel.window_name) return [self.control_panel]
def onBoxSum(self, event): """ """ from sas.sasgui.guiframe.gui_manager import MDIFrame from boxSum import BoxSum self.onClearSlicer(event) self.slicer_z += 1 self.slicer = BoxSum(self, self.subplot, zorder=self.slicer_z) self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax) self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax) self.update() self.slicer.update() ## Value used to initially set the slicer panel params = self.slicer.get_params() ## Create a new panel to display results of summation of Data2D from parameters_panel_boxsum import SlicerPanel win = MDIFrame(self.parent, None, 'None', (100, 200)) new_panel = SlicerPanel(parent=win, id=-1, base=self, type=self.slicer.__class__.__name__, params=params, style=wx.RAISED_BORDER) new_panel.window_caption = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) new_panel.window_name = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) ## Store a reference of the new created panel ## save the window_caption of the new panel in the current slicer self.slicer.set_panel_name(name=new_panel.window_caption) ## post slicer panel to guiframe to display it from sas.sasgui.guiframe.events import SlicerPanelEvent win.set_panel(new_panel) new_panel.frame = win wx.PostEvent(self.parent, SlicerPanelEvent(panel=new_panel, main_panel=self)) wx.CallAfter(new_panel.frame.Show) self.panel_slicer = new_panel
def onBoxSum(self, event): """ """ from sas.sasgui.guiframe.gui_manager import MDIFrame from boxSum import BoxSum self.onClearSlicer(event) self.slicer_z += 1 self.slicer = BoxSum(self, self.subplot, zorder=self.slicer_z) self.subplot.set_ylim(self.data2D.ymin, self.data2D.ymax) self.subplot.set_xlim(self.data2D.xmin, self.data2D.xmax) self.update() self.slicer.update() ## Value used to initially set the slicer panel params = self.slicer.get_params() ## Create a new panel to display results of summation of Data2D from slicerpanel import SlicerPanel win = MDIFrame(self.parent, None, 'None', (100, 200)) new_panel = SlicerPanel(parent=win, id=-1, base=self, type=self.slicer.__class__.__name__, params=params, style=wx.RAISED_BORDER) new_panel.window_caption = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) new_panel.window_name = self.slicer.__class__.__name__ + " " + \ str(self.data2D.name) ## Store a reference of the new created panel ## save the window_caption of the new panel in the current slicer self.slicer.set_panel_name(name=new_panel.window_caption) ## post slicer panel to guiframe to display it from sas.sasgui.guiframe.events import SlicerPanelEvent win.set_panel(new_panel) new_panel.frame = win wx.PostEvent(self.parent, SlicerPanelEvent(panel=new_panel, main_panel=self)) wx.CallAfter(new_panel.frame.Show) self.panel_slicer = new_panel
def get_panels(self, parent): """ Define the GUI panels """ self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100,200)) self.data_id = IQ_DATA_LABEL self.corfunc_panel = CorfuncPanel(parent=self.frame) self.frame.set_panel(self.corfunc_panel) self.corfunc_panel.set_manager(self) self._frame_set_helper() self.perspective.append(self.corfunc_panel.window_name) l = Loader() l.associate_file_reader('.crf', self.state_reader) return [self.corfunc_panel]
class Plugin(PluginBase): """ This class defines the interface for a plugin class for a correlation function perspective """ def __init__(self): PluginBase.__init__(self, name="Correlation Function") logging.info("Correlation function plug-in started") self._always_active = True self.state_reader = Reader(self.set_state) self._extensions = '.cor' def get_panels(self, parent): """ Define the GUI panels """ self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100,200)) self.data_id = IQ_DATA_LABEL self.corfunc_panel = CorfuncPanel(parent=self.frame) self.frame.set_panel(self.corfunc_panel) self.corfunc_panel.set_manager(self) self._frame_set_helper() self.perspective.append(self.corfunc_panel.window_name) l = Loader() l.associate_file_reader('.cor', self.state_reader) return [self.corfunc_panel] def get_context_menu(self, plotpanel=None): """ Get the context menu items available for Corfunc. :param plotpanel: A Plotter1D panel :return: a list of menu items with call-back function :note: if Data1D was generated from Theory1D the fitting option is not allowed """ graph = plotpanel.graph if graph.selected_plottable not in plotpanel.plots: return [] data = plotpanel.plots[graph.selected_plottable] if data.id == IQ_DATA_LABEL or data.id == IQ_EXTRAPOLATED_DATA_LABEL or data.id == TRANSFORM_LABEL: return [] item = plotpanel.plots[graph.selected_plottable] if item.__class__.__name__ is "Data2D": return [] elif item.__class__.__name__ is "Data1D": return [["Select data in corfunc", "Send this data to the correlation function perspective", self._on_select_data]] def set_state(self, state=None, datainfo=None): """ Callback for CorfuncState reader. Called when a .cor file is loaded """ if isinstance(datainfo, list): data = datainfo[0] else: data = datainfo self.corfunc_panel.set_state(state=state, data=data) self.on_perspective(event=None) data = self.parent.create_gui_data(data, None) self.parent.add_data({ data.id: data }) def set_data(self, data_list=None): """ Load the data that's been selected :param data_list: The data to load in """ if data_list is None: data_list = [] if len(data_list) >= 1: msg = "" if len(data_list) == 1: data = data_list[0] else: data_1d_list = [] data_2d_list = [] err_msg = "" for data in data_list: if data is not None: if issubclass(data.__class__, Data1D): data_1d_list.append(data) else: err_msg += "{} type {} \n".format(str(data.name), str(data.__class__)) data_2d_list.append(data) if len(data_2d_list) > 0: msg = "Corfunc doesn't support the following data types:\n" msg += err_msg if len(data_1d_list) == 0: msg += "No data recieved" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return elif len(data_list) > 1: msg = "Corfunc does not allow multiple data\n" msg += "Please select one.\n" dlg = DataDialog(data_list=data_1d_list, text=msg) if dlg.ShowModal() == wx.ID_OK: data = dlg.get_data() else: data = None dlg.Destroy() if data is None: msg += "Corfunc recieved no data\n" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return if issubclass(data.__class__, Data1D): try: wx.PostEvent(self.parent, NewPlotEvent(action='remove', group_id=GROUP_ID_IQ_DATA, id=self.data_id)) self.data_id = data.id self.corfunc_panel.set_data(data) except: msg = "Corfunc set_data: " + str(sys.exc_value) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) def delete_data(self, data): """ Delete the data from the perspective """ self.clear_data() self.corfunc_panel.set_data(None) def show_data(self, data, label, reset=False, active_ctrl=None): """ Show data read from a file :param data: The data to plot (Data1D) :param label: What to label the plot. Also used as the plot ID :param reset: If True, all other plottables will be cleared """ new_plot = Data1D(data.x, copy.deepcopy(data.y), dy=data.dy) group_id = "" if label == IQ_DATA_LABEL or label == IQ_EXTRAPOLATED_DATA_LABEL: new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.y -= self.corfunc_panel.background # Show data on a log(Q)/I scale new_plot.ytransform = 'y' group_id = GROUP_ID_IQ_DATA if label == IQ_EXTRAPOLATED_DATA_LABEL: # Show the extrapolation as a curve instead of points new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM elif label == TRANSFORM_LABEL: new_plot.xaxis("{x}", 'A') new_plot.yaxis("{\\Gamma}", '') # Show transform on a linear scale new_plot.xtransform = 'x' new_plot.ytransform = 'y' group_id = GROUP_ID_TRANSFORM # Show the transformation as a curve instead of points new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.id = label new_plot.name = label new_plot.group_id = group_id new_plot.interactive = True new_plot.title = group_id.replace('$', '').replace('\\', '') wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.title, reset=reset)) if label == IQ_DATA_LABEL or label == IQ_EXTRAPOLATED_DATA_LABEL: wx.CallAfter(self.corfunc_panel.plot_qrange, active=active_ctrl, leftdown=True) if label == IQ_EXTRAPOLATED_DATA_LABEL: # Zoom in to the region we're interested in xlim = (min(self.corfunc_panel._extrapolated_data.x), self.corfunc_panel.qmax[1]*1.2) wx.CallAfter(wx.PostEvent, self.parent, PlotLimitEvent(id=IQ_DATA_LABEL, group_id=GROUP_ID_IQ_DATA, xlim=xlim)) def clear_data(self): wx.PostEvent(self.parent, NewPlotEvent(action='delete', group_id=GROUP_ID_TRANSFORM)) wx.PostEvent(self.parent, NewPlotEvent(action='clear', group_id=GROUP_ID_IQ_DATA)) def _on_select_data(self, event): panel = event.GetEventObject() if not panel.graph.selected_plottable in panel.plots: return data = panel.plots[panel.graph.selected_plottable] self.set_data([data])
class Plugin(PluginBase): """ This class defines the interface for invariant Plugin class that can be used by the gui_manager. """ def __init__(self): PluginBase.__init__(self, name="Invariant") # dictionary containing data name and error on dy of that data self.err_dy = {} # default state objects self.state_reader = None self._extensions = '.inv' self.temp_state = None self.__data = None # Log startup logging.info("Invariant plug-in started") def get_data(self): """ """ return self.__data def get_panels(self, parent): """ Create and return the list of wx.Panels for your plug-in. Define the plug-in perspective. Panels should inherit from DefaultPanel defined below, or should present the same interface. They must define "window_caption" and "window_name". :param parent: parent window :return: list of panels """ # # Save a reference to the parent self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100, 200)) self.invariant_panel = InvariantPanel(parent=self.frame) self.frame.set_panel(self.invariant_panel) self._frame_set_helper() self.invariant_panel.set_manager(manager=self) self.perspective.append(self.invariant_panel.window_name) # Create reader when fitting panel are created self.state_reader = reader(self.set_state) # append that reader to list of available reader loader = Loader() loader.associate_file_reader(".inv", self.state_reader) # loader.associate_file_reader(".svs", self.state_reader) # Return the list of panels return [self.invariant_panel] def get_context_menu(self, plotpanel=None): """ This method is optional. When the context menu of a plot is rendered, the get_context_menu method will be called to give you a chance to add a menu item to the context menu. A ref to a Graph object is passed so that you can investigate the plot content and decide whether you need to add items to the context menu. This method returns a list of menu items. Each item is itself a list defining the text to appear in the menu, a tool-tip help text, and a call-back method. :param graph: the Graph object to which we attach the context menu :return: a list of menu items with call-back function """ graph = plotpanel.graph invariant_option = "Compute invariant" invariant_hint = "Will displays the invariant panel for" invariant_hint += " further computation" if graph.selected_plottable not in plotpanel.plots: return [] data = plotpanel.plots[graph.selected_plottable] if issubclass(data.__class__, Data1D): if data.name != "$I_{obs}(q)$" and data.name != " $P_{fit}(r)$": return [[invariant_option, invariant_hint, self._compute_invariant]] return [] def _compute_invariant(self, event): """ Open the invariant panel to invariant computation """ self.panel = event.GetEventObject() Plugin.on_perspective(self, event=event) id = self.panel.graph.selected_plottable data = self.panel.plots[self.panel.graph.selected_plottable] if data is None: return if not issubclass(data.__class__, Data1D): name = data.__class__.__name__ msg = "Invariant use only Data1D got: [%s] " % str(name) raise ValueError, msg self.compute_helper(data=data) def set_data(self, data_list=None): """ receive a list of data and compute invariant """ msg = "" data = None if data_list is None: data_list = [] if len(data_list) >= 1: if len(data_list) == 1: data = data_list[0] else: data_1d_list = [] data_2d_list = [] error_msg = "" # separate data into data1d and data2d list for data in data_list: if data is not None: if issubclass(data.__class__, Data1D): data_1d_list.append(data) else: error_msg += " %s type %s \n" % (str(data.name), str(data.__class__.__name__)) data_2d_list.append(data) if len(data_2d_list) > 0: msg = "Invariant does not support the following data types:\n" msg += error_msg if len(data_1d_list) == 0: wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return msg += "Invariant panel does not allow multiple data!\n" msg += "Please select one.\n" if len(data_list) > 1: from invariant_widgets import DataDialog dlg = DataDialog(data_list=data_1d_list, text=msg) if dlg.ShowModal() == wx.ID_OK: data = dlg.get_data() else: data = None dlg.Destroy() if data is None: msg += "invariant receives no data. \n" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return if not issubclass(data.__class__, Data1D): msg += "invariant cannot be computed for data of " msg += "type %s\n" % (data.__class__.__name__) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return else: wx.PostEvent(self.parent, NewPlotEvent(plot=data, title=data.title)) try: self.compute_helper(data) except: msg = "Invariant Set_data: " + str(sys.exc_value) wx.PostEvent(self.parent, StatusEvent(status=msg, info="error")) else: msg = "invariant cannot be computed for data of " msg += "type %s" % (data.__class__.__name__) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) def delete_data(self, data_id): """ """ if self.__data is None: return for id in data_id: if id == self.__data.id: self.clear_panel() def clear_panel(self): """ """ self.invariant_panel.clear_panel() def compute_helper(self, data): """ """ if data is None: return # set current data if not it's a state data if not self.invariant_panel.is_state_data: # Store reference to data self.__data = data # Set the data set to be user for invariant calculation self.invariant_panel.set_data(data=data) def save_file(self, filepath, state=None): """ Save data in provided state object. :param filepath: path of file to write to :param state: invariant state """ # Write the state to file # First, check that the data is of the right type current_plottable = self.__data if issubclass(current_plottable.__class__, Data1D): self.state_reader.write(filepath, current_plottable, state) else: msg = "invariant.save_file: the data being saved is" msg += " not a sas.sascalc.dataloader.data_info.Data1D object" raise RuntimeError, msg def set_state(self, state=None, datainfo=None): """ Call-back method for the state reader. This method is called when a .inv/.svs file is loaded. :param state: State object """ self.temp_state = None try: if datainfo.__class__.__name__ == 'list': data = datainfo[0] else: data = datainfo if data is None: msg = "invariant.set_state: datainfo parameter cannot" msg += " be None in standalone mode" raise RuntimeError, msg # Make sure the user sees the invariant panel after loading # self.parent.set_perspective(self.perspective) self.on_perspective(event=None) name = data.meta_data['invstate'].file data.meta_data['invstate'].file = name data.name = name data.filename = name data = self.parent.create_gui_data(data, None) self.__data = data wx.PostEvent(self.parent, NewPlotEvent(plot=self.__data, reset=True, title=self.__data.title)) data_dict = {self.__data.id:self.__data} self.parent.add_data(data_list=data_dict) # set state self.invariant_panel.is_state_data = True # Load the invariant states self.temp_state = state # Requires to have self.__data and self.temp_state first. self.on_set_state_helper(None) except: logging.error("invariant.set_state: %s" % sys.exc_value) def on_set_state_helper(self, event=None): """ Set the state when called by EVT_STATE_UPDATE event from guiframe after a .inv/.svs file is loaded """ self.invariant_panel.set_state(state=self.temp_state, data=self.__data) self.temp_state = None def plot_theory(self, data=None, name=None): """ Receive a data set and post a NewPlotEvent to parent. :param data: extrapolated data to be plotted :param name: Data's name to use for the legend """ # import copy if data is None: id = str(self.__data.id) + name group_id = self.__data.group_id wx.PostEvent(self.parent, NewPlotEvent(id=id, group_id=group_id, action='Remove')) return new_plot = Data1D(x=[], y=[], dy=None) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM scale = self.invariant_panel.get_scale() background = self.invariant_panel.get_background() if scale != 0: # Put back the sacle and bkg for plotting data.y = (data.y + background) / scale new_plot = Data1D(x=data.x, y=data.y, dy=None) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM else: msg = "Scale can not be zero." raise ValueError, msg if len(new_plot.x) == 0: return new_plot.name = name new_plot.xaxis(self.__data._xaxis, self.__data._xunit) new_plot.yaxis(self.__data._yaxis, self.__data._yunit) new_plot.group_id = self.__data.group_id new_plot.id = str(self.__data.id) + name new_plot.title = self.__data.title # Save theory_data in a state if data != None: name_head = name.split('-') if name_head[0] == 'Low': self.invariant_panel.state.theory_lowQ = copy.deepcopy(new_plot) elif name_head[0] == 'High': self.invariant_panel.state.theory_highQ = copy.deepcopy(new_plot) self.parent.update_theory(data_id=self.__data.id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=self.__data.title)) def plot_data(self, scale, background): """ replot the current data if the user enters a new scale or background """ new_plot = scale * self.__data - background new_plot.name = self.__data.name new_plot.group_id = self.__data.group_id new_plot.id = self.__data.id new_plot.title = self.__data.title # Save data in a state: but seems to never happen if new_plot != None: self.invariant_panel.state.data = copy.deepcopy(new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.title))
class Plugin(PluginBase): """ P(r) inversion perspective """ DEFAULT_ALPHA = 0.0001 DEFAULT_NFUNC = 10 DEFAULT_DMAX = 140.0 def __init__(self): PluginBase.__init__(self, name="Pr Inversion") ## Simulation window manager self.simview = None ## State data self.alpha = self.DEFAULT_ALPHA self.nfunc = self.DEFAULT_NFUNC self.max_length = self.DEFAULT_DMAX self.q_min = None self.q_max = None self.est_bck = False self.bck_val = 0 self.slit_height = 0 self.slit_width = 0 ## Remember last plottable processed self.last_data = "" self._current_file_data = None ## Time elapsed for last computation [sec] # Start with a good default self.elapsed = 0.022 self.iq_data_shown = False ## Current invertor self.invertor = None self.pr = None self.data_id = IQ_DATA_LABEL # Copy of the last result in case we need to display it. self._last_pr = None self._last_out = None self._last_cov = None ## Calculation thread self.calc_thread = None ## Estimation thread self.estimation_thread = None ## Result panel self.control_panel = None ## Currently views plottable self.current_plottable = None ## Number of P(r) points to display on the output plot self._pr_npts = 51 self._normalize_output = False self._scale_output_unity = False ## List of added P(r) plots self._added_plots = {} self._default_Iq = {} self.list_plot_id = [] # Associate the inversion state reader with .prv files from inversion_state import Reader # Create a CanSAS/Pr reader self.state_reader = Reader(self.set_state) self._extensions = '.prv' l = Loader() l.associate_file_reader('.prv', self.state_reader) #l.associate_file_reader(".svs", self.state_reader) # Log startup logger.info("Pr(r) plug-in started") def delete_data(self, data_id): """ delete the data association with prview """ self.control_panel.clear_panel() def get_data(self): """ Returns the current data """ return self.current_plottable def set_state(self, state=None, datainfo=None): """ Call-back method for the inversion state reader. This method is called when a .prv file is loaded. :param state: InversionState object :param datainfo: Data1D object [optional] """ try: if datainfo.__class__.__name__ == 'list': if len(datainfo) >= 1: data = datainfo[0] else: data = None else: data = datainfo if data is None: msg = "Pr.set_state: datainfo parameter cannot " msg += "be None in standalone mode" raise RuntimeError, msg # Ensuring that plots are coordinated correctly t = time.localtime(data.meta_data['prstate'].timestamp) time_str = time.strftime("%b %d %H:%M", t) # Check that no time stamp is already appended max_char = data.meta_data['prstate'].file.find("[") if max_char < 0: max_char = len(data.meta_data['prstate'].file) datainfo.meta_data['prstate'].file = \ data.meta_data['prstate'].file[0:max_char]\ + ' [' + time_str + ']' data.filename = data.meta_data['prstate'].file # TODO: #remove this call when state save all information about the gui data # such as ID , Group_ID, etc... #make self.current_plottable = datainfo directly self.current_plottable = self.parent.create_gui_data(data, None) self.current_plottable.group_id = data.meta_data['prstate'].file # Make sure the user sees the P(r) panel after loading #self.parent.set_perspective(self.perspective) self.on_perspective(event=None) # Load the P(r) results #state = self.state_reader.get_state() data_dict = {self.current_plottable.id: self.current_plottable} self.parent.add_data(data_list=data_dict) wx.PostEvent( self.parent, NewPlotEvent(plot=self.current_plottable, title=self.current_plottable.title)) self.control_panel.set_state(state) except: logger.error("prview.set_state: %s" % sys.exc_value) def help(self, evt): """ Show a general help dialog. :TODO: replace the text with a nice image """ from inversion_panel import HelpDialog dialog = HelpDialog(None, -1) if dialog.ShowModal() == wx.ID_OK: dialog.Destroy() else: dialog.Destroy() def _fit_pr(self, evt): """ """ # Generate P(r) for sphere radius = 60.0 d_max = 2 * radius r = pylab.arange(0.01, d_max, d_max / 51.0) M = len(r) y = np.zeros(M) pr_err = np.zeros(M) total = 0.0 for j in range(M): value = self.pr_theory(r[j], radius) total += value y[j] = value pr_err[j] = math.sqrt(y[j]) y = y / total * d_max / len(r) # Perform fit pr = Invertor() pr.d_max = d_max pr.alpha = 0 pr.x = r pr.y = y pr.err = pr_err out, cov = pr.pr_fit() for i in range(len(out)): print("%g +- %g" % (out[i], math.sqrt(cov[i][i]))) # Show input P(r) title = "Pr" new_plot = Data1D(pr.x, pr.y, dy=pr.err) new_plot.name = "P_{obs}(r)" new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.group_id = "P_{obs}(r)" new_plot.id = "P_{obs}(r)" new_plot.title = title self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) # Show P(r) fit self.show_pr(out, pr) # Show I(q) fit q = pylab.arange(0.001, 0.1, 0.01 / 51.0) self.show_iq(out, pr, q) def show_shpere(self, x, radius=70.0, x_range=70.0): """ """ # Show P(r) y_true = np.zeros(len(x)) sum_true = 0.0 for i in range(len(x)): y_true[i] = self.pr_theory(x[i], radius) sum_true += y_true[i] y_true = y_true / sum_true * x_range / len(x) # Show the theory P(r) new_plot = Data1D(x, y_true) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = "P_{true}(r)" new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.id = "P_{true}(r)" new_plot.group_id = "P_{true}(r)" self.parent.update_theory(data_id=self.data_id, theory=new_plot) #Put this call in plottables/guitools wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="Sphere P(r)")) def get_npts(self): """ Returns the number of points in the I(q) data """ try: return len(self.pr.x) except: return 0 def show_iq(self, out, pr, q=None): """ Display computed I(q) """ qtemp = pr.x if q is not None: qtemp = q # Make a plot maxq = -1 for q_i in qtemp: if q_i > maxq: maxq = q_i minq = 0.001 # Check for user min/max if pr.q_min is not None: minq = pr.q_min if pr.q_max is not None: maxq = pr.q_max x = pylab.arange(minq, maxq, maxq / 301.0) y = np.zeros(len(x)) err = np.zeros(len(x)) for i in range(len(x)): value = pr.iq(out, x[i]) y[i] = value try: err[i] = math.sqrt(math.fabs(value)) except: err[i] = 1.0 print("Error getting error", value, x[i]) new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = IQ_FIT_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") title = "I(q)" new_plot.title = title # If we have a group ID, use it if 'plot_group_id' in pr.info: new_plot.group_id = pr.info["plot_group_id"] new_plot.id = IQ_FIT_LABEL self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) # If we have used slit smearing, plot the smeared I(q) too if pr.slit_width > 0 or pr.slit_height > 0: x = pylab.arange(minq, maxq, maxq / 301.0) y = np.zeros(len(x)) err = np.zeros(len(x)) for i in range(len(x)): value = pr.iq_smeared(out, x[i]) y[i] = value try: err[i] = math.sqrt(math.fabs(value)) except: err[i] = 1.0 print("Error getting error", value, x[i]) new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = IQ_SMEARED_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") # If we have a group ID, use it if 'plot_group_id' in pr.info: new_plot.group_id = pr.info["plot_group_id"] new_plot.id = IQ_SMEARED_LABEL new_plot.title = title self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) def _on_pr_npts(self, evt): """ Redisplay P(r) with a different number of points """ from inversion_panel import PrDistDialog dialog = PrDistDialog(None, -1) dialog.set_content(self._pr_npts) if dialog.ShowModal() == wx.ID_OK: self._pr_npts = dialog.get_content() dialog.Destroy() self.show_pr(self._last_out, self._last_pr, self._last_cov) else: dialog.Destroy() def show_pr(self, out, pr, cov=None): """ """ # Show P(r) x = pylab.arange(0.0, pr.d_max, pr.d_max / self._pr_npts) y = np.zeros(len(x)) dy = np.zeros(len(x)) y_true = np.zeros(len(x)) total = 0.0 pmax = 0.0 cov2 = np.ascontiguousarray(cov) for i in range(len(x)): if cov2 is None: value = pr.pr(out, x[i]) else: (value, dy[i]) = pr.pr_err(out, cov2, x[i]) total += value * pr.d_max / len(x) # keep track of the maximum P(r) value if value > pmax: pmax = value y[i] = value if self._normalize_output: y = y / total dy = dy / total elif self._scale_output_unity: y = y / pmax dy = dy / pmax if cov2 is None: new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM else: new_plot = Data1D(x, y, dy=dy) new_plot.name = PR_FIT_LABEL new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.title = "P(r) fit" new_plot.id = PR_FIT_LABEL # Make sure that the plot is linear new_plot.xtransform = "x" new_plot.ytransform = "y" new_plot.group_id = GROUP_ID_PR_FIT self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="P(r) fit")) return x, pr.d_max def load(self, data): """ Load data. This will eventually be replaced by our standard DataLoader class. """ class FileData(object): x = None y = None err = None path = None def __init__(self, path): self.path = path self._current_file_data = FileData(data.path) # Use data loader to load file dataread = data # Notify the user if we could not read the file if dataread is None: raise RuntimeError, "Invalid data" x = None y = None err = None if dataread.__class__.__name__ == 'Data1D': x = dataread.x y = dataread.y err = dataread.dy else: if isinstance(dataread, list) and len(dataread) > 0: x = dataread[0].x y = dataread[0].y err = dataread[0].dy msg = "PrView only allows a single data set at a time. " msg += "Only the first data set was loaded." wx.PostEvent(self.parent, StatusEvent(status=msg)) else: if dataread is None: return x, y, err raise RuntimeError, "This tool can only read 1D data" self._current_file_data.x = x self._current_file_data.y = y self._current_file_data.err = err return x, y, err def load_columns(self, path="sphere_60_q0_2.txt"): """ Load 2- or 3- column ascii """ # Read the data from the data file data_x = np.zeros(0) data_y = np.zeros(0) data_err = np.zeros(0) scale = None min_err = 0.0 if path is not None: input_f = open(path, 'r') buff = input_f.read() lines = buff.split('\n') for line in lines: try: toks = line.split() x = float(toks[0]) y = float(toks[1]) if len(toks) > 2: err = float(toks[2]) else: if scale is None: scale = 0.05 * math.sqrt(y) #scale = 0.05/math.sqrt(y) min_err = 0.01 * y err = scale * math.sqrt(y) + min_err #err = 0 data_x = np.append(data_x, x) data_y = np.append(data_y, y) data_err = np.append(data_err, err) except: logger.error(sys.exc_value) if scale is not None: message = "The loaded file had no error bars, statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) else: wx.PostEvent(self.parent, StatusEvent(status='')) return data_x, data_y, data_err def load_abs(self, path): """ Load an IGOR .ABS reduced file :param path: file path :return: x, y, err vectors """ # Read the data from the data file data_x = np.zeros(0) data_y = np.zeros(0) data_err = np.zeros(0) scale = None min_err = 0.0 data_started = False if path is not None: input_f = open(path, 'r') buff = input_f.read() lines = buff.split('\n') for line in lines: if data_started: try: toks = line.split() x = float(toks[0]) y = float(toks[1]) if len(toks) > 2: err = float(toks[2]) else: if scale is None: scale = 0.05 * math.sqrt(y) #scale = 0.05/math.sqrt(y) min_err = 0.01 * y err = scale * math.sqrt(y) + min_err #err = 0 data_x = np.append(data_x, x) data_y = np.append(data_y, y) data_err = np.append(data_err, err) except: logger.error(sys.exc_value) elif line.find("The 6 columns") >= 0: data_started = True if scale is not None: message = "The loaded file had no error bars, statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) else: wx.PostEvent(self.parent, StatusEvent(status='')) return data_x, data_y, data_err def pr_theory(self, r, R): """ Return P(r) of a sphere for a given R For test purposes """ if r <= 2 * R: return 12.0 * ((0.5 * r / R)**2) * ( (1.0 - 0.5 * r / R)**2) * (2.0 + 0.5 * r / R) else: return 0.0 def get_context_menu(self, plotpanel=None): """ Get the context menu items available for P(r) :param graph: the Graph object to which we attach the context menu :return: a list of menu items with call-back function """ graph = plotpanel.graph # Look whether this Graph contains P(r) data if graph.selected_plottable not in plotpanel.plots: return [] item = plotpanel.plots[graph.selected_plottable] if item.id == PR_FIT_LABEL: #add_data_hint = "Load a data file and display it on this plot" #["Add P(r) data",add_data_hint , self._on_add_data], change_n_hint = "Change the number of" change_n_hint += " points on the P(r) output" change_n_label = "Change number of P(r) points" m_list = [[change_n_label, change_n_hint, self._on_pr_npts]] if self._scale_output_unity or self._normalize_output: hint = "Let the output P(r) keep the scale of the data" m_list.append( ["Disable P(r) scaling", hint, self._on_disable_scaling]) if not self._scale_output_unity: m_list.append([ "Scale P_max(r) to unity", "Scale P(r) so that its maximum is 1", self._on_scale_unity ]) if not self._normalize_output: m_list.append([ "Normalize P(r) to unity", "Normalize the integral of P(r) to 1", self._on_normalize ]) return m_list elif item.id in [ PR_LOADED_LABEL, IQ_DATA_LABEL, IQ_FIT_LABEL, IQ_SMEARED_LABEL ]: return [] elif item.id == graph.selected_plottable: if issubclass(item.__class__, Data1D): return [[ "Compute P(r)", "Compute P(r) from distribution", self._on_context_inversion ]] return [] def _on_disable_scaling(self, evt): """ Disable P(r) scaling :param evt: Menu event """ self._normalize_output = False self._scale_output_unity = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now replot the original added data for plot in self._added_plots: self._added_plots[plot].y = np.copy(self._default_Iq[plot]) wx.PostEvent( self.parent, NewPlotEvent(plot=self._added_plots[plot], title=self._added_plots[plot].name, update=True)) # Need the update flag in the NewPlotEvent to protect against # the plot no longer being there... def _on_normalize(self, evt): """ Normalize the area under the P(r) curve to 1. This operation is done for all displayed plots. :param evt: Menu event """ self._normalize_output = True self._scale_output_unity = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now scale the added plots too for plot in self._added_plots: total = np.sum(self._added_plots[plot].y) npts = len(self._added_plots[plot].x) total *= self._added_plots[plot].x[npts - 1] / npts y = self._added_plots[plot].y / total new_plot = Data1D(self._added_plots[plot].x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.group_id = self._added_plots[plot].group_id new_plot.id = self._added_plots[plot].id new_plot.title = self._added_plots[plot].title new_plot.name = self._added_plots[plot].name new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent( self.parent, NewPlotEvent(plot=new_plot, update=True, title=self._added_plots[plot].name)) def _on_scale_unity(self, evt): """ Scale the maximum P(r) value on each displayed plot to 1. :param evt: Menu event """ self._scale_output_unity = True self._normalize_output = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now scale the added plots too for plot in self._added_plots: _max = 0 for y in self._added_plots[plot].y: if y > _max: _max = y y = self._added_plots[plot].y / _max new_plot = Data1D(self._added_plots[plot].x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = self._added_plots[plot].name new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent( self.parent, NewPlotEvent(plot=new_plot, update=True, title=self._added_plots[plot].name)) def start_thread(self): """ Start a calculation thread """ from pr_thread import CalcPr # If a thread is already started, stop it if self.calc_thread is not None and self.calc_thread.isrunning(): self.calc_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() self.calc_thread = CalcPr(pr, self.nfunc, error_func=self._thread_error, completefn=self._completed, updatefn=None) self.calc_thread.queue() self.calc_thread.ready(2.5) def _thread_error(self, error): """ Call-back method for calculation errors """ wx.PostEvent(self.parent, StatusEvent(status=error)) def _estimate_completed(self, alpha, message, elapsed): """ Parameter estimation completed, display the results to the user :param alpha: estimated best alpha :param elapsed: computation time """ # Save useful info self.elapsed = elapsed self.control_panel.alpha_estimate = alpha if message is not None: wx.PostEvent(self.parent, StatusEvent(status=str(message))) self.perform_estimateNT() def _estimateNT_completed(self, nterms, alpha, message, elapsed): """ Parameter estimation completed, display the results to the user :param alpha: estimated best alpha :param nterms: estimated number of terms :param elapsed: computation time """ # Save useful info self.elapsed = elapsed self.control_panel.nterms_estimate = nterms self.control_panel.alpha_estimate = alpha if message is not None: wx.PostEvent(self.parent, StatusEvent(status=str(message))) def _completed(self, out, cov, pr, elapsed): """ wxCallAfter Method called with the results when the inversion is done :param out: output coefficient for the base functions :param cov: covariance matrix :param pr: Invertor instance :param elapsed: time spent computing """ # Ensure hat you have all inputs are ready at the time call happens: # Without CallAfter, it will freeze with wx >= 2.9. wx.CallAfter(self._completed_call, out, cov, pr, elapsed) def _completed_call(self, out, cov, pr, elapsed): """ Method called with the results when the inversion is done :param out: output coefficient for the base functions :param cov: covariance matrix :param pr: Invertor instance :param elapsed: time spent computing """ # Save useful info self.elapsed = elapsed # Keep a copy of the last result self._last_pr = pr.clone() self._last_out = out self._last_cov = cov # Save Pr invertor self.pr = pr cov = np.ascontiguousarray(cov) # Show result on control panel self.control_panel.chi2 = pr.chi2 self.control_panel.elapsed = elapsed self.control_panel.oscillation = pr.oscillations(out) self.control_panel.positive = pr.get_positive(out) self.control_panel.pos_err = pr.get_pos_err(out, cov) self.control_panel.rg = pr.rg(out) self.control_panel.iq0 = pr.iq0(out) self.control_panel.bck = pr.background self.control_panel.bck_input.SetValue("{:.2g}".format(pr.background)) # Show I(q) fit self.show_iq(out, self.pr) # Show P(r) fit self.show_pr(out, self.pr, cov) def show_data(self, path=None, data=None, reset=False): """ Show data read from a file :param path: file path :param reset: if True all other plottables will be cleared """ #if path is not None: if data is not None: try: pr = self._create_file_pr(data) except: status = "Problem reading data: %s" % sys.exc_value wx.PostEvent(self.parent, StatusEvent(status=status)) raise RuntimeError, status # If the file contains nothing, just return if pr is None: raise RuntimeError, "Loaded data is invalid" self.pr = pr # Make a plot of I(q) data if self.pr.err is None: new_plot = Data1D(self.pr.x, self.pr.y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM else: new_plot = Data1D(self.pr.x, self.pr.y, dy=self.pr.err) new_plot.name = IQ_DATA_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.interactive = True new_plot.group_id = GROUP_ID_IQ_DATA new_plot.id = self.data_id new_plot.title = "I(q)" wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="I(q)", reset=reset)) self.current_plottable = new_plot # Get Q range self.control_panel.q_min = min(self.pr.x) self.control_panel.q_max = max(self.pr.x) def save_data(self, filepath, prstate=None): """ Save data in provided state object. :TODO: move the state code away from inversion_panel and move it here. Then remove the "prstate" input and make this method private. :param filepath: path of file to write to :param prstate: P(r) inversion state """ #TODO: do we need this or can we use DataLoader.loader.save directly? # Add output data and coefficients to state prstate.coefficients = self._last_out prstate.covariance = self._last_cov # Write the output to file # First, check that the data is of the right type if issubclass(self.current_plottable.__class__, sas.sascalc.dataloader.data_info.Data1D): self.state_reader.write(filepath, self.current_plottable, prstate) else: msg = "pr.save_data: the data being saved is not a" msg += " sas.data_info.Data1D object" raise RuntimeError, msg def setup_plot_inversion(self, alpha, nfunc, d_max, q_min=None, q_max=None, est_bck=False, bck_val=0, height=0, width=0): """ Set up inversion from plotted data """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = est_bck self.bck_val = bck_val self.slit_height = height self.slit_width = width try: pr = self._create_plot_pr() if pr is not None: self.pr = pr self.perform_inversion() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def estimate_plot_inversion(self, alpha, nfunc, d_max, q_min=None, q_max=None, est_bck=False, bck_val=0, height=0, width=0): """ Estimate parameters from plotted data """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = est_bck self.bck_val = bck_val self.slit_height = height self.slit_width = width try: pr = self._create_plot_pr() if pr is not None: self.pr = pr self.perform_estimate() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def _create_plot_pr(self, estimate=False): """ Create and prepare invertor instance from a plottable data set. :param path: path of the file to read in """ # Sanity check if self.current_plottable is None: msg = "Please load a valid data set before proceeding." wx.PostEvent(self.parent, StatusEvent(status=msg)) return None # Get the data from the chosen data set and perform inversion pr = Invertor() pr.d_max = self.max_length pr.alpha = self.alpha pr.q_min = self.q_min pr.q_max = self.q_max pr.x = self.current_plottable.x pr.y = self.current_plottable.y pr.est_bck = self.est_bck pr.slit_height = self.slit_height pr.slit_width = self.slit_width pr.background = self.bck_val # Keep track of the plot window title to ensure that # we can overlay the plots pr.info["plot_group_id"] = self.current_plottable.group_id # Fill in errors if none were provided err = self.current_plottable.dy all_zeros = True if err is None: err = np.zeros(len(pr.y)) else: for i in range(len(err)): if err[i] > 0: all_zeros = False if all_zeros: scale = None min_err = 0.0 for i in range(len(pr.y)): # Scale the error so that we can fit over several decades of Q if scale is None: scale = 0.05 * math.sqrt(pr.y[i]) min_err = 0.01 * pr.y[i] err[i] = scale * math.sqrt(math.fabs(pr.y[i])) + min_err message = "The loaded file had no error bars, " message += "statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) pr.err = err return pr def setup_file_inversion(self, alpha, nfunc, d_max, data, path=None, q_min=None, q_max=None, bck=False, height=0, width=0): """ Set up inversion """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = bck self.slit_height = height self.slit_width = width try: pr = self._create_file_pr(data) if pr is not None: self.pr = pr self.perform_inversion() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def estimate_file_inversion(self, alpha, nfunc, d_max, data, path=None, q_min=None, q_max=None, bck=False, height=0, width=0): """ Estimate parameters for inversion """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = bck self.slit_height = height self.slit_width = width try: pr = self._create_file_pr(data) if pr is not None: self.pr = pr self.perform_estimate() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def _create_file_pr(self, data): """ Create and prepare invertor instance from a file data set. :param path: path of the file to read in """ # Reset the status bar so that we don't get mixed up # with old messages. #TODO: refactor this into a proper status handling wx.PostEvent(self.parent, StatusEvent(status='')) try: class FileData(object): x = None y = None err = None path = None def __init__(self, path): self.path = path self._current_file_data = FileData(data.path) self._current_file_data.x = data.x self._current_file_data.y = data.y self._current_file_data.err = data.dy x, y, err = data.x, data.y, data.dy except: load_error(sys.exc_value) return None # If the file contains no data, just return if x is None or len(x) == 0: load_error("The loaded file contains no data") return None # If we have not errors, add statistical errors if y is not None: if err is None or np.all(err) == 0: err = np.zeros(len(y)) scale = None min_err = 0.0 for i in range(len(y)): # Scale the error so that we can fit over several decades of Q if scale is None: scale = 0.05 * math.sqrt(y[i]) min_err = 0.01 * y[i] err[i] = scale * math.sqrt(math.fabs(y[i])) + min_err message = "The loaded file had no error bars, " message += "statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) try: # Get the data from the chosen data set and perform inversion pr = Invertor() pr.d_max = self.max_length pr.alpha = self.alpha pr.q_min = self.q_min pr.q_max = self.q_max pr.x = x pr.y = y pr.err = err pr.est_bck = self.est_bck pr.slit_height = self.slit_height pr.slit_width = self.slit_width return pr except: load_error(sys.exc_value) return None def perform_estimate(self): """ Perform parameter estimation """ from pr_thread import EstimatePr # If a thread is already started, stop it if self.estimation_thread is not None and \ self.estimation_thread.isrunning(): self.estimation_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() self.estimation_thread = EstimatePr( pr, self.nfunc, error_func=self._thread_error, completefn=self._estimate_completed, updatefn=None) self.estimation_thread.queue() self.estimation_thread.ready(2.5) def perform_estimateNT(self): """ Perform parameter estimation """ from pr_thread import EstimateNT # If a thread is already started, stop it if self.estimation_thread is not None and self.estimation_thread.isrunning( ): self.estimation_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() # Skip the slit settings for the estimation # It slows down the application and it doesn't change the estimates pr.slit_height = 0.0 pr.slit_width = 0.0 self.estimation_thread = EstimateNT( pr, self.nfunc, error_func=self._thread_error, completefn=self._estimateNT_completed, updatefn=None) self.estimation_thread.queue() self.estimation_thread.ready(2.5) def perform_inversion(self): """ Perform inversion """ self.start_thread() def _on_context_inversion(self, event): """ Call-back method for plot context menu """ panel = event.GetEventObject() Plugin.on_perspective(self, event=event) # If we have more than one displayed plot, make the user choose if len(panel.plots) >= 1 and \ panel.graph.selected_plottable in panel.plots: dataset = panel.plots[panel.graph.selected_plottable].name else: logger.info("Prview Error: No data is available") return # Store a reference to the current plottable # If we have a suggested value, use it. try: estimate = float(self.control_panel.alpha_estimate) self.control_panel.alpha = estimate except: self.control_panel.alpha = self.alpha logger.info("Prview :Alpha Not estimate yet") try: estimate = int(self.control_panel.nterms_estimate) self.control_panel.nfunc = estimate except: self.control_panel.nfunc = self.nfunc logger.info("Prview : ntemrs Not estimate yet") self.current_plottable = panel.plots[panel.graph.selected_plottable] self.set_data([self.current_plottable]) self.control_panel.plotname = dataset #self.control_panel.nfunc = self.nfunc self.control_panel.d_max = self.max_length #self.parent.set_perspective(self.perspective) self.control_panel._on_invert(None) def get_panels(self, parent): """ Create and return a list of panel objects """ from inversion_panel import InversionControl self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100, 200)) self.control_panel = InversionControl(self.frame, -1, style=wx.RAISED_BORDER) self.frame.set_panel(self.control_panel) self._frame_set_helper() self.control_panel.set_manager(self) self.control_panel.nfunc = self.nfunc self.control_panel.d_max = self.max_length self.control_panel.alpha = self.alpha self.perspective = [] self.perspective.append(self.control_panel.window_name) return [self.control_panel] def set_data(self, data_list=None): """ receive a list of data to compute pr """ if data_list is None: data_list = [] if len(data_list) >= 1: if len(data_list) == 1: data = data_list[0] else: data_1d_list = [] data_2d_list = [] error_msg = "" # separate data into data1d and data2d list for data in data_list: if data is not None: if issubclass(data.__class__, Data1D): data_1d_list.append(data) else: error_msg += " %s type %s \n" % (str( data.name), str(data.__class__.__name__)) data_2d_list.append(data) if len(data_2d_list) > 0: msg = "PrView does not support the following data types:\n" msg += error_msg if len(data_1d_list) == 0: wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return msg = "Prview does not allow multiple data!\n" msg += "Please select one.\n" if len(data_list) > 1: from pr_widgets import DataDialog dlg = DataDialog(data_list=data_1d_list, text=msg) if dlg.ShowModal() == wx.ID_OK: data = dlg.get_data() else: data = None dlg.Destroy() if data is None: msg += "PrView receives no data. \n" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return if issubclass(data.__class__, Data1D): try: wx.PostEvent( self.parent, NewPlotEvent(action='remove', group_id=GROUP_ID_IQ_DATA, id=self.data_id)) self.data_id = data.id self.control_panel._change_file(evt=None, data=data) except: msg = "Prview Set_data: " + str(sys.exc_value) wx.PostEvent(self.parent, StatusEvent(status=msg, info="error")) else: msg = "Pr cannot be computed for data of " msg += "type %s" % (data_list[0].__class__.__name__) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) else: msg = "Pr contain no data" wx.PostEvent(self.parent, StatusEvent(status=msg, info='warning')) def post_init(self): """ Post initialization call back to close the loose ends [Somehow openGL needs this call] """ pass
class Plugin(PluginBase): """ P(r) inversion perspective """ DEFAULT_ALPHA = 0.0001 DEFAULT_NFUNC = 10 DEFAULT_DMAX = 140.0 def __init__(self): PluginBase.__init__(self, name="Pr Inversion") ## Simulation window manager self.simview = None ## State data self.alpha = self.DEFAULT_ALPHA self.nfunc = self.DEFAULT_NFUNC self.max_length = self.DEFAULT_DMAX self.q_min = None self.q_max = None self.est_bck = False self.bck_val = 0 self.slit_height = 0 self.slit_width = 0 ## Remember last plottable processed self.last_data = "" self._current_file_data = None ## Time elapsed for last computation [sec] # Start with a good default self.elapsed = 0.022 self.iq_data_shown = False ## Current invertor self.invertor = None self.pr = None self.data_id = IQ_DATA_LABEL # Copy of the last result in case we need to display it. self._last_pr = None self._last_out = None self._last_cov = None ## Calculation thread self.calc_thread = None ## Estimation thread self.estimation_thread = None ## Result panel self.control_panel = None ## Currently views plottable self.current_plottable = None ## Number of P(r) points to display on the output plot self._pr_npts = 51 self._normalize_output = False self._scale_output_unity = False ## List of added P(r) plots self._added_plots = {} self._default_Iq = {} self.list_plot_id = [] # Associate the inversion state reader with .prv files from inversion_state import Reader # Create a CanSAS/Pr reader self.state_reader = Reader(self.set_state) self._extensions = '.prv' l = Loader() l.associate_file_reader('.prv', self.state_reader) #l.associate_file_reader(".svs", self.state_reader) # Log startup logger.info("Pr(r) plug-in started") def delete_data(self, data_id): """ delete the data association with prview """ self.control_panel.clear_panel() def get_data(self): """ Returns the current data """ return self.current_plottable def set_state(self, state=None, datainfo=None): """ Call-back method for the inversion state reader. This method is called when a .prv file is loaded. :param state: InversionState object :param datainfo: Data1D object [optional] """ try: if datainfo.__class__.__name__ == 'list': if len(datainfo) >= 1: data = datainfo[0] else: data = None else: data = datainfo if data is None: msg = "Pr.set_state: datainfo parameter cannot " msg += "be None in standalone mode" raise RuntimeError, msg # Ensuring that plots are coordinated correctly t = time.localtime(data.meta_data['prstate'].timestamp) time_str = time.strftime("%b %d %H:%M", t) # Check that no time stamp is already appended max_char = data.meta_data['prstate'].file.find("[") if max_char < 0: max_char = len(data.meta_data['prstate'].file) datainfo.meta_data['prstate'].file = \ data.meta_data['prstate'].file[0:max_char]\ + ' [' + time_str + ']' data.filename = data.meta_data['prstate'].file # TODO: #remove this call when state save all information about the gui data # such as ID , Group_ID, etc... #make self.current_plottable = datainfo directly self.current_plottable = self.parent.create_gui_data(data, None) self.current_plottable.group_id = data.meta_data['prstate'].file # Make sure the user sees the P(r) panel after loading #self.parent.set_perspective(self.perspective) self.on_perspective(event=None) # Load the P(r) results #state = self.state_reader.get_state() data_dict = {self.current_plottable.id:self.current_plottable} self.parent.add_data(data_list=data_dict) wx.PostEvent(self.parent, NewPlotEvent(plot=self.current_plottable, title=self.current_plottable.title)) self.control_panel.set_state(state) except: logger.error("prview.set_state: %s" % sys.exc_value) def help(self, evt): """ Show a general help dialog. :TODO: replace the text with a nice image """ from inversion_panel import HelpDialog dialog = HelpDialog(None, -1) if dialog.ShowModal() == wx.ID_OK: dialog.Destroy() else: dialog.Destroy() def _fit_pr(self, evt): """ """ # Generate P(r) for sphere radius = 60.0 d_max = 2 * radius r = pylab.arange(0.01, d_max, d_max / 51.0) M = len(r) y = np.zeros(M) pr_err = np.zeros(M) total = 0.0 for j in range(M): value = self.pr_theory(r[j], radius) total += value y[j] = value pr_err[j] = math.sqrt(y[j]) y = y / total * d_max / len(r) # Perform fit pr = Invertor() pr.d_max = d_max pr.alpha = 0 pr.x = r pr.y = y pr.err = pr_err out, cov = pr.pr_fit() for i in range(len(out)): print("%g +- %g" % (out[i], math.sqrt(cov[i][i]))) # Show input P(r) title = "Pr" new_plot = Data1D(pr.x, pr.y, dy=pr.err) new_plot.name = "P_{obs}(r)" new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.group_id = "P_{obs}(r)" new_plot.id = "P_{obs}(r)" new_plot.title = title self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) # Show P(r) fit self.show_pr(out, pr) # Show I(q) fit q = pylab.arange(0.001, 0.1, 0.01 / 51.0) self.show_iq(out, pr, q) def show_shpere(self, x, radius=70.0, x_range=70.0): """ """ # Show P(r) y_true = np.zeros(len(x)) sum_true = 0.0 for i in range(len(x)): y_true[i] = self.pr_theory(x[i], radius) sum_true += y_true[i] y_true = y_true / sum_true * x_range / len(x) # Show the theory P(r) new_plot = Data1D(x, y_true) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = "P_{true}(r)" new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.id = "P_{true}(r)" new_plot.group_id = "P_{true}(r)" self.parent.update_theory(data_id=self.data_id, theory=new_plot) #Put this call in plottables/guitools wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="Sphere P(r)")) def get_npts(self): """ Returns the number of points in the I(q) data """ try: return len(self.pr.x) except: return 0 def show_iq(self, out, pr, q=None): """ Display computed I(q) """ qtemp = pr.x if q is not None: qtemp = q # Make a plot maxq = -1 for q_i in qtemp: if q_i > maxq: maxq = q_i minq = 0.001 # Check for user min/max if pr.q_min is not None: minq = pr.q_min if pr.q_max is not None: maxq = pr.q_max x = pylab.arange(minq, maxq, maxq / 301.0) y = np.zeros(len(x)) err = np.zeros(len(x)) for i in range(len(x)): value = pr.iq(out, x[i]) y[i] = value try: err[i] = math.sqrt(math.fabs(value)) except: err[i] = 1.0 print("Error getting error", value, x[i]) new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = IQ_FIT_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") title = "I(q)" new_plot.title = title # If we have a group ID, use it if 'plot_group_id' in pr.info: new_plot.group_id = pr.info["plot_group_id"] new_plot.id = IQ_FIT_LABEL self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) # If we have used slit smearing, plot the smeared I(q) too if pr.slit_width > 0 or pr.slit_height > 0: x = pylab.arange(minq, maxq, maxq / 301.0) y = np.zeros(len(x)) err = np.zeros(len(x)) for i in range(len(x)): value = pr.iq_smeared(out, x[i]) y[i] = value try: err[i] = math.sqrt(math.fabs(value)) except: err[i] = 1.0 print("Error getting error", value, x[i]) new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = IQ_SMEARED_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") # If we have a group ID, use it if 'plot_group_id' in pr.info: new_plot.group_id = pr.info["plot_group_id"] new_plot.id = IQ_SMEARED_LABEL new_plot.title = title self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=title)) def _on_pr_npts(self, evt): """ Redisplay P(r) with a different number of points """ from inversion_panel import PrDistDialog dialog = PrDistDialog(None, -1) dialog.set_content(self._pr_npts) if dialog.ShowModal() == wx.ID_OK: self._pr_npts = dialog.get_content() dialog.Destroy() self.show_pr(self._last_out, self._last_pr, self._last_cov) else: dialog.Destroy() def show_pr(self, out, pr, cov=None): """ """ # Show P(r) x = pylab.arange(0.0, pr.d_max, pr.d_max / self._pr_npts) y = np.zeros(len(x)) dy = np.zeros(len(x)) y_true = np.zeros(len(x)) total = 0.0 pmax = 0.0 cov2 = np.ascontiguousarray(cov) for i in range(len(x)): if cov2 is None: value = pr.pr(out, x[i]) else: (value, dy[i]) = pr.pr_err(out, cov2, x[i]) total += value * pr.d_max / len(x) # keep track of the maximum P(r) value if value > pmax: pmax = value y[i] = value if self._normalize_output == True: y = y / total dy = dy / total elif self._scale_output_unity == True: y = y / pmax dy = dy / pmax if cov2 is None: new_plot = Data1D(x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM else: new_plot = Data1D(x, y, dy=dy) new_plot.name = PR_FIT_LABEL new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") new_plot.title = "P(r) fit" new_plot.id = PR_FIT_LABEL # Make sure that the plot is linear new_plot.xtransform = "x" new_plot.ytransform = "y" new_plot.group_id = GROUP_ID_PR_FIT self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="P(r) fit")) return x, pr.d_max def load(self, data): """ Load data. This will eventually be replaced by our standard DataLoader class. """ class FileData(object): x = None y = None err = None path = None def __init__(self, path): self.path = path self._current_file_data = FileData(data.path) # Use data loader to load file dataread = data # Notify the user if we could not read the file if dataread is None: raise RuntimeError, "Invalid data" x = None y = None err = None if dataread.__class__.__name__ == 'Data1D': x = dataread.x y = dataread.y err = dataread.dy else: if isinstance(dataread, list) and len(dataread) > 0: x = dataread[0].x y = dataread[0].y err = dataread[0].dy msg = "PrView only allows a single data set at a time. " msg += "Only the first data set was loaded." wx.PostEvent(self.parent, StatusEvent(status=msg)) else: if dataread is None: return x, y, err raise RuntimeError, "This tool can only read 1D data" self._current_file_data.x = x self._current_file_data.y = y self._current_file_data.err = err return x, y, err def load_columns(self, path="sphere_60_q0_2.txt"): """ Load 2- or 3- column ascii """ # Read the data from the data file data_x = np.zeros(0) data_y = np.zeros(0) data_err = np.zeros(0) scale = None min_err = 0.0 if path is not None: input_f = open(path, 'r') buff = input_f.read() lines = buff.split('\n') for line in lines: try: toks = line.split() x = float(toks[0]) y = float(toks[1]) if len(toks) > 2: err = float(toks[2]) else: if scale is None: scale = 0.05 * math.sqrt(y) #scale = 0.05/math.sqrt(y) min_err = 0.01 * y err = scale * math.sqrt(y) + min_err #err = 0 data_x = np.append(data_x, x) data_y = np.append(data_y, y) data_err = np.append(data_err, err) except: logger.error(sys.exc_value) if scale is not None: message = "The loaded file had no error bars, statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) else: wx.PostEvent(self.parent, StatusEvent(status='')) return data_x, data_y, data_err def load_abs(self, path): """ Load an IGOR .ABS reduced file :param path: file path :return: x, y, err vectors """ # Read the data from the data file data_x = np.zeros(0) data_y = np.zeros(0) data_err = np.zeros(0) scale = None min_err = 0.0 data_started = False if path is not None: input_f = open(path, 'r') buff = input_f.read() lines = buff.split('\n') for line in lines: if data_started == True: try: toks = line.split() x = float(toks[0]) y = float(toks[1]) if len(toks) > 2: err = float(toks[2]) else: if scale is None: scale = 0.05 * math.sqrt(y) #scale = 0.05/math.sqrt(y) min_err = 0.01 * y err = scale * math.sqrt(y) + min_err #err = 0 data_x = np.append(data_x, x) data_y = np.append(data_y, y) data_err = np.append(data_err, err) except: logger.error(sys.exc_value) elif line.find("The 6 columns") >= 0: data_started = True if scale is not None: message = "The loaded file had no error bars, statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) else: wx.PostEvent(self.parent, StatusEvent(status='')) return data_x, data_y, data_err def pr_theory(self, r, R): """ Return P(r) of a sphere for a given R For test purposes """ if r <= 2 * R: return 12.0 * ((0.5 * r / R) ** 2) * ((1.0 - 0.5 * r / R) ** 2) * (2.0 + 0.5 * r / R) else: return 0.0 def get_context_menu(self, plotpanel=None): """ Get the context menu items available for P(r) :param graph: the Graph object to which we attach the context menu :return: a list of menu items with call-back function """ graph = plotpanel.graph # Look whether this Graph contains P(r) data if graph.selected_plottable not in plotpanel.plots: return [] item = plotpanel.plots[graph.selected_plottable] if item.id == PR_FIT_LABEL: #add_data_hint = "Load a data file and display it on this plot" #["Add P(r) data",add_data_hint , self._on_add_data], change_n_hint = "Change the number of" change_n_hint += " points on the P(r) output" change_n_label = "Change number of P(r) points" m_list = [[change_n_label, change_n_hint, self._on_pr_npts]] if self._scale_output_unity or self._normalize_output: hint = "Let the output P(r) keep the scale of the data" m_list.append(["Disable P(r) scaling", hint, self._on_disable_scaling]) if not self._scale_output_unity: m_list.append(["Scale P_max(r) to unity", "Scale P(r) so that its maximum is 1", self._on_scale_unity]) if not self._normalize_output: m_list.append(["Normalize P(r) to unity", "Normalize the integral of P(r) to 1", self._on_normalize]) return m_list elif item.id in [PR_LOADED_LABEL, IQ_DATA_LABEL, IQ_FIT_LABEL, IQ_SMEARED_LABEL]: return [] elif item.id == graph.selected_plottable: if issubclass(item.__class__, Data1D): return [["Compute P(r)", "Compute P(r) from distribution", self._on_context_inversion]] return [] def _on_disable_scaling(self, evt): """ Disable P(r) scaling :param evt: Menu event """ self._normalize_output = False self._scale_output_unity = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now replot the original added data for plot in self._added_plots: self._added_plots[plot].y = np.copy(self._default_Iq[plot]) wx.PostEvent(self.parent, NewPlotEvent(plot=self._added_plots[plot], title=self._added_plots[plot].name, update=True)) # Need the update flag in the NewPlotEvent to protect against # the plot no longer being there... def _on_normalize(self, evt): """ Normalize the area under the P(r) curve to 1. This operation is done for all displayed plots. :param evt: Menu event """ self._normalize_output = True self._scale_output_unity = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now scale the added plots too for plot in self._added_plots: total = np.sum(self._added_plots[plot].y) npts = len(self._added_plots[plot].x) total *= self._added_plots[plot].x[npts - 1] / npts y = self._added_plots[plot].y / total new_plot = Data1D(self._added_plots[plot].x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.group_id = self._added_plots[plot].group_id new_plot.id = self._added_plots[plot].id new_plot.title = self._added_plots[plot].title new_plot.name = self._added_plots[plot].name new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, update=True, title=self._added_plots[plot].name)) def _on_scale_unity(self, evt): """ Scale the maximum P(r) value on each displayed plot to 1. :param evt: Menu event """ self._scale_output_unity = True self._normalize_output = False self.show_pr(self._last_out, self._last_pr, self._last_cov) # Now scale the added plots too for plot in self._added_plots: _max = 0 for y in self._added_plots[plot].y: if y > _max: _max = y y = self._added_plots[plot].y / _max new_plot = Data1D(self._added_plots[plot].x, y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.name = self._added_plots[plot].name new_plot.xaxis("\\rm{r}", 'A') new_plot.yaxis("\\rm{P(r)} ", "cm^{-3}") self.parent.update_theory(data_id=self.data_id, theory=new_plot) wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, update=True, title=self._added_plots[plot].name)) def start_thread(self): """ Start a calculation thread """ from pr_thread import CalcPr # If a thread is already started, stop it if self.calc_thread is not None and self.calc_thread.isrunning(): self.calc_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() self.calc_thread = CalcPr(pr, self.nfunc, error_func=self._thread_error, completefn=self._completed, updatefn=None) self.calc_thread.queue() self.calc_thread.ready(2.5) def _thread_error(self, error): """ Call-back method for calculation errors """ wx.PostEvent(self.parent, StatusEvent(status=error)) def _estimate_completed(self, alpha, message, elapsed): """ Parameter estimation completed, display the results to the user :param alpha: estimated best alpha :param elapsed: computation time """ # Save useful info self.elapsed = elapsed self.control_panel.alpha_estimate = alpha if message is not None: wx.PostEvent(self.parent, StatusEvent(status=str(message))) self.perform_estimateNT() def _estimateNT_completed(self, nterms, alpha, message, elapsed): """ Parameter estimation completed, display the results to the user :param alpha: estimated best alpha :param nterms: estimated number of terms :param elapsed: computation time """ # Save useful info self.elapsed = elapsed self.control_panel.nterms_estimate = nterms self.control_panel.alpha_estimate = alpha if message is not None: wx.PostEvent(self.parent, StatusEvent(status=str(message))) def _completed(self, out, cov, pr, elapsed): """ wxCallAfter Method called with the results when the inversion is done :param out: output coefficient for the base functions :param cov: covariance matrix :param pr: Invertor instance :param elapsed: time spent computing """ # Ensure hat you have all inputs are ready at the time call happens: # Without CallAfter, it will freeze with wx >= 2.9. wx.CallAfter(self._completed_call, out, cov, pr, elapsed) def _completed_call(self, out, cov, pr, elapsed): """ Method called with the results when the inversion is done :param out: output coefficient for the base functions :param cov: covariance matrix :param pr: Invertor instance :param elapsed: time spent computing """ # Save useful info self.elapsed = elapsed # Keep a copy of the last result self._last_pr = pr.clone() self._last_out = out self._last_cov = cov # Save Pr invertor self.pr = pr cov = np.ascontiguousarray(cov) # Show result on control panel self.control_panel.chi2 = pr.chi2 self.control_panel.elapsed = elapsed self.control_panel.oscillation = pr.oscillations(out) self.control_panel.positive = pr.get_positive(out) self.control_panel.pos_err = pr.get_pos_err(out, cov) self.control_panel.rg = pr.rg(out) self.control_panel.iq0 = pr.iq0(out) self.control_panel.bck = pr.background self.control_panel.bck_input.SetValue("{:.2g}".format(pr.background)) # Show I(q) fit self.show_iq(out, self.pr) # Show P(r) fit self.show_pr(out, self.pr, cov) def show_data(self, path=None, data=None, reset=False): """ Show data read from a file :param path: file path :param reset: if True all other plottables will be cleared """ #if path is not None: if data is not None: try: pr = self._create_file_pr(data) except: status = "Problem reading data: %s" % sys.exc_value wx.PostEvent(self.parent, StatusEvent(status=status)) raise RuntimeError, status # If the file contains nothing, just return if pr is None: raise RuntimeError, "Loaded data is invalid" self.pr = pr # Make a plot of I(q) data if self.pr.err is None: new_plot = Data1D(self.pr.x, self.pr.y) new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM else: new_plot = Data1D(self.pr.x, self.pr.y, dy=self.pr.err) new_plot.name = IQ_DATA_LABEL new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.interactive = True new_plot.group_id = GROUP_ID_IQ_DATA new_plot.id = self.data_id new_plot.title = "I(q)" wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title="I(q)", reset=reset)) self.current_plottable = new_plot # Get Q range self.control_panel.q_min = min(self.pr.x) self.control_panel.q_max = max(self.pr.x) def save_data(self, filepath, prstate=None): """ Save data in provided state object. :TODO: move the state code away from inversion_panel and move it here. Then remove the "prstate" input and make this method private. :param filepath: path of file to write to :param prstate: P(r) inversion state """ #TODO: do we need this or can we use DataLoader.loader.save directly? # Add output data and coefficients to state prstate.coefficients = self._last_out prstate.covariance = self._last_cov # Write the output to file # First, check that the data is of the right type if issubclass(self.current_plottable.__class__, sas.sascalc.dataloader.data_info.Data1D): self.state_reader.write(filepath, self.current_plottable, prstate) else: msg = "pr.save_data: the data being saved is not a" msg += " sas.data_info.Data1D object" raise RuntimeError, msg def setup_plot_inversion(self, alpha, nfunc, d_max, q_min=None, q_max=None, est_bck=False, bck_val=0, height=0, width=0): """ Set up inversion from plotted data """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = est_bck self.bck_val = bck_val self.slit_height = height self.slit_width = width try: pr = self._create_plot_pr() if pr is not None: self.pr = pr self.perform_inversion() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def estimate_plot_inversion(self, alpha, nfunc, d_max, q_min=None, q_max=None, est_bck=False, bck_val=0, height=0, width=0): """ Estimate parameters from plotted data """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = est_bck self.bck_val = bck_val self.slit_height = height self.slit_width = width try: pr = self._create_plot_pr() if pr is not None: self.pr = pr self.perform_estimate() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def _create_plot_pr(self, estimate=False): """ Create and prepare invertor instance from a plottable data set. :param path: path of the file to read in """ # Sanity check if self.current_plottable is None: msg = "Please load a valid data set before proceeding." wx.PostEvent(self.parent, StatusEvent(status=msg)) return None # Get the data from the chosen data set and perform inversion pr = Invertor() pr.d_max = self.max_length pr.alpha = self.alpha pr.q_min = self.q_min pr.q_max = self.q_max pr.x = self.current_plottable.x pr.y = self.current_plottable.y pr.est_bck = self.est_bck pr.slit_height = self.slit_height pr.slit_width = self.slit_width pr.background = self.bck_val # Keep track of the plot window title to ensure that # we can overlay the plots pr.info["plot_group_id"] = self.current_plottable.group_id # Fill in errors if none were provided err = self.current_plottable.dy all_zeros = True if err is None: err = np.zeros(len(pr.y)) else: for i in range(len(err)): if err[i] > 0: all_zeros = False if all_zeros: scale = None min_err = 0.0 for i in range(len(pr.y)): # Scale the error so that we can fit over several decades of Q if scale is None: scale = 0.05 * math.sqrt(pr.y[i]) min_err = 0.01 * pr.y[i] err[i] = scale * math.sqrt(math.fabs(pr.y[i])) + min_err message = "The loaded file had no error bars, " message += "statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) pr.err = err return pr def setup_file_inversion(self, alpha, nfunc, d_max, data, path=None, q_min=None, q_max=None, bck=False, height=0, width=0): """ Set up inversion """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = bck self.slit_height = height self.slit_width = width try: pr = self._create_file_pr(data) if pr is not None: self.pr = pr self.perform_inversion() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def estimate_file_inversion(self, alpha, nfunc, d_max, data, path=None, q_min=None, q_max=None, bck=False, height=0, width=0): """ Estimate parameters for inversion """ self.alpha = alpha self.nfunc = nfunc self.max_length = d_max self.q_min = q_min self.q_max = q_max self.est_bck = bck self.slit_height = height self.slit_width = width try: pr = self._create_file_pr(data) if pr is not None: self.pr = pr self.perform_estimate() except: wx.PostEvent(self.parent, StatusEvent(status=sys.exc_value)) def _create_file_pr(self, data): """ Create and prepare invertor instance from a file data set. :param path: path of the file to read in """ # Reset the status bar so that we don't get mixed up # with old messages. #TODO: refactor this into a proper status handling wx.PostEvent(self.parent, StatusEvent(status='')) try: class FileData(object): x = None y = None err = None path = None def __init__(self, path): self.path = path self._current_file_data = FileData(data.path) self._current_file_data.x = data.x self._current_file_data.y = data.y self._current_file_data.err = data.dy x, y, err = data.x, data.y, data.dy except: load_error(sys.exc_value) return None # If the file contains no data, just return if x is None or len(x) == 0: load_error("The loaded file contains no data") return None # If we have not errors, add statistical errors if y is not None: if err is None or np.all(err) == 0: err = np.zeros(len(y)) scale = None min_err = 0.0 for i in range(len(y)): # Scale the error so that we can fit over several decades of Q if scale is None: scale = 0.05 * math.sqrt(y[i]) min_err = 0.01 * y[i] err[i] = scale * math.sqrt(math.fabs(y[i])) + min_err message = "The loaded file had no error bars, " message += "statistical errors are assumed." wx.PostEvent(self.parent, StatusEvent(status=message)) try: # Get the data from the chosen data set and perform inversion pr = Invertor() pr.d_max = self.max_length pr.alpha = self.alpha pr.q_min = self.q_min pr.q_max = self.q_max pr.x = x pr.y = y pr.err = err pr.est_bck = self.est_bck pr.slit_height = self.slit_height pr.slit_width = self.slit_width return pr except: load_error(sys.exc_value) return None def perform_estimate(self): """ Perform parameter estimation """ from pr_thread import EstimatePr # If a thread is already started, stop it if self.estimation_thread is not None and \ self.estimation_thread.isrunning(): self.estimation_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() self.estimation_thread = EstimatePr(pr, self.nfunc, error_func=self._thread_error, completefn=self._estimate_completed, updatefn=None) self.estimation_thread.queue() self.estimation_thread.ready(2.5) def perform_estimateNT(self): """ Perform parameter estimation """ from pr_thread import EstimateNT # If a thread is already started, stop it if self.estimation_thread is not None and self.estimation_thread.isrunning(): self.estimation_thread.stop() ## stop just raises the flag -- the thread is supposed to ## then kill itself. In August 2014 it was shown that this is ## incorrectly handled by fitting.py and a fix implemented. ## It is not clear whether it is properly used here, but the ## "fix" of waiting for the previous thread to end breaks the ## pr perspective completely as it causes an infinite loop. ## Thus it is likely the threading is bing properly handled. ## While the "fix" is no longer implemented the comment is ## left here till somebody ascertains that in fact the threads ## are being properly handled. ## ## -PDB January 25, 2015 pr = self.pr.clone() # Skip the slit settings for the estimation # It slows down the application and it doesn't change the estimates pr.slit_height = 0.0 pr.slit_width = 0.0 self.estimation_thread = EstimateNT(pr, self.nfunc, error_func=self._thread_error, completefn=self._estimateNT_completed, updatefn=None) self.estimation_thread.queue() self.estimation_thread.ready(2.5) def perform_inversion(self): """ Perform inversion """ self.start_thread() def _on_context_inversion(self, event): """ Call-back method for plot context menu """ panel = event.GetEventObject() Plugin.on_perspective(self, event=event) # If we have more than one displayed plot, make the user choose if len(panel.plots) >= 1 and \ panel.graph.selected_plottable in panel.plots: dataset = panel.plots[panel.graph.selected_plottable].name else: logger.info("Prview Error: No data is available") return # Store a reference to the current plottable # If we have a suggested value, use it. try: estimate = float(self.control_panel.alpha_estimate) self.control_panel.alpha = estimate except: self.control_panel.alpha = self.alpha logger.info("Prview :Alpha Not estimate yet") try: estimate = int(self.control_panel.nterms_estimate) self.control_panel.nfunc = estimate except: self.control_panel.nfunc = self.nfunc logger.info("Prview : ntemrs Not estimate yet") self.current_plottable = panel.plots[panel.graph.selected_plottable] self.set_data([self.current_plottable]) self.control_panel.plotname = dataset #self.control_panel.nfunc = self.nfunc self.control_panel.d_max = self.max_length #self.parent.set_perspective(self.perspective) self.control_panel._on_invert(None) def get_panels(self, parent): """ Create and return a list of panel objects """ from inversion_panel import InversionControl self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100, 200)) self.control_panel = InversionControl(self.frame, -1, style=wx.RAISED_BORDER) self.frame.set_panel(self.control_panel) self._frame_set_helper() self.control_panel.set_manager(self) self.control_panel.nfunc = self.nfunc self.control_panel.d_max = self.max_length self.control_panel.alpha = self.alpha self.perspective = [] self.perspective.append(self.control_panel.window_name) return [self.control_panel] def set_data(self, data_list=None): """ receive a list of data to compute pr """ if data_list is None: data_list = [] if len(data_list) >= 1: if len(data_list) == 1: data = data_list[0] else: data_1d_list = [] data_2d_list = [] error_msg = "" # separate data into data1d and data2d list for data in data_list: if data is not None: if issubclass(data.__class__, Data1D): data_1d_list.append(data) else: error_msg += " %s type %s \n" % (str(data.name), str(data.__class__.__name__)) data_2d_list.append(data) if len(data_2d_list) > 0: msg = "PrView does not support the following data types:\n" msg += error_msg if len(data_1d_list) == 0: wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return msg = "Prview does not allow multiple data!\n" msg += "Please select one.\n" if len(data_list) > 1: from pr_widgets import DataDialog dlg = DataDialog(data_list=data_1d_list, text=msg) if dlg.ShowModal() == wx.ID_OK: data = dlg.get_data() else: data = None dlg.Destroy() if data is None: msg += "PrView receives no data. \n" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return if issubclass(data.__class__, Data1D): try: wx.PostEvent(self.parent, NewPlotEvent(action='remove', group_id=GROUP_ID_IQ_DATA, id=self.data_id)) self.data_id = data.id self.control_panel._change_file(evt=None, data=data) except: msg = "Prview Set_data: " + str(sys.exc_value) wx.PostEvent(self.parent, StatusEvent(status=msg, info="error")) else: msg = "Pr cannot be computed for data of " msg += "type %s" % (data_list[0].__class__.__name__) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) else: msg = "Pr contain no data" wx.PostEvent(self.parent, StatusEvent(status=msg, info='warning')) def post_init(self): """ Post initialization call back to close the loose ends [Somehow openGL needs this call] """ pass
class Plugin(PluginBase): """ This class defines the interface for a plugin class for a correlation function perspective """ def __init__(self): PluginBase.__init__(self, name="Correlation Function") logger.info("Correlation function plug-in started") self._always_active = True self.state_reader = Reader(self.set_state) self._extensions = '.crf' def get_panels(self, parent): """ Define the GUI panels """ self.parent = parent self.frame = MDIFrame(self.parent, None, 'None', (100,200)) self.data_id = IQ_DATA_LABEL self.corfunc_panel = CorfuncPanel(parent=self.frame) self.frame.set_panel(self.corfunc_panel) self.corfunc_panel.set_manager(self) self._frame_set_helper() self.perspective.append(self.corfunc_panel.window_name) l = Loader() l.associate_file_reader('.crf', self.state_reader) return [self.corfunc_panel] def get_context_menu(self, plotpanel=None): """ Get the context menu items available for Corfunc. :param plotpanel: A Plotter1D panel :return: a list of menu items with call-back function :note: if Data1D was generated from Theory1D the fitting option is not allowed """ graph = plotpanel.graph if graph.selected_plottable not in plotpanel.plots: return [] data = plotpanel.plots[graph.selected_plottable] if data.id == IQ_DATA_LABEL or data.id == IQ_EXTRAPOLATED_DATA_LABEL or data.id == TRANSFORM_LABEL1 or data.id == TRANSFORM_LABEL3: return [] item = plotpanel.plots[graph.selected_plottable] if item.__class__.__name__ is "Data2D": return [] elif item.__class__.__name__ is "Data1D": return [["Select data in corfunc", "Send this data to the correlation function perspective", self._on_select_data]] def set_state(self, state=None, datainfo=None): """ Callback for CorfuncState reader. Called when a .crf file is loaded """ if isinstance(datainfo, list): data = datainfo[0] else: data = datainfo self.corfunc_panel.set_state(state=state, data=data) self.on_perspective(event=None) data = self.parent.create_gui_data(data, None) self.parent.add_data({ data.id: data }) def set_data(self, data_list=None): """ Load the data that's been selected :param data_list: The data to load in """ if data_list is None: data_list = [] if len(data_list) >= 1: msg = "" if len(data_list) == 1: data = data_list[0] else: data_1d_list = [] data_2d_list = [] err_msg = "" for data in data_list: if data is not None: if issubclass(data.__class__, Data1D): data_1d_list.append(data) else: err_msg += "{} type {} \n".format(str(data.name), str(data.__class__)) data_2d_list.append(data) if len(data_2d_list) > 0: msg = "Corfunc doesn't support the following data types:\n" msg += err_msg if len(data_1d_list) == 0: msg += "No data recieved" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return elif len(data_list) > 1: msg = "Corfunc does not allow multiple data\n" msg += "Please select one.\n" dlg = DataDialog(data_list=data_1d_list, text=msg) if dlg.ShowModal() == wx.ID_OK: data = dlg.get_data() else: data = None dlg.Destroy() if data is None: msg += "Corfunc recieved no data\n" wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) return if issubclass(data.__class__, Data1D): try: wx.PostEvent(self.parent, NewPlotEvent(action='remove', group_id=GROUP_ID_IQ_DATA, id=self.data_id)) self.data_id = data.id self.corfunc_panel.set_data(data) except: msg = "Corfunc set_data: " + str(sys.exc_value) wx.PostEvent(self.parent, StatusEvent(status=msg, info='error')) def delete_data(self, data): """ Delete the data from the perspective """ self.clear_data() self.corfunc_panel.set_data(None) def show_data(self, data, label, reset=False, active_ctrl=None): """ Show data read from a file :param data: The data to plot (Data1D) :param label: What to label the plot. Also used as the plot ID :param reset: If True, all other plottables will be cleared """ new_plot = Data1D(data.x, copy.deepcopy(data.y), dy=data.dy) group_id = "" if label == IQ_DATA_LABEL or label == IQ_EXTRAPOLATED_DATA_LABEL: new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") new_plot.y -= self.corfunc_panel.background # Show data on a log(Q)/I scale new_plot.ytransform = 'y' group_id = GROUP_ID_IQ_DATA if label == IQ_EXTRAPOLATED_DATA_LABEL: # Show the extrapolation as a curve instead of points new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM elif label == TRANSFORM_LABEL1 or label == TRANSFORM_LABEL3: new_plot.xaxis("{x}", 'A') new_plot.yaxis("{\\Gamma}", '') # Show transform on a linear scale new_plot.xtransform = 'x' new_plot.ytransform = 'y' group_id = GROUP_ID_TRANSFORM # Show the transformation as a curve instead of points new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM elif label == IDF_LABEL: new_plot.xaxis("{x}", 'A') new_plot.yaxis("{g_1}", '') # Linear scale new_plot.xtransform = 'x' new_plot.ytransform = 'y' group_id = GROUP_ID_IDF # Show IDF as a curve instead of points new_plot.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM new_plot.id = label new_plot.name = label new_plot.group_id = group_id new_plot.interactive = True new_plot.title = group_id.replace('$', '').replace('\\', '') wx.PostEvent(self.parent, NewPlotEvent(plot=new_plot, title=new_plot.title, reset=reset)) if label == IQ_DATA_LABEL or label == IQ_EXTRAPOLATED_DATA_LABEL: wx.CallAfter(self.corfunc_panel.plot_qrange, active=active_ctrl, leftdown=True) if label == IQ_EXTRAPOLATED_DATA_LABEL: # Zoom in to the region we're interested in xlim = (min(self.corfunc_panel._extrapolated_data.x), self.corfunc_panel.qmax[1]*1.2) wx.CallAfter(wx.PostEvent, self.parent, PlotLimitEvent(id=IQ_DATA_LABEL, group_id=GROUP_ID_IQ_DATA, xlim=xlim)) def clear_data(self): wx.PostEvent(self.parent, NewPlotEvent(action='delete', group_id=GROUP_ID_TRANSFORM)) wx.PostEvent(self.parent, NewPlotEvent(action='clear', group_id=GROUP_ID_IQ_DATA)) def _on_select_data(self, event): panel = event.GetEventObject() if not panel.graph.selected_plottable in panel.plots: return data = panel.plots[panel.graph.selected_plottable] self.set_data([data])