Index: src/sas/sasgui/perspectives/invariant/__init__.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/__init__.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/__init__.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,45 @@
+PLUGIN_ID = "Invariant plug-in 1.0"
+import os
+
+from distutils.filelist import findall
+from invariant import *
+
+def get_data_path(media):
+    """
+    """
+    # Check for data path in the package
+    path = os.path.join(os.path.dirname(__file__), media)
+    if os.path.isdir(path):
+        return path
+
+    # Check for data path next to exe/zip file.
+    # If we are inside a py2exe zip file, we need to go up
+    # to get to the directory containing 
+    # the media for this module
+    path = os.path.dirname(__file__)
+    #Look for maximum n_dir up of the current dir to find media
+    n_dir = 12
+    for i in range(n_dir):
+        path, _ = os.path.split(path)
+        media_path = os.path.join(path, media)
+        if os.path.isdir(media_path):
+            module_media_path = os.path.join(media_path, 'invariant_media')
+            if os.path.isdir(module_media_path):
+                return module_media_path
+            return media_path
+   
+    raise RuntimeError('Could not find invariant media files')
+
+def data_files():
+    """
+    Return the data files associated with media invariant.
+    
+    The format is a list of (directory, [files...]) pairs which can be
+    used directly in setup(...,data_files=...) for setup.py.
+
+    """
+    data_files = []
+    path = get_data_path(media="media")
+    for f in findall(path):
+        data_files.append(('media/invariant_media', [f]))
+    return data_files
Index: src/sas/sasgui/perspectives/invariant/invariant.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/invariant.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/invariant.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,357 @@
+
+
+
+################################################################################
+# This software was developed by the University of Tennessee as part of the
+# Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
+# project funded by the US National Science Foundation.
+#
+# See the license text in license.txt
+#
+# copyright 2009, University of Tennessee
+################################################################################
+
+
+import sys
+import wx
+import copy
+import logging
+from sas.sasgui.guiframe.gui_manager import MDIFrame
+from sas.sasgui.guiframe.dataFitting import Data1D
+from sas.sasgui.guiframe.events import NewPlotEvent
+from sas.sasgui.guiframe.events import StatusEvent
+from sas.sasgui.guiframe.gui_style import GUIFRAME_ID
+from sas.sasgui.perspectives.invariant.invariant_state import Reader as reader
+from sas.sascalc.dataloader.loader import Loader
+from sas.sasgui.perspectives.invariant.invariant_panel import InvariantPanel
+from sas.sasgui.guiframe.plugin_base import PluginBase
+
+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.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))
+
Index: src/sas/sasgui/perspectives/invariant/invariant_details.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/invariant_details.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/invariant_details.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,542 @@
+"""
+    Invariant panel
+"""
+import wx
+import sys
+
+from sas.sasgui.guiframe.utils import format_number
+from invariant_widgets import OutputTextCtrl
+# Dimensions related to chart
+RECTANGLE_WIDTH = 400.0
+RECTANGLE_HEIGHT = 20
+# Invariant panel size
+_BOX_WIDTH = 76
+
+# scale to use for a bar of value zero
+RECTANGLE_SCALE = 0.0001
+DEFAULT_QSTAR = 1.0
+
+if sys.platform.count("win32") > 0:
+    _STATICBOX_WIDTH = 450
+    PANEL_WIDTH = 500
+    PANEL_HEIGHT = 430
+    FONT_VARIANT = 0
+else:
+    _STATICBOX_WIDTH = 480
+    PANEL_WIDTH = 530
+    PANEL_HEIGHT = 430
+    FONT_VARIANT = 1
+
+ERROR_COLOR = wx.Colour(255, 0, 0, 128)
+EXTRAPOLATION_COLOR = wx.Colour(169, 169, 168, 128)
+INVARIANT_COLOR = wx.Colour(67, 208, 128, 128)
+
+
+class InvariantContainer(wx.Object):
+    """
+    This class stores some values resulting resulting from invariant
+    calculations. Given the value of total invariant, this class can also
+    determine the percentage of invariants resulting from extrapolation.
+    """
+    def __init__(self):
+        # invariant at low range
+        self.qstar_low = None
+        # invariant at low range error
+        self.qstar_low_err = None
+        # invariant
+        self.qstar = None
+        # invariant error
+        self.qstar_err = None
+        # invariant at high range
+        self.qstar_high = None
+        # invariant at high range error
+        self.qstar_high_err = None
+        # invariant total
+        self.qstar_total = None
+        # invariant error
+        self.qstar_total_err = None
+        # scale
+        self.qstar_low_percent = None
+        self.qstar_high_percent = None
+        self.qstar_percent = None
+        # warning message
+        self.existing_warning = False
+        self.warning_msg = "No Details on calculations available...\n"
+
+    def compute_percentage(self):
+        """
+        Compute percentage of each invariant
+        """
+        if self.qstar_total is None:
+            self.qstar_percent = None
+            self.qstar_low = None
+            self.qstar_high = None
+            self.check_values()
+            return
+
+        # compute invariant percentage
+        if self.qstar is None:
+            self.qstar_percent = None
+        else:
+            try:
+                self.qstar_percent = float(self.qstar) / float(self.qstar_total)
+            except:
+                self.qstar_percent = 'error'
+        # compute low q invariant percentage
+        if self.qstar_low is None:
+            self.qstar_low_percent = None
+        else:
+            try:
+                self.qstar_low_percent = float(self.qstar_low)\
+                                            / float(self.qstar_total)
+            except:
+                self.qstar_low_percent = 'error'
+        # compute high q invariant percentage
+        if self.qstar_high is None:
+            self.qstar_high_percent = None
+        else:
+            try:
+                self.qstar_high_percent = float(self.qstar_high)\
+                                                / float(self.qstar_total)
+            except:
+                self.qstar_high_percent = 'error'
+        wx.CallAfter(self.check_values)
+
+    def check_values(self):
+        """
+        check the validity if invariant
+        """
+        if self.qstar_total is None and self.qstar is None:
+            self.warning_msg = "Invariant not calculated.\n"
+            return
+        if self.qstar_total == 0:
+            self.existing_warning = True
+            self.warning_msg = "Invariant is zero. \n"
+            self.warning_msg += "The calculations are likely "
+            self.warning_msg += "to be unreliable!\n"
+            return
+        # warning to the user when the extrapolated invariant is greater than %5
+        msg = ''
+        if self.qstar_percent == 'error':
+            try:
+                float(self.qstar)
+            except:
+                self.existing_warning = True
+                msg += 'Error occurred when computing invariant from data.\n '
+        if self.qstar_percent > 1:
+            self.existing_warning = True
+            msg += "Invariant Q  contribution is greater "
+            msg += "than 100% .\n"
+        if self.qstar_low_percent == 'error':
+            try:
+                float(self.qstar_low)
+            except:
+                self.existing_warning = True
+                msg += "Error occurred when computing extrapolated invariant"
+                msg += " at low-Q region.\n"
+        elif self.qstar_low_percent is not None:
+            if self.qstar_low_percent >= 0.05:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at Low Q is higher "
+                msg += "than 5% of the invariant.\n"
+            elif self.qstar_low_percent < 0:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at Low Q < 0.\n"
+            elif self.qstar_low_percent > 1:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at Low Q is greater "
+                msg += "than 100% .\n"
+        if self.qstar_high_percent == 'error':
+            try:
+                float(self.qstar_high)
+            except:
+                self.existing_warning = True
+                msg += 'Error occurred when computing extrapolated'
+                msg += ' invariant at high-Q region.\n'
+        elif self.qstar_high_percent is not None:
+            if self.qstar_high_percent >= 0.05:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at High Q is higher "
+                msg += "than 5% of the invariant.\n"
+            elif self.qstar_high_percent < 0:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at High Q < 0.\n"
+            elif self.qstar_high_percent > 1:
+                self.existing_warning = True
+                msg += "Extrapolated contribution at High Q is greater "
+                msg += "than 100% .\n"
+        if (self.qstar_low_percent not in [None, "error"]) and \
+            (self.qstar_high_percent not in [None, "error"])\
+            and self.qstar_low_percent + self.qstar_high_percent >= 0.05:
+            self.existing_warning = True
+            msg += "The sum of all extrapolated contributions is higher "
+            msg += "than 5% of the invariant.\n"
+
+        if self.existing_warning:
+            self.warning_msg = ''
+            self.warning_msg += msg
+            self.warning_msg += "The calculations are likely to be"
+            self.warning_msg += " unreliable!\n"
+        else:
+            self.warning_msg = "No Details on calculations available...\n"
+
+class InvariantDetailsPanel(wx.Dialog):
+    """
+    This panel describes proportion of invariants
+    """
+    def __init__(self, parent=None, id=-1, qstar_container=None,
+                 title="Invariant Details",
+                 size=(PANEL_WIDTH, PANEL_HEIGHT)):
+        wx.Dialog.__init__(self, parent=parent, id=id, title=title, size=size)
+
+        # Font size
+        self.SetWindowVariant(variant=FONT_VARIANT)
+        self.parent = parent
+        # self.qstar_container
+        self.qstar_container = qstar_container
+        # warning message
+        self.warning_msg = self.qstar_container.warning_msg
+
+        # Define scale of each bar
+        self.low_inv_percent = self.qstar_container.qstar_low_percent
+        self.low_scale = self.get_scale(percentage=self.low_inv_percent,
+                                        scale_name="Extrapolated at Low Q")
+        self.inv_percent = self.qstar_container.qstar_percent
+        self.inv_scale = self.get_scale(percentage=self.inv_percent,
+                                        scale_name="Inv in Q range")
+        self.high_inv_percent = self.qstar_container.qstar_high_percent
+        self.high_scale = self.get_scale(percentage=self.high_inv_percent,
+                                         scale_name="Extrapolated at High Q")
+
+        # Default color the extrapolation bar is grey
+        self.extrapolation_color_low = EXTRAPOLATION_COLOR
+        self.extrapolation_color_high = EXTRAPOLATION_COLOR
+        self.invariant_color = INVARIANT_COLOR
+        # change color of high and low bar when necessary
+        self.set_color_bar()
+        # draw the panel itself
+        self._do_layout()
+        self.set_values()
+
+    def _define_structure(self):
+        """
+        Define main sizers needed for this panel
+        """
+        # Box sizers must be defined first before defining buttons/textctrls
+        # (MAC).
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        # Sizer related to chart
+        chart_box = wx.StaticBox(self, -1, "Invariant Chart")
+        self.chart_sizer = wx.StaticBoxSizer(chart_box, wx.VERTICAL)
+        self.chart_sizer.SetMinSize((PANEL_WIDTH - 50, 110))
+        # Sizer related to invariant values
+        self.invariant_sizer = wx.GridBagSizer(4, 4)
+        invariant_box = wx.StaticBox(self, -1, "Numerical Values")
+        self.invariant_box_sizer = wx.StaticBoxSizer(invariant_box, wx.HORIZONTAL)
+
+        self.invariant_box_sizer.SetMinSize((PANEL_WIDTH - 50, -1))
+        # Sizer related to warning message
+        warning_box = wx.StaticBox(self, -1, "Warning")
+        self.warning_sizer = wx.StaticBoxSizer(warning_box, wx.VERTICAL)
+        self.warning_sizer.SetMinSize((PANEL_WIDTH - 50, -1))
+        # Sizer related to button
+        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+    def _layout_shart(self):
+        """
+        Draw widgets related to chart
+        """
+        self.panel_chart = wx.Panel(self)
+        self.panel_chart.Bind(wx.EVT_PAINT, self.on_paint)
+        self.chart_sizer.Add(self.panel_chart, 1, wx.EXPAND | wx.ALL, 0)
+
+    def _layout_invariant(self):
+        """
+        Draw widgets related to invariant
+        """
+        uncertainty = "+/-"
+        unit_invariant = '[1/(cm * A^3)]'
+
+        invariant_txt = wx.StaticText(self, -1, 'Q* from Data ')
+        invariant_txt.SetToolTipString("Invariant in the data set's Q range.")
+        self.invariant_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1))
+        hint_msg = "Invariant in the data set's Q range."
+        self.invariant_tcl.SetToolTipString(hint_msg)
+        self.invariant_err_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1))
+        hint_msg = "Uncertainty on the invariant from data's range."
+        self.invariant_err_tcl.SetToolTipString(hint_msg)
+        invariant_units_txt = wx.StaticText(self, -1, unit_invariant)
+        hint_msg = "Unit of the invariant from data's Q range"
+        invariant_units_txt.SetToolTipString(hint_msg)
+
+        invariant_low_txt = wx.StaticText(self, -1, 'Q* from Low-Q')
+        hint_msg = "Extrapolated invariant from low-Q range."
+        invariant_low_txt.SetToolTipString(hint_msg)
+        self.invariant_low_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1))
+        hint_msg = "Extrapolated invariant from low-Q range."
+        self.invariant_low_tcl.SetToolTipString(hint_msg)
+        self.invariant_low_err_tcl = OutputTextCtrl(self, -1,
+                                                    size=(_BOX_WIDTH, -1))
+        hint_msg = "Uncertainty on the invariant from low-Q range."
+        self.invariant_low_err_tcl.SetToolTipString(hint_msg)
+        invariant_low_units_txt = wx.StaticText(self, -1, unit_invariant)
+        hint_msg = "Unit of the extrapolated invariant from  low-Q range"
+        invariant_low_units_txt.SetToolTipString(hint_msg)
+
+        invariant_high_txt = wx.StaticText(self, -1, 'Q* from High-Q')
+        hint_msg = "Extrapolated invariant from  high-Q range"
+        invariant_high_txt.SetToolTipString(hint_msg)
+        self.invariant_high_tcl = OutputTextCtrl(self, -1,
+                                                 size=(_BOX_WIDTH, -1))
+        hint_msg = "Extrapolated invariant from  high-Q range"
+        self.invariant_high_tcl.SetToolTipString(hint_msg)
+        self.invariant_high_err_tcl = OutputTextCtrl(self, -1,
+                                                     size=(_BOX_WIDTH, -1))
+        hint_msg = "Uncertainty on the invariant from high-Q range."
+        self.invariant_high_err_tcl.SetToolTipString(hint_msg)
+        invariant_high_units_txt = wx.StaticText(self, -1, unit_invariant)
+        hint_msg = "Unit of the extrapolated invariant from  high-Q range"
+        invariant_high_units_txt.SetToolTipString(hint_msg)
+
+        # Invariant low
+        iy = 0
+        ix = 0
+        self.invariant_sizer.Add(invariant_low_txt, (iy, ix), (1, 1),
+                                 wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_low_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_low_err_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(invariant_low_units_txt,
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        # Invariant
+        iy += 1
+        ix = 0
+        self.invariant_sizer.Add(invariant_txt, (iy, ix), (1, 1),
+                                 wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_err_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(invariant_units_txt,
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        # Invariant high
+        iy += 1
+        ix = 0
+        self.invariant_sizer.Add(invariant_high_txt, (iy, ix), (1, 1),
+                                 wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_high_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_high_err_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.invariant_sizer.Add(invariant_high_units_txt,
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        self.invariant_box_sizer.Add(self.invariant_sizer, 0, wx.TOP | wx.BOTTOM, 10)
+
+    def _layout_warning(self):
+        """
+        Draw widgets related to warning
+        """
+        # Warning [string]
+        self.warning_msg_txt = wx.StaticText(self, -1, self.warning_msg)
+        if self.qstar_container.existing_warning:
+            self.warning_msg_txt.SetForegroundColour('red')
+        self.warning_sizer.AddMany([(self.warning_msg_txt, 0,
+                                     wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 10)])
+
+    def _layout_button(self):
+        """
+        Draw widgets related to button
+        """
+        # Close button
+        id = wx.NewId()
+        button_ok = wx.Button(self, id, "Ok")
+        button_ok.SetToolTipString("Give Details on Computation")
+        self.Bind(wx.EVT_BUTTON, self.on_close, id=id)
+        self.button_sizer.AddMany([((20, 20), 0, wx.LEFT, 350),
+                                   (button_ok, 0, wx.RIGHT, 10)])
+    def _do_layout(self):
+        """
+        Draw window content
+        """
+        self._define_structure()
+        self._layout_shart()
+        self._layout_invariant()
+        self._layout_warning()
+        self._layout_button()
+        self.main_sizer.AddMany([(self.chart_sizer, 0, wx.ALL, 10),
+                                 (self.invariant_box_sizer, 0, wx.ALL, 10),
+                                 (self.warning_sizer, 0, wx.ALL, 10),
+                                 (self.button_sizer, 0, wx.ALL, 10)])
+        self.SetSizer(self.main_sizer)
+
+
+    def set_values(self):
+        """
+        Set value of txtcrtl
+        """
+        value = format_number(self.qstar_container.qstar)
+        self.invariant_tcl.SetValue(value)
+        value = format_number(self.qstar_container.qstar_err)
+        self.invariant_err_tcl.SetValue(value)
+        value = format_number(self.qstar_container.qstar_low)
+        self.invariant_low_tcl.SetValue(value)
+        value = format_number(self.qstar_container.qstar_low_err)
+        self.invariant_low_err_tcl.SetValue(value)
+        value = format_number(self.qstar_container.qstar_high)
+        self.invariant_high_tcl.SetValue(value)
+        value = format_number(self.qstar_container.qstar_high_err)
+        self.invariant_high_err_tcl.SetValue(value)
+
+    def get_scale(self, percentage, scale_name='scale'):
+        """
+        Check scale receive in this panel.
+        """
+        scale = RECTANGLE_SCALE
+        try:
+            if percentage in [None, 0.0, "error"]:
+                scale = RECTANGLE_SCALE
+                return scale
+            elif percentage < 0:
+                scale = RECTANGLE_SCALE
+                return scale
+            scale = float(percentage)
+        except:
+            scale = RECTANGLE_SCALE
+            self.warning_msg += "Recieve an invalid scale for %s\n"
+            self.warning_msg += "check this value : %s\n" % str(percentage)
+        return  scale
+
+    def set_color_bar(self):
+        """
+        Change the color for low and high bar when necessary
+        """
+        self.extrapolation_color_low = EXTRAPOLATION_COLOR
+        self.extrapolation_color_high = EXTRAPOLATION_COLOR
+        self.invariant_color = INVARIANT_COLOR
+        # warning to the user when the extrapolated invariant is greater than %5
+        if self.low_scale >= 0.05 or self.low_scale > 1 or self.low_scale < 0:
+            self.extrapolation_color_low = ERROR_COLOR
+        if self.high_scale >= 0.05 or self.high_scale > 1 or self.high_scale < 0:
+            self.extrapolation_color_high = ERROR_COLOR
+        if self.inv_scale > 1 or self.inv_scale < 0:
+            self.invariant_color = ERROR_COLOR
+
+    def on_close(self, event):
+        """
+        Close the current window
+        """
+        self.Close()
+
+    def on_paint(self, event):
+        """
+        Draw the chart
+        """
+        dc = wx.PaintDC(self.panel_chart)
+        try:
+            gc = wx.GraphicsContext.Create(dc)
+        except NotImplementedError:
+            msg = "This build of wxPython does not support "
+            msg += "the wx.GraphicsContext family of classes."
+            dc.DrawText(msg, 25, 25)
+            return
+        # Start the drawing
+        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        font.SetWeight(wx.BOLD)
+        gc.SetFont(font)
+        # Draw a rectangle
+        path = gc.CreatePath()
+        path.AddRectangle(-RECTANGLE_WIDTH / 2, -RECTANGLE_HEIGHT / 2,
+                          RECTANGLE_WIDTH / 2, RECTANGLE_HEIGHT / 2)
+        x_origine = 20
+        y_origine = 15
+        # Draw low rectangle
+        gc.PushState()
+        label = "Q* from Low-Q "
+        PathFunc = gc.DrawPath
+        w, h = gc.GetTextExtent(label)
+        gc.DrawText(label, x_origine, y_origine)
+        # Translate the rectangle
+        x_center = x_origine + RECTANGLE_WIDTH * self.low_scale / 2 + w + 10
+        y_center = y_origine + h
+        gc.Translate(x_center, y_center)
+        gc.SetPen(wx.Pen("black", 1))
+        gc.SetBrush(wx.Brush(self.extrapolation_color_low))
+        if self.low_inv_percent is None:
+            low_percent = 'Not Computed'
+        elif self.low_inv_percent == 'error':
+            low_percent = 'Error'
+        else:
+            low_percent = format_number(self.low_inv_percent * 100) + '%'
+        x_center = 20
+        y_center = -h
+        gc.DrawText(low_percent, x_center, y_center)
+        # Increase width by self.low_scale
+        gc.Scale(self.low_scale, 1.0)
+        PathFunc(path)
+        gc.PopState()
+        # Draw rectangle for invariant
+        gc.PushState()  # save it again
+        y_origine += 20
+        gc.DrawText("Q* from Data ", x_origine, y_origine)
+        # offset to the lower part of the window
+        x_center = x_origine + RECTANGLE_WIDTH * self.inv_scale / 2 + w + 10
+        y_center = y_origine + h
+        gc.Translate(x_center, y_center)
+        # 128 == half transparent
+        gc.SetBrush(wx.Brush(self.invariant_color))
+        # Increase width by self.inv_scale
+        if self.inv_percent is None:
+            inv_percent = 'Not Computed'
+        elif self.inv_percent == 'error':
+            inv_percent = 'Error'
+        else:
+            inv_percent = format_number(self.inv_percent * 100) + '%'
+        x_center = 20
+        y_center = -h
+        gc.DrawText(inv_percent, x_center, y_center)
+        gc.Scale(self.inv_scale, 1.0)
+        gc.DrawPath(path)
+        gc.PopState()
+        # restore saved state
+        # Draw rectangle for high invariant
+        gc.PushState()
+        y_origine += 20
+        gc.DrawText("Q* from High-Q ", x_origine, y_origine)
+        # define the position of the new rectangle
+        x_center = x_origine + RECTANGLE_WIDTH * self.high_scale / 2 + w + 10
+        y_center = y_origine + h
+        gc.Translate(x_center, y_center)
+        gc.SetBrush(wx.Brush(self.extrapolation_color_high))
+        # increase scale by self.high_scale
+        if self.high_inv_percent is None:
+            high_percent = 'Not Computed'
+        elif self.high_inv_percent == 'error':
+            high_percent = 'Error'
+        else:
+            high_percent = format_number(self.high_inv_percent * 100) + '%'
+        x_center = 20
+        y_center = -h
+        gc.DrawText(high_percent, x_center, y_center)
+
+        gc.Scale(self.high_scale, 1.0)
+        gc.DrawPath(path)
+        gc.PopState()
Index: src/sas/sasgui/perspectives/invariant/invariant_panel.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/invariant_panel.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/invariant_panel.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,1956 @@
+"""
+This module provides the GUI for the invariant perspective panel
+
+"""
+import copy
+import time
+import sys
+import os
+import wx
+import logging
+
+from wx.lib.scrolledpanel import ScrolledPanel
+from sas.sascalc.invariant import invariant
+
+from sas.sasgui.guiframe.utils import format_number
+from sas.sasgui.guiframe.utils import check_float
+from sas.sasgui.guiframe.events import StatusEvent
+from sas.sasgui.guiframe.events import AppendBookmarkEvent
+from sas.sasgui.perspectives.invariant.invariant_details import InvariantDetailsPanel
+from sas.sasgui.perspectives.invariant.invariant_details import InvariantContainer
+from sas.sasgui.perspectives.invariant.invariant_widgets import OutputTextCtrl
+from sas.sasgui.perspectives.invariant.invariant_widgets import InvTextCtrl
+from sas.sasgui.perspectives.invariant.invariant_state import InvariantState as IState
+from sas.sasgui.guiframe.panel_base import PanelBase
+from sas.sasgui.guiframe.documentation_window import DocumentationWindow
+
+# The minimum q-value to be used when extrapolating
+Q_MINIMUM = 1e-5
+# The maximum q-value to be used when extrapolating
+Q_MAXIMUM = 10
+# the ratio of maximum q value/(qmax of data) to plot the theory data
+Q_MAXIMUM_PLOT = 3
+# the number of points to consider during fit
+NPTS = 10
+#Default value for background
+BACKGROUND = 0.0
+#default value for the scale
+SCALE = 1.0
+#default value of the contrast
+CONTRAST = 1.0
+#default value of the power used for power law
+POWER = 4.0
+#Invariant panel size
+_BOX_WIDTH = 76
+
+
+if sys.platform.count("win32") > 0:
+    _STATICBOX_WIDTH = 450
+    PANEL_WIDTH = 500
+    PANEL_HEIGHT = 700
+    FONT_VARIANT = 0
+else:
+    _STATICBOX_WIDTH = 490
+    PANEL_WIDTH = 530
+    PANEL_HEIGHT = 700
+    FONT_VARIANT = 1
+
+
+class InvariantPanel(ScrolledPanel, PanelBase):
+    """
+    Main class defining the sizers (wx "panels") used to draw the
+    Invariant GUI.
+    """
+    ## Internal nickname for the window, used by the AUI manager
+    window_name = "Invariant"
+    ## Name to appear on the window title bar
+    window_caption = "Invariant"
+    ## Flag to tell the AUI manager to put this panel in the center pane
+    CENTER_PANE = True
+    def __init__(self, parent, data=None, manager=None, *args, **kwds):
+        kwds["size"] = (PANEL_WIDTH, PANEL_HEIGHT)
+        kwds["style"] = wx.FULL_REPAINT_ON_RESIZE
+        ScrolledPanel.__init__(self, parent=parent, *args, **kwds)
+        PanelBase.__init__(self, parent)
+        self.SetupScrolling()
+        #Font size
+        self.SetWindowVariant(variant=FONT_VARIANT)
+        #Object that receive status event
+        self.parent = parent.parent
+        #plug-in using this panel
+        self._manager = manager
+        #Data uses for computation
+        self._data = data
+        self._scale = SCALE
+        self._background = BACKGROUND
+        self._bmark = None
+        self.bookmark_num = 0
+        self.state = None
+        self.popUpMenu = None
+        self._set_bookmark_menu()
+        #Init state
+        self.set_state()
+        # default flags for state
+        self.new_state = False
+        self.is_state_data = False
+        self.is_power_out = False
+
+        #container of invariant value
+        self.inv_container = None
+        #sizers
+        self.main_sizer = None
+        self.outputs_sizer = None
+        self.data_name_boxsizer = None
+        self.hint_msg_sizer = None
+        self.data_name_sizer = None
+        self.data_range_sizer = None
+        self.sizer_input = None
+        self.inputs_sizer = None
+        self.extrapolation_sizer = None
+        self.extrapolation_range_sizer = None
+        self.extrapolation_low_high_sizer = None
+        self.low_extrapolation_sizer = None
+        self.low_q_sizer = None
+        self.high_extrapolation_sizer = None
+        self.high_q_sizer = None
+        self.volume_surface_sizer = None
+        self.invariant_sizer = None
+        self.button_sizer = None
+        self.save_button_sizer = None
+        self.hint_msg_txt = None
+        self.data_name_tcl = None
+        self.data_min_tcl = None
+        self.data_max_tcl = None
+        #Draw the panel
+        self._do_layout()
+        self.reset_panel()
+        self._reset_state_list()
+        ## Default file location for save
+        self._default_save_location = os.getcwd()
+        if self.parent is not None:
+            msg = ""
+            wx.PostEvent(self.parent, StatusEvent(status=msg, info="info"))
+            self._default_save_location = \
+                        self.parent.get_save_location()
+
+        self._set_bookmark_flag(False)
+
+    def get_data(self):
+        """
+        """
+        return self._manager.get_data()
+
+    def get_state(self):
+        """
+        """
+        return self.state
+
+    def set_data(self, data):
+        """
+        Set the data
+        """
+        self._data = data
+        #edit the panel
+        if self._data is not None:
+            self._delete_bookmark_items()
+            self.get_state_by_num(0)
+            data_name = self._data.name
+            data_qmin = min(self._data.x)
+            data_qmax = max(self._data.x)
+            self.data_name_tcl.SetValue(str(data_name))
+            self.data_min_tcl.SetValue(str(data_qmin))
+            self.data_max_tcl.SetValue(str(data_qmax))
+            self.reset_panel()
+            self.compute_invariant(event=None)
+            self.state.file = self._data.name
+            #Reset the list of states
+            self.state.data = copy.deepcopy(data)
+            self._set_save_flag(True)
+            self._set_preview_flag(False)
+            self._reset_state_list()
+            self._set_bookmark_flag(True)
+        return True
+
+    def _delete_bookmark_items(self):
+        """
+        Delete bookmark menu items
+        """
+        # delete toolbar menu
+        self.parent.reset_bookmark_menu(self)
+        self.parent._update_toolbar_helper()
+        # delete popUpMenu items
+        pos = 0
+        for item in self.popUpMenu.GetMenuItems():
+            pos += 1
+            if pos < 3:
+                continue
+            self.popUpMenu.DestroyItem(item)
+
+    def set_message(self):
+        """
+        Display warning message if available
+        """
+        if self.inv_container is not None:
+            if self.inv_container.existing_warning:
+                msg = "Warning! Computations on invariant require your "
+                msg += "attention.\nPlease click on Details button."
+                self.hint_msg_txt.SetForegroundColour("red")
+
+                wx.PostEvent(self.parent,
+                             StatusEvent(status=msg, info="warning"))
+            else:
+                msg = "For more information, click on Details button."
+                self.hint_msg_txt.SetForegroundColour("black")
+                wx.PostEvent(self.parent,
+                             StatusEvent(status=msg, info="info"))
+            self.hint_msg_txt.SetLabel(msg)
+        self.Layout()
+
+    def set_manager(self, manager):
+        """
+        set value for the manager
+        """
+        self._manager = manager
+
+    def save_project(self, doc=None):
+        """
+        return an xml node containing state of the panel
+         that guiframe can write to file
+        """
+        data = self.get_data()
+        state = self.get_state()
+        if data is not None:
+            new_doc = self._manager.state_reader.write_toXML(data, state)
+            if new_doc is not None:
+                if doc is not None and hasattr(doc, "firstChild"):
+                    child = new_doc.getElementsByTagName("SASentry")
+                    for item in child:
+                        doc.firstChild.appendChild(item)
+                else:
+                    doc = new_doc
+        return doc
+
+    def set_state(self, state=None, data=None):
+        """
+        set state when loading it from a .inv/.svs file
+        """
+
+        if state == None and data == None:
+            self.state = IState()
+        elif state == None or data == None:
+            return
+        else:
+            new_state = copy.deepcopy(state)
+            self.new_state = True
+            if not self.set_data(data):
+                return
+
+            self.state = new_state
+            self.state.file = data.name
+
+            num = self.state.saved_state['state_num']
+            if num > 0:
+                self._set_undo_flag(True)
+            if num < len(state.state_list) - 1:
+                self._set_redo_flag(True)
+
+            # get bookmarks
+            self.bookmark_num = len(self.state.bookmark_list)
+            total_bookmark_num = self.bookmark_num + 1
+
+            for ind in range(1, total_bookmark_num):
+                #bookmark_num = ind
+                value = self.state.bookmark_list[ind]
+                name = "%d] bookmarked at %s on %s" % (ind, value[0], value[1])
+                # append it to menu
+                id = wx.NewId()
+                self.popUpMenu.Append(id, name, str(''))
+                wx.EVT_MENU(self, id, self._back_to_bookmark)
+                wx.PostEvent(self.parent,
+                             AppendBookmarkEvent(title=name,
+                                                 hint='',
+                                                 handler=self._back_to_bookmark))
+
+            self.get_state_by_num(state_num=str(num))
+
+            self._get_input_list()
+            #make sure that the data is reset (especially
+            # when loaded from a inv file)
+            self.state.data = self._data
+            self._set_preview_flag(False)
+            self.new_state = False
+            self.is_state_data = False
+
+    def clear_panel(self):
+        """
+        Clear panel to defaults, used by set_state of manager
+        """
+
+        self._data = None
+        # default data testctrl
+        self.hint_msg_txt.SetLabel('')
+        data_name = ''
+        data_qmin = ''
+        data_qmax = ''
+        self.data_name_tcl.SetValue(str(data_name))
+        self.data_min_tcl.SetValue(str(data_qmin))
+        self.data_max_tcl.SetValue(str(data_qmax))
+        #reset output textctrl
+        self._reset_output()
+        #reset panel
+        self.reset_panel()
+        #reset state w/o data
+        self.set_state()
+        # default flags for state
+        self.new_state = False
+        self.is_state_data = False
+        self.is_power_out = False
+
+    def get_background(self):
+        """
+        return the background textcrtl value as a float
+        """
+        background = self.background_tcl.GetValue().lstrip().rstrip()
+        if background == "":
+            raise ValueError, "Need a background"
+        if check_float(self.background_tcl):
+            return float(background)
+        else:
+            msg = "Receive invalid value for background : %s" % (background)
+            raise ValueError, msg
+
+    def get_scale(self):
+        """
+        return the scale textcrtl value as a float
+        """
+        scale = self.scale_tcl.GetValue().lstrip().rstrip()
+        if scale == "":
+            raise ValueError, "Need a background"
+        if check_float(self.scale_tcl):
+            if float(scale) <= 0.0:
+                self.scale_tcl.SetBackgroundColour("pink")
+                self.scale_tcl.Refresh()
+                msg = "Receive invalid value for scale: %s" % (scale)
+                raise ValueError, msg
+            return float(scale)
+        else:
+            raise ValueError, "Receive invalid value for scale : %s" % (scale)
+
+    def get_contrast(self):
+        """
+        return the contrast textcrtl value as a float
+        """
+        par_str = self.contrast_tcl.GetValue().strip()
+        contrast = None
+        if par_str != " " and check_float(self.contrast_tcl):
+            contrast = float(par_str)
+        return contrast
+
+    def get_extrapolation_type(self, low_q, high_q):
+        """
+        get extrapolation type
+        """
+        extrapolation = None
+        if low_q  and not high_q:
+            extrapolation = "low"
+        elif not low_q  and high_q:
+            extrapolation = "high"
+        elif low_q and high_q:
+            extrapolation = "both"
+        return extrapolation
+
+    def get_porod_const(self):
+        """
+        return the porod constant textcrtl value as a float
+        """
+        par_str = self.porod_constant_tcl.GetValue().strip()
+        porod_const = None
+        if par_str != "" and check_float(self.porod_constant_tcl):
+            porod_const = float(par_str)
+        return porod_const
+
+    def get_volume(self, inv, contrast, extrapolation):
+        """
+        get volume fraction
+        """
+        if contrast is not None:
+            try:
+                v, dv = inv.get_volume_fraction_with_error(contrast=contrast,
+                                                           extrapolation=extrapolation)
+                self.volume_tcl.SetValue(format_number(v))
+                self.volume_err_tcl.SetValue(format_number(dv))
+            except:
+                self.volume_tcl.SetValue(format_number(None))
+                self.volume_err_tcl.SetValue(format_number(None))
+                msg = "Error occurred computing volume "
+                msg += " fraction: %s" % sys.exc_value
+                wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                      info="error",
+                                                      type="stop"))
+
+    def get_surface(self, inv, contrast, porod_const, extrapolation):
+        """
+        get surface value
+        """
+        if contrast is not None and porod_const is not None:
+            try:
+                s, ds = inv.get_surface_with_error(contrast=contrast,
+                                                   porod_const=porod_const,
+                                                   extrapolation=extrapolation)
+                self.surface_tcl.SetValue(format_number(s))
+                self.surface_err_tcl.SetValue(format_number(ds))
+            except:
+                self.surface_tcl.SetValue(format_number(None))
+                self.surface_err_tcl.SetValue(format_number(None))
+                msg = "Error occurred computing "
+                msg += "specific surface: %s" % sys.exc_value
+                wx.PostEvent(self.parent, StatusEvent(status=msg, info="error",
+                                                      type="stop"))
+
+    def get_total_qstar(self, inv, extrapolation):
+        """
+        get total qstar
+        """
+        try:
+            qstar_total, qstar_total_err = \
+                                    inv.get_qstar_with_error(extrapolation)
+            self.invariant_total_tcl.SetValue(format_number(qstar_total))
+            self.invariant_total_err_tcl.SetValue(\
+                                    format_number(qstar_total_err))
+            self.inv_container.qstar_total = qstar_total
+            self.inv_container.qstar_total_err = qstar_total_err
+        except:
+            self.inv_container.qstar_total = "Error"
+            self.inv_container.qstar_total_err = "Error"
+            self.invariant_total_tcl.SetValue(format_number(None))
+            self.invariant_total_err_tcl.SetValue(format_number(None))
+            msg = "Error occurred computing invariant using"
+            msg += " extrapolation: %s" % sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg, type="stop"))
+
+    def get_low_qstar(self, inv, npts_low, low_q=False):
+        """
+        get low qstar
+        """
+        if low_q:
+            try:
+                qstar_low, qstar_low_err = inv.get_qstar_low()
+                self.inv_container.qstar_low = qstar_low
+                self.inv_container.qstar_low_err = qstar_low_err
+                extrapolated_data = inv.get_extra_data_low(npts_in=npts_low)
+                power_low = inv.get_extrapolation_power(range='low')
+                if self.power_law_low.GetValue():
+                    self.power_low_tcl.SetValue(format_number(power_low))
+                self._manager.plot_theory(data=extrapolated_data,
+                                          name="Low-Q extrapolation")
+            except:
+                self.inv_container.qstar_low = "ERROR"
+                self.inv_container.qstar_low_err = "ERROR"
+                self._manager.plot_theory(name="Low-Q extrapolation")
+                msg = "Error occurred computing low-Q "
+                msg += "invariant: %s" % sys.exc_value
+                wx.PostEvent(self.parent,
+                             StatusEvent(status=msg, type="stop"))
+                raise
+        else:
+            try:
+                self._manager.plot_theory(name="Low-Q extrapolation")
+            except:
+                logging.error(sys.exc_value)
+
+    def get_high_qstar(self, inv, high_q=False):
+        """
+        get high qstar
+        """
+        if high_q:
+            try:
+                qmax_plot = Q_MAXIMUM_PLOT * max(self._data.x)
+                if qmax_plot > Q_MAXIMUM:
+                    qmax_plot = Q_MAXIMUM
+                qstar_high, qstar_high_err = inv.get_qstar_high()
+                self.inv_container.qstar_high = qstar_high
+                self.inv_container.qstar_high_err = qstar_high_err
+                power_high = inv.get_extrapolation_power(range='high')
+                self.power_high_tcl.SetValue(format_number(power_high))
+                high_out_data = inv.get_extra_data_high(q_end=qmax_plot,
+                                                        npts=500)
+                self._manager.plot_theory(data=high_out_data,
+                                          name="High-Q extrapolation")
+            except:
+                #raise
+                self.inv_container.qstar_high = "ERROR"
+                self.inv_container.qstar_high_err = "ERROR"
+                self._manager.plot_theory(name="High-Q extrapolation")
+                msg = "Error occurred computing high-Q "
+                msg += "invariant: %s" % sys.exc_value
+                wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                      type="stop"))
+                raise
+        else:
+            try:
+                self._manager.plot_theory(name="High-Q extrapolation")
+            except:
+                logging.error(sys.exc_value)
+
+    def get_qstar(self, inv):
+        """
+        get qstar
+        """
+        qstar, qstar_err = inv.get_qstar_with_error()
+        self.inv_container.qstar = qstar
+        self.inv_container.qstar_err = qstar_err
+
+    def set_extrapolation_low(self, inv, low_q=False):
+        """
+        return float value necessary to compute invariant a low q
+        """
+        #get funtion
+        if self.guinier.GetValue():
+            function_low = "guinier"
+        # get the function
+        power_low = None #2.0/3.0
+        if self.power_law_low.GetValue():
+            function_low = "power_law"
+            if self.fit_enable_low.GetValue():
+                #set value of power_low to none to allow fitting
+                power_low = None
+            else:
+                power_low = self.power_low_tcl.GetValue().lstrip().rstrip()
+                if check_float(self.power_low_tcl):
+                    power_low = float(power_low)
+                else:
+                    if low_q:
+                        #Raise error only when qstar at low q is requested
+                        msg = "Expect float for power at low q, "
+                        msg += " got %s" % (power_low)
+                        wx.PostEvent(self.parent,
+                                     StatusEvent(status=msg,
+                                                 info="error",
+                                                 type="stop"))
+
+        #Get the number of points to extrapolated
+        npts_low = self.npts_low_tcl.GetValue().lstrip().rstrip()
+        if check_float(self.npts_low_tcl):
+            npts_low = float(npts_low)
+        else:
+            if low_q:
+                msg = "Expect float for number of points at low q,"
+                msg += " got %s" % (npts_low)
+                wx.PostEvent(self.parent,
+                             StatusEvent(status=msg,
+                                         info="error",
+                                         type="stop"))
+        #Set the invariant calculator
+        inv.set_extrapolation(range="low", npts=npts_low,
+                              function=function_low, power=power_low)
+        return inv, npts_low
+
+
+    def set_extrapolation_high(self, inv, high_q=False):
+        """
+        return float value necessary to compute invariant a high q
+        """
+        power_high = None
+        #if self.power_law_high.GetValue():
+        function_high = "power_law"
+        if self.fit_enable_high.GetValue():
+            #set value of power_high to none to allow fitting
+            power_high = None
+        else:
+            power_high = self.power_high_tcl.GetValue().lstrip().rstrip()
+            if check_float(self.power_high_tcl):
+                power_high = float(power_high)
+            else:
+                if high_q:
+                    #Raise error only when qstar at high q is requested
+                    msg = "Expect float for power at high q,"
+                    msg += " got %s" % (power_high)
+                    wx.PostEvent(self.parent,
+                                 StatusEvent(status=msg,
+                                             info="error",
+                                             type="stop"))
+
+        npts_high = self.npts_high_tcl.GetValue().lstrip().rstrip()
+        if check_float(self.npts_high_tcl):
+            npts_high = float(npts_high)
+        else:
+            if high_q:
+                msg = "Expect float for number of points at high q,"
+                msg += " got %s" % (npts_high)
+                wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                      info="error",
+                                                      type="stop"))
+        inv.set_extrapolation(range="high", npts=npts_high,
+                              function=function_high, power=power_high)
+        return inv, npts_high
+
+    def display_details(self, event):
+        """
+        open another panel for more details on invariant calculation
+        """
+        panel = InvariantDetailsPanel(parent=self,
+                                      qstar_container=self.inv_container)
+        panel.ShowModal()
+        panel.Destroy()
+        self.button_calculate.SetFocus()
+
+    def compute_invariant(self, event=None):
+        """
+        compute invariant
+        """
+        if self._data == None:
+            msg = "\n\nData must be loaded first in order"
+            msg += " to perform a compution..."
+            wx.PostEvent(self.parent, StatusEvent(status=msg))
+        # set a state for this computation for saving
+        elif event != None:
+            self._set_compute_state(state='compute')
+            self._set_bookmark_flag(True)
+            msg = "\n\nStarting a new invariant computation..."
+            wx.PostEvent(self.parent, StatusEvent(status=msg))
+
+
+        if self._data is None:
+            return
+        self.button_details.Enable()
+        #clear outputs textctrl
+        self._reset_output()
+        try:
+            background = self.get_background()
+            scale = self.get_scale()
+        except:
+            msg = "Invariant Error: %s" % (sys.exc_value)
+            wx.PostEvent(self.parent, StatusEvent(status=msg, type="stop"))
+            return
+
+        low_q = self.enable_low_cbox.GetValue()
+        high_q = self.enable_high_cbox.GetValue()
+        temp_data = copy.deepcopy(self._data)
+
+        #set invariant calculator
+        inv = invariant.InvariantCalculator(data=temp_data,
+                                            background=background,
+                                            scale=scale)
+        try:
+            inv, npts_low = self.set_extrapolation_low(inv=inv, low_q=low_q)
+            inv, npts_high = self.set_extrapolation_high(inv=inv, high_q=high_q)
+        except:
+            msg = "Error occurred computing invariant: %s" % sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                  info="warning", type="stop"))
+            return
+        #check the type of extrapolation
+        extrapolation = self.get_extrapolation_type(low_q=low_q, high_q=high_q)
+
+        #Compute invariant
+        try:
+            self.get_qstar(inv=inv)
+        except:
+            msg = "Error occurred computing invariant: %s" % sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                  info="warning",
+                                                  type="stop"))
+            return
+        #self.Show(False)
+        r_msg = ''
+        try:
+            r_msg = 'Low Q: '
+            #Compute qstar extrapolated to low q range
+            self.get_low_qstar(inv=inv, npts_low=npts_low, low_q=low_q)
+            r_msg = 'High Q: '
+            #Compute qstar extrapolated to high q range
+            self.get_high_qstar(inv=inv, high_q=high_q)
+            r_msg = ''
+            #Compute qstar extrapolated to total q range
+            #and set value to txtcrtl
+            self.get_total_qstar(inv=inv, extrapolation=extrapolation)
+            # Parse additional parameters
+            porod_const = self.get_porod_const()
+            contrast = self.get_contrast()
+        except:
+            msg = r_msg + "Error occurred computing invariant: %s" % \
+                                                            sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                  info="error",
+                                                  type="stop"))
+        try:
+            #Compute volume and set value to txtcrtl
+            self.get_volume(inv=inv, contrast=contrast,
+                            extrapolation=extrapolation)
+            #compute surface and set value to txtcrtl
+        except:
+            msg = "Error occurred computing invariant: %s" % sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                  info="warning",
+                                                  type="stop"))
+        try:
+            self.get_surface(inv=inv, contrast=contrast,
+                             porod_const=porod_const,
+                             extrapolation=extrapolation)
+
+        except:
+            msg = "Error occurred computing invariant: %s" % sys.exc_value
+            wx.PostEvent(self.parent, StatusEvent(status=msg,
+                                                  info="warning",
+                                                  type="stop"))
+
+        #compute percentage of each invariant
+        self.inv_container.compute_percentage()
+
+        #display a message
+        self.set_message()
+
+        # reset power_out to default to get ready for another '_on_text'
+        if self.is_power_out == True:
+            self.state.container = copy.deepcopy(self.inv_container)
+            self.state.timestamp = self._get_time_stamp()
+            msg = self.state.__str__()
+            self.state.set_report_string()
+            self.is_power_out = False
+            wx.PostEvent(self.parent, StatusEvent(status=msg))
+
+        #enable the button_ok for more details
+        self._set_preview_flag(True)
+
+        if event != None:
+            self._set_preview_flag(True)
+            self._set_save_flag(True)
+            wx.PostEvent(self.parent,
+                         StatusEvent(status='\nFinished invariant computation...'))
+        #self.Show(True)
+        self.Refresh()
+
+    def on_undo(self, event=None):
+        """
+        Go back to the previous state
+
+        : param event: undo button event
+        """
+        if self.state.state_num < 0:
+            return
+        self.is_power_out = True
+        # get the previous state_num
+        pre_state_num = int(self.state.saved_state['state_num']) - 1
+
+        self.get_state_by_num(state_num=str(pre_state_num))
+
+        if float(pre_state_num) <= 0:
+            self._set_undo_flag(False)
+        else:
+            self._set_undo_flag(True)
+        self._set_redo_flag(True)
+        self.is_power_out = False
+        self._info_state_num()
+
+
+    def on_redo(self, event=None):
+        """
+        Go forward to the previous state
+
+        : param event: redo button event
+        """
+        self.is_power_out = True
+        # get the next state_num
+        next_state_num = int(self.state.saved_state['state_num']) + 1
+
+        self.get_state_by_num(state_num=str(next_state_num))
+
+        if float(next_state_num) + 2 > len(self.state.state_list):
+            self._set_redo_flag(False)
+        else:
+            self._set_redo_flag(True)
+
+        self._set_undo_flag(True)
+        self.is_power_out = False
+        self._info_state_num()
+
+    def on_preview(self, event=None):
+        """
+        Invoke report dialog panel
+
+        : param event: report button event
+        """
+        from sas.sasgui.perspectives.invariant.report_dialog import ReportDialog
+
+        self.state.set_report_string()
+        report_html_str = self.state.report_str
+        report_text_str = self.state.__str__()
+        report_img = self.state.image
+        report_list = [report_html_str, report_text_str, report_img]
+        dialog = ReportDialog(report_list, None, -1, "")
+        dialog.Show()
+
+    def get_state_by_num(self, state_num=None):
+        """
+        Get the state given by number
+
+        : param state_num: the given state number
+        """
+        if state_num == None:
+            return
+
+        backup_state_list = copy.deepcopy(self.state.state_list)
+
+        # get the previous state
+        try:
+            current_state = copy.deepcopy(self.state.state_list[str(state_num)])
+            # get the previously computed state number
+            #(computation before the state changes happened)
+            current_compute_num = str(current_state['compute_num'])
+        except:
+            raise
+
+        # get the state at pre_compute_num
+        comp_state = copy.deepcopy(self.state.state_list[current_compute_num])
+
+        # set the parameters
+        for key in comp_state:
+            value = comp_state[key]
+            self._set_property_value(key, value)
+
+        self.compute_invariant(event=None)
+
+        # set the input params at the state at pre_state_num
+        for key in current_state:
+            # set the inputs and boxes
+            value = current_state[key]
+            self._set_property_value(key, value)
+
+        self._enable_high_q_section(event=None)
+        self._enable_low_q_section(event=None)
+        self.state.state_list = backup_state_list
+        self.state.saved_state = current_state
+        self.state.state_num = state_num
+
+    def _set_property_value(self, key, value):
+        """
+            Set a property value
+            :param key: property name
+            :param value: value of the property
+        """
+        try:
+            attr = getattr(self, key)
+            if attr.__class__.__name__ == "StaticText":
+                return
+            if type(value) is not bool:
+                value = str(value)
+            attr.SetValue(value)
+        except:
+            logging.error("Invariant state: %s", sys.exc_value)
+
+    def get_bookmark_by_num(self, num=None):
+        """
+        Get the bookmark state given by number
+
+        : param num: the given bookmark number
+
+        """
+        current_state = {}
+        comp_state = {}
+        backup_state_list = copy.deepcopy(self.state.state_list)
+
+        # get the previous state
+        try:
+            _, _, current_state, comp_state = self.state.bookmark_list[int(num)]
+        except:
+            logging.error(sys.exc_value)
+            raise ValueError, "No such bookmark exists"
+
+        # set the parameters
+        for key in comp_state:
+            value = comp_state[key]
+            self._set_property_value(key, value)
+
+        self.compute_invariant(event=None)
+        # set the input params at the state of pre_state_num
+        for key in current_state:
+            value = current_state[key]
+            self._set_property_value(key, value)
+        self.state.saved_state = copy.deepcopy(current_state)
+
+        self._enable_high_q_section(event=None)
+        self._enable_low_q_section(event=None)
+        self.state.state_list = backup_state_list
+        #self.state.saved_state = current_state
+        #self.state.state_num = state_num
+
+    def reset_panel(self):
+        """
+        set the panel at its initial state.
+        """
+        self.background_tcl.SetValue(str(BACKGROUND))
+        self.scale_tcl.SetValue(str(SCALE))
+        self.contrast_tcl.SetValue(str(CONTRAST))
+        self.porod_constant_tcl.SetValue('')
+        self.npts_low_tcl.SetValue(str(NPTS))
+        self.enable_low_cbox.SetValue(False)
+        self.fix_enable_low.SetValue(True)
+        self.power_low_tcl.SetValue(str(POWER))
+        self.guinier.SetValue(True)
+        self.power_low_tcl.Disable()
+        self.enable_high_cbox.SetValue(False)
+        self.fix_enable_high.SetValue(True)
+        self.power_high_tcl.SetValue(str(POWER))
+        self.npts_high_tcl.SetValue(str(NPTS))
+        self.button_details.Disable()
+        #Change the state of txtcrtl to enable/disable
+        self._enable_low_q_section()
+        #Change the state of txtcrtl to enable/disable
+        self._enable_high_q_section()
+        self._reset_output()
+        self._set_undo_flag(False)
+        self._set_redo_flag(False)
+        self._set_bookmark_flag(False)
+        self._set_preview_flag(False)
+        self._set_save_flag(False)
+        self.button_calculate.SetFocus()
+        #self.SetupScrolling()
+
+    def _set_state(self, event):
+        """
+        Set the state list
+
+        :param event: rb/cb event
+        """
+        if event == None:
+            return
+        obj = event.GetEventObject()
+        name = str(obj.GetName())
+        value = str(obj.GetValue())
+        rb_list = [['power_law_low', 'guinier'],
+                   ['fit_enable_low', 'fix_enable_low'],
+                   ['fit_enable_high', 'fix_enable_high']]
+
+        try:
+            if value == None or value.lstrip().rstrip() == '':
+                value = 'None'
+            setattr(self.state, name, str(value))
+            self.state.saved_state[name] = str(value)
+
+            # set the count part of radio button clicked
+            #False for the saved_state
+            for title, content in rb_list:
+                if name == title:
+                    name = content
+                    value = False
+                elif name == content:
+                    name = title
+                    value = False
+            self.state.saved_state[name] = str(value)
+
+            # Instead of changing the future, create a new future.
+            max_state_num = len(self.state.state_list) - 1
+            self.state.saved_state['state_num'] = max_state_num
+
+            self.state.saved_state['state_num'] += 1
+            self.state.state_num = self.state.saved_state['state_num']
+            self.state.state_list[str(self.state.state_num)] = \
+                    self.state.clone_state()
+        except:
+            logging.error(sys.exc_value)
+
+        self._set_undo_flag(True)
+        self._set_redo_flag(False)
+        #event.Skip()
+
+    def _set_compute_state(self, state=None):
+        """
+        Notify the compute_invariant state to self.state
+
+        : param state: set 'compute' when the computation is
+        activated by the 'compute' button, else None
+
+        """
+        # reset the default
+        if state != 'compute':
+            self.new_state = False
+            self.is_power_out = False
+        else:
+            self.is_power_out = True
+        # Instead of changing the future, create a new future.
+        max_state_num = len(self.state.state_list) - 1
+        self.state.saved_state['state_num'] = max_state_num
+        # A new computation is also A state
+        #copy.deepcopy(self.state.saved_state)
+        temp_saved_states = self.state.clone_state()
+        temp_saved_states['state_num'] += 1
+        self.state.state_num = temp_saved_states['state_num']
+
+
+        # set the state number of the computation
+        if state == 'compute':
+            temp_saved_states['compute_num'] = self.state.state_num
+        self.state.saved_state = copy.deepcopy(temp_saved_states)
+        #copy.deepcopy(self.state.saved_state)
+        self.state.state_list[str(self.state.state_num)] = \
+                                        self.state.clone_state()
+
+        # A computation is a new state, so delete the states with any higher
+        # state numbers
+        for i in range(self.state.state_num + 1, len(self.state.state_list)):
+            try:
+                del self.state.state_list[str(i)]
+            except:
+                logging.error(sys.exc_value)
+        # Enable the undo button if it was not
+        self._set_undo_flag(True)
+        self._set_redo_flag(False)
+
+    def _reset_state_list(self, data=None):
+        """
+        Reset the state_list just before data was loading:
+        Used in 'set_current_data()'
+        """
+        #if data == None: return
+        #temp_state = self.state.clone_state()
+        #copy.deepcopy(self.state.saved_state)
+        # Clear the list
+        self.state.state_list.clear()
+        self.state.bookmark_list.clear()
+        # Set defaults
+        self.state.saved_state['state_num'] = 0
+        self.state.saved_state['compute_num'] = 0
+        if self._data != None:
+            self.state.saved_state['file'] = str(self._data.name)
+        else:
+            self.state.saved_state['file'] = 'None'
+        self.state.file = self.state.saved_state['file']
+
+        self.state.state_num = self.state.saved_state['state_num']
+        self.state.timestamp = "('00:00:00', '00/00/0000')"
+
+        # Put only the current state in the list
+        #copy.deepcopy(self.state.saved_state)
+        self.state.state_list[str(self.state.state_num)] = \
+                                                self.state.clone_state()
+        self._set_undo_flag(False)
+        self._set_redo_flag(False)
+        self._set_bookmark_flag(False)
+        self._set_preview_flag(False)
+        self._set_save_flag(False)
+
+
+    def _on_text(self, event):
+        """
+        Catch text change event to add the state to the state_list
+
+        :param event: txtctr event ; assumes not None
+
+        """
+        if self._data == None:
+            return
+        # check if this event is from do/undo button
+        if self.state.saved_state['is_time_machine'] or self.new_state:
+            #event.Skip()
+            return
+
+        # get the object
+        obj = event.GetEventObject()
+        name = str(obj.GetName())
+        value = str(obj.GetValue())
+        state_num = self.state.saved_state['state_num']
+
+        # text event is a new state, so delete the states with higher state_num
+        # i.e., change the future
+        for i in range(int(state_num) + 1, len(self.state.state_list)):
+            try:
+                del self.state.state_list[str(i)]
+            except:
+                logging.error(sys.exc_value)
+
+        # try to add new state of the text changes in the state_list
+        try:
+            if value.strip() == None:
+                value = ''
+            setattr(self.state, name, str(value))
+            self.state.saved_state[name] = str(value)
+            self.state.input_list[name] = str(value)
+            if not self.is_power_out:
+                if name != 'power_low_tcl' and name != 'power_high_tcl':
+                    self.state.saved_state['state_num'] += 1
+            self.state.state_num = self.state.saved_state['state_num']
+            self.state.state_list[str(self.state.state_num)] = self.state.clone_state()
+        except:
+            logging.error(sys.exc_value)
+
+        self._set_undo_flag(True)
+        self._set_redo_flag(False)
+        self._set_bookmark_flag(True)
+        self._set_preview_flag(False)
+
+    def _on_out_text(self, event):
+        """
+        Catch ouput text change to add the state
+
+        :param event: txtctr event ; assumes not None
+
+        """
+        # get the object
+        obj = event.GetEventObject()
+        name = str(obj.GetName())
+        value = str(obj.GetValue())
+        try:
+            self.state.saved_state[name] = str(value)
+            self.state.state_list[str(self.state.state_num)] = self.state.clone_state()
+        except:
+            logging.error(sys.exc_value)
+
+    def _get_input_list(self):
+        """
+        get input_list; called by set_state
+        """
+        # get state num of the last compute state
+        compute_num = self.state.saved_state['compute_num']
+        # find values and put into the input list
+        for key1, value1 in self.state.state_list[str(compute_num)].iteritems():
+            for key, _ in self.state.input_list.iteritems():
+                if key == key1:
+                    self.state.input_list[key] = value1
+                    break
+
+    def _set_bookmark_menu(self):
+        """
+        Setup 'bookmark' context menu
+        """
+        ## Create context menu for page
+        self.popUpMenu = wx.Menu()
+        id = wx.NewId()
+        self._bmark = wx.MenuItem(self.popUpMenu, id, "BookMark",
+                                  " Bookmark the panel to recall it later")
+        self.popUpMenu.AppendItem(self._bmark)
+        self._bmark.Enable(True)
+        wx.EVT_MENU(self, id, self.on_bookmark)
+        self.popUpMenu.AppendSeparator()
+        self.Bind(wx.EVT_CONTEXT_MENU, self._on_context_menu)
+
+    def on_bookmark(self, event):
+        """
+        Save the panel state in memory and add the list on
+        the popup menu on bookmark context menu event
+        """
+        if self._data == None:
+            return
+        if event == None:
+            return
+        self.bookmark_num += 1
+        # date and time of the event
+        my_time, date = self._get_time_stamp()
+        _ = self.state.state_num
+        compute_num = self.state.saved_state['compute_num']
+        # name and message of the bookmark list
+        msg = "State saved at %s on %s" % (my_time, date)
+        ## post help message for the selected model
+        msg += " Right click on the panel to retrieve this state"
+        #wx.PostEvent(self.parent.parent, StatusEvent(status = msg ))
+        name = "%d] bookmarked at %s on %s" % (self.bookmark_num, my_time, date)
+
+        # append it to menu
+        id = wx.NewId()
+        self.popUpMenu.Append(id, name, str(msg))
+        wx.EVT_MENU(self, id, self._back_to_bookmark)
+        state = self.state.clone_state()
+        comp_state = copy.deepcopy(self.state.state_list[str(compute_num)])
+        self.state.bookmark_list[self.bookmark_num] = [my_time, date,
+                                                       state, comp_state]
+        self.state.toXML(self, doc=None, entry_node=None)
+
+        wx.PostEvent(self.parent, StatusEvent(status=msg, info="info"))
+        wx.PostEvent(self.parent,
+                     AppendBookmarkEvent(title=name,
+                                         hint=str(msg),
+                                         handler=self._back_to_bookmark))
+
+    def _back_to_bookmark(self, event):
+        """
+        Bring the panel back to the state of bookmarked requested by
+        context menu event
+        and set it as a new state
+        """
+        self._manager.on_perspective(event)
+        menu = event.GetEventObject()
+        ## post help message for the selected model
+        msg = menu.GetHelpString(event.GetId())
+        msg += " reloaded"
+        wx.PostEvent(self.parent, StatusEvent(status=msg))
+
+        name = menu.GetLabel(event.GetId())
+
+        num, _ = name.split(']')
+        current_state_num = self.state.state_num
+        self.get_bookmark_by_num(num)
+        state_num = int(current_state_num) + 1
+
+        self.state.saved_state['state_num'] = state_num
+        #copy.deepcopy(self.state.saved_state)
+        self.state.state_list[str(state_num)] = self.state.clone_state()
+        self.state.state_num = state_num
+
+        self._set_undo_flag(True)
+        self._info_bookmark_num(event)
+
+    def _info_bookmark_num(self, event=None):
+        """
+        print the bookmark number in info
+
+        : event: popUpMenu event
+        """
+        if event == None:
+            return
+        # get the object
+        menu = event.GetEventObject()
+        item = menu.FindItemById(event.GetId())
+        text = item.GetText()
+        num = text.split(']')[0]
+        msg = "bookmark num = %s " % num
+
+        wx.PostEvent(self.parent, StatusEvent(status=msg))
+
+    def _info_state_num(self):
+        """
+        print the current state number in info
+        """
+        msg = "state num = "
+        msg += self.state.state_num
+
+        wx.PostEvent(self.parent, StatusEvent(status=msg))
+
+    def _get_time_stamp(self):
+        """
+        return time and date stings
+        """
+        # date and time
+        year, month, day, hour, minute, second, _, _, _ = \
+                                    time.localtime()
+        my_time = str(hour) + ":" + str(minute) + ":" + str(second)
+        date = str(month) + "/" + str(day) + "/" + str(year)
+        return my_time, date
+
+
+    def on_save(self, evt=None):
+        """
+        Save invariant state into a file
+        """
+        # Ask the user the location of the file to write to.
+        path = None
+        if self.parent != None:
+            self._default_save_location = self.parent.get_save_location()
+        if self._default_save_location == None:
+            self._default_save_location = os.getcwd()
+        dlg = wx.FileDialog(self, "Choose a file",
+                            self._default_save_location, \
+                            self.window_caption, "*.inv", wx.SAVE)
+        if dlg.ShowModal() == wx.ID_OK:
+            path = dlg.GetPath()
+            self._default_save_location = os.path.dirname(path)
+            if self.parent != None:
+                self.parent._default_save_location = \
+                    self._default_save_location
+        else:
+            return None
+
+        dlg.Destroy()
+        # MAC always needs the extension for saving
+        extens = ".inv"
+        # Make sure the ext included in the file name
+        fName = os.path.splitext(path)[0] + extens
+        self._manager.save_file(filepath=fName, state=self.state)
+
+    def _show_message(self, mssg='', msg='Warning'):
+        """
+        Show warning message when resetting data
+        """
+        # no message for now
+        return True
+
+    def _reset_output(self):
+        """
+        clear outputs textcrtl
+        """
+        self.invariant_total_tcl.Clear()
+        self.invariant_total_err_tcl.Clear()
+        self.volume_tcl.Clear()
+        self.volume_err_tcl.Clear()
+        self.surface_tcl.Clear()
+        self.surface_err_tcl.Clear()
+        #prepare a new container to put result of invariant
+        self.inv_container = InvariantContainer()
+
+
+    def _on_context_menu(self, event):
+        """
+        On context menu
+        """
+        pos = event.GetPosition()
+        pos = self.ScreenToClient(pos)
+
+        self.PopupMenu(self.popUpMenu, pos)
+
+    def _define_structure(self):
+        """
+        Define main sizers needed for this panel
+        """
+        ## Box sizers must be defined first before
+        #defining buttons/textctrls (MAC).
+        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+        #Sizer related to outputs
+        outputs_box = wx.StaticBox(self, -1, "Outputs")
+        self.outputs_sizer = wx.StaticBoxSizer(outputs_box, wx.VERTICAL)
+        self.outputs_sizer.SetMinSize((_STATICBOX_WIDTH, -1))
+        #Sizer related to data
+        data_name_box = wx.StaticBox(self, -1, "I(q) Data Source")
+        self.data_name_boxsizer = wx.StaticBoxSizer(data_name_box, wx.VERTICAL)
+        self.data_name_boxsizer.SetMinSize((_STATICBOX_WIDTH, -1))
+        self.hint_msg_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.data_name_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+        self.data_range_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        #Sizer related to inputs
+        self.sizer_input = wx.FlexGridSizer(2, 6, 0, 0)
+        #Sizer related to inputs
+        inputs_box = wx.StaticBox(self, -1, "Customized Inputs")
+        self.inputs_sizer = wx.StaticBoxSizer(inputs_box, wx.VERTICAL)
+        self.inputs_sizer.SetMinSize((_STATICBOX_WIDTH, -1))
+        #Sizer related to extrapolation
+        extrapolation_box = wx.StaticBox(self, -1, "Extrapolation")
+        self.extrapolation_sizer = wx.StaticBoxSizer(extrapolation_box,
+                                                     wx.VERTICAL)
+        self.extrapolation_sizer.SetMinSize((_STATICBOX_WIDTH, -1))
+        self.extrapolation_range_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.extrapolation_low_high_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        #Sizer related to extrapolation at low q range
+        low_q_box = wx.StaticBox(self, -1, "Low Q")
+        self.low_extrapolation_sizer = wx.StaticBoxSizer(low_q_box, wx.VERTICAL)
+
+        self.low_q_sizer = wx.GridBagSizer(5, 5)
+        #Sizer related to extrapolation at low q range
+        high_q_box = wx.StaticBox(self, -1, "High Q")
+        self.high_extrapolation_sizer = wx.StaticBoxSizer(high_q_box,
+                                                          wx.VERTICAL)
+        self.high_q_sizer = wx.GridBagSizer(5, 5)
+        #sizer to define outputs
+        self.volume_surface_sizer = wx.GridBagSizer(5, 5)
+        #Sizer related to invariant output
+        self.invariant_sizer = wx.GridBagSizer(5, 5)
+        #Sizer related to button
+        self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.button_sizer.SetMinSize((_STATICBOX_WIDTH, -1))
+        #Sizer related to save button
+        self.save_button_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+    def _layout_data_name(self):
+        """
+        Draw widgets related to data's name
+        """
+        #Sizer hint
+        hint_msg = ""
+
+        self.hint_msg_txt = wx.StaticText(self, -1, hint_msg)
+        self.hint_msg_txt.SetForegroundColour("red")
+        msg = "Highlight = mouse the mouse's cursor on the data until"
+        msg += " the plot's color changes to yellow"
+        self.hint_msg_txt.SetToolTipString(msg)
+        self.hint_msg_sizer.Add(self.hint_msg_txt)
+        #Data name [string]
+        data_name_txt = wx.StaticText(self, -1, 'Name:')
+
+        self.data_name_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH * 4, 20),
+                                            style=0)
+        self.data_name_tcl.SetToolTipString("Data's name.")
+        self.data_name_sizer.AddMany([(data_name_txt, 0, wx.LEFT | wx.RIGHT, 10),
+                                      (self.data_name_tcl, 0, wx.EXPAND)])
+        #Data range [string]
+        data_range_txt = wx.StaticText(self, -1, 'Total Q Range (1/A): ')
+        data_min_txt = wx.StaticText(self, -1, 'Min : ')
+        self.data_min_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, 20),
+                                           style=0, name='data_min_tcl')
+        self.data_min_tcl.SetToolTipString("The minimum value of q range.")
+        data_max_txt = wx.StaticText(self, -1, 'Max : ')
+        self.data_max_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, 20),
+                                           style=0, name='data_max_tcl')
+        self.data_max_tcl.SetToolTipString("The maximum value of q range.")
+        self.data_range_sizer.AddMany([(data_range_txt, 0, wx.RIGHT, 5),
+                                       (data_min_txt, 0, wx.RIGHT, 5),
+                                       (self.data_min_tcl, 0, wx.RIGHT, 20),
+                                       (data_max_txt, 0, wx.RIGHT, 5),
+                                       (self.data_max_tcl, 0, wx.RIGHT, 10)])
+        self.data_name_boxsizer.AddMany([(self.hint_msg_sizer, 0, wx.ALL, 5),
+                                         (self.data_name_sizer, 0, wx.ALL, 10),
+                                         (self.data_range_sizer, 0, wx.ALL, 10)])
+
+    def _enable_fit_power_law_low(self, event=None):
+        """
+        Enable and disable the power value editing
+        """
+        if event != None:
+            self._set_bookmark_flag(True)
+            self._set_preview_flag(False)
+
+        if self.fix_enable_low.IsEnabled():
+
+            if self.fix_enable_low.GetValue():
+                self.fit_enable_low.SetValue(False)
+                self.power_low_tcl.Enable()
+            else:
+                self.fit_enable_low.SetValue(True)
+                self.power_low_tcl.Disable()
+        self._set_state(event=event)
+
+    def _enable_low_q_section(self, event=None):
+        """
+        Disable or enable some button if the user enable low q extrapolation
+        """
+        if event != None:
+            self._set_bookmark_flag(True)
+            self._set_preview_flag(False)
+
+        if self.enable_low_cbox.GetValue():
+            self.npts_low_tcl.Enable()
+            self.fix_enable_low.Enable()
+            self.fit_enable_low.Enable()
+            self.guinier.Enable()
+            self.power_law_low.Enable()
+
+        else:
+            self.npts_low_tcl.Disable()
+            self.fix_enable_low.Disable()
+            self.fit_enable_low.Disable()
+            self.guinier.Disable()
+            self.power_law_low.Disable()
+
+        self._enable_power_law_low()
+        self._enable_fit_power_law_low()
+        self._set_state(event=event)
+        self.button_calculate.SetFocus()
+
+    def _enable_power_law_low(self, event=None):
+        """
+        Enable editing power law section at low q range
+        """
+        if event != None:
+            self._set_bookmark_flag(True)
+            self._set_preview_flag(False)
+        if self.guinier.GetValue():
+            self.power_law_low.SetValue(False)
+            self.fix_enable_low.Disable()
+            self.fit_enable_low.Disable()
+            self.power_low_tcl.Disable()
+        else:
+            self.power_law_low.SetValue(True)
+            self.fix_enable_low.Enable()
+            self.fit_enable_low.Enable()
+            self.power_low_tcl.Enable()
+        self._enable_fit_power_law_low()
+        self._set_state(event=event)
+
+    def _layout_extrapolation_low(self):
+        """
+        Draw widgets related to extrapolation at low q range
+        """
+        self.enable_low_cbox = wx.CheckBox(self, -1,
+                                           "Enable Extrapolate Low Q",
+                                           name='enable_low_cbox')
+        wx.EVT_CHECKBOX(self, self.enable_low_cbox.GetId(),
+                        self._enable_low_q_section)
+        self.fix_enable_low = wx.RadioButton(self, -1, 'Fix',
+                                             (10, 10), style=wx.RB_GROUP,
+                                             name='fix_enable_low')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_fit_power_law_low,
+                  id=self.fix_enable_low.GetId())
+        self.fit_enable_low = wx.RadioButton(self, -1, 'Fit', (10, 10),
+                                             name='fit_enable_low')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_fit_power_law_low,
+                  id=self.fit_enable_low.GetId())
+        self.guinier = wx.RadioButton(self, -1, 'Guinier',
+                                      (10, 10), style=wx.RB_GROUP,
+                                      name='guinier')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_power_law_low,
+                  id=self.guinier.GetId())
+        self.power_law_low = wx.RadioButton(self, -1, 'Power Law',
+                                            (10, 10), name='power_law_low')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_power_law_low,
+                  id=self.power_law_low.GetId())
+
+        npts_low_txt = wx.StaticText(self, -1, 'Npts')
+        self.npts_low_tcl = InvTextCtrl(self, -1,
+                                        size=(_BOX_WIDTH * 2 / 3, -1),
+                                        name='npts_low_tcl')
+        wx.EVT_TEXT(self, self.npts_low_tcl.GetId(), self._on_text)
+        msg_hint = "Number of Q points to consider"
+        msg_hint += "while extrapolating the low-Q region"
+        self.npts_low_tcl.SetToolTipString(msg_hint)
+        power_txt = wx.StaticText(self, -1, 'Power')
+        self.power_low_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH * 2 / 3, -1),
+                                         name='power_low_tcl')
+        wx.EVT_TEXT(self, self.power_low_tcl.GetId(), self._on_text)
+
+        power_hint_txt = "Exponent to apply to the Power_law function."
+        self.power_low_tcl.SetToolTipString(power_hint_txt)
+        iy = 0
+        ix = 0
+        self.low_q_sizer.Add(self.enable_low_cbox, (iy, ix), (1, 5),
+                             wx.TOP | wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        iy += 1
+        ix = 0
+        self.low_q_sizer.Add(npts_low_txt, (iy, ix), (1, 1),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.low_q_sizer.Add(self.npts_low_tcl, (iy, ix), (1, 1),
+                             wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        iy += 1
+        ix = 0
+        self.low_q_sizer.Add(self.guinier, (iy, ix), (1, 2),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        iy += 1
+        ix = 0
+        self.low_q_sizer.Add(self.power_law_low, (iy, ix), (1, 2),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        # Parameter controls for power law
+        ix = 1
+        iy += 1
+        self.low_q_sizer.Add(self.fix_enable_low, (iy, ix), (1, 1),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.low_q_sizer.Add(self.fit_enable_low, (iy, ix), (1, 1),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix = 1
+        iy += 1
+        self.low_q_sizer.Add(power_txt, (iy, ix), (1, 1),
+                             wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.low_q_sizer.Add(self.power_low_tcl, (iy, ix), (1, 1),
+                             wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        self.low_extrapolation_sizer.Add(self.low_q_sizer)
+
+    def _enable_fit_power_law_high(self, event=None):
+        """
+        Enable and disable the power value editing
+        """
+        if event != None:
+            self._set_bookmark_flag(True)
+
+            self._set_preview_flag(False)
+        if self.fix_enable_high.IsEnabled():
+            if self.fix_enable_high.GetValue():
+                self.fit_enable_high.SetValue(False)
+                self.power_high_tcl.Enable()
+            else:
+                self.fit_enable_high.SetValue(True)
+                self.power_high_tcl.Disable()
+        self._set_state(event=event)
+
+    def _enable_high_q_section(self, event=None):
+        """
+        Disable or enable some button if the user enable high q extrapolation
+        """
+        if event != None:
+            self._set_bookmark_flag(True)
+            self._set_preview_flag(False)
+        if self.enable_high_cbox.GetValue():
+            self.npts_high_tcl.Enable()
+            self.power_law_high.Enable()
+            self.power_high_tcl.Enable()
+            self.fix_enable_high.Enable()
+            self.fit_enable_high.Enable()
+        else:
+            self.npts_high_tcl.Disable()
+            self.power_law_high.Disable()
+            self.power_high_tcl.Disable()
+            self.fix_enable_high.Disable()
+            self.fit_enable_high.Disable()
+        self._enable_fit_power_law_high()
+        self._set_state(event=event)
+        self.button_calculate.SetFocus()
+
+    def _layout_extrapolation_high(self):
+        """
+        Draw widgets related to extrapolation at high q range
+        """
+        self.enable_high_cbox = wx.CheckBox(self, -1,
+                                            "Enable Extrapolate high-Q",
+                                            name='enable_high_cbox')
+        wx.EVT_CHECKBOX(self, self.enable_high_cbox.GetId(),
+                        self._enable_high_q_section)
+        self.fix_enable_high = wx.RadioButton(self, -1, 'Fix',
+                                              (10, 10), style=wx.RB_GROUP,
+                                              name='fix_enable_high')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_fit_power_law_high,
+                  id=self.fix_enable_high.GetId())
+        self.fit_enable_high = wx.RadioButton(self, -1, 'Fit', (10, 10),
+                                              name='fit_enable_high')
+        self.Bind(wx.EVT_RADIOBUTTON, self._enable_fit_power_law_high,
+                  id=self.fit_enable_high.GetId())
+
+        self.power_law_high = wx.StaticText(self, -1, 'Power Law')
+        msg_hint = "Check to extrapolate data at high-Q"
+        self.power_law_high.SetToolTipString(msg_hint)
+        npts_high_txt = wx.StaticText(self, -1, 'Npts')
+        self.npts_high_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH * 2 / 3, -1),
+                                         name='npts_high_tcl')
+        wx.EVT_TEXT(self, self.npts_high_tcl.GetId(), self._on_text)
+        msg_hint = "Number of Q points to consider"
+        msg_hint += "while extrapolating the high-Q region"
+        self.npts_high_tcl.SetToolTipString(msg_hint)
+        power_txt = wx.StaticText(self, -1, 'Power')
+        self.power_high_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH * 2 / 3, -1),
+                                          name='power_high_tcl')
+        wx.EVT_TEXT(self, self.power_high_tcl.GetId(), self._on_text)
+        power_hint_txt = "Exponent to apply to the Power_law function."
+        self.power_high_tcl.SetToolTipString(power_hint_txt)
+        iy = 0
+        ix = 0
+        self.high_q_sizer.Add(self.enable_high_cbox, (iy, ix), (1, 5),
+                              wx.TOP | wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        iy += 1
+        ix = 0
+        self.high_q_sizer.Add(npts_high_txt, (iy, ix), (1, 1),
+                              wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.high_q_sizer.Add(self.npts_high_tcl, (iy, ix), (1, 1),
+                              wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        iy += 1
+        ix = 0
+        self.high_q_sizer.Add(self.power_law_high, (iy, ix), (1, 2),
+                              wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+
+        # Parameter controls for power law
+        ix = 1
+        iy += 1
+        self.high_q_sizer.Add(self.fix_enable_high, (iy, ix), (1, 1),
+                              wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix += 1
+        self.high_q_sizer.Add(self.fit_enable_high, (iy, ix), (1, 1),
+                              wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        ix = 1
+        iy += 1
+        self.high_q_sizer.Add(power_txt, (iy, ix), (1, 1),
+                              wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.high_q_sizer.Add(self.power_high_tcl, (iy, ix), (1, 1),
+                              wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        self.high_extrapolation_sizer.Add(self.high_q_sizer, 0,
+                                          wx.BOTTOM, 20)
+
+    def _layout_extrapolation(self):
+        """
+        Draw widgets related to extrapolation
+        """
+        extra_hint = "Extrapolation \nMaximum Q Range [1/A]:"
+        extra_hint_txt = wx.StaticText(self, -1, extra_hint)
+        #Extrapolation range [string]
+        extrapolation_min_txt = wx.StaticText(self, -1, 'Min:')
+        self.extrapolation_min_tcl = OutputTextCtrl(self, -1,
+                                                    size=(_BOX_WIDTH, 20), style=0,
+                                                    name='extrapolation_min_tcl')
+        self.extrapolation_min_tcl.SetValue(str(Q_MINIMUM))
+        hint_msg = "The minimum extrapolated q value."
+        self.extrapolation_min_tcl.SetToolTipString(hint_msg)
+        extrapolation_max_txt = wx.StaticText(self, -1, 'Max:')
+        self.extrapolation_max_tcl = OutputTextCtrl(self, -1,
+                                                    size=(_BOX_WIDTH, 20),
+                                                    style=0,
+                                                    name='extrapolation_max_tcl')
+        self.extrapolation_max_tcl.SetValue(str(Q_MAXIMUM))
+        hint_msg = "The maximum extrapolated q value."
+        self.extrapolation_max_tcl.SetToolTipString(hint_msg)
+        self.extrapolation_range_sizer.AddMany([(extra_hint_txt, 0,
+                                                 wx.LEFT, 5),
+                                                (extrapolation_min_txt, 0,
+                                                 wx.LEFT, 10),
+                                                (self.extrapolation_min_tcl,
+                                                 0, wx.LEFT, 5),
+                                                (extrapolation_max_txt, 0,
+                                                 wx.LEFT, 20),
+                                                (self.extrapolation_max_tcl,
+                                                 0, wx.LEFT, 5)])
+        self._layout_extrapolation_low()
+        self._layout_extrapolation_high()
+        self.extrapolation_low_high_sizer.AddMany([(self.low_extrapolation_sizer,
+                                                    0, wx.LEFT | wx.BOTTOM | wx.TOP, 5),
+                                                   (self.high_extrapolation_sizer,
+                                                    0, wx.LEFT | wx.BOTTOM | wx.TOP, 5)])
+        self.extrapolation_sizer.AddMany([(self.extrapolation_range_sizer),
+                                          (self.extrapolation_low_high_sizer)])
+
+    def _layout_volume_surface_sizer(self):
+        """
+        Draw widgets related to volume and surface
+        """
+        unit_volume = ''
+        unit_surface = '[1/A]'
+        uncertainty = "+/-"
+        volume_txt = wx.StaticText(self, -1, 'Volume Fraction')
+        self.volume_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1),
+                                         name='volume_tcl')
+        wx.EVT_TEXT(self, self.volume_tcl.GetId(), self._on_out_text)
+        self.volume_tcl.SetToolTipString("Volume fraction.")
+        self.volume_err_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1),
+                                             name='volume_err_tcl')
+        wx.EVT_TEXT(self, self.volume_err_tcl.GetId(), self._on_out_text)
+        hint_msg = "Uncertainty on the volume fraction."
+        self.volume_err_tcl.SetToolTipString(hint_msg)
+        volume_units_txt = wx.StaticText(self, -1, unit_volume)
+
+        surface_txt = wx.StaticText(self, -1, 'Specific Surface')
+        self.surface_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1),
+                                          name='surface_tcl')
+        wx.EVT_TEXT(self, self.surface_tcl.GetId(), self._on_out_text)
+        self.surface_tcl.SetToolTipString("Specific surface value.")
+        self.surface_err_tcl = OutputTextCtrl(self, -1, size=(_BOX_WIDTH, -1),
+                                              name='surface_err_tcl')
+        wx.EVT_TEXT(self, self.surface_err_tcl.GetId(), self._on_out_text)
+        hint_msg = "Uncertainty on the specific surface."
+        self.surface_err_tcl.SetToolTipString(hint_msg)
+        surface_units_txt = wx.StaticText(self, -1, unit_surface)
+        iy = 0
+        ix = 0
+        self.volume_surface_sizer.Add(volume_txt, (iy, ix), (1, 1),
+                                      wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.volume_surface_sizer.Add(self.volume_tcl, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 20)
+        ix += 1
+        self.volume_surface_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                      (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.volume_surface_sizer.Add(self.volume_err_tcl, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.volume_surface_sizer.Add(volume_units_txt, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        iy += 1
+        ix = 0
+        self.volume_surface_sizer.Add(surface_txt, (iy, ix), (1, 1),
+                                      wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.volume_surface_sizer.Add(self.surface_tcl, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 20)
+        ix += 1
+        self.volume_surface_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                      (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.volume_surface_sizer.Add(self.surface_err_tcl, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.volume_surface_sizer.Add(surface_units_txt, (iy, ix), (1, 1),
+                                      wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        static_line = wx.StaticLine(self, -1)
+        iy += 1
+        ix = 0
+
+    def _layout_invariant_sizer(self):
+        """
+        Draw widgets related to invariant
+        """
+        uncertainty = "+/-"
+        unit_invariant = '[1/(cm*A^3)]'
+        invariant_total_txt = wx.StaticText(self, -1, 'Invariant Total [Q*]')
+        self.invariant_total_tcl = OutputTextCtrl(self, -1,
+                                                  size=(_BOX_WIDTH, -1),
+                                                  name='invariant_total_tcl')
+        msg_hint = "Total invariant [Q*], including extrapolated regions."
+        self.invariant_total_tcl.SetToolTipString(msg_hint)
+        self.invariant_total_err_tcl = OutputTextCtrl(self, -1,
+                                                      size=(_BOX_WIDTH, -1),
+                                                      name='invariant_total_err_tcl')
+        hint_msg = "Uncertainty on invariant."
+        self.invariant_total_err_tcl.SetToolTipString(hint_msg)
+        invariant_total_units_txt = wx.StaticText(self, -1, unit_invariant,
+                                                  size=(80, -1))
+
+        #Invariant total
+        iy = 0
+        ix = 0
+        self.invariant_sizer.Add(invariant_total_txt, (iy, ix), (1, 1),
+                                 wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_total_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.invariant_sizer.Add(wx.StaticText(self, -1, uncertainty),
+                                 (iy, ix), (1, 1), wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.invariant_sizer.Add(self.invariant_total_err_tcl, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+        ix += 1
+        self.invariant_sizer.Add(invariant_total_units_txt, (iy, ix), (1, 1),
+                                 wx.EXPAND | wx.ADJUST_MINSIZE, 10)
+
+    def _layout_inputs_sizer(self):
+        """
+        Draw widgets related to inputs
+        """
+        contrast_txt = wx.StaticText(self, -1, 'Contrast:')
+        self.contrast_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH, 20),
+                                        style=0, name='contrast_tcl')
+        wx.EVT_TEXT(self, self.contrast_tcl.GetId(), self._on_text)
+        contrast_hint_txt = "Contrast"
+        self.contrast_tcl.SetToolTipString(contrast_hint_txt)
+        contrast_unit_txt = wx.StaticText(self, -1, '[1/A^2]', size=(40, -1))
+        porod_const_txt = wx.StaticText(self, -1,
+                                        'Porod\nConstant:\n(optional)\n')
+        porod_unit_txt = wx.StaticText(self, -1, '[1/(cm*A^4)]', size=(80, -1))
+        self.porod_constant_tcl = InvTextCtrl(self, -1,
+                                              size=(_BOX_WIDTH, 20), style=0,
+                                              name='porod_constant_tcl')
+        wx.EVT_TEXT(self, self.porod_constant_tcl.GetId(), self._on_text)
+        porod_const_hint_txt = "Porod Constant"
+        self.porod_constant_tcl.SetToolTipString(porod_const_hint_txt)
+
+        background_txt = wx.StaticText(self, -1, 'Background:')
+        self.background_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH, 20),
+                                          style=0, name='background_tcl')
+        wx.EVT_TEXT(self, self.background_tcl.GetId(), self._on_text)
+        background_hint_txt = "Background"
+        self.background_tcl.SetToolTipString(background_hint_txt)
+        background_unit_txt = wx.StaticText(self, -1, '[1/cm]')
+        scale_txt = wx.StaticText(self, -1, 'Scale:')
+        self.scale_tcl = InvTextCtrl(self, -1, size=(_BOX_WIDTH, 20), style=0,
+                                     name='scale_tcl')
+        wx.EVT_TEXT(self, self.scale_tcl.GetId(), self._on_text)
+        scale_hint_txt = "Scale"
+        self.scale_tcl.SetToolTipString(scale_hint_txt)
+        self.sizer_input.AddMany([(background_txt, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (self.background_tcl, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (background_unit_txt, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (scale_txt, 0, wx.LEFT | wx.BOTTOM, 10),
+                                  (self.scale_tcl, 0, wx.LEFT | wx.BOTTOM | wx.RIGHT, 5),
+                                  (10, 10),
+                                  (contrast_txt, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (self.contrast_tcl, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (contrast_unit_txt, 0, wx.LEFT | wx.BOTTOM, 5),
+                                  (porod_const_txt, 0, wx.LEFT, 10),
+                                  (self.porod_constant_tcl, 0, wx.LEFT | wx.BOTTOM | wx.RIGHT, 5),
+                                  (porod_unit_txt, 0, wx.LEFT | wx.BOTTOM, 5)])
+        self.inputs_sizer.Add(self.sizer_input)
+
+    def _layout_outputs_sizer(self):
+        """
+        Draw widgets related to outputs
+        """
+        self._layout_volume_surface_sizer()
+        self._layout_invariant_sizer()
+        static_line = wx.StaticLine(self, -1)
+        self.outputs_sizer.AddMany([(self.volume_surface_sizer,
+                                     0, wx.TOP | wx.BOTTOM, 10),
+                                    (static_line, 0, wx.EXPAND, 0),
+                                    (self.invariant_sizer, 0, wx.TOP | wx.BOTTOM, 10)])
+    def _layout_button(self):
+        """
+        Do the layout for the button widgets
+        """
+        #compute button
+        id = wx.NewId()
+        self.button_calculate = wx.Button(self, id, "Compute",
+                                          name='compute_invariant')
+        self.button_calculate.SetToolTipString("Compute invariant")
+        self.Bind(wx.EVT_BUTTON, self.compute_invariant, id=id)
+        #detail button
+        id = wx.NewId()
+        self.button_details = wx.Button(self, id, "Details?")
+        hint_msg = "Get more details of computation such as fraction from extrapolation"
+        self.button_details.SetToolTipString(hint_msg)
+        self.Bind(wx.EVT_BUTTON, self.display_details, id=id)
+        #help button
+        id = wx.NewId()
+        self.button_help = wx.Button(self, id, "HELP")
+        self.button_help.SetToolTipString("Invariant Documentation")
+        self.Bind(wx.EVT_BUTTON, self.on_help, id=id)
+        self.button_sizer.AddMany([((20, 20), 1, wx.EXPAND | wx.ADJUST_MINSIZE, 0),
+                                   (self.button_details, 0, wx.ALL, 10),
+                                   (self.button_calculate, 0,
+                                    wx.RIGHT | wx.TOP | wx.BOTTOM, 10),
+                                   (self.button_help, 0, 
+                                    wx.RIGHT | wx.TOP | wx.BOTTOM, 10),])
+    def _do_layout(self):
+        """
+        Draw window content
+        """
+        self._define_structure()
+        self._layout_data_name()
+        self._layout_extrapolation()
+        self._layout_inputs_sizer()
+        self._layout_outputs_sizer()
+        self._layout_button()
+        self.main_sizer.AddMany([(self.data_name_boxsizer, 0, wx.ALL, 10),
+                                 (self.outputs_sizer, 0,
+                                  wx.LEFT | wx.RIGHT | wx.BOTTOM, 10),
+                                 (self.button_sizer, 0, wx.LEFT | wx.RIGHT, 15),
+                                 (self.inputs_sizer, 0,
+                                  wx.LEFT | wx.RIGHT | wx.BOTTOM, 10),
+                                 (self.extrapolation_sizer, 0,
+                                  wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)])
+        self.SetSizer(self.main_sizer)
+        self.SetAutoLayout(True)
+        
+    def on_help(self, event):
+        """
+        Bring up the Invariant Documentation whenever the HELP button is 
+        clicked.
+
+        Calls DocumentationWindow with the path of the location within the
+        documentation tree (after /doc/ ....".  Note that when using old
+        versions of Wx (before 2.9) and thus not the release version of
+        installers, the help comes up at the top level of the file as
+        webbrowser does not pass anything past the # to the browser when it is
+        running "file:///...."
+
+    :param evt: Triggers on clicking the help button
+    """
+
+        _TreeLocation = "user/perspectives/invariant/invariant_help.html"
+        _doc_viewer = DocumentationWindow(self, -1, _TreeLocation, "",
+                                          "Invariant Help")
+
+
+class InvariantDialog(wx.Dialog):
+    """
+    Invariant Dialog
+    """
+    def __init__(self, parent=None, id=1, graph=None,
+                 data=None, title="Invariant", base=None):
+        wx.Dialog.__init__(self, parent, id, title, size=(PANEL_WIDTH,
+                                                          PANEL_HEIGHT))
+        self.panel = InvariantPanel(self)
+        self.Centre()
+        self.Show(True)
+
+class InvariantWindow(wx.Frame):
+    """
+    Invariant Window
+    """
+    def __init__(self, parent=None, id=1, graph=None,
+                 data=None, title="Invariant", base=None):
+
+        wx.Frame.__init__(self, parent, id, title, size=(PANEL_WIDTH + 100,
+                                                         PANEL_HEIGHT + 100))
+        from sas.sascalc.dataloader.loader import  Loader
+        self.loader = Loader()
+        path = "C:/ECLPS/workspace/trunk/sasdataloader/test/ascii_test_3.txt"
+        data = self.loader.load(path)
+        self.panel = InvariantPanel(self)
+
+        data.name = data.filename
+        self.panel.set_data(data)
+        self.Centre()
+        self.Show(True)
+
+class MyApp(wx.App):
+    """
+    Test App
+    """
+    def OnInit(self):
+        """
+        Init
+        """
+        wx.InitAllImageHandlers()
+        frame = InvariantWindow()
+        frame.Show(True)
+        self.SetTopWindow(frame)
+
+        return True
+
+# end of class MyApp
+
+if __name__ == "__main__":
+    app = MyApp(0)
+    app.MainLoop()
Index: src/sas/sasgui/perspectives/invariant/invariant_state.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/invariant_state.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/invariant_state.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,792 @@
+"""
+    State class for the invariant UI
+"""
+
+# import time
+import os
+import sys
+import logging
+import copy
+import sas.sascalc.dataloader
+# from xml.dom.minidom import parse
+from lxml import etree
+from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
+from sas.sascalc.dataloader.readers.cansas_reader import get_content
+from sas.sasgui.guiframe.utils import format_number
+from sas.sasgui.guiframe.gui_style import GUIFRAME_ID
+from sas.sasgui.guiframe.dataFitting import Data1D
+
+INVNODE_NAME = 'invariant'
+CANSAS_NS = "cansas1d/1.0"
+
+# default state
+DEFAULT_STATE = {'file': 'None',
+                 'compute_num':0,
+                 'state_num':0,
+                 'is_time_machine':False,
+                 'background_tcl':0.0,
+                 'scale_tcl':1.0,
+                 'contrast_tcl':1.0,
+                 'porod_constant_tcl':'',
+                 'npts_low_tcl':10,
+                 'npts_high_tcl':10,
+                 'power_high_tcl':4.0,
+                 'power_low_tcl': 4.0,
+                 'enable_high_cbox':False,
+                 'enable_low_cbox':False,
+                 'guinier': True,
+                 'power_law_high': False,
+                 'power_law_low': False,
+                 'fit_enable_high': False,
+                 'fit_enable_low': False,
+                 'fix_enable_high':True,
+                 'fix_enable_low':True,
+                 'volume_tcl':'',
+                 'volume_err_tcl':'',
+                 'surface_tcl':'',
+                 'surface_err_tcl':''}
+# list of states: This list will be filled as panel
+# init and the number of states increases
+state_list = {}
+bookmark_list = {}
+# list of input parameters (will be filled up on panel init) used by __str__
+input_list = {'background_tcl':0,
+              'scale_tcl':0,
+              'contrast_tcl':0,
+              'porod_constant_tcl':'',
+              'npts_low_tcl':0,
+              'npts_high_tcl':0,
+              'power_high_tcl':0,
+              'power_low_tcl': 0}
+# list of output parameters (order sensitive) used by __str__
+output_list = [["qstar_low", "Q* from low Q extrapolation [1/(cm*A)]"],
+               ["qstar_low_err", "dQ* from low Q extrapolation"],
+               ["qstar_low_percent", "Q* percent from low Q extrapolation"],
+               ["qstar", "Q* from data [1/(cm*A)]"],
+               ["qstar_err", "dQ* from data"],
+               ["qstar_percent", "Q* percent from data"],
+               ["qstar_high", "Q* from high Q extrapolation [1/(cm*A)]"],
+               ["qstar_high_err", "dQ* from high Q extrapolation"],
+               ["qstar_high_percent", "Q* percent from low Q extrapolation"],
+               ["qstar_total", "total Q* [1/(cm*A)]"],
+               ["qstar_total_err", "total dQ*"],
+               ["volume", "volume fraction"],
+               ["volume_err", "volume fraction error"],
+               ["surface", "specific surface"],
+               ["surface_err", "specific surface error"]]
+
+
+
+class InvariantState(object):
+    """
+    Class to hold the state information of the InversionControl panel.
+    """
+    def __init__(self):
+        """
+        Default values
+        """
+        # Input
+        self.file = None
+        self.data = Data1D(x=[], y=[], dx=None, dy=None)
+        self.theory_lowQ = Data1D(x=[], y=[], dy=None)
+        self.theory_lowQ.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM
+        self.theory_highQ = Data1D(x=[], y=[], dy=None)
+        self.theory_highQ.symbol = GUIFRAME_ID.CURVE_SYMBOL_NUM
+        # self.is_time_machine = False
+        self.saved_state = DEFAULT_STATE
+        self.state_list = state_list
+        self.bookmark_list = bookmark_list
+        self.input_list = input_list
+        self.output_list = output_list
+
+        self.compute_num = 0
+        self.state_num = 0
+        self.timestamp = ('00:00:00', '00/00/0000')
+        self.container = None
+        # plot image
+        self.wximbmp = None
+        # report_html strings
+        import sas.sasgui.perspectives.invariant as invariant
+        path = invariant.get_data_path(media='media')
+        path_report_html = os.path.join(path, "report_template.html")
+        html_template = open(path_report_html, "r")
+        self.template_str = html_template.read()
+        self.report_str = self.template_str
+        # self.report_str_save = None
+        html_template.close()
+
+    def __str__(self):
+        """
+        Pretty print
+
+        : return: string representing the state
+        """
+        # Input string
+        compute_num = self.saved_state['compute_num']
+        compute_state = self.state_list[str(compute_num)]
+        my_time, date = self.timestamp
+        file_name = self.file
+
+        state_num = int(self.saved_state['state_num'])
+        state = "\n[Invariant computation for %s: " % file_name
+        state += "performed at %s on %s] \n" % (my_time, date)
+        state += "State No.: %d \n" % state_num
+        state += "\n=== Inputs ===\n"
+
+        # text ctl general inputs ( excluding extrapolation text ctl)
+        for key, value in self.input_list.iteritems():
+            if value == '':
+                continue
+            key_split = key.split('_')
+            max_ind = len(key_split) - 1
+            if key_split[max_ind] == 'tcl':
+                name = ""
+                if key_split[1] == 'low' or key_split[1] == 'high':
+                    continue
+                for ind in range(0, max_ind):
+                    name += " %s" % key_split[ind]
+                state += "%s:   %s\n" % (name.lstrip(" "), value)
+
+        # other input parameters
+        extra_lo = compute_state['enable_low_cbox']
+        if compute_state['enable_low_cbox']:
+            if compute_state['guinier']:
+                extra_lo = 'Guinier'
+            else:
+                extra_lo = 'Power law'
+        extra_hi = compute_state['enable_high_cbox']
+        if compute_state['enable_high_cbox']:
+            extra_hi = 'Power law'
+        state += "\nExtrapolation:  High=%s; Low=%s\n" % (extra_hi, extra_lo)
+        low_off = False
+        high_off = False
+        for key, value in self.input_list.iteritems():
+            key_split = key.split('_')
+            max_ind = len(key_split) - 1
+            if key_split[max_ind] == 'tcl':
+                name = ""
+                # check each buttons whether or not ON or OFF
+                if key_split[1] == 'low' or key_split[1] == 'high':
+                    if not compute_state['enable_low_cbox'] and \
+                        key_split[max_ind - 1] == 'low':
+                        low_off = True
+                        continue
+                    elif not compute_state['enable_high_cbox'] and \
+                        key_split[max_ind - 1] == 'high':
+                        high_off = True
+                        continue
+                    elif extra_lo == 'Guinier' and key_split[0] == 'power' and \
+                        key_split[max_ind - 1] == 'low':
+                        continue
+                    for ind in range(0, max_ind):
+                        name += " %s" % key_split[ind]
+                    name = name.lstrip(" ")
+                    if name == "power low":
+                        if compute_state['fix_enable_low']:
+                            name += ' (Fixed)'
+                        else:
+                            name += ' (Fitted)'
+                    if name == "power high":
+                        if compute_state['fix_enable_high']:
+                            name += ' (Fixed)'
+                        else:
+                            name += ' (Fitted)'
+                    state += "%s:   %s\n" % (name, value)
+        # Outputs
+        state += "\n=== Outputs ==="
+        for item in output_list:
+            item_split = item[0].split('_')
+            # Exclude the extrapolation that turned off
+            if len(item_split) > 1:
+                if low_off and item_split[1] == 'low':
+                    continue
+                if high_off and item_split[1] == 'high':
+                    continue
+            max_ind = len(item_split) - 1
+            value = None
+            if hasattr(self.container, item[0]):
+                # Q* outputs
+                value = getattr(self.container, item[0])
+            else:
+                # other outputs than Q*
+                name = item[0] + "_tcl"
+                if name in self.saved_state.keys():
+                    value = self.saved_state[name]
+
+            # Exclude the outputs w/''
+            if value == '':
+                continue
+            # Error outputs
+            if item_split[max_ind] == 'err':
+                state += "+- %s " % format_number(value)
+            # Percentage outputs
+            elif item_split[max_ind] == 'percent':
+                value = float(value) * 100
+                state += "(%s %s)" % (format_number(value), '%')
+            # Outputs
+            else:
+                state += "\n%s:   %s " % (item[1],
+                                          format_number(value, high=True))
+        # Include warning msg
+        if self.container is not None:
+            state += "\n\nNote:\n" + self.container.warning_msg
+        return state
+
+    def clone_state(self):
+        """
+        deepcopy the state
+        """
+        return copy.deepcopy(self.saved_state)
+
+    def toXML(self, file="inv_state.inv", doc=None, entry_node=None):
+        """
+        Writes the state of the InversionControl panel to file, as XML.
+
+        Compatible with standalone writing, or appending to an
+        already existing XML document. In that case, the XML document
+        is required. An optional entry node in the XML document
+        may also be given.
+
+        : param file: file to write to
+        : param doc: XML document object [optional]
+        : param entry_node: XML node within the document at which we will append the data [optional]
+        """
+        # TODO: Get this to work
+        from xml.dom.minidom import getDOMImplementation
+        import time
+        timestamp = time.time()
+        # Check whether we have to write a standalone XML file
+        if doc is None:
+            impl = getDOMImplementation()
+
+            doc_type = impl.createDocumentType(INVNODE_NAME, "1.0", "1.0")
+
+            newdoc = impl.createDocument(None, INVNODE_NAME, doc_type)
+            top_element = newdoc.documentElement
+        else:
+            # We are appending to an existing document
+            newdoc = doc
+            top_element = newdoc.createElement(INVNODE_NAME)
+            if entry_node is None:
+                newdoc.documentElement.appendChild(top_element)
+            else:
+                entry_node.appendChild(top_element)
+
+        attr = newdoc.createAttribute("version")
+        attr.nodeValue = '1.0'
+        top_element.setAttributeNode(attr)
+
+        # File name
+        element = newdoc.createElement("filename")
+        if self.file != None and self.file != '':
+            element.appendChild(newdoc.createTextNode(str(self.file)))
+        else:
+            element.appendChild(newdoc.createTextNode(str(file)))
+        top_element.appendChild(element)
+
+        element = newdoc.createElement("timestamp")
+        element.appendChild(newdoc.createTextNode(time.ctime(timestamp)))
+        attr = newdoc.createAttribute("epoch")
+        attr.nodeValue = str(timestamp)
+        element.setAttributeNode(attr)
+        top_element.appendChild(element)
+
+        # Current state
+        state = newdoc.createElement("state")
+        top_element.appendChild(state)
+
+        for name, value in self.saved_state.iteritems():
+            element = newdoc.createElement(str(name))
+            element.appendChild(newdoc.createTextNode(str(value)))
+            state.appendChild(element)
+
+        # State history list
+        history = newdoc.createElement("history")
+        top_element.appendChild(history)
+
+        for name, value in self.state_list.iteritems():
+            history_element = newdoc.createElement('state_' + str(name))
+            for state_name, state_value in value.iteritems():
+                state_element = newdoc.createElement(str(state_name))
+                child = newdoc.createTextNode(str(state_value))
+                state_element.appendChild(child)
+                history_element.appendChild(state_element)
+            # history_element.appendChild(state_list_element)
+            history.appendChild(history_element)
+
+        # Bookmarks  bookmark_list[self.bookmark_num] = [\
+        # my_time,date,state,comp_state]
+        bookmark = newdoc.createElement("bookmark")
+        top_element.appendChild(bookmark)
+        item_list = ['time', 'date', 'state', 'comp_state']
+        for name, value_list in self.bookmark_list.iteritems():
+            element = newdoc.createElement('mark_' + str(name))
+            _, date, state, comp_state = value_list
+            time_element = newdoc.createElement('time')
+            time_element.appendChild(newdoc.createTextNode(str(value_list[0])))
+            date_element = newdoc.createElement('date')
+            date_element.appendChild(newdoc.createTextNode(str(value_list[1])))
+            state_list_element = newdoc.createElement('state')
+            comp_state_list_element = newdoc.createElement('comp_state')
+            for state_name, state_value in value_list[2].iteritems():
+                state_element = newdoc.createElement(str(state_name))
+                child = newdoc.createTextNode(str(state_value))
+                state_element.appendChild(child)
+                state_list_element.appendChild(state_element)
+            for comp_name, comp_value in value_list[3].iteritems():
+                comp_element = newdoc.createElement(str(comp_name))
+                comp_element.appendChild(newdoc.createTextNode(str(comp_value)))
+                comp_state_list_element.appendChild(comp_element)
+            element.appendChild(time_element)
+            element.appendChild(date_element)
+            element.appendChild(state_list_element)
+            element.appendChild(comp_state_list_element)
+            bookmark.appendChild(element)
+
+        # Save the file
+        if doc is None:
+            fd = open('test000', 'w')
+            fd.write(newdoc.toprettyxml())
+            fd.close()
+            return None
+        else:
+            return newdoc
+
+    def fromXML(self, file=None, node=None):
+        """
+        Load invariant states from a file
+
+        : param file: .inv file
+        : param node: node of a XML document to read from
+        """
+        if file is not None:
+            msg = "InvariantSate no longer supports non-CanSAS"
+            msg += " format for invariant files"
+            raise RuntimeError, msg
+
+        if node.get('version')\
+            and node.get('version') == '1.0':
+
+            # Get file name
+            entry = get_content('ns:filename', node)
+            if entry is not None:
+                file_name = entry.text.strip()
+
+            # Get time stamp
+            entry = get_content('ns:timestamp', node)
+            if entry is not None and entry.get('epoch'):
+                try:
+                    timestamp = (entry.get('epoch'))
+                except:
+                    msg = "InvariantSate.fromXML: Could not read"
+                    msg += " timestamp\n %s" % sys.exc_value
+                    logging.error(msg)
+
+            # Parse bookmarks
+            entry_bookmark = get_content('ns:bookmark', node)
+
+            for ind in range(1, len(entry_bookmark) + 1):
+                temp_state = {}
+                temp_bookmark = {}
+                entry = get_content('ns:mark_%s' % ind, entry_bookmark)
+
+                if entry is not None:
+                    my_time = get_content('ns:time', entry)
+                    val_time = str(my_time.text.strip())
+                    date = get_content('ns:date', entry)
+                    val_date = str(date.text.strip())
+                    state_entry = get_content('ns:state', entry)
+                    for item in DEFAULT_STATE:
+                        input_field = get_content('ns:%s' % item, state_entry)
+                        val = str(input_field.text.strip())
+                        if input_field is not None:
+                            temp_state[item] = val
+                    comp_entry = get_content('ns:comp_state', entry)
+
+                    for item in DEFAULT_STATE:
+                        input_field = get_content('ns:%s' % item, comp_entry)
+                        val = str(input_field.text.strip())
+                        if input_field is not None:
+                            temp_bookmark[item] = val
+                    try:
+                        self.bookmark_list[ind] = [val_time, val_date, temp_state, temp_bookmark]
+                    except:
+                        raise "missing components of bookmarks..."
+            # Parse histories
+            entry_history = get_content('ns:history', node)
+
+            for ind in range(0, len(entry_history)):
+                temp_state = {}
+                entry = get_content('ns:state_%s' % ind, entry_history)
+
+                if entry is not None:
+                    for item in DEFAULT_STATE:
+                        input_field = get_content('ns:%s' % item, entry)
+                        val = str(input_field.text.strip())
+                        if input_field is not None:
+                            temp_state[item] = val
+                            self.state_list[ind] = temp_state
+
+            # Parse current state (ie, saved_state)
+            entry = get_content('ns:state', node)
+            if entry is not None:
+                for item in DEFAULT_STATE:
+                    input_field = get_content('ns:%s' % item, entry)
+                    val = str(input_field.text.strip())
+                    if input_field is not None:
+                        self.set_saved_state(name=item, value=val)
+            self.file = file_name
+
+    def set_report_string(self):
+        """
+        Get the values (strings) from __str__ for report
+        """
+        strings = self.__str__()
+
+        # default string values
+        for num in range(1, 19):
+            exec "s_%s = 'NA'" % str(num)
+        lines = strings.split('\n')
+        # get all string values from __str__()
+        for line in range(0, len(lines)):
+            if line == 1:
+                s_1 = lines[1]
+            elif line == 2:
+                s_2 = lines[2]
+            else:
+                item = lines[line].split(':')
+                item[0] = item[0].strip()
+                if item[0] == "scale":
+                    s_3 = item[1]
+                elif item[0] == "porod constant":
+                    s_4 = item[1]
+                elif item[0] == "background":
+                    s_5 = item[1]
+                elif item[0] == "contrast":
+                    s_6 = item[1]
+                elif item[0] == "Extrapolation":
+                    extra = item[1].split(";")
+                    bool_0 = extra[0].split("=")
+                    bool_1 = extra[1].split("=")
+                    s_8 = " " + bool_0[0] + "Q region = " + bool_0[1]
+                    s_7 = " " + bool_1[0] + "Q region = " + bool_1[1]
+                elif item[0] == "npts low":
+                    s_9 = item[1]
+                elif item[0] == "npts high":
+                    s_10 = item[1]
+                elif item[0] == "volume fraction":
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    s_17 = val + " &plusmn; " + error
+                elif item[0] == "specific surface":
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    s_18 = val + " &plusmn; " + error
+                elif item[0].split("(")[0].strip() == "power low":
+                    s_11 = item[0] + " =" + item[1]
+                elif item[0].split("(")[0].strip() == "power high":
+                    s_12 = item[0] + " =" + item[1]
+                elif item[0].split("[")[0].strip() == "Q* from low Q extrapolation":
+                    # looks messy but this way the symbols +_ and % work on html
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    err = error.split("%")[0].strip()
+                    percent = error.split("%")[1].strip()
+                    s_13 = val + " &plusmn; " + err + "&#37" + percent
+                elif item[0].split("[")[0].strip() == "Q* from data":
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    err = error.split("%")[0].strip()
+                    percent = error.split("%")[1].strip()
+                    s_14 = val + " &plusmn; " + err + "&#37" + percent
+                elif item[0].split("[")[0].strip() == "Q* from high Q extrapolation":
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    err = error.split("%")[0].strip()
+                    percent = error.split("%")[1].strip()
+                    s_15 = val + " &plusmn; " + err + "&#37" + percent
+                elif item[0].split("[")[0].strip() == "total Q*":
+                    val = item[1].split("+-")[0].strip()
+                    error = item[1].split("+-")[1].strip()
+                    s_16 = val + " &plusmn; " + error
+                else:
+                    continue
+
+        s_1 = self._check_html_format(s_1)
+        file_name = self._check_html_format(self.file)
+
+        # make plot image
+        self.set_plot_state(extra_high=bool_0[1], extra_low=bool_1[1])
+        # get ready for report with setting all the html strings
+        self.report_str = str(self.template_str) % (s_1, s_2,
+                                                    s_3, s_4, s_5, s_6, s_7, s_8,
+                                                    s_9, s_10, s_11, s_12, s_13, s_14, s_15,
+                                                    s_16, s_17, s_18, file_name, "%s")
+
+    def _check_html_format(self, name):
+        """
+        Check string '%' for html format
+        """
+        if name.count('%'):
+            name = name.replace('%', '&#37')
+
+        return name
+
+    def set_saved_state(self, name, value):
+        """
+        Set the state list
+
+        : param name: name of the state component
+        : param value: value of the state component
+        """
+        rb_list = [['power_law_low', 'guinier'],
+                   ['fit_enable_low', 'fix_enable_low'],
+                   ['fit_enable_high', 'fix_enable_high']]
+
+        self.name = value
+        self.saved_state[name] = value
+        # set the count part of radio button clicked
+        # False for the saved_state
+        for title, content in rb_list:
+            if name == title:
+                name = content
+                value = False
+            elif name == content:
+                name = title
+                value = False
+        self.saved_state[name] = value
+        self.state_num = self.saved_state['state_num']
+
+    def set_plot_state(self, extra_high=False, extra_low=False):
+        """
+        Build image state that wx.html understand
+        by plotting, putting it into wx.FileSystem image object
+
+        : extrap_high,extra_low: low/high extrapolations
+        are possible extra-plots
+        """
+        # some imports
+        import wx
+        import matplotlib.pyplot as plt
+        from matplotlib.backends.backend_agg import FigureCanvasAgg
+
+        # we use simple plot, not plotpanel
+        # make matlab figure
+        fig = plt.figure()
+        fig.set_facecolor('w')
+        graph = fig.add_subplot(111)
+
+        # data plot
+        graph.errorbar(self.data.x, self.data.y, yerr=self.data.dy, fmt='o')
+        # low Q extrapolation fit plot
+        if not extra_low == 'False':
+            graph.plot(self.theory_lowQ.x, self.theory_lowQ.y)
+        # high Q extrapolation fit plot
+        if not extra_high == 'False':
+            graph.plot(self.theory_highQ.x, self.theory_highQ.y)
+        graph.set_xscale("log", nonposx='clip')
+        graph.set_yscale("log", nonposy='clip')
+        graph.set_xlabel('$\\rm{Q}(\\AA^{-1})$', fontsize=12)
+        graph.set_ylabel('$\\rm{Intensity}(cm^{-1})$', fontsize=12)
+        canvas = FigureCanvasAgg(fig)
+        # actually make image
+        canvas.draw()
+
+        # make python.Image object
+        # size
+        w, h = canvas.get_width_height()
+        # convert to wx.Image
+        wximg = wx.EmptyImage(w, h)
+        # wxim.SetData(img.convert('RGB').tostring() )
+        wximg.SetData(canvas.tostring_rgb())
+        # get the dynamic image for the htmlwindow
+        wximgbmp = wx.BitmapFromImage(wximg)
+        # store the image in wx.FileSystem Object
+        wx.FileSystem.AddHandler(wx.MemoryFSHandler())
+        # use wx.MemoryFSHandler
+        self.imgRAM = wx.MemoryFSHandler()
+        # AddFile, image can be retrieved with 'memory:filename'
+        self.imgRAM.AddFile('img_inv.png', wximgbmp, wx.BITMAP_TYPE_PNG)
+
+        self.wximgbmp = 'memory:img_inv.png'
+        self.image = fig
+
+class Reader(CansasReader):
+    """
+    Class to load a .inv invariant file
+    """
+    # # File type
+    type_name = "Invariant"
+
+    # # Wildcards
+    type = ["Invariant file (*.inv)|*.inv",
+            "SASView file (*.svs)|*.svs"]
+    # # List of allowed extensions
+    ext = ['.inv', '.INV', '.svs', 'SVS']
+
+    def __init__(self, call_back, cansas=True):
+        """
+        Initialize the call-back method to be called
+        after we load a file
+
+        : param call_back: call-back method
+        : param cansas:  True = files will be written/read in CanSAS format
+                        False = write CanSAS format
+        """
+        # # Call back method to be executed after a file is read
+        self.call_back = call_back
+        # # CanSAS format flag
+        self.cansas = cansas
+        self.state = None
+
+    def read(self, path):
+        """
+        Load a new invariant state from file
+
+        : param path: file path
+        : return: None
+        """
+        if self.cansas == True:
+            return self._read_cansas(path)
+        else:
+            return self._read_standalone(path)
+
+    def _read_standalone(self, path):
+        """
+        Load a new invariant state from file.
+        The invariant node is assumed to be the top element.
+
+        : param path: file path
+        : return: None
+        """
+        # Read the new state from file
+        state = InvariantState()
+
+        state.fromXML(file=path)
+
+        # Call back to post the new state
+        self.call_back(state)
+        return None
+
+    def _parse_state(self, entry):
+        """
+        Read an invariant result from an XML node
+
+        : param entry: XML node to read from
+        : return: InvariantState object
+        """
+        state = None
+        # Locate the invariant node
+        try:
+            nodes = entry.xpath('ns:%s' % INVNODE_NAME,
+                                namespaces={'ns': CANSAS_NS})
+            # Create an empty state
+            if nodes != []:
+                state = InvariantState()
+                state.fromXML(node=nodes[0])
+        except:
+            msg = "XML document does not contain invariant"
+            msg += " information.\n %s" % sys.exc_value
+            logging.info(msg)
+        return state
+
+    def _read_cansas(self, path):
+        """
+        Load data and invariant information from a CanSAS XML file.
+
+        : param path: file path
+        : return: Data1D object if a single SASentry was found,
+                    or a list of Data1D objects if multiple entries were found,
+                    or None of nothing was found
+        : raise RuntimeError: when the file can't be opened
+        : raise ValueError: when the length of the data vectors are inconsistent
+        """
+        output = []
+        if os.path.isfile(path):
+            basename = os.path.basename(path)
+            root, extension = os.path.splitext(basename)
+
+            if  extension.lower() in self.ext or \
+                extension.lower() == '.xml':
+                tree = etree.parse(path, parser=etree.ETCompatXMLParser())
+
+                # Check the format version number
+                # Specifying the namespace will take care of
+                # the file format version
+                root = tree.getroot()
+
+                entry_list = root.xpath('/ns:SASroot/ns:SASentry',
+                                        namespaces={'ns': CANSAS_NS})
+
+                for entry in entry_list:
+
+                    sas_entry, _ = self._parse_entry(entry)
+                    invstate = self._parse_state(entry)
+
+                    # invstate could be None when .svs file is loaded
+                    # in this case, skip appending to output
+                    if invstate != None:
+                        sas_entry.meta_data['invstate'] = invstate
+                        sas_entry.filename = invstate.file
+                        output.append(sas_entry)
+        else:
+            raise RuntimeError, "%s is not a file" % path
+
+        # Return output consistent with the loader's api
+        if len(output) == 0:
+            return None
+        elif len(output) == 1:
+            # Call back to post the new state
+            self.state = output[0].meta_data['invstate']
+            self.call_back(state=output[0].meta_data['invstate'],
+                           datainfo=output[0])
+            return output[0]
+        else:
+            return output
+
+    def get_state(self):
+        return self.state
+
+    def write(self, filename, datainfo=None, invstate=None):
+        """
+        Write the content of a Data1D as a CanSAS XML file
+
+        : param filename: name of the file to write
+        : param datainfo: Data1D object
+        : param invstate: InvariantState object
+        """
+        # Sanity check
+        if self.cansas == True:
+            doc = self.write_toXML(datainfo, invstate)
+            # Write the XML document
+            fd = open(filename, 'w')
+            fd.write(doc.toprettyxml())
+            fd.close()
+        else:
+            invstate.toXML(file=filename)
+
+    def write_toXML(self, datainfo=None, state=None):
+        """
+        Write toXML, a helper for write()
+
+        : return: xml doc
+        """
+        if datainfo is None:
+            datainfo = sas.dataloader.data_info.Data1D(x=[], y=[])
+        elif not issubclass(datainfo.__class__, sas.dataloader.data_info.Data1D):
+            msg = "The cansas writer expects a Data1D"
+            msg += " instance: %s" % str(datainfo.__class__.__name__)
+            raise RuntimeError, msg
+        # make sure title and data run is filled up.
+        if datainfo.title == None or datainfo.title == '':
+            datainfo.title = datainfo.name
+        if datainfo.run_name == None or datainfo.run_name == {}:
+            datainfo.run = [str(datainfo.name)]
+            datainfo.run_name[0] = datainfo.name
+        # Create basic XML document
+        doc, sasentry = self._to_xml_doc(datainfo)
+        # Add the invariant information to the XML document
+        if state is not None:
+            doc = state.toXML(datainfo.name, doc=doc, entry_node=sasentry)
+        return doc
+
Index: src/sas/sasgui/perspectives/invariant/invariant_widgets.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/invariant_widgets.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/invariant_widgets.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,165 @@
+################################################################################
+# This software was developed by the University of Tennessee as part of the
+# Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
+# project funded by the US National Science Foundation.
+#
+# See the license text in license.txt
+#
+# copyright 2009, University of Tennessee
+################################################################################
+
+import wx
+from wx.lib.scrolledpanel import ScrolledPanel
+
+WIDTH = 400
+HEIGHT = 350
+
+class DialogPanel(ScrolledPanel):
+    def __init__(self, *args, **kwds):
+        ScrolledPanel.__init__(self, *args, **kwds)
+        self.SetupScrolling()
+
+class InvTextCtrl(wx.TextCtrl):
+    """
+    Text control for model and fit parameters.
+    Binds the appropriate events for user interactions.
+    """
+    def __init__(self, *args, **kwds):
+        wx.TextCtrl.__init__(self, *args, **kwds)
+        # # Set to True when the mouse is clicked while
+        # the whole string is selected
+        self.full_selection = False
+        # # Call back for EVT_SET_FOCUS events
+        _on_set_focus_callback = None
+
+        # Bind appropriate events
+        self.Bind(wx.EVT_LEFT_UP, self._highlight_text)
+        self.Bind(wx.EVT_SET_FOCUS, self._on_set_focus)
+
+    def _on_set_focus(self, event):
+        """
+        Catch when the text control is set in focus to highlight the whole
+        text if necessary
+
+        :param event: mouse event
+        """
+        event.Skip()
+        self.full_selection = True
+
+    def _highlight_text(self, event):
+        """
+        Highlight text of a TextCtrl only of no text has be selected
+
+        :param event: mouse event
+        """
+        # Make sure the mouse event is available to other listeners
+        event.Skip()
+        control = event.GetEventObject()
+        if self.full_selection:
+            self.full_selection = False
+            # Check that we have a TextCtrl
+            if issubclass(control.__class__, wx.TextCtrl):
+                # Check whether text has been selected,
+                # if not, select the whole string
+                (start, end) = control.GetSelection()
+                if start == end:
+                    control.SetSelection(-1, -1)
+
+
+class OutputTextCtrl(wx.TextCtrl):
+    """
+    Text control used to display outputs.
+    No editing allowed. The background is
+    grayed out. User can't select text.
+    """
+    def __init__(self, *args, **kwds):
+        wx.TextCtrl.__init__(self, *args, **kwds)
+        self.SetEditable(False)
+        self.SetBackgroundColour(self.GetParent().GetBackgroundColour())
+
+        # Bind to mouse event to avoid text highlighting
+        # The event will be skipped once the call-back
+        # is called.
+
+        self.Bind(wx.EVT_MOUSE_EVENTS, self._click)
+
+
+    def _click(self, event):
+        """
+        Prevent further handling of the mouse event
+        by not calling Skip().
+        """
+        pass
+
+class DataDialog(wx.Dialog):
+    """
+    Allow file selection at loading time
+    """
+    def __init__(self, data_list, parent=None, text='', *args, **kwds):
+        kwds['size'] = (WIDTH, HEIGHT)
+        kwds['title'] = "Data Selection"
+        wx.Dialog.__init__(self, parent, *args, **kwds)
+        self.list_of_ctrl = []
+        self._sizer_main = wx.BoxSizer(wx.VERTICAL)
+        self._sizer_txt = wx.BoxSizer(wx.VERTICAL)
+        self._sizer_button = wx.BoxSizer(wx.HORIZONTAL)
+        self._choice_sizer = wx.GridBagSizer(5, 5)
+        self._panel = DialogPanel(self, style=wx.RAISED_BORDER,
+                                  size=(WIDTH - 20, HEIGHT / 3))
+        self.SetSizer(self._sizer_main)
+        self.__do_layout(data_list, text=text)
+        self.Layout()
+
+    def __do_layout(self, data_list, text=''):
+        """
+        layout the dialog
+        """
+        # add text
+        if text.strip() == "":
+            text = "This Perspective does not allow multiple data !\n"
+            text += "Please select only one Data.\n"
+        text_ctrl = wx.TextCtrl(self, -1, str(text), style=wx.TE_MULTILINE,
+                                size=(-1, HEIGHT / 3))
+        text_ctrl.SetEditable(False)
+        self._sizer_txt.Add(text_ctrl, 1, wx.EXPAND | wx.ALL, 10)
+        iy = 0
+        ix = 0
+        rbox = wx.RadioButton(self._panel, -1, str(data_list[0].name),
+                              (10, 10), style=wx.RB_GROUP)
+        rbox.SetValue(True)
+        self.list_of_ctrl.append((rbox, data_list[0]))
+        self._choice_sizer.Add(rbox, (iy, ix), (1, 1),
+                               wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        for i in range(1, len(data_list)):
+            iy += 1
+            rbox = wx.RadioButton(self._panel, -1,
+                                  str(data_list[i].name), (10, 10))
+            rbox.SetValue(False)
+            self.list_of_ctrl.append((rbox, data_list[i]))
+            self._choice_sizer.Add(rbox, (iy, ix),
+                                   (1, 1), wx.LEFT | wx.EXPAND | wx.ADJUST_MINSIZE, 15)
+        self._panel.SetSizer(self._choice_sizer)
+        # add sizer
+        self._sizer_button.Add((20, 20), 1, wx.EXPAND | wx.ADJUST_MINSIZE, 0)
+        button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
+        self._sizer_button.Add(button_cancel, 0,
+                               wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE, 10)
+        button_OK = wx.Button(self, wx.ID_OK, "Ok")
+        button_OK.SetFocus()
+        self._sizer_button.Add(button_OK, 0,
+                               wx.LEFT | wx.RIGHT | wx.ADJUST_MINSIZE, 10)
+        static_line = wx.StaticLine(self, -1)
+
+        self._sizer_txt.Add(self._panel, 0, wx.EXPAND | wx.ALL, 5)
+        self._sizer_main.Add(self._sizer_txt, 0, wx.EXPAND | wx.ALL, 5)
+        self._sizer_main.Add(static_line, 0, wx.EXPAND, 0)
+        self._sizer_main.Add(self._sizer_button, 0, wx.EXPAND | wx.ALL, 10)
+
+    def get_data(self):
+        """
+        return the selected data
+        """
+        for item in self.list_of_ctrl:
+            rbox, data = item
+            if rbox.GetValue():
+                return data
Index: src/sas/sasgui/perspectives/invariant/media/invariant_help.rst
===================================================================
--- src/sas/sasgui/perspectives/invariant/media/invariant_help.rst	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/media/invariant_help.rst	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,128 @@
+.. invariant_help.rst
+
+.. This is a port of the original SasView html help file to ReSTructured text
+.. by S King, ISIS, during SasView CodeCamp-III in Feb 2015.
+
+.. |Ang| unicode:: U+212B
+.. |pi| unicode:: U+03C0
+.. |bigdelta| unicode:: U+0394
+.. |rho| unicode:: U+03C1
+.. |phi| unicode:: U+03C6
+
+Invariant Calculation Perspective
+=================================
+
+Description
+-----------
+
+The scattering, or Porod, invariant (Q*\) is a model-independent quantity that 
+can be easily calculated from scattering data.
+
+For two phase systems, the scattering invariant is defined as the integral of 
+the square of the wave transfer (Q) multiplied by the scattering cross section 
+over the full range of Q from zero to infinity, that is
+
+.. image:: image001.gif
+
+where *g = Q* for pinhole geometry (SAS) and *g = Qv* (the slit height) for  
+slit geometry (USAS).
+
+The worth of Q*\  is that it can be used to determine the volume fraction and 
+the specific area of a sample. Whilst these quantities are useful in their own 
+right they can also be used in further analysis.
+
+The difficulty with using Q*\  arises from the fact that experimental data is 
+never measured over the range 0 =< *Q* =< infinity. At best, combining USAS and 
+WAS data might cover the range 1e-5 =< *Q* =< 10 1/\ |Ang| . Thus it is usually 
+necessary to extrapolate the experimental data to low and high *Q*. For this
+
+High-*Q* region (>= *Qmax* in data)
+
+*  The power law function *C*/*Q*\ :sup:`4` is used where the constant 
+   *C* (= 2.\ |pi|\ .(\ |bigdelta|\ |rho|\ ).\ *Sv*\ ) is to be found by fitting part of data 
+   within the range *Q*\ :sub:`N-m` to *Q*\ :sub:`N` (where m < N).
+
+Low-*Q* region (<= *Qmin* in data)
+
+*  The Guinier function *I0.exp(-Rg*\ :sup:`2`\ *Q*\ :sup:`2`\ */3)* where *I0* 
+   and *Rg* are obtained by fitting as for the high-*Q* region above. 
+   Alternatively a power law can be used.
+
+.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+
+Using the perspective
+---------------------
+
+1) Select *Invariant* from the *Analysis* menu on the SasView toolbar.
+
+2) Load some data with the *Data Explorer*.
+
+3) Select a dataset and use the *Send To* button on the *Data Explorer* to load 
+   the dataset into the *Invariant* perspective.
+
+4) Use the *Customised Input* boxes on the *Invariant* perspective to subtract 
+   any background, specify the contrast (i.e. difference in SLDs - this must be 
+   specified for the eventual value of Q*\  to be on an absolute scale), or to 
+   rescale the data.
+
+5) Adjust the extrapolation range as necessary. In most cases the default 
+   values will suffice.
+
+6) Click the *Compute* button.
+
+7) To include a lower and/or higher Q range, check the relevant *Enable 
+   Extrapolate* check boxes.
+   
+   If power law extrapolations are chosen, the exponent can be either held 
+   fixed or fitted. The number of points, Npts, to be used for the basis of the 
+   extrapolation can also be specified.
+
+8) If the value of Q*\  calculated with the extrapolated regions is invalid, a 
+   red warning will appear at the top of the *Invariant* perspective panel.
+
+   The details of the calculation are available by clicking the *Details* 
+   button in the middle of the panel.
+
+.. image:: image005.gif
+
+.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+
+Parameters
+----------
+
+Volume Fraction
+^^^^^^^^^^^^^^^
+
+The volume fraction |phi| is related to Q*\  by
+
+.. image:: image002.gif
+
+where |bigdelta|\ |rho| is the SLD contrast.
+
+.. image:: image003.gif
+
+.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+
+Specific Surface Area
+^^^^^^^^^^^^^^^^^^^^^
+
+The specific surface area *Sv* is related to Q*\  by
+
+.. image:: image004.gif
+
+where *Cp* is the Porod constant.
+
+.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+
+Reference
+---------
+
+O. Glatter and O. Kratky
+Chapter 2 in *Small Angle X-Ray Scattering*
+Academic Press, New York, 1982
+
+http://physchem.kfunigraz.ac.at/sm/
+
+.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+
+.. note::  This help document was last changed by Steve King, 01May2015
Index: src/sas/sasgui/perspectives/invariant/media/report_template.html
===================================================================
--- src/sas/sasgui/perspectives/invariant/media/report_template.html	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/media/report_template.html	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,201 @@
+<html>
+<head>
+<meta http-equiv=Content-Type content="text/html; charset=windows-1252">
+<meta name=Generator >
+</head>
+<body lang=EN-US>
+<div class=WordSection1>
+<font size='4'>
+<p class=MsoNormal><b><span >%s</span></b></p>
+<p class=MsoNormal><span >%s</span></p>
+</font>
+<font size='3'>
+<p class=MsoNormal><b><span > Input
+parameters</span></b></p>
+<table class=MsoTableGrid border=1 cellspacing=0 cellpadding=0>
+ <tr>
+  <td width=170 valign=top>
+  <p class=MsoNormal > Parameter</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > Value</p>
+  </td>
+  <td width=170 valign=top >
+  <p class=MsoNormal > Unit</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=170 valign=top >
+  <p class=MsoNormal > scale</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=170 valign=top >&nbsp;</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=170 valign=top >
+  <p class=MsoNormal > porod constant</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=170 valign=top >&nbsp;</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=170 valign=top>
+  <p class=MsoNormal > background</p>
+  </td>
+  <td width=170 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=170 valign=top ><span > cm<sup>-1</sup></span></p>
+  </td>
+ </tr>
+ <tr>
+  <td width=170 valign=top >
+  <p class=MsoNormal > contrast</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=170 valign=top >
+  <p class=MsoNormal ><span > A<sup>-2</sup></span></p>
+  </td>
+ </tr>
+</table>
+<p class=MsoNormal><b><span > Extrapolation</span></b></p>
+<table class=MsoTableGrid border=1 cellspacing=0 cellpadding=0>
+ <tr>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=213 valign=top >
+  <p class=MsoNormal > npts low = %s</p>
+  </td>
+  <td width=213 valign=top > npts high = %s</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=213 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+ </tr>
+</table>
+<p class=MsoNormal><b><span > Outputs</span></b></p>
+<table class=MsoTableGrid border=1 cellspacing=0 cellpadding=0>
+ <tr>
+  <td width=110 valign=top >
+  <p class=MsoNormal s> Parameter</p>
+  </td>
+  <td width=150 valign=top >
+  <p class=MsoNormal >&nbsp;</p>
+  </td>
+  <td width=255 valign=top >
+  <p class=MsoNormal > Value</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal > Unit</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=110 rowspan=4 >
+  <p class=MsoNormal > invariant (Q<sup>*</sup>)</p>
+  </td>
+  <td width=150 valign=top >
+  <p class=MsoNormal > low Q extrapolation</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal > (cmA)<sup>-1</sup></p>
+  </td>
+ </tr>
+ <tr>
+  <td width=150 valign=top >
+  <p class=MsoNormal > data</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal > (cmA)<sup>-1</sup></p>
+  </td>
+ </tr>
+ <tr>
+  <td width=150 valign=top >
+  <p class=MsoNormal > high Q extrapolation</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal > (cmA)<sup>-1</sup></p>
+  </td>
+ </tr>
+ <tr>
+  <td width=150 valign=top >
+  <p class=MsoNormal > total</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal > (cmA)<sup>-1</sup></p>
+  </td>
+ </tr>
+ <tr>
+  <td width=110 valign=top >
+  <p class=MsoNormal > volume fraction</p>
+  </td>
+  <td width=150 valign=top >
+  <p class=MsoNormal >&nbsp;</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal >&nbsp;</p>
+  </td>
+ </tr>
+ <tr>
+  <td width=110 valign=top >
+  <p class=MsoNormal > specific surface</p>
+  </td>
+  <td width=150 valign=top >
+  <p class=MsoNormal >&nbsp;</p>
+  </td>
+  <td width=230 valign=top >
+  <p class=MsoNormal > %s</p>
+  </td>
+  <td width=65 valign=top >
+  <p class=MsoNormal >&nbsp;</p>
+  </td>
+ </tr>
+</table>
+</font>
+<p class=MsoNormal>&nbsp;</p>
+<font size=4>
+<br><b><span > Plot</span></b>
+<center>
+<br>Invariant Computation
+<br>Data: "%s"
+<img src="%s" height=400 width=550>
+</center>
+</font>
+</div>
+
+</body>
+
+</html>
Index: src/sas/sasgui/perspectives/invariant/report_dialog.py
===================================================================
--- src/sas/sasgui/perspectives/invariant/report_dialog.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/perspectives/invariant/report_dialog.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
@@ -0,0 +1,124 @@
+
+################################################################################
+# This software was developed by the University of Tennessee as part of the
+# Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
+# project funded by the US National Science Foundation.
+#
+# See the license text in license.txt
+#
+# copyright 2009, University of Tennessee
+################################################################################
+
+"""
+Dialog report panel to show and summarize the results of
+the invariant calculation.
+"""
+import wx
+import os
+import sys
+import logging
+
+from sas.sasgui.guiframe.report_dialog import BaseReportDialog
+
+class ReportDialog(BaseReportDialog):
+    """
+    The report dialog box.
+    """
+
+    def __init__(self, report_list, *args, **kwds):
+        """
+        Initialization. The parameters added to Dialog are:
+
+        :param report_list: list of html_str, text_str, image
+        from invariant_state
+        """
+        super(ReportDialog, self).__init__(report_list, *args, **kwds)
+
+        # title
+        self.SetTitle("Report: Invariant computaion")
+
+        # put image path in the report string
+        self.report_html = self.report_list[0] % "memory:img_inv.png"
+        # layout
+        self._setup_layout()
+
+    def onSave(self, event=None):
+        """
+        Save
+        """
+        # todo: complete saving fig file and as a txt file
+        dlg = wx.FileDialog(self, "Choose a file",
+                            wildcard=self.wild_card,
+                            style=wx.SAVE | wx.OVERWRITE_PROMPT | wx.CHANGE_DIR)
+        dlg.SetFilterIndex(0)  # Set .html files to be default
+
+        if dlg.ShowModal() != wx.ID_OK:
+            dlg.Destroy()
+            return
+
+        fName = dlg.GetPath()
+        ext_num = dlg.GetFilterIndex()
+        # set file extensions
+        if ext_num == (0 + 2 * self.index_offset):
+            # TODO: Sort this case out
+            ext = '.pdf'
+            img_ext = '_img.png'
+            fName = os.path.splitext(fName)[0] + ext
+            dlg.Destroy()
+
+            # pic (png) file path/name
+            pic_fname = os.path.splitext(fName)[0] + img_ext
+            # save the image for use with pdf writer
+            self.report_list[2].savefig(pic_fname)
+
+            # put the image file path in the html data
+            html = self.report_list[0] % str(pic_fname)
+
+            # make/open file in case of absence
+            f = open(fName, 'w')
+            f.close()
+            # write pdf as a pdf file
+            pdf = self.HTML2PDF(data=html, filename=fName)
+
+            # open pdf
+            if pdf:
+                try:
+                    # Windows
+                    os.startfile(str(fName))
+                except:
+                    try:
+                        # Mac
+                        os.system("open %s" % fName)
+                    except:
+                        # DO not open
+                        logging.error("Could not open file: %s" % sys.exc_value)
+            # delete image file
+            os.remove(pic_fname)
+            return
+        elif ext_num == (1 - self.index_offset):
+            ext = '.html'
+            img_ext = '_img4html.png'
+            report_frame = self.report_list[0]
+        elif ext_num == (2 - self.index_offset):
+            ext = '.txt'
+            # changing the image extension actually changes the image
+            # format on saving
+            img_ext = '_img4txt.pdf'
+            report = self.report_list[1]
+        else:
+            return
+
+        # file name
+        fName = os.path.splitext(fName)[0] + ext
+        dlg.Destroy()
+        # pic (png) file path/name
+        pic_fname = os.path.splitext(fName)[0] + img_ext
+        # put the image path in html string
+        if ext_num == (1 - self.index_offset):
+            report = report_frame % os.path.basename(pic_fname)
+
+        f = open(fName, 'w')
+        f.write(report)
+        f.close()
+        # save png file using pic_fname
+        self.report_list[2].savefig(pic_fname)
