diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd4555915..18af3cf2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,8 @@ jobs: - name: Run PyRS tests run: | conda activate PyRS - xvfb-run --server-args="-screen 0 640x480x24" -a pytest --cov=pyrs --cov-report=xml --cov-report=term tests + conda install conda-forge::pytest-xvfb + pytest tests - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/environment.yml b/environment.yml index 04f37021a..e4286204c 100644 --- a/environment.yml +++ b/environment.yml @@ -1,12 +1,12 @@ name: PyRS channels: - conda-forge -- mantid/label/nightly +- mantid dependencies: - python=3.10 - anaconda-client - boa -- mantidworkbench>=6.8.20231213 +- mantidworkbench>=6.10 - qtpy - pip - pyqt=5.15,<6 diff --git a/pyrs/core/peak_profile_utility.py b/pyrs/core/peak_profile_utility.py index 418f7268c..54e550305 100644 --- a/pyrs/core/peak_profile_utility.py +++ b/pyrs/core/peak_profile_utility.py @@ -6,13 +6,12 @@ # Effective peak and background parameters -EFFECTIVE_PEAK_PARAMETERS = ['Center', 'Height', 'FWHM', 'Mixing', 'A0', 'A1', 'Intensity'] +EFFECTIVE_PEAK_PARAMETERS = ['Center', 'Height', 'FWHM', 'Mixing', 'Intensity', 'A0', 'A1', 'A2'] class PeakShape(Enum): GAUSSIAN = 'Gaussian' PSEUDOVOIGT = 'PseudoVoigt' - VOIGT = 'Voigt' def __str__(self): return self.value @@ -35,14 +34,14 @@ def getShape(shape): def native_parameters(self): # Native peak parameters in Mantid naming convention NATIVE_PEAK_PARAMETERS = {'Gaussian': ['Height', 'PeakCentre', 'Sigma'], - 'PseudoVoigt': ['Mixing', 'Intensity', 'PeakCentre', 'FWHM'], - 'Voigt': ['LorentzAmp', 'LorentzPos', 'LorentzFWHM', 'GaussianFWHM']} + 'PseudoVoigt': ['Mixing', 'Intensity', 'PeakCentre', 'FWHM']} return NATIVE_PEAK_PARAMETERS[self.value][:] class BackgroundFunction(Enum): LINEAR = 'Linear' # so far, one and only supported + QUADRATIC = 'Quadratic' # so far, one and only supported def __str__(self): return self.value @@ -64,7 +63,8 @@ def getFunction(function): @property def native_parameters(self): # Native background parameters in Mantid naming convention - NATIVE_BACKGROUND_PARAMETERS = {'Linear': ['A0', 'A1']} + NATIVE_BACKGROUND_PARAMETERS = {'Linear': ['A0', 'A1'], + 'Quadratic': ['A0', 'A1', 'A2']} return NATIVE_BACKGROUND_PARAMETERS[self.value][:] @@ -106,8 +106,6 @@ def get_effective_parameters_converter(peak_profile): converter = Gaussian() elif peak_profile == PeakShape.PSEUDOVOIGT: converter = PseudoVoigt() - elif peak_profile == PeakShape.VOIGT: - converter = Voigt() else: raise RuntimeError('if/else tree is incomplete') @@ -223,6 +221,13 @@ def calculate_effective_parameters(self, param_value_array, param_error_array): eff_error_array['A1'] = param_error_array['A1'] # A1 eff_error_array['Intensity'] = intensity_error_array[:] # intensity + try: + eff_value_array['A2'] = param_value_array['A2'] # A2 + eff_error_array['A2'] = param_error_array['A2'] # A2 + except ValueError: + eff_value_array['A2'] = np.zeros_like(param_value_array['A1']) # A2 + eff_error_array['A2'] = np.zeros_like(param_value_array['A1']) + 0.01 # A2 + return eff_value_array, eff_error_array @staticmethod @@ -396,6 +401,13 @@ def calculate_effective_parameters(self, param_value_array, param_error_array): eff_error_array['A1'] = param_error_array['A1'] # A1 eff_error_array['Intensity'] = param_error_array['Intensity'] # intensity + try: + eff_value_array['A2'] = param_value_array['A2'] # A2 + eff_error_array['A2'] = param_error_array['A2'] # A2 + except ValueError: + eff_value_array['A2'] = np.zeros_like(param_value_array['A1']) # A2 + eff_error_array['A2'] = np.zeros_like(param_value_array['A1']) + 0.01 # A2 + return eff_value_array, eff_error_array @staticmethod @@ -496,37 +508,6 @@ def cal_intensity(height, fwhm, mixing): return intensity -class Voigt(PeakParametersConverter): - """ - class for handling peak profile parameters' conversion - """ - def __init__(self): - super(Voigt, self).__init__(PeakShape.VOIGT) - - def calculate_effective_parameters(self, param_value_array, param_error_array): - """Calculate effective peak parameter values - - If input parameter values include fitting error, then this method will calculate - the propagation of error - - Native PseudoVoigt: ['Mixing', 'Intensity', 'PeakCentre', 'FWHM'] - - Parameters - ---------- - native_param_names: list or None - param_value_array : numpy.ndarray - (p, n, 1) or (p, n, 2) vector for parameter values and optionally fitting error - p = number of native parameters , n = number of sub runs - param_error_array : numpy.ndarray - Returns - ------- - np.ndarray - (p', n, 1) or (p', n, 2) array for parameter values and optionally fitting error - p' = number of effective parameters , n = number of sub runs - """ - raise NotImplementedError('Somebody should write this') - - """ From here are a list of static method of peak profiles """ diff --git a/pyrs/interface/combine_runs/combine_runs_crtl.py b/pyrs/interface/combine_runs/combine_runs_crtl.py new file mode 100644 index 000000000..a6903522d --- /dev/null +++ b/pyrs/interface/combine_runs/combine_runs_crtl.py @@ -0,0 +1,45 @@ +import numpy as np +from pyrs.utilities import get_input_project_file # type: ignore + + +class CombineRunsCrtl: + def __init__(self, _model): + self._model = _model + + def parse_file_path(self, runs): + filepaths = [] + for run in runs: + try: + filepaths.append(get_input_project_file(int(run))) + except FileNotFoundError: + pass + + return filepaths + + def parse_entry_list(self, entry_list): + entry_list = entry_list.replace(' ', '') + split_text = entry_list.split(',') + ranges = [entry for entry in split_text if (':' in entry) or (';' in entry)] + entries = [entry for entry in split_text if (':' not in entry) and (';' not in entry)] + + runs = np.array([np.int16(entry) for entry in entries]) + for entry in ranges: + split_entry = None + if ':' in entry: + split_entry = np.array(entry.split(':'), dtype=np.int16) + elif ';' in entry: + split_entry = np.array(entry.split(';'), dtype=np.int16) + + if split_entry is not None: + split_entry = np.sort(split_entry) + runs = np.append(runs, np.arange(split_entry[0], + split_entry[1])) + + return self.parse_file_path(runs) + + def load_combine_projects(self, project_files): + if len(project_files) > 1: + self._model.combine_project_files(project_files) + return 1 + else: + return 0 diff --git a/pyrs/interface/combine_runs/combine_runs_model.py b/pyrs/interface/combine_runs/combine_runs_model.py new file mode 100644 index 000000000..68c8a2d94 --- /dev/null +++ b/pyrs/interface/combine_runs/combine_runs_model.py @@ -0,0 +1,31 @@ +from qtpy.QtCore import Signal, QObject # type:ignore +from pyrs.core.workspaces import HidraWorkspace +from pyrs.projectfile import HidraProjectFile # type: ignore + + +class CombineRunsModel(QObject): + propertyUpdated = Signal(str) + failureMsg = Signal(str, str, str) + + def __init__(self): + super().__init__() + self._hidra_ws = None + + def combine_project_files(self, project_files): + self._hidra_ws = HidraWorkspace('Combined Project Files') + _project = HidraProjectFile(project_files[0]) + self._hidra_ws.load_hidra_project(_project, load_raw_counts=False, load_reduced_diffraction=True) + _project.close() + + for project in project_files[1:]: + _project = HidraProjectFile(project) + self._hidra_ws.append_hidra_project(_project) + _project.close() + + def export_project_files(self, fileout): + export_project = HidraProjectFile(fileout, 'w') + self._hidra_ws.save_experimental_data(export_project, + sub_runs=self._hidra_ws._sample_logs.subruns, + ignore_raw_counts=True) + self._hidra_ws.save_reduced_diffraction_data(export_project) + export_project.save() diff --git a/pyrs/interface/combine_runs/combine_runs_viewer.py b/pyrs/interface/combine_runs/combine_runs_viewer.py new file mode 100644 index 000000000..3be0d3219 --- /dev/null +++ b/pyrs/interface/combine_runs/combine_runs_viewer.py @@ -0,0 +1,130 @@ +from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget # type:ignore +from qtpy.QtWidgets import QLineEdit, QPushButton # type:ignore +from qtpy.QtWidgets import QFileDialog, QGroupBox # type:ignore + +from qtpy.QtWidgets import QGridLayout # type:ignore +from qtpy.QtWidgets import QMainWindow # type:ignore +from qtpy.QtCore import Qt # type: ignore + +from pyrs.interface.gui_helper import pop_message + + +class FileLoad(QWidget): + def __init__(self, name=None, fileType="HidraProjectFile (*.h5);;All Files (*)", parent=None): + self._parent = parent + super().__init__(parent) + self.name = name + self.fileType = fileType + layout = QHBoxLayout() + if name == "Run Numbers:": + label = QLabel(name) + label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + layout.addWidget(label) + self.lineEdit = QLineEdit() + self.lineEdit.setReadOnly(False) + self.lineEdit.setFixedWidth(300) + + layout.addWidget(self.lineEdit) + + self.browse_button = QPushButton("Load") + self.browse_button.clicked.connect(self.loadRunNumbers) + else: + if name is None: + self.browse_button = QPushButton("Browse Exp Data") + else: + self.browse_button = QPushButton("Browse") + + self.browse_button.clicked.connect(self.openFileDialog) + + layout.addWidget(self.browse_button) + self.setLayout(layout) + + def _reset_fit_data(self): + self._parent.fit_summary.fit_table_operator.fits = None + self._parent.fit_summary.fit_table_operator.fit_result = None + + def openFileDialog(self): + self._parent._project_files, _ = QFileDialog.getOpenFileNames(self, + self.name, + "", + self.fileType, + options=QFileDialog.DontUseNativeDialog) + + if self._parent._project_files: + self._parent.load_project_files = self._parent._project_files + + self.load_project_files() + + def saveFileDialog(self, combined_files): + if combined_files != 0: + self._parent._project_files, _ = QFileDialog.getSaveFileName(self, + 'Save Combined Proeject File', + "", + self.fileType, + options=QFileDialog.DontUseNativeDialog) + + def loadRunNumbers(self): + self._parent._project_files = self._parent.controller.parse_entry_list(self.lineEdit.text()) + combined_files = self._parent.controller.load_combine_projects(self._parent._project_files) + self.saveFileDialog(combined_files) + + def load_project_files(self): + try: + combined_files = self._parent.controller.load_combine_projects(self._parent._project_files) + self.saveFileDialog(combined_files) + + except (FileNotFoundError, RuntimeError, ValueError) as run_err: + pop_message(self, f'Failed to find run {self._parent._project_files}', + str(run_err), 'error') + + self._parent.load_project_files = None + + def setFilenamesText(self, filenames): + self.lineEdit.setText(filenames) + + +class FileLoading(QGroupBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Load Project Files") + layout = QHBoxLayout() + + self.file_load_run_number = FileLoad(name="Run Numbers:", parent=parent) + self.file_load_dilg = FileLoad(name=None, parent=parent) + + layout.addWidget(self.file_load_run_number) + layout.addWidget(self.file_load_dilg) + + self.setLayout(layout) + + def set_text_values(self, direction, text): + getattr(self, f"file_load_e{direction}").setFilenamesText(text) + + +class CombineRunsViewer(QMainWindow): + def __init__(self, combine_runs_model, combine_runs_ctrl, parent=None): + + self._model = combine_runs_model + self._ctrl = combine_runs_ctrl + self._nexus_file = None + self._run_number = None + self._calibration_input = None + + super().__init__(parent) + + self.setWindowTitle("PyRS Combine Projectfiles Window") + + self.fileLoading = FileLoading(self) + + self.window = QWidget() + self.layout = QGridLayout() + self.setCentralWidget(self.fileLoading) + self.window.setLayout(self.layout) + + @property + def controller(self): + return self._ctrl + + @property + def model(self): + return self._model diff --git a/pyrs/interface/designer/peakfitwindow.ui b/pyrs/interface/designer/peakfitwindow.ui index 9e106a88d..c89359963 100644 --- a/pyrs/interface/designer/peakfitwindow.ui +++ b/pyrs/interface/designer/peakfitwindow.ui @@ -398,11 +398,6 @@ Gaussian - - - Voigt - - @@ -412,11 +407,6 @@ Linear - - - Flat - - Quadratic @@ -942,7 +932,7 @@ 0 0 1536 - 23 + 22 diff --git a/pyrs/interface/designer/pyrsmain.ui b/pyrs/interface/designer/pyrsmain.ui index e45336aac..5852d84c4 100644 --- a/pyrs/interface/designer/pyrsmain.ui +++ b/pyrs/interface/designer/pyrsmain.ui @@ -7,7 +7,7 @@ 0 0 271 - 182 + 192 @@ -83,7 +83,7 @@ 0 0 271 - 20 + 22 @@ -97,6 +97,7 @@ Advanced + @@ -115,6 +116,11 @@ Calibration + + + Combine Runs + + diff --git a/pyrs/interface/peak_fitting/fit.py b/pyrs/interface/peak_fitting/fit.py index 4ca8b2ac2..85240bab5 100644 --- a/pyrs/interface/peak_fitting/fit.py +++ b/pyrs/interface/peak_fitting/fit.py @@ -24,6 +24,7 @@ def fit_multi_peaks(self): _peak_center_list = [np.mean([left, right]) for (left, right) in _peak_range_list] _peak_tag_list = ["peak{}".format(_index) for _index, _ in enumerate(_peak_center_list)] _peak_function_name = str(self.parent.ui.comboBox_peakType.currentText()) + _peak_background_name = str(self.parent.ui.comboBox_backgroundType.currentText()) _peak_xmin_list = [left for (left, _) in _peak_range_list] _peak_xmax_list = [right for (_, right) in _peak_range_list] @@ -31,9 +32,10 @@ def fit_multi_peaks(self): # Fit peak hd_ws = self.parent.hidra_workspace + print(_peak_background_name) _wavelength = hd_ws.get_wavelength(True, True) fit_engine = PeakFitEngineFactory.getInstance(hd_ws, - _peak_function_name, 'Linear', + _peak_function_name, _peak_background_name, wavelength=_wavelength) fit_result = fit_engine.fit_multiple_peaks(_peak_tag_list, _peak_xmin_list, diff --git a/pyrs/interface/pyrs_main.py b/pyrs/interface/pyrs_main.py index 09cc53865..4e63b67eb 100644 --- a/pyrs/interface/pyrs_main.py +++ b/pyrs/interface/pyrs_main.py @@ -23,6 +23,10 @@ from pyrs.interface.detector_calibration.detector_calibration_model import DetectorCalibrationModel # noqa: E402 from pyrs.interface.detector_calibration.detector_calibration_crtl import DetectorCalibrationCrtl # noqa: E402 +from pyrs.interface.combine_runs.combine_runs_viewer import CombineRunsViewer # noqa: E402 +from pyrs.interface.combine_runs.combine_runs_model import CombineRunsModel # noqa: E402 +from pyrs.interface.combine_runs.combine_runs_crtl import CombineRunsCrtl # noqa: E402 + class PyRSLauncher(QMainWindow): """ @@ -46,12 +50,14 @@ def __init__(self): self.ui.actionQuit.triggered.connect(self.do_quit) self.ui.actionCalibration.triggered.connect(self.do_launch_calibration_window) + self.ui.actionCombine_Runs.triggered.connect(self.do_launch_combeineruns) # child windows self.peak_fit_window = None self.manual_reduction_window = None self.strain_stress_window = None self.texture_fit_window = None + self.combine_run_window = None def do_launch_fit_texture_window(self): """ @@ -68,6 +74,17 @@ def do_launch_fit_texture_window(self): # launch self.texture_fit_window.show() + def do_launch_combeineruns(self): + if self.combine_run_window is not None: + self.combine_run_window.close() + + self.combine_runs_model = CombineRunsModel() + self.combine_runs_ctrl = CombineRunsCrtl(self.combine_runs_model) + self.combine_run_window = CombineRunsViewer(self.combine_runs_model, self.combine_runs_ctrl) + + # launch + self.combine_run_window.show() + def do_launch_calibration_window(self): """ launch peak fit window diff --git a/pyrs/interface/strainstressviewer/strain_stress_view.py b/pyrs/interface/strainstressviewer/strain_stress_view.py index 2d0ce9564..06cce6eb0 100644 --- a/pyrs/interface/strainstressviewer/strain_stress_view.py +++ b/pyrs/interface/strainstressviewer/strain_stress_view.py @@ -19,6 +19,8 @@ from qtpy.QtGui import QDoubleValidator # type:ignore try: from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor + # from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor + # from vtkmodules.util.numpy_support import numpy_to_vtk, get_vtk_array_type from vtk.util.numpy_support import numpy_to_vtk, get_vtk_array_type import vtk DISABLE_3D = False @@ -697,6 +699,11 @@ def __init__(self, ws, parent=None): layout.addWidget(self.vtkWidget) self.setLayout(layout) + camera = self.renderer.GetActiveCamera() + assert camera is not None + + self.vtkWidget.show() + self.iren.Initialize() def set_ws(self, ws): diff --git a/pyrs/interface/texture_fitting/texture_fitting_crtl.py b/pyrs/interface/texture_fitting/texture_fitting_crtl.py index 3a64630d8..3dce36240 100644 --- a/pyrs/interface/texture_fitting/texture_fitting_crtl.py +++ b/pyrs/interface/texture_fitting/texture_fitting_crtl.py @@ -271,6 +271,7 @@ def plot_2D_params(self, ax_object, xlabel, ylabel, peak_number, fit_object, out fit_object=fit_object, out_of_plane=out_of_plane) + print(xdata, ydata) if isinstance(ydata[0], np.ndarray): yerr = ydata[1] ydata = ydata[0] diff --git a/pyrs/interface/texture_fitting/texture_fitting_viewer.py b/pyrs/interface/texture_fitting/texture_fitting_viewer.py index 1287ebdc3..017adc832 100644 --- a/pyrs/interface/texture_fitting/texture_fitting_viewer.py +++ b/pyrs/interface/texture_fitting/texture_fitting_viewer.py @@ -968,7 +968,7 @@ def __init__(self, fit_peak_model, fit_peak_ctrl, parent=None): self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 5) - self.resize(1024, 1024) + self.resize(1200, 1800) @property def controller(self): diff --git a/pyrs/peaks/fit_factory.py b/pyrs/peaks/fit_factory.py index fc288971e..26589c126 100644 --- a/pyrs/peaks/fit_factory.py +++ b/pyrs/peaks/fit_factory.py @@ -1,5 +1,5 @@ # Peak fitting engine -SupportedPeakProfiles = ['Gaussian', 'PseudoVoigt', 'Voigt'] +SupportedPeakProfiles = ['Gaussian', 'PseudoVoigt'] SupportedBackgroundTypes = ['Flat', 'Linear', 'Quadratic'] __all__ = ['FitEngineFactory', 'SupportedPeakProfiles', 'SupportedBackgroundTypes'] diff --git a/tests/conftest.py b/tests/conftest.py index c76fbf0b6..584822502 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,8 @@ from pyrs.dataobjects.sample_logs import _coerce_to_ndarray, PointList from pyrs.core.peak_profile_utility import get_parameter_dtype from pyrs.peaks.peak_collection import PeakCollection +from pytestqt.qtbot import QtBot +from pytestqt.exceptions import format_captured_exceptions, capture_exceptions # set to True when running on build servers ON_GITHUB_ACTIONS = bool(os.environ.get('GITHUB_ACTIONS', False)) @@ -595,3 +597,16 @@ def strain_stress_object_1(strain_builder): 'in-plane-stress': StressField(strain11, strain22, None, 3. / 2, 1. / 2, 'in-plane-stress') } } + + +@pytest.fixture(scope="session") +def my_qtbot(qapp, request): + r""" + Fixture to provide a QtBot instance for testing Qt-based applications with a session scope. This fixture will + behave like the `qtbot` fixture, but with a session scope to avoid a segmentation fault when running UI tests. + """ + result = QtBot(qapp) + with capture_exceptions() as exceptions: + yield result + if exceptions: + pytest.fail(format_captured_exceptions(exceptions)) diff --git a/tests/integration/pyrs/__init__.py b/tests/integration/pyrs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/test_peak_fitting.py b/tests/integration/test_peak_fitting.py index 4ee45fc58..b0d81ac24 100644 --- a/tests/integration/test_peak_fitting.py +++ b/tests/integration/test_peak_fitting.py @@ -213,8 +213,7 @@ def test_retrieve_fit_metadata(source_project_file, output_project_file, peak_ty [('data/Hidra_16-1_cor_log.h5', 'Hidra_16-1_cor_log_peak.h5', 'Gaussian', PeakInfo(94.5, 91, 97, 'Fe111')), # NSFR2 peak ('data/HB2B_938.h5', 'HB2B_938_peak.h5', 'PseudoVoigt', - PeakInfo(95.5, 91, 97, 'Si111'))], - ids=('FakeHB2B', 'HB2B_938')) + PeakInfo(95.5, 91, 97, 'Si111'))], ids=('FakeHB2B', 'HB2B_938')) def xtest_main(project_file_name, peak_file_name, peak_type, peak_info): """Test peak fitting @@ -247,17 +246,6 @@ def xtest_main(project_file_name, peak_file_name, peak_type, peak_info): os.remove(peak_file_name) -# TODO - MAKE IT WORK! -def test_calculating_com(): - # calculate_center_of_mass(self, peak_tag, peak_range): - pass - - -def test_convert_peaks_centers_to_dspacing(): - # - pass - - def test_improve_quality(): """This is a test to improve the quality of peak fitting. @@ -372,7 +360,7 @@ def test_write_csv(): # verify that the number of columns is correct # columns are (subruns, one log, parameter values, uncertainties, chisq) for line in contents[len(EXPECTED_HEADER) + 1:]: # skip past header and constant log - assert len(line.split(',')) == 1 + 1 + 9 * 2 + 1 + assert len(line.split(',')) == 1 + 1 + 10 * 2 + 1 # cleanup os.remove(csv_filename) @@ -465,7 +453,7 @@ def test_write_csv_from_project(project_file_name, csv_filename, expected_header # columns are (subruns, seven logs, parameter values, uncertainties, d_spacing values, # strain values and uncertainties, chisq) for line in contents[len(expected_header) + 1:]: # skip past header and constant log - assert len(line.split(',')) == 1 + num_logs + 7 * 2 + (2*2) + 1 + assert len(line.split(',')) == 1 + num_logs + 8 * 2 + (2*2) + 1 # cleanup os.remove(csv_filename) diff --git a/tests/ui/test_calibration_ui.py b/tests/ui/test_calibration_ui.py index 54b1ea307..59e32d00c 100644 --- a/tests/ui/test_calibration_ui.py +++ b/tests/ui/test_calibration_ui.py @@ -9,20 +9,25 @@ # import json import pytest -from tests.conftest import ON_GITHUB_ACTIONS # set to True when running on build servers - wait = 200 plot_wait = 100 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_texture_fitting_viewer(qtbot): - +@pytest.fixture(scope="session") +def calibration_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ model = DetectorCalibrationModel(pyrscore.PyRsCore()) ctrl = DetectorCalibrationCrtl(model) window = DetectorCalibrationViewer(model, ctrl) + return window, my_qtbot + + +def test_detector_calibration(calibration_window): + window, qtbot = calibration_window - qtbot.addWidget(window) window.show() qtbot.wait(wait) @@ -97,3 +102,5 @@ def handle_dialog(filename): window.param_window.plot_paramX.setCurrentIndex(1) qtbot.wait(wait) + + window.hide() diff --git a/tests/ui/test_manual_reduction.py b/tests/ui/test_manual_reduction.py index 9aa0a05e6..274353d73 100644 --- a/tests/ui/test_manual_reduction.py +++ b/tests/ui/test_manual_reduction.py @@ -1,18 +1,26 @@ from qtpy import QtCore import os import pytest -from tests.conftest import ON_GITHUB_ACTIONS # set to True when running on build servers import matplotlib matplotlib.use("Agg") from pyrs.interface.manual_reduction import manualreductionwindow # noqa E402 + wait = 100 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_manual_reduction(qtbot, tmpdir): +@pytest.fixture(scope="session") +def manual_reduction_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ window = manualreductionwindow.ManualReductionWindow(None) - qtbot.addWidget(window) + return window, my_qtbot + + +def test_manual_reduction(tmpdir, manual_reduction_window): + window, qtbot = manual_reduction_window window.show() qtbot.wait(wait) @@ -79,10 +87,8 @@ def test_manual_reduction(qtbot, tmpdir): assert line.get_ydata()[1::].max() == pytest.approx(580.4936170212766) -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_manual_reduction_subruns(qtbot, tmpdir): - window = manualreductionwindow.ManualReductionWindow(None) - qtbot.addWidget(window) +def test_manual_reduction_subruns(tmpdir, manual_reduction_window): + window, qtbot = manual_reduction_window window.show() qtbot.wait(wait) @@ -142,3 +148,5 @@ def test_manual_reduction_subruns(qtbot, tmpdir): for _ in range(10): qtbot.keyClick(window.ui.comboBox_sub_runs, QtCore.Qt.Key_Up) qtbot.wait(wait) + + window.hide() diff --git a/tests/ui/test_peak_fitting.py b/tests/ui/test_peak_fitting.py index 79c769ef1..2fef689b5 100644 --- a/tests/ui/test_peak_fitting.py +++ b/tests/ui/test_peak_fitting.py @@ -5,16 +5,23 @@ import functools import pytest import os -from tests.conftest import ON_GITHUB_ACTIONS # set to True when running on build servers wait = 300 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_peak_fitting(qtbot, tmpdir): +@pytest.fixture(scope="session") +def fit_peaks_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ fit_peak_core = pyrscore.PyRsCore() window = fitpeakswindow.FitPeaksWindow(None, fit_peak_core=fit_peak_core) - qtbot.addWidget(window) + return window, my_qtbot + + +def test_peak_fitting(tmpdir, fit_peaks_window): + window, qtbot = fit_peaks_window window.show() qtbot.wait(wait) @@ -83,7 +90,7 @@ def handle_dialog(filename): # check number of lines assert len(file_contents) == 127 # check number of values in line - assert len(np.fromstring(file_contents[-1], dtype=np.float64, sep=',')) == 33 + assert len(np.fromstring(file_contents[-1], dtype=np.float64, sep=',')) == 35 # look at 1D results plot line = window.ui.graphicsView_fitResult.canvas().get_axis().lines[0] @@ -127,7 +134,7 @@ def handle_dialog(filename): # check number of lines assert len(file_contents) == 127 # check number of values in line - assert len(np.fromstring(file_contents[-1], dtype=np.float64, sep=',')) == 33 + assert len(np.fromstring(file_contents[-1], dtype=np.float64, sep=',')) == 35 # look at 1D results plot line = window.ui.graphicsView_fitResult.canvas().get_axis().lines[0] @@ -170,11 +177,8 @@ def handle_dialog(filename): qtbot.wait(wait) -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_peak_selection(qtbot, tmpdir): - fit_peak_core = pyrscore.PyRsCore() - window = fitpeakswindow.FitPeaksWindow(None, fit_peak_core=fit_peak_core) - qtbot.addWidget(window) +def test_peak_selection(tmpdir, fit_peaks_window): + window, qtbot = fit_peaks_window window.show() qtbot.wait(wait) @@ -217,3 +221,5 @@ def handle_dialog(filename): qtbot.wait(wait) qtbot.mouseRelease(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, QtCore.QPoint(int(end_x2), int(end_y2))) qtbot.wait(wait) + + window.hide() diff --git a/tests/ui/test_pyrslauncher.py b/tests/ui/test_pyrslauncher.py index 32d1d691e..d71aaad2e 100644 --- a/tests/ui/test_pyrslauncher.py +++ b/tests/ui/test_pyrslauncher.py @@ -1,15 +1,22 @@ from pyrs.interface.pyrs_main import PyRSLauncher from qtpy import QtCore -from tests.conftest import ON_GITHUB_ACTIONS # set to True when running on build servers import pytest wait = 100 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_launcher(qtbot): - main_window = PyRSLauncher() - qtbot.addWidget(main_window) +@pytest.fixture(scope="session") +def main_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ + window = PyRSLauncher() + return window, my_qtbot + + +def test_launcher(main_window): + main_window, qtbot = main_window main_window.show() qtbot.wait(wait) @@ -28,3 +35,6 @@ def test_launcher(qtbot): qtbot.wait(wait) assert main_window.peak_fit_window is not None assert main_window.peak_fit_window.isVisible() + main_window.peak_fit_window.close() + main_window.manual_reduction_window.close() + main_window.close() diff --git a/tests/ui/test_stress_strain_viewer.py b/tests/ui/test_stress_strain_viewer.py index 2e4fc11cf..1f52db3ca 100644 --- a/tests/ui/test_stress_strain_viewer.py +++ b/tests/ui/test_stress_strain_viewer.py @@ -7,7 +7,6 @@ import os import pytest import json -from tests.conftest import ON_GITHUB_ACTIONS # set to True when running on build servers import mantid mantid_version = mantid._version_str().split('.') @@ -20,7 +19,6 @@ # This is a test of the model component of the strain/stress viewer -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") def test_model(tmpdir, test_data_dir): model = Model() @@ -336,7 +334,6 @@ def test_model(tmpdir, test_data_dir): model.e11 is not None -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") def test_model_multiple_files(tmpdir, test_data_dir): model = Model() @@ -506,7 +503,6 @@ def test_model_multiple_files(tmpdir, test_data_dir): assert len(open(filename).readlines()) == 318 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") def test_model_from_json(tmpdir, test_data_dir): model_json = dict() model_json['stress_case'] = 'in-plane-stress' @@ -604,15 +600,23 @@ def test_model_from_json(tmpdir, test_data_dir): assert d0.errors[0] == 0.000123 -# changes to SliceViewer from Mantid in the version 5.1 is needed for the stress/strain viewer to run -@pytest.mark.skipif(ON_GITHUB_ACTIONS or old_mantid, reason='Need mantid version >= 5.1') -def test_stress_strain_viewer(qtbot): - +@pytest.fixture(scope="session") +def strain_stress_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ model = Model() ctrl = Controller(model) window = StrainStressViewer(model, ctrl) + return window, my_qtbot + + +# changes to SliceViewer from Mantid in the version 5.1 is needed for the stress/strain viewer to run +@pytest.mark.skipif(old_mantid, reason='Need mantid version >= 5.1') +def test_stress_strain_viewer(strain_stress_window): + window, qtbot = strain_stress_window - qtbot.addWidget(window) window.show() qtbot.wait(wait) @@ -704,3 +708,5 @@ def handle_dialog(filename): qtbot.wait(wait) # check that the sliceviewer widget is created assert window.viz_tab.strainSliceViewer is not None + + window.hide() diff --git a/tests/ui/test_texture_fitting.py b/tests/ui/test_texture_fitting.py index b54240df8..beaef1a49 100644 --- a/tests/ui/test_texture_fitting.py +++ b/tests/ui/test_texture_fitting.py @@ -16,14 +16,21 @@ plot_wait = 100 -@pytest.mark.skipif(ON_GITHUB_ACTIONS, reason="UI tests segfault on GitHub Actions") -def test_texture_fitting_viewer(qtbot): - +@pytest.fixture(scope="session") +def texture_fitting_window(my_qtbot): + r""" + Fixture for the detector calibration window. Creating the window with a session scope and reusing it for all tests. + This is done to avoid the segmentation fault error that occurs when the window is created with a function scope. + """ model = TextureFittingModel(pyrscore.PyRsCore()) ctrl = TextureFittingCrtl(model) window = TextureFittingViewer(model, ctrl) + return window, my_qtbot + + +def test_texture_fitting_viewer(texture_fitting_window): + window, qtbot = texture_fitting_window - qtbot.addWidget(window) window.show() qtbot.wait(wait) @@ -67,7 +74,7 @@ def handle_dialog(filename): canvas = window.fit_window._myCanvas # The get start and end mouse points to drag select - fit_ranges = [[62.346, 66.568], [71.2917, 76.0151]] + fit_ranges = [[62.864, 66.9115], [71.87344, 76.5544]] if ON_GITHUB_ACTIONS: rtol = 0.5 @@ -76,13 +83,13 @@ def handle_dialog(filename): for i_loop in range(len(fit_ranges)): # Drag select with mouse control + canvas.figure.canvas.draw() start_x, start_y = canvas.figure.axes[0].transData.transform((fit_ranges[i_loop][0], 40)) end_x, end_y = canvas.figure.axes[0].transData.transform((fit_ranges[i_loop][1], 40)) - # Drag select with mouse control - qtbot.mousePress(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, QtCore.QPoint(start_x / 2, 40)) + qtbot.mousePress(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, QtCore.QPoint(int(start_x), int(start_y))) qtbot.wait(wait) - qtbot.mouseRelease(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, QtCore.QPoint(end_x / 2, 40)) + qtbot.mouseRelease(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, QtCore.QPoint(int(end_x), int(end_y))) qtbot.wait(wait) np.testing.assert_allclose(float(window.fit_setup.fit_range_table.item(i_loop, 0).text()), @@ -166,3 +173,4 @@ def handle_dialog(filename): qtbot.keyClick(window.plot_select.out_of_plane, QtCore.Qt.Key_Down) # qtbot.wait(plot_wait) + window.hide() diff --git a/tests/unit/pyrs/core/test_stress_facade.py b/tests/unit/pyrs/core/test_stress_facade.py index 2147f23c9..a35beccfc 100644 --- a/tests/unit/pyrs/core/test_stress_facade.py +++ b/tests/unit/pyrs/core/test_stress_facade.py @@ -412,8 +412,8 @@ def test_set_d_reference(self, strain_stress_object_0, strain_stress_object_1): def test_peak_parameters(self, strain_stress_object_1): facade = StressFacade(strain_stress_object_1['stresses']['diagonal']) - assert set(facade.peak_parameters) == {'d', 'Center', 'Height', 'FWHM', 'Mixing', - 'A0', 'A1', 'Intensity'} + assert set(facade.peak_parameters) == {'d', 'Center', 'Height', 'FWHM', 'Mixing', 'Intensity', + 'A0', 'A1', 'A2'} def test_peak_parameter_field(self, strain_stress_object_1): r"""Retrieve the effective peak parameters for a particular run, or for a particular direction""" diff --git a/tests/unit/pyrs/peaks/test_peak_collection.py b/tests/unit/pyrs/peaks/test_peak_collection.py index 49dc2db88..e0ee8d509 100644 --- a/tests/unit/pyrs/peaks/test_peak_collection.py +++ b/tests/unit/pyrs/peaks/test_peak_collection.py @@ -56,7 +56,7 @@ def test_peak_collection_init(): np.testing.assert_equal(d_ref_err, np.asarray((0.,))) -def check_peak_collection(peak_shape, NUM_SUBRUN, target_errors, +def check_peak_collection(peak_shape, background, NUM_SUBRUN, target_errors, wavelength=None, d_reference=None, target_d_spacing_center=np.nan, target_d_spacing_center_error=np.asarray([0., 0.]), @@ -85,7 +85,7 @@ def check_peak_collection(peak_shape, NUM_SUBRUN, target_errors, """ subruns = np.arange(NUM_SUBRUN) + 1 chisq = np.array([42., 43.]) - raw_peaks_array = np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(peak_shape, 'Linear')) + raw_peaks_array = np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(peak_shape, background)) if peak_shape == 'PseudoVoigt': raw_peaks_array['Intensity'] = [1, 2] raw_peaks_array['FWHM'] = np.array([4, 5], dtype=float) @@ -96,11 +96,11 @@ def check_peak_collection(peak_shape, NUM_SUBRUN, target_errors, raw_peaks_array['PeakCentre'] = [90., 91.] # background terms are both zeros - raw_peaks_errors = np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(peak_shape, 'Linear')) + raw_peaks_errors = np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(peak_shape, background)) if wavelength is None: - peaks = PeakCollection('testing', peak_shape, 'Linear') + peaks = PeakCollection('testing', peak_shape, background) else: - peaks = PeakCollection('testing', peak_shape, 'Linear', wavelength=wavelength) + peaks = PeakCollection('testing', peak_shape, background, wavelength=wavelength) # uncertainties are being set to zero peaks.set_peak_fitting_values(subruns, raw_peaks_array, @@ -118,6 +118,7 @@ def check_peak_collection(peak_shape, NUM_SUBRUN, target_errors, obs_raw_peaks, obs_raw_errors = peaks.get_native_params() np.testing.assert_equal(obs_raw_peaks, raw_peaks_array) np.testing.assert_equal(obs_raw_errors, raw_peaks_errors) + # check effective parameters obs_eff_peaks, obs_eff_errors = peaks.get_effective_params() assert obs_eff_peaks.size == NUM_SUBRUN @@ -156,9 +157,15 @@ def check_peak_collection(peak_shape, NUM_SUBRUN, target_errors, def test_peak_collection_Gaussian(): NUM_SUBRUN = 2 # without wavelength - check_peak_collection('Gaussian', NUM_SUBRUN, np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(effective=True))) + check_peak_collection('Gaussian', 'Linear', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01)], + dtype=get_parameter_dtype(effective=True))) # Error array # with wavelength - check_peak_collection('Gaussian', NUM_SUBRUN, np.zeros(NUM_SUBRUN, dtype=get_parameter_dtype(effective=True)), + check_peak_collection('Gaussian', 'Linear', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01)], + dtype=get_parameter_dtype(effective=True)), wavelength=1.53229, d_reference=1.08, target_d_spacing_center=[1.08, 1.07], target_d_spacing_center_error=[0.0, 0.0], target_strain=[3234., -5408.], target_strain_error=[0.0, 0.0]) @@ -167,14 +174,48 @@ def test_peak_collection_Gaussian(): def test_peak_collection_PseudoVoigt(): NUM_SUBRUN = 2 # without wavelength - check_peak_collection('PseudoVoigt', NUM_SUBRUN, - np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], + check_peak_collection('PseudoVoigt', 'Linear', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01)], + dtype=get_parameter_dtype(effective=True))) + # with wavelength + check_peak_collection('PseudoVoigt', 'Linear', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01)], + dtype=get_parameter_dtype(effective=True)), + wavelength=1.53229, d_reference=1.08, target_d_spacing_center=[1.08, 1.07], + target_d_spacing_center_error=[0.0, 0.0], target_strain=[3234., -5408.], + target_strain_error=[0.0, 0.0]) + + +def test_peak_collection_Gaussian_Quadratic(): + NUM_SUBRUN = 2 + # without wavelength + check_peak_collection('Gaussian', 'Quadratic', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], + dtype=get_parameter_dtype(effective=True))) # Error array + # with wavelength + check_peak_collection('Gaussian', 'Quadratic', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], + dtype=get_parameter_dtype(effective=True)), + wavelength=1.53229, d_reference=1.08, target_d_spacing_center=[1.08, 1.07], + target_d_spacing_center_error=[0.0, 0.0], target_strain=[3234., -5408.], + target_strain_error=[0.0, 0.0]) + + +def test_peak_collection_PseudoVoigt_Quadratic(): + NUM_SUBRUN = 2 + # without wavelength + check_peak_collection('PseudoVoigt', 'Quadratic', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], dtype=get_parameter_dtype(effective=True))) # with wavelength - check_peak_collection('PseudoVoigt', NUM_SUBRUN, - np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], + check_peak_collection('PseudoVoigt', 'Quadratic', NUM_SUBRUN, + np.array([(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)], dtype=get_parameter_dtype(effective=True)), wavelength=1.53229, d_reference=1.08, target_d_spacing_center=[1.08, 1.07], target_d_spacing_center_error=[0.0, 0.0], target_strain=[3234., -5408.], diff --git a/tests/unit/pyrs/peaks/test_peak_fit_engine.py b/tests/unit/pyrs/peaks/test_peak_fit_engine.py index 0c101d1fc..fb82026b7 100644 --- a/tests/unit/pyrs/peaks/test_peak_fit_engine.py +++ b/tests/unit/pyrs/peaks/test_peak_fit_engine.py @@ -372,7 +372,7 @@ def test_2_gaussian_1_subrun(setup_1_subrun, fit_domain): fit_costs = fit_result.peakcollections[0].fitting_costs eff_param_values, eff_param_errors = fit_result.peakcollections[0].get_effective_params() assert eff_param_values.size == 1, '1 sub run' - assert len(eff_param_values.dtype.names) == 7, '7 effective parameters' + assert len(eff_param_values.dtype.names) == 8, '8 effective parameters' ''' if abs(eff_param_values[2][0] - expected_intensity) < 1E-03: plt.plot(data_x, data_y, label='Test 2 Gaussian') @@ -492,7 +492,7 @@ def test_2_gaussian_3_subruns(target_values): # Get effective peak parameters effective_param_values, effective_param_errors = fit_result.peakcollections[0].get_effective_params() assert effective_param_values.size == 3, '3 subruns' - assert len(effective_param_values.dtype.names) == 7, '7 effective parameters' + assert len(effective_param_values.dtype.names) == 8, '8 effective parameters' # TODO it is odd that there are only two in the the setup function and 3 in the result np.testing.assert_allclose(param_values_lp['Height'][:2], target_values['peak_height'], atol=20.) @@ -503,7 +503,7 @@ def test_2_gaussian_3_subruns(target_values): effective_param_values, effective_param_errors = fit_result.peakcollections[1].get_effective_params() assert effective_param_values.size == 3, '3 subruns' - assert len(effective_param_values.dtype.names) == 7, '7 effective parameters' + assert len(effective_param_values.dtype.names) == 8, '8 effective parameters' # Plot # model_x, model_y = fit_engine.calculate_fitted_peaks(3, None)