Index: src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision eb1a386ba445241493784066d14c478c418cd3c6)
+++ src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision 116dd4c1f555326683a1e22876683ebde4458258)
@@ -2,11 +2,16 @@
 import sys
 
+from twisted.internet import threads
+
 import sas.qtgui.Utilities.GuiUtils as GuiUtils
 from PyQt5 import QtGui, QtCore, QtWidgets
 
+from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
+
 import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
-
 from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
 from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
+from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
+from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
 from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
 from sas.qtgui.Perspectives.Fitting.Constraints import Constraint
@@ -17,9 +22,13 @@
     """
 
-    def __init__(self, parent=None):
+    def __init__(self, parent=None, tab_id=1):
         super(ConstraintWidget, self).__init__()
         self.parent = parent
         self.setupUi(self)
         self.currentType = "FitPage"
+        self.tab_id = tab_id
+        # Page id for fitting
+        # To keep with previous SasView values, use 300 as the start offset
+        self.page_id = 300 + self.tab_id
 
         # Remember previous content of modified cell
@@ -114,5 +123,60 @@
         Perform the constrained/simultaneous fit
         """
-        pass
+        # Find out all tabs to fit
+        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
+
+        # Single fitter for the simultaneous run
+        sim_fitter = Fit()
+        sim_fitter.fitter_id = self.page_id
+
+        # prepare fitting problems for each tab
+        #
+        page_ids = []
+        fitter_id = 0
+        sim_fitter=[sim_fitter]
+        for tab in tabs_to_fit:
+            tab_object = ObjectLibrary.getObject(tab)
+            sim_fitter, fitter_id = tab_object.prepareFitters(fitter=sim_fitter[0], fit_id=fitter_id)
+            page_ids.append([tab_object.page_id])
+
+        # Create the fitting thread, based on the fitter
+        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
+
+        #if USING_TWISTED:
+        handler = None
+        updater = None
+        #else:
+        #    handler = ConsoleUpdate(parent=self.parent,
+        #                            manager=self,
+        #                            improvement_delta=0.1)
+        #    updater = handler.update_fit
+
+        batch_inputs = {}
+        batch_outputs = {}
+
+        # new fit thread object
+        calc_fit = FitThread(handler=handler,
+                             fn=sim_fitter,
+                             batch_inputs=batch_inputs,
+                             batch_outputs=batch_outputs,
+                             page_id=page_ids,
+                             updatefn=updater,
+                             completefn=completefn)
+
+        #if USING_TWISTED:
+        # start the trhrhread with twisted
+        calc_thread = threads.deferToThread(calc_fit.compute)
+        calc_thread.addCallback(self.onFitComplete)
+        calc_thread.addErrback(self.onFitFailed)
+        #else:
+        #    # Use the old python threads + Queue
+        #    calc_fit.queue()
+        #    calc_fit.ready(2.5)
+
+
+        #disable the Fit button
+        self.cmdFit.setText('Running...')
+        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
+        self.cmdFit.setEnabled(False)
 
     def onHelp(self):
@@ -172,5 +236,5 @@
     def onConstraintChange(self, row, column):
         """
-        Modify the constraint in-place.
+        Modify the constraint's "active" instance variable.
         """
         item = self.tblConstraints.item(row, column)
@@ -208,4 +272,22 @@
         pass
 
+    def onFitComplete(self, result):
+        """
+        Respond to the successful fit complete signal
+        """
+        pass
+
+    def onBatchFitComplete(self, result):
+        """
+        Respond to the successful batch fit complete signal
+        """
+        pass
+
+    def onFitFailed(self, reason):
+        """
+        """
+        print("FIT FAILED: ", reason)
+        pass
+ 
     def isTabImportable(self, tab):
         """
Index: src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision be8f4b04004041064478b2c80059a875e52e30d3)
+++ src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 116dd4c1f555326683a1e22876683ebde4458258)
@@ -124,5 +124,5 @@
         Add a new C&S fitting tab
         """
-        tab	= ConstraintWidget(parent=self)
+        tab	= ConstraintWidget(parent=self, tab_id=self.maxCSIndex)
         # Add this tab to the object library so it can be retrieved by scripting/jupyter
         tab_name = self.getCSTabName() # TODO update the tab name scheme
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision ba01ad131050e05053ee2ab2bf5c139e9617c9d9)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 116dd4c1f555326683a1e22876683ebde4458258)
@@ -212,4 +212,8 @@
         self.orig_poly_index = 3
 
+        # Page id for fitting
+        # To keep with previous SasView values, use 200 as the start offset
+        self.page_id = 200 + self.tab_id
+
         # Data for chosen model
         self.model_data = None
@@ -732,5 +736,15 @@
                 return True
         return False
-        #return True if (item.hasChildren() and isinstance(item.child(0).data(), Constraint)) else False
+
+    def rowHasActiveConstraint(self, row):
+        """
+        Finds out if row of the main model has an active constraint child
+        """
+        item = self._model_model.item(row,1)
+        if item.hasChildren():
+            c = item.child(0).data()
+            if isinstance(c, Constraint) and c.func and c.active:
+                return True
+        return False
 
     def selectParameters(self):
@@ -780,5 +794,5 @@
                     #preamble(s) +self._model_model.item(s, 1).child(0).data().func)
                     self._model_model.item(s, 1).child(0).data().func)
-                    for s in range(param_number) if self.rowHasConstraint(s)]
+                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
         return params
 
@@ -1056,29 +1070,9 @@
         Perform fitting on the current data
         """
-
-        # Data going in
-        data = self.logic.data
-        model = self.kernel_module
-        qmin = self.q_range_min
-        qmax = self.q_range_max
-        params_to_fit = self.parameters_to_fit
-
-        # Potential weights added directly to data
-        self.addWeightingToData(data)
-
-        # Potential smearing added
-        # Remember that smearing_min/max can be None ->
-        # deal with it until Python gets discriminated unions
-        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
-
-        # These should be updating somehow?
+        # initialize fitter constants
         fit_id = 0
-        constraints = self.getConstraintsForModel()
-        smearer = None
-        page_id = [210]
         handler = None
         batch_inputs = {}
         batch_outputs = {}
-        list_page_id = [page_id]
         #---------------------------------
         if USING_TWISTED:
@@ -1091,23 +1085,6 @@
             updater = handler.update_fit
 
-        # Parameterize the fitter
-        fitters = []
-        for fit_index in self.all_data:
-            fitter = Fit()
-            data = GuiUtils.dataFromItem(fit_index)
-            try:
-                fitter.set_model(model, fit_id, params_to_fit, data=data,
-                             constraints=constraints)
-            except ValueError as ex:
-                logging.error("Setting model parameters failed with: %s" % ex)
-                return
-
-            qmin, qmax, _ = self.logic.computeRangeFromData(data)
-            fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
-                            qmax=qmax)
-            fitter.select_problem_for_fit(id=fit_id, value=1)
-            fitter.fitter_id = page_id
-            fit_id += 1
-            fitters.append(fitter)
+        # Prepare the fitter object
+        fitters, _ = self.prepareFitters()
 
         # Create the fitting thread, based on the fitter
@@ -1115,10 +1092,10 @@
 
         calc_fit = FitThread(handler=handler,
-                                fn=fitters,
-                                batch_inputs=batch_inputs,
-                                batch_outputs=batch_outputs,
-                                page_id=list_page_id,
-                                updatefn=updater,
-                                completefn=completefn)
+                            fn=fitters,
+                            batch_inputs=batch_inputs,
+                            batch_outputs=batch_outputs,
+                            page_id=[[self.page_id]],
+                            updatefn=updater,
+                            completefn=completefn)
 
         if USING_TWISTED:
@@ -1209,4 +1186,55 @@
         chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
         self.lblChi2Value.setText(chi2_repr)
+
+    def prepareFitters(self, fitter=None, fit_id=0):
+        """
+        Prepare the Fitter object for use in fitting
+        """
+        # fitter = None -> single/batch fitting
+        # fitter = Fit() -> simultaneous fitting
+
+        # Data going in
+        data = self.logic.data
+        model = self.kernel_module
+        qmin = self.q_range_min
+        qmax = self.q_range_max
+        params_to_fit = self.parameters_to_fit
+
+        # Potential weights added directly to data
+        self.addWeightingToData(data)
+
+        # Potential smearing added
+        # Remember that smearing_min/max can be None ->
+        # deal with it until Python gets discriminated unions
+        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
+
+        constraints = self.getConstraintsForModel()
+        smearer = None
+        handler = None
+        batch_inputs = {}
+        batch_outputs = {}
+
+        fitters = []
+        for fit_index in self.all_data:
+            fitter_single = Fit() if fitter is None else fitter
+            data = GuiUtils.dataFromItem(fit_index)
+            try:
+                fitter_single.set_model(model, fit_id, params_to_fit, data=data,
+                             constraints=constraints)
+            except ValueError as ex:
+                logging.error("Setting model parameters failed with: %s" % ex)
+                return
+
+            qmin, qmax, _ = self.logic.computeRangeFromData(data)
+            fitter_single.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
+                            qmax=qmax)
+            fitter_single.select_problem_for_fit(id=fit_id, value=1)
+            if fitter is None:
+                # Assign id to the new fitter only
+                fitter_single.fitter_id = [self.page_id]
+            fit_id += 1
+            fitters.append(fitter_single)
+
+        return fitters, fit_id
 
     def iterateOverModel(self, func):
