Index: run.py
===================================================================
--- run.py	(revision db05c441f1bca349861936b538a8d7e34c9af8a6)
+++ run.py	(revision 328f2ef2edfbc752b7d78076340abac403652384)
@@ -155,6 +155,9 @@
     # so logging can be done during initialization process too
     root = abspath(dirname(realpath(sys.argv[0])))
-    addpath(joinpath(root, 'src','sas'))
-    from logger_config import SetupLogger
+
+    addpath(joinpath(root, 'src'))
+    addpath(joinpath(root, joinpath('..', 'sasmodels'))) # dependency (for loading custom_config.py during log setup)
+
+    from sas.logger_config import SetupLogger
     logger = SetupLogger(__name__).config_development()
 
Index: src/sas/logger_config.py
===================================================================
--- src/sas/logger_config.py	(revision e4335ae394f216c131b40e0ebbd740e37d9c6aa7)
+++ src/sas/logger_config.py	(revision 799963cda61da475c5dadfb1e9cfad6f1a3de534)
@@ -7,4 +7,6 @@
 
 import pkg_resources
+
+from sas import get_custom_config
 
 '''
@@ -39,5 +41,22 @@
         logging.captureWarnings(True)
 
+        self._disable_debug_from_config()
+
         return logger
+
+    def _disable_debug_from_config(self):
+        '''disable DEBUG logs as per user configuration (DEBUG logs disabled by default)'''
+        disable_debug = True
+        custom_config = get_custom_config()
+
+        if hasattr(custom_config, "FILTER_DEBUG_LOGS"):
+            if type(custom_config.FILTER_DEBUG_LOGS) is bool:
+                disable_debug = custom_config.FILTER_DEBUG_LOGS
+            else:
+                logging.warning("FILTER_DEBUG_LOGS has invalid value in custom_config.py")
+
+        if disable_debug:
+            # logging.info("Note: DEBUG logs are disabled.")
+            logging.disable(logging.DEBUG)
 
     def _read_config_file(self):
Index: src/sas/qtgui/MainWindow/DataExplorer.py
===================================================================
--- src/sas/qtgui/MainWindow/DataExplorer.py	(revision c7f259d9c361099d23d32a7c777368284401301c)
+++ src/sas/qtgui/MainWindow/DataExplorer.py	(revision 4bf582930e4cac2a0e2217cc1a4748498d059668)
@@ -4,4 +4,5 @@
 import time
 import logging
+import re
 
 from PyQt5 import QtCore
@@ -36,4 +37,12 @@
     # The controller which is responsible for managing signal slots connections
     # for the gui and providing an interface to the data model.
+
+    # This matches the ID of a plot created using FittingLogic._create1DPlot, e.g.
+    # "5 [P(Q)] modelname"
+    # or
+    # "4 modelname".
+    # Useful for determining whether the plot in question is for an intermediate result, such as P(Q) or S(Q) in the
+    # case of a product model; the identifier for this is held in square brackets, as in the example above.
+    theory_plot_ID_pattern = re.compile(r"^([0-9]+)\s+(\[(.*)\]\s+)?(.*)$")
 
     def __init__(self, parent=None, guimanager=None, manager=None):
@@ -525,4 +534,9 @@
                 self.active_plots[plot_id].replacePlot(plot_id, plot)
             else:
+                # Don't plot intermediate results, e.g. P(Q), S(Q)
+                match = self.theory_plot_ID_pattern.match(plot_id)
+                # 2nd match group contains the identifier for the intermediate result, if present (e.g. "[P(Q)]")
+                if match and match.groups()[1] != None:
+                    continue
                 # 'sophisticated' test to generate standalone plot for residuals
                 if 'esiduals' in plot.title:
@@ -1240,5 +1254,4 @@
             #if current_tab_name in self.theory_model.item(current_index).text():
             if current_tab_name == self.theory_model.item(current_index).text():
-                return
                 self.theory_model.removeRow(current_index)
                 break
Index: src/sas/qtgui/MainWindow/DataManager.py
===================================================================
--- src/sas/qtgui/MainWindow/DataManager.py	(revision 0261bc13615593f5eb2e08941252c044b8bb2e3b)
+++ src/sas/qtgui/MainWindow/DataManager.py	(revision 4e255d1e4be187b7380836fb95aaec4b4f362283)
@@ -24,5 +24,5 @@
 import json
 import time
-from io import StringIO
+from io import BytesIO
 import numpy as np
 
@@ -368,5 +368,5 @@
             # ndarray
             if isinstance(o, np.ndarray):
-                buffer = StringIO()
+                buffer = BytesIO()
                 np.save(buffer, o)
                 buffer.seek(0)
@@ -435,5 +435,5 @@
             # ndarray
             if cls == np.ndarray:
-                buffer = StringIO()
+                buffer = BytesIO()
                 buffer.write(data['data'].encode('latin-1'))
                 buffer.seek(0)
Index: src/sas/qtgui/Perspectives/Fitting/FittingLogic.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingLogic.py	(revision 87dfca4108319b9f2e8294ec16eebae1042a3693)
+++ src/sas/qtgui/Perspectives/Fitting/FittingLogic.py	(revision b4d05bdc03c6c40fee47932a9a2d20921b1bbe4e)
@@ -132,14 +132,9 @@
         self._data.ymax = ymax
 
-    def new1DPlot(self, return_data, tab_id):
-        """
-        Create a new 1D data instance based on fitting results
-        """
-        # Unpack return data from Calc1D
-        x, y, page_id, state, weight,\
-        fid, toggle_mode_on, \
-        elapsed, index, model,\
-        data, update_chisqr, source = return_data
-
+    def _create1DPlot(self, tab_id, x, y, model, data, component=None):
+        """
+        For internal use: create a new 1D data instance based on fitting results.
+        'component' is a string indicating the model component, e.g. "P(Q)"
+        """
         # Create the new plot
         new_plot = Data1D(x=x, y=y)
@@ -150,10 +145,9 @@
 
         new_plot.group_id = data.group_id
-        new_plot.id = str(tab_id) + " " + model.id
-
-        if data.filename:
-            new_plot.name = model.name + " [" + data.filename + "]" # data file
-        else:
-            new_plot.name = model.name + " [" + model.id + "]"  # theory
+        new_plot.id = str(tab_id) + " " + ("[" + component + "] " if component else "") + model.id
+
+        # use data.filename for data, use model.id for theory
+        id_str = data.filename if data.filename else model.id
+        new_plot.name = model.name + ((" " + component) if component else "") + " [" + id_str + "]"
 
         new_plot.title = new_plot.name
@@ -162,4 +156,18 @@
 
         return new_plot
+
+    def new1DPlot(self, return_data, tab_id):
+        """
+        Create a new 1D data instance based on fitting results
+        """
+        # Unpack return data from Calc1D
+        x, y, page_id, state, weight,\
+        fid, toggle_mode_on, \
+        elapsed, index, model, \
+        data, update_chisqr, source, \
+        unsmeared_output, unsmeared_data, unsmeared_error, \
+        pq_values, sq_values = return_data
+
+        return self._create1DPlot(tab_id, x, y, model, data)
 
     def new2DPlot(self, return_data):
@@ -205,4 +213,27 @@
         return new_plot
 
+    def new1DProductPlots(self, return_data, tab_id):
+        """
+        If return_data contains separated P(Q) and/or S(Q) data, create 1D plots for each and return as the tuple
+        (pq_plot, sq_plot). If either are unavailable, the corresponding plot is None.
+        """
+        # Unpack return data from Calc1D
+        x, y, page_id, state, weight, \
+        fid, toggle_mode_on, \
+        elapsed, index, model, \
+        data, update_chisqr, source, \
+        unsmeared_output, unsmeared_data, unsmeared_error, \
+        pq_values, sq_values = return_data
+
+        pq_plot = None
+        sq_plot = None
+
+        if pq_values is not None:
+            pq_plot = self._create1DPlot(tab_id, x, pq_values, model, data, component="P(Q)")
+        if sq_values is not None:
+            sq_plot = self._create1DPlot(tab_id, x, sq_values, model, data, component="S(Q)")
+
+        return pq_plot, sq_plot
+
     def computeDataRange(self):
         """
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 10fee3755828e8d3a7f1176fc9df275946d33e1d)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision b4d05bdc03c6c40fee47932a9a2d20921b1bbe4e)
@@ -2113,7 +2113,9 @@
             self.updateModelIndex(fitted_data)
         else:
-            name = self.nameForFittedData(self.kernel_module.id)
+            if not fitted_data.name:
+                name = self.nameForFittedData(self.kernel_module.id)
+            else:
+                name = fitted_data.name
             fitted_data.title = name
-            fitted_data.name = name
             fitted_data.filename = name
             fitted_data.symbol = "Line"
@@ -2229,4 +2231,13 @@
         self.model_data = fitted_data
 
+        # Create plots for intermediate product data
+        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
+        if pq_data is not None:
+            pq_data.symbol = "Line"
+            self.createNewIndex(pq_data)
+        if sq_data is not None:
+            sq_data.symbol = "Line"
+            self.createNewIndex(sq_data)
+
     def complete2D(self, return_data):
         """
Index: src/sas/qtgui/Perspectives/Fitting/ModelThread.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ModelThread.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
+++ src/sas/qtgui/Perspectives/Fitting/ModelThread.py	(revision 3ae917937db3898ec8e94fe0aa83d82bec4edd19)
@@ -222,12 +222,15 @@
             s_model = self.model.s_model
             p_model = self.model.p_model
-        elif hasattr(self.model, "get_composition_models"):
-            p_model, s_model = self.model.get_composition_models()
-
-        if p_model is not None and s_model is not None:
-            sq_values = numpy.zeros((len(self.data.x)))
-            pq_values = numpy.zeros((len(self.data.x)))
-            sq_values[index] = s_model.evalDistribution(self.data.x[index])
-            pq_values[index] = p_model.evalDistribution(self.data.x[index])
+        elif hasattr(self.model, "calc_composition_models"):
+            results = self.model.calc_composition_models(self.data.x[index])
+            if results is not None:
+                pq_values, sq_values = results
+
+        if pq_values is None or sq_values is None:
+            if p_model is not None and s_model is not None:
+                sq_values = numpy.zeros((len(self.data.x)))
+                pq_values = numpy.zeros((len(self.data.x)))
+                sq_values[index] = s_model.evalDistribution(self.data.x[index])
+                pq_values[index] = p_model.evalDistribution(self.data.x[index])
 
         elapsed = time.time() - self.starttime
@@ -243,5 +246,7 @@
                     self.data,
                     self.update_chisqr,
-                    self.source)
+                    self.source,
+                    unsmeared_output, unsmeared_data, unsmeared_error,
+                    pq_values, sq_values)
         else:
             self.complete(x=self.data.x[index], y=output[index],
Index: src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingLogicTest.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingLogicTest.py	(revision 80468f6c91cf372023748231c493da2c7009a763)
+++ src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingLogicTest.py	(revision e752ab8241051d2dfff8481f44214d3ad205adac)
@@ -100,6 +100,8 @@
         data.id = "poop"
         return_data = (data.x,data.y, 7, None, None,
-                        0, True, 0.0, 1, data,
-                        data, False, None)
+                       0, True, 0.0, 1, data,
+                       data, False, None,
+                       None, None, None,
+                       None, None)
 
         new_plot = self.logic.new1DPlot(return_data=return_data, tab_id=0)
Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision 685e0e3447e0a2b4fe6f20479d5eac41f86c67f8)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision 3933ee9d9e6a4912f1c24b7d1818dd791af58b8a)
@@ -312,4 +312,12 @@
         return clone
 
+
+def getMonospaceFont():
+    """Convenience function; returns a monospace font to be used in any shells, code editors, etc."""
+
+    # Note: Consolas is only available on Windows; the style hint is used on other operating systems
+    font = QtGui.QFont("Consolas", 10)
+    font.setStyleHint(QtGui.QFont.Monospace, QtGui.QFont.PreferQuality)
+    return font
 
 def createModelItemWithPlot(update_data, name=""):
Index: src/sas/qtgui/Utilities/IPythonWidget.py
===================================================================
--- src/sas/qtgui/Utilities/IPythonWidget.py	(revision fbfc488f82ca4192217ad32b69f00fc4a4e137ab)
+++ src/sas/qtgui/Utilities/IPythonWidget.py	(revision 3933ee9d9e6a4912f1c24b7d1818dd791af58b8a)
@@ -4,4 +4,6 @@
 
 from PyQt5 import QtSvg
+
+from sas.qtgui.Utilities import GuiUtils
 
 def new_load_qt(api_options):
@@ -35,4 +37,7 @@
         app = guisupport.get_app_qt4()
 
+        font = GuiUtils.getMonospaceFont()
+        self.font = font
+
         # Create an in-process kernel
         kernel_manager = QtInProcessKernelManager()
Index: src/sas/qtgui/Utilities/ModelEditor.py
===================================================================
--- src/sas/qtgui/Utilities/ModelEditor.py	(revision 26970b3780381fa0bceb5820f27d9e321d8b0a6e)
+++ src/sas/qtgui/Utilities/ModelEditor.py	(revision 3933ee9d9e6a4912f1c24b7d1818dd791af58b8a)
@@ -3,4 +3,5 @@
 
 from sas.qtgui.Utilities.UI.ModelEditor import Ui_ModelEditor
+from sas.qtgui.Utilities import GuiUtils
 
 class ModelEditor(QtWidgets.QDialog, Ui_ModelEditor):
@@ -30,4 +31,6 @@
         self.highlight = PythonHighlighter(self.txtEditor.document())
 
+        self.txtEditor.setFont(GuiUtils.getMonospaceFont())
+
     def addSignals(self):
         """
Index: src/sas/qtgui/Utilities/PluginDefinition.py
===================================================================
--- src/sas/qtgui/Utilities/PluginDefinition.py	(revision 060413cb937895c088815c31b92fc9252f723562)
+++ src/sas/qtgui/Utilities/PluginDefinition.py	(revision 3933ee9d9e6a4912f1c24b7d1818dd791af58b8a)
@@ -4,4 +4,5 @@
 
 from sas.qtgui.Utilities.UI.PluginDefinitionUI import Ui_PluginDefinition
+from sas.qtgui.Utilities import GuiUtils
 
 # txtName
@@ -64,4 +65,5 @@
 """
         self.txtFunction.insertPlainText(text)
+        self.txtFunction.setFont(GuiUtils.getMonospaceFont())
 
         # Validators
Index: src/sas/sasview/custom_config.py
===================================================================
--- src/sas/sasview/custom_config.py	(revision efe730d4bba7a05c203957e4495eece8359fcd77)
+++ src/sas/sasview/custom_config.py	(revision 799963cda61da475c5dadfb1e9cfad6f1a3de534)
@@ -16,2 +16,5 @@
 DEFAULT_PERSPECTIVE = "Fitting"
 SAS_OPENCL = "None"
+
+# Logging options
+FILTER_DEBUG_LOGS = True
