diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index b9fcacb39e..f1fbd015d6 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -23,6 +23,7 @@ from sas.qtgui.Perspectives.Fitting.FitPage import FitPage from sas.qtgui.Perspectives.Fitting.FitThread import FitThread from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic +from sas.qtgui.Perspectives.Fitting.InViewWidget import InViewWidget from sas.qtgui.Perspectives.Fitting.MagnetismWidget import MagnetismWidget from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D, Calc2D from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint @@ -108,6 +109,8 @@ def __init__(self, parent: QtWidgets.QWidget | None = None, data: Any | None = N # Logics.data contains a single Data1D/Data2D object self._logic = [FittingLogic()] + self._in_view_widget = None + # Main GUI setup up self.setupUi(self) self.setWindowTitle("Fitting") @@ -457,6 +460,7 @@ def setEnablementOnDataLoad(self) -> None: self.chkChainFit.setVisible(True) # This panel is not designed to view individual fits, so disable plotting self.cmdPlot.setVisible(False) + self.cmdInView.setEnabled(False) # Similarly on other tabs self.options_widget.setEnablementOnDataLoad() self.onSelectModel() @@ -536,6 +540,16 @@ def toggle2D(self, isChecked: bool) -> None: """ Enable/disable the controls dependent on 1D/2D data instance """ self.chkMagnetism.setEnabled(isChecked) self.is2D = isChecked + + if isChecked: + # 2D view is toggled on, therefore disable InView button, + # and close InView widget if opened + self.cmdInView.setEnabled(False) + if getattr(self, '_in_view_widget', None): + self._in_view_widget.close() + else: + # 2D view is toggled off, therefore enable InView button + self.cmdInView.setEnabled(True) # Reload the current model if self.logic.kernel_module: self.onSelectModel() @@ -591,6 +605,7 @@ def initializeSignals(self) -> None: self.cmdFit.clicked.connect(self.onFit) self.cmdPlot.clicked.connect(self.onPlot) self.cmdHelp.clicked.connect(self.onHelp) + self.cmdInView.clicked.connect(self.onInView) # Respond to change in parameters from the UI self._model_model.dataChanged.connect(self.onMainParamsChange) @@ -1658,6 +1673,72 @@ def onSelectCategory(self) -> None: self.cbModel.addItems(sorted(models_to_show)) self.cbModel.blockSignals(False) + def onInView(self): + """ + Trigers the display of 'InView' + """ + if self._in_view_widget is None: + self._in_view_widget = InViewWidget(parent=self) + self._in_view_widget.destroyed.connect(lambda: setattr(self, '_in_view_widget', None)) + + # When InView is closed, re-enable editing and clear reference + def _on_inview_closed(): + self._in_view_widget = None + # Enabling paramters edits + self.lstParams.setEnabled(True) + self.cbCategory.setEnabled(True) + self.cmdFit.setEnabled(True) + self.cmdPlot.setEnabled(True) + + + self._in_view_widget.destroyed.connect(_on_inview_closed) + + self.lstParams.setEnabled(False) + self.cbCategory.setEnabled(False) + self.cmdFit.setEnabled(False) + self.cmdPlot.setEnabled(False) + + ### + # Validate parameter bounds for InView (no +/-inf allowed) + params = list(self.main_params_to_fit) + if self.chkPolydispersity.isChecked(): + params += list(self.polydispersity_widget.poly_params_to_fit) + has_infinite_bounds = False + for p in params: + try: + details = self.logic.kernel_module.details.get(p) + if details is None or len(details) < 3: + continue + pmin, pmax = details[1], details[2] + # Disallow non-finite bounds + if not np.isfinite(pmin) or not np.isfinite(pmax): + has_infinite_bounds = True + break + except Exception: + # If anything odd, be conservative and flag + has_infinite_bounds = True + break + if has_infinite_bounds: + QtWidgets.QMessageBox.warning(self, "InView", + "Some parameters have infinite bounds. Please set appropriate min/max ranges.") + # Re-enable controls since we are not opening InView + self.lstParams.setEnabled(True) + self.cbCategory.setEnabled(True) + self.cmdFit.setEnabled(True) + self.cmdPlot.setEnabled(True) + return + ### + + # Pushing the 1D data set to InView + self._in_view_widget.setData(self.logic.data) + + # Construct sliders from the Fit Page + self._in_view_widget.initFromFitPage(self) + + self._in_view_widget.show() + self._in_view_widget.raise_() + self._in_view_widget.activateWindow() + def onHelp(self): """ Show the "Fitting" section of help @@ -3362,6 +3443,10 @@ def enableInteractiveElements(self) -> None: self.fit_started = False self.setInteractiveElements(True) + # Re-enable InView button for 1D data + if not getattr(self, 'is2D', False): + self.cmdInView.setEnabled(True) + def disableInteractiveElements(self) -> None: """ Set button caption on fitting/calculate start @@ -3373,6 +3458,15 @@ def disableInteractiveElements(self) -> None: self.cmdFit.setText('Stop fit') self.setInteractiveElements(False) + # Disable InView button and close the widget if opened + self.cmdInView.setEnabled(False) + isInView = getattr(self, '_in_inview_widget', None) + if isInView is not None: + try: + isInView.close() + except Exception: + pass + def disableInteractiveElementsOnCalculate(self) -> None: """ Set button caption on fitting/calculate start @@ -3384,6 +3478,15 @@ def disableInteractiveElementsOnCalculate(self) -> None: self.cmdFit.setText('Running...') self.setInteractiveElements(False) + # Disable InView button and close the widget if opened + self.cmdInView.setEnabled(False) + isInView = getattr(self, '_in_inview_widget', None) + if isInView is not None: + try: + isInView.close() + except Exception: + pass + def readFitPage(self, fp: FitPage) -> None: """ Read in state from a fitpage object and update GUI diff --git a/src/sas/qtgui/Perspectives/Fitting/InViewWidget.py b/src/sas/qtgui/Perspectives/Fitting/InViewWidget.py new file mode 100644 index 0000000000..638e2e3179 --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/InViewWidget.py @@ -0,0 +1,318 @@ +import copy +import logging + +from PySide6 import QtCore, QtWidgets + +from sas.qtgui.Perspectives.Fitting import FittingUtilities +from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D +from sas.qtgui.Perspectives.Fitting.UI.InViewWidgetUI import Ui_InViewWidgetUI +from sas.qtgui.Plotting.Plotter import PlotterWidget +from sas.qtgui.Plotting.PlotterData import Data1D +from sas.qtgui.Utilities import GuiUtils +from sas.system import HELP_SYSTEM + +logger = logging.getLogger(__name__) + +class InViewWidget(QtWidgets.QWidget, Ui_InViewWidgetUI): + def __init__(self, parent: QtWidgets.QWidget): + super().__init__(parent) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.setWindowFlag(QtCore.Qt.Window, True) + self.setupUi(self) + self.communicator = GuiUtils.Communicate() + self.plotter = PlotterWidget(self, self) + self._has_data = False + if self.plotBox.layout() is not None: + self.plotBox.layout().addWidget(self.plotter) + self._fw = None + self._plot_name = "InView Model" + self._param_values = {} + self._param_info = {} + self._slider_meta = {} + self._update_timer = QtCore.QTimer(self) + self._update_timer.setSingleShot(True) + self._update_timer.setInterval(75) + self._update_timer.timeout.connect(self._recompute_model) + if hasattr(self, 'cmdUpdateParam'): + self.cmdUpdateParam.clicked.connect(self._apply_to_fitpage) + if hasattr(self, 'cmdHelp'): + self.cmdHelp.clicked.connect(self.onHelp) + if hasattr(self, 'cmdClose'): + self.cmdClose.clicked.connect(self.close) + + def setData(self, data): + if not isinstance(data, Data1D): + return + self._has_data = True + #self.plotter.plot(data=data) + + try: + x = data.x + y = data.y + if y is not None and hasattr(y, "__len__") and len(x) == len(y): + self.plotter.plot(data=data) + except Exception: + pass + + def initFromFitPage(self, fitting_widget): + self._fw = fitting_widget + params = list(self._fw.main_params_to_fit) + try: + if self._fw.chkPolydispersity.isChecked(): + params += list(self._fw.polydispersity_widget.poly_params_to_fit) + except Exception: + pass + seen = set() + params = [p for p in params if not (p in seen or seen.add(p))] + self._param_info = {} + for name in params: + try: + model_key = self._fw.getModelKeyFromName(name) + row = self._fw.getRowFromName(name) + if row is None: + continue + val = GuiUtils.toDouble(self._fw.model_dict[model_key].item(row, 1).text()) + except Exception: + continue + try: + _, pmin, pmax = self._fw.logic.kernel_module.details.get(name, ('', None, None)) + except Exception: + pmin = None + pmax = None + if pmin is None or pmax is None or not (pmax > pmin): + delt = abs(val) if val is not None else 1.0 + if delt <= 0: + delt = 1.0 + pmin = (val - delt) if val is not None else -1.0 + pmax = (val + delt) if val is not None else 1.0 + ### + unit = "" + try: + d = self._fw.logic.kernel_module.details.get(name) + if d and len(d) >= 1 and d[0]: + unit = str(d[0]) + elif name.endswith('.width'): + base = name[:-6] + bd = self._fw.logic.kernel_module.details.get(base) + if bd and len(bd) >= 1 and bd[0]: + unit = str(bd[0]) + except Exception: + unit = "" + + self._param_info[name] = { + 'min': float(pmin), + 'max': float(pmax), + 'value': float(val), + 'unit': unit + } + #self._param_info[name] = {'min': float(pmin), 'max': float(pmax), 'value': float(val)} + ### + self._build_sliders(list(self._param_info.keys())) + self._recompute_model() + + def _build_sliders(self, params): + layout = self.sliderBox.layout() + while layout.count(): + item = layout.takeAt(0) + w = item.widget() + if w is not None: + w.deleteLater() + else: + sub = item.layout() + if sub is not None: + while sub.count(): + child = sub.takeAt(0) + cw = child.widget() + if cw is not None: + cw.deleteLater() + sub.deleteLater() + self._slider_meta = {} + steps = 1000 + for name in params: + info = self._param_info[name] + container = QtWidgets.QWidget(self) + h = QtWidgets.QHBoxLayout(container) + h.setContentsMargins(0, 0, 0, 0) + lbl = QtWidgets.QLabel(self._display_label_for_param(name), container) + slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, container) + slider.setMinimum(0) + slider.setMaximum(steps) + slider.setTracking(True) + spin = QtWidgets.QDoubleSpinBox(container) + spin.setDecimals(8) + spin.setMinimum(info['min']) + spin.setMaximum(info['max']) + ### + if info['max'] > info['min']: + spin.setSingleStep((info['max'] - info['min'])/100.0) + + unit = info.get('unit', '') + unit_lbl = QtWidgets.QLabel("", container) + unit_lbl.setObjectName(f"lblUnit_{name}") + unit_lbl.setContentsMargins(6, 0, 0, 0) + if unit: + unit_lbl.setTextFormat(QtCore.Qt.RichText) + unit_lbl.setText(GuiUtils.convertUnitToHTML(unit)) + unit_lbl.setVisible(bool(unit)) + + val = info['value'] + spin.setValue(val) + ### + pos = self._value_to_slider(val, info['min'], info['max'], steps) + slider.setValue(pos) + h.addWidget(lbl) + h.addWidget(slider, 2) + h.addWidget(spin) + h.addWidget(unit_lbl) + layout.addWidget(container) + self._slider_meta[name] = {'slider': slider, 'spin': spin, 'min': info['min'], 'max': info['max'], 'steps': steps} + self._param_values[name] = val + slider.valueChanged.connect(lambda v, p=name: self._on_slider_changed(p, v)) + spin.valueChanged.connect(lambda v, p=name: self._on_spin_changed(p, v)) + + def _display_label_for_param(self, name: str) -> str: + if name.endswith('.width'): + try: + poly_model = self._fw.polydispersity_widget.poly_model + for row in range(poly_model.rowCount()): + item = poly_model.item(row, 0) + if item is None: + continue + if item.data(QtCore.Qt.UserRole) == name: + txt = item.text() + if txt: + return str(txt) + except Exception: + pass + base = name[:-6] + return f"Distribution of {base}" + return name + + def _slider_to_value(self, pos, min_v, max_v, steps): + return float(min_v + (max_v - min_v) * (float(pos) / float(steps))) + + def _value_to_slider(self, val, min_v, max_v, steps): + if max_v == min_v: + return 0 + t = (val - min_v) / (max_v - min_v) + t = min(max(t, 0.0), 1.0) + return int(round(t * steps)) + + def _on_slider_changed(self, name, pos): + meta = self._slider_meta.get(name) + if not meta: + return + val = self._slider_to_value(pos, meta['min'], meta['max'], meta['steps']) + self._param_values[name] = val + spin = meta['spin'] + try: + spin.blockSignals(True) + spin.setValue(val) + finally: + spin.blockSignals(False) + self._update_timer.start() + + def _on_spin_changed(self, name, val): + meta = self._slider_meta.get(name) + if not meta: + return + self._param_values[name] = float(val) + slider = meta['slider'] + pos = self._value_to_slider(val, meta['min'], meta['max'], meta['steps']) + try: + slider.blockSignals(True) + slider.setValue(pos) + finally: + slider.blockSignals(False) + self._update_timer.start() + + def _recompute_model(self): + if self._fw is None or not self._has_data: + return + model = copy.deepcopy(self._fw.logic.kernel_module) + ##### + # 1) If polydispersity is enabled, push slider widths into the polydispersity widget + try: + if self._fw.chkPolydispersity.isChecked(): + for pname, pinfo in self._param_info.items(): + if '.width' in pname: + val = float(self._param_values.get(pname, pinfo['value'])) + self._fw.polydispersity_widget.poly_params[pname] = val + except Exception: + pass + + # 2) Apply extras (polydispersity + magnetism) using the Fit page widgets + try: + # This calls polydispersity_widget.updateModel(model) and magnetism_widget.updateModel(model) + self._fw.updateKernelModelWithExtraParams(model) + except Exception: + pass + + # 3) Apply all slider params to the model (non-PDI and as a final override for PDI widths) + for pname, pinfo in self._param_info.items(): + try: + model.setParam(pname, float(self._param_values.get(pname, pinfo['value']))) + except Exception: + pass + ##### + data = self._fw.logic.data + qmin = self._fw.q_range_min + qmax = self._fw.q_range_max + smearer = self._fw.smearing_widget.smearer() + weight = FittingUtilities.getWeight(data=data, is2d=self._fw.is2D, flag=self._fw.weighting) + calc = Calc1D(model=model, page_id=0, data=data, qmin=qmin, qmax=qmax, smearer=smearer, weight=weight, update_chisqr=False, completefn=self._complete_inview) + try: + ret = calc.compute() + if ret is not None: + self._complete_inview(ret) + except Exception: + return + + def _complete_inview(self, return_data): + try: + fitted = self._fw.logic.new1DPlot(return_data, self._fw.tab_id) + except Exception: + return + fitted.name = self._plot_name + fitted.symbol = "Line" + + if self._plot_name in self.plotter.plot_dict: + self.plotter.replacePlot(self._plot_name, fitted) + else: + self.plotter.plot(data=fitted) + + def _apply_to_fitpage(self): + if self._fw is None: + return + # Update the Fit page table only; + for name, val in self._param_values.items(): + row = self._fw.getRowFromName(name) + if row is None: + continue + model_key = self._fw.getModelKeyFromName(name) + model = self._fw.model_dict.get(model_key) + if model is None: + continue + try: + model.item(row, 1).setText(GuiUtils.formatNumber(val, high=True)) + except Exception: + model.item(row, 1).setText(str(val)) + # Let the view refresh and dataChanged handlers run + QtWidgets.QApplication.processEvents() + + # Stoping any pending slider-triggered recompute , after that, closing the window + if hasattr(self, '_update_timer'): + self._update_timer.stop() + + def onHelp(self): + """ + Show the InView help in the embedded documentation window + """ + if not HELP_SYSTEM.path: + logger.error("Help documentation was not found.") + return + base = HELP_SYSTEM.path + help_location = base / "user" / "qtgui" / "Perspectives" / "Fitting" / "inview.html" + if not help_location.exists(): + help_location = base / "user" / "qtgui" / "Perspectives" / "Fitting" / "fitting_help.html" + self._help_window = GuiUtils.showHelp(help_location) diff --git a/src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui b/src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui index f1fea3c5aa..df810fd518 100755 --- a/src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui +++ b/src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui @@ -41,6 +41,31 @@ + + + + true + + + + 0 + 0 + + + + + 93 + 28 + + + + InView + + + false + + + @@ -150,6 +175,9 @@ + + true + 0 @@ -440,7 +468,7 @@ false - + Magnetism @@ -465,8 +493,6 @@ chk2DView chkMagnetism chkChainFit - lstMagnetic - cmdMagneticDisplay cmdPlot cmdFit cmdHelp diff --git a/src/sas/qtgui/Perspectives/Fitting/UI/InViewWidgetUI.ui b/src/sas/qtgui/Perspectives/Fitting/UI/InViewWidgetUI.ui new file mode 100644 index 0000000000..8a0d7e4acc --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/UI/InViewWidgetUI.ui @@ -0,0 +1,108 @@ + + + InViewWidgetUI + + + + 0 + 0 + 800 + 515 + + + + + 800 + 500 + + + + InView + + + + + + + + + 0 + 0 + + + + + 0 + 450 + + + + + 600 + 16777215 + + + + Sliders + + + + + + + + + + Update fitted parameters + + + + + + + + 65 + 16777215 + + + + Close + + + + + + + + 65 + 16777215 + + + + Help + + + + + + + + + + + + 0 + 0 + + + + InView + + + + + + + + + diff --git a/src/sas/qtgui/Perspectives/Fitting/UnitTesting/InViewWidgetTest.py b/src/sas/qtgui/Perspectives/Fitting/UnitTesting/InViewWidgetTest.py new file mode 100644 index 0000000000..b98755d380 --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/UnitTesting/InViewWidgetTest.py @@ -0,0 +1,183 @@ +import logging +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from PySide6 import QtCore, QtGui, QtWidgets + +from sas.qtgui.Perspectives.Fitting import FittingUtilities +from sas.qtgui.Perspectives.Fitting.InViewWidget import InViewWidget +from sas.qtgui.Plotting.PlotterData import Data1D + +logger = logging.getLogger(__name__) + +class InViewWidgetTest: + + @pytest.fixture(autouse=True) + def widget(self, qapp, qtbot): + w = InViewWidget(parent=None) + # Prevent auto-deletion on close during tests + w.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + qtbot.addWidget(w) # keeps it alive and cleans up reliably + w.show() + qapp.processEvents() + return w + + def _make_fw(self, *, params, values, details, data=None, pdi=False): + """ + Building dummy widget of Fit Page with elements that are connected with InViewWidget + """ + + class _FW: + pass + + fw = _FW() + fw.main_params_to_fit = list(params) + + class _Chk: + def __init__(self, checked): + self._c = checked + + def isChecked(self): + return self._c + + class _Poly: + def __init__(self): + self.poly_params_to_fit = [n for n in params if n.endswith('.width')] + self.poly_model = QtGui.QStandardItemModel() + for n in self.poly_params_to_fit: + it = QtGui.QStandardItem(f"Distribution of {n[:-6]}") + it.setData(n, QtCore.Qt.UserRole) + self.poly_model.appendRow(it) + self.poly_params = {} + + fw.polydispersity_widget = _Poly() + + + # Adding controls that are getting turned on and off when InViewWidget is active + fw.lstParams = QtWidgets.QTableView() + fw.cbCategory = QtWidgets.QComboBox() + fw.cmdFit = QtWidgets.QPushButton() + fw.cmdPlot = QtWidgets.QPushButton() + + fw.getModelKeyFromName = lambda name: 'model' + fw.getRowFromName = lambda name: 0 + + # Param Table + m = QtGui.QStandardItemModel(1, 2) + + first_param = params[0] + m.setItem(0, 1, QtGui.QStandardItem(str(values[first_param]))) + fw.model_dict = {'model': m} + + class _Kernel: + def __init__(self, _details): + self.details = dict(_details) + + def setParam(self, name, value): #### <----------------- + pass + + class _Logic: + def __init__(self): + self.kernel_module = _Kernel(details) + self.data = data or Data1D(x=[0.01, 0.02], y=[1.0, 1.0]) + + def new1DPlot(self, return_data, tab_id): + return SimpleNamespace() + + + fw.logic = _Logic() + fw.q_range_min = None + fw.q_range_max = None + + class _Smearing: + def smearer(self): + return None + + fw.smearing_widget = _Smearing() + fw.is2D = False + fw.weighting = None + fw.tab_id = 1 + return fw + + def test_defaults(self, widget): + assert isinstance(widget, QtWidgets.QWidget) + assert widget._update_timer.isSingleShot() + assert widget._update_timer.interval() == 75 #### <----------------- + assert widget.plotter is not None + + def test_setData_plots(self, widget, mocker): + widget.plotter.plot = MagicMock() + d = Data1D(x=[1.0, 2.0], y=[1.0, 2.0]) + widget.setData(d) + assert widget._has_data is True + widget.plotter.plot.assert_called_once() + + def test_initFromFitPage_builds_sliders(self, widget): + fw = self._make_fw( + params=['scale'], + values={'scale': 1.0}, + details={'scale': ('', 0.5, 1.5)} + ) + widget.initFromFitPage(fw) + # slider metadata exists + assert 'scale' in widget._slider_meta + meta = widget._slider_meta['scale'] + spin = meta['spin'] + assert spin.minimum() == 0.5 + assert spin.maximum() == 1.5 + assert spin.value() == 1.0 + + def test_recompute_model_triggers_calc_and_plot(self, widget, mocker): + d = Data1D(x=[0.01, 0.02], y=[1.0, 1.0]) + widget.setData(d) + fw = self._make_fw( + params=['scale'], + values={'scale': 1.0}, + details={'scale': ('', 0.5, 1.5)}, + data=d + ) + + # Keep weight trivial + mocker.patch.object(FittingUtilities, 'getWeight', return_value=None) + + # Dummy Calc1D which returns something to feed _complete_inview + class DummyCalc: + def __init__(self, **kwargs): self.kwargs = kwargs + def compute(self): return {'theory': [1.0, 1.0]} + + mocker.patch('sas.qtgui.Perspectives.Fitting.InViewWidget.Calc1D', + side_effect=lambda **kw: DummyCalc(**kw)) + + # Plotter interactions: ensure plot_dict and methods are safe + widget.plotter.plot = MagicMock() + widget.plotter.replacePlot = MagicMock() + widget.plotter.plot_dict = {} + + # Observe new1DPlot call and plotting + fw.logic.new1DPlot = MagicMock(return_value=SimpleNamespace()) + widget.initFromFitPage(fw) # calls _recompute_model inside + assert fw.logic.new1DPlot.called + assert widget.plotter.plot.called or widget.plotter.replacePlot.called + + def test_apply_to_fitpage_updates_model(self, widget, mocker): + fw = self._make_fw( + params=['scale'], + values={'scale': 1.0}, + details={'scale': ('', 0.5, 1.5)} + ) + widget.initFromFitPage(fw) + widget._param_values['scale'] = 1.2345 + + # Stable formatting for assertion + mocker.patch('sas.qtgui.Perspectives.Fitting.InViewWidget.GuiUtils.formatNumber', + side_effect=lambda v, high=True: str(v)) + + widget._apply_to_fitpage() + model = fw.model_dict['model'] + assert model.item(0, 1).text() == '1.2345' + + + @pytest.mark.xfail(reason="2022-09 already broken") + def test_onHelp(self, widget): + pass diff --git a/src/sas/qtgui/Perspectives/Fitting/media/inview.rst b/src/sas/qtgui/Perspectives/Fitting/media/inview.rst new file mode 100644 index 0000000000..4a6c71bd1c --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/media/inview.rst @@ -0,0 +1,27 @@ +.. _InView_Documentation: + +InView Documentation +===================== + +.. toctree:: + :maxdepth: 1 + +qtgui/Perspectives/Fitting/inview + +The 'InView' widget (Interactive View) is a tool that allows user to visualize in real-time how the model is changing when selected parameters are adjusted, +and to compare this changes with 1D experimental data. + +Accessing InView +---------------- +By clicking button 'InView' in Fitting, InView widget window is opened. This button is disabled in case of Batch fitting (REF) and when 2D option in Fitting is selected. +The InView window is divided into two sections, InvView and Sliders. + +.. image:: FittingWidget_marked.png + +How to use +---------- +After sending data into Fitting and choosing the model, one has to tick desired parameters and open the InView window. +Next to the interactive plot, one would see sliders for each parameter that has been previously selected. Once the model is satisfactory, it can be send back to Fitting +via 'Update fitted parameters'. + +.. image:: InViewWindow.png \ No newline at end of file