예제 #1
0
    def __init__(self, release_config_file_path, parent=None):
        self.__log = logger.getLogger(__name__)
        self.__log.info("")
        super().__init__(parent)

        self.info = Config(release_config_file_path)['release']

        self.projectChanged.connect(self.onProjectChanged)

        self._project_rcif_path = None
        self._samples_rcif_path = None
        self._experiment_rcif_path = None
        self._calculator_interface = QtCalculatorInterface(CryspyCalculator())
        self._project_dict_copy = {}

        self._project_control = ProjectControl()
        self._measured_data_model = MeasuredDataModel()
        self._calculated_data_model = CalculatedDataModel()
        self._bragg_peaks_model = BraggPeaksModel()
        self._cell_parameters_model = CellParametersModel()
        self._cell_box_model = CellBoxModel()
        self._atom_sites_model = AtomSitesModel()
        self._atom_adps_model = AtomAdpsModel()
        self._atom_msps_model = AtomMspsModel()
        self._fitables_model = FitablesModel()
        self._status_model = StatusModel()

        self._refine_thread = None
        self._refinement_running = False
        self._refinement_done = False
        self._refinement_result = {}

        self._calculator_interface.clearUndoStack()
        self._need_to_save = False
예제 #2
0
def test_setPhaseDefinition(cal):
    calc = CryspyCalculator('')
    interface = QtCalculatorInterface(calc)
    interface.setPhaseDefinition(phase_path)
    phase_added = interface.getPhase('Fe3O4')
    phase_ref = cal.getPhase('Fe3O4')
    assert phase_added['phasename'] == phase_ref['phasename']
    assert phase_added['spacegroup']['crystal_system'].value == phase_ref[
        'spacegroup']['crystal_system'].value
    assert phase_added['spacegroup'][
        'space_group_name_HM_ref'].value == phase_ref['spacegroup'][
            'space_group_name_HM_ref'].value
    assert phase_added['spacegroup'][
        'space_group_IT_number'].value == phase_ref['spacegroup'][
            'space_group_IT_number'].value
    assert phase_added['spacegroup']['origin_choice'].value == phase_ref[
        'spacegroup']['origin_choice'].value
    assert phase_added['cell']['length_a'].value == phase_ref['cell'][
        'length_a'].value
    assert phase_added['cell']['length_b'].value == phase_ref['cell'][
        'length_b'].value
    assert phase_added['cell']['length_c'].value == phase_ref['cell'][
        'length_c'].value
    assert phase_added['cell']['angle_alpha'].value == phase_ref['cell'][
        'angle_alpha'].value
    assert phase_added['cell']['angle_beta'].value == phase_ref['cell'][
        'angle_beta'].value
    assert phase_added['cell']['angle_gamma'].value == phase_ref['cell'][
        'angle_gamma'].value
    assert phase_added['atoms']['Fe3A']['fract_x'].value == phase_ref['atoms'][
        'Fe3A']['fract_x'].value
    assert phase_added['atoms']['Fe3B']['fract_y'].value == phase_ref['atoms'][
        'Fe3B']['fract_y'].value
    assert phase_added['atoms']['O']['fract_z'].value == phase_ref['atoms'][
        'O']['fract_z'].value
def test_CellBoxModelModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert m._x_role == 257
    assert m._y_role == 258
    assert m._z_role == 259

    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._project_dict, ProjectDict)

    assert len(m._model.roleNames()) == 3

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 3000
    assert m._model.columnCount() == 1

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data(role=m._x_role) == 0.0
    assert m._model.item(0, 0).data(role=m._y_role) == 0.0
    assert m._model.item(0, 0).data(role=m._z_role) == 0.0

    assert m._model.item(2999, 0).data(role=m._x_role) == 8.36212
    assert m._model.item(2999, 0).data(role=m._y_role) == 8.36212
    assert m._model.item(2999,
                         0).data(role=m._z_role) == pytest.approx(8.32867)

    # test asModel
    assert m._model == m.asModel()
def test_FileStructureModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._model, QStandardItemModel)

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 1
    assert m._model.columnCount() == 1

    phaseRole = Qt.UserRole + 1
    expRole = Qt.UserRole + 2
    calcRole = Qt.UserRole + 3

    assert len(m._model.roleNames()) == 3
    assert m._model.roleNames()[phaseRole] == b"phasesRole"
    assert str(m._model.roleNames()[expRole]) == "b'experimentsRole'"
    assert str(m._model.roleNames()[calcRole]) == "b'calculationsRole'"

    assert 'data_Fe3O4' in m._model.item(0, 0).data(role=phaseRole)
    assert 'data_pd' in m._model.item(0, 0).data(role=expRole)
    assert '_refln_index_k' in m._model.item(0, 0).data(role=calcRole)
예제 #5
0
def test_CellParametersModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert m._a_role == 257
    assert m._gamma_role == 262

    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._project_dict, ProjectDict)

    assert len(m._model.roleNames()) == 6

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 1
    assert m._model.columnCount() == 1

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data(role=m._a_role) == 8.36212
    assert m._model.item(0, 0).data(role=m._b_role) == 8.36212
    assert m._model.item(0, 0).data(role=m._c_role) == 8.36212
    assert m._model.item(0, 0).data(role=m._alpha_role) == 90.0
    assert m._model.item(0, 0).data(role=m._beta_role) == 90.0
    assert m._model.item(0, 0).data(role=m._gamma_role) == 90.0

    # test asModel
    assert m._model == m.asModel()
예제 #6
0
def test_CalculatedDataModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._headers_model, QStandardItemModel)
    assert isinstance(m._project_dict, ProjectDict)

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 381
    assert m._model.columnCount() == 8

    assert m._headers_model.rowCount() == 1
    assert m._headers_model.columnCount() == 8

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data(role=Qt.DisplayRole) == 4.0
    assert pytest.approx(
        m._model.item(0, 3).data(role=Qt.DisplayRole), 438.3046174533981)
    assert m._model.item(380, 0).data(role=Qt.DisplayRole) == 80.0
    assert pytest.approx(
        m._model.item(380, 3).data(role=Qt.DisplayRole), -58.83263649312255)

    # test asModel
    assert m._model == m.asModel()
    assert m._headers_model == m.asHeadersModel()
예제 #7
0
    def initialize(self):
        self.__log.info("")
        self._project_rcif_path = self._project_control.project_rcif_path
        #logging.info(self._calculator.asCifDict())
        # TODO This is where you would choose the calculator and import the module
        self._calculator_interface = QtCalculatorInterface(
            CryspyCalculator(self._project_rcif_path))
        self._calculator_interface.project_dict['app']['name'] = self.info[
            'name']
        self._calculator_interface.project_dict['app']['version'] = self.info[
            'version']
        self._calculator_interface.project_dict['app']['url'] = self.info[
            'url']
        self._calculator_interface.projectDictChanged.connect(
            self.projectChanged)
        self._calculator_interface.canUndoOrRedoChanged.connect(
            self.canUndoOrRedoChanged)
        self._calculator_interface.clearUndoStack()
        # TODO generates dictdiffer
        #  `ValueError: The truth value of an array with more than one element is ambiguous`
        #  Temp fix - Andrew
        self.onProjectSaved()

        models = [
            self._measured_data_model, self._calculated_data_model,
            self._bragg_peaks_model, self._cell_parameters_model,
            self._cell_box_model, self._atom_sites_model,
            self._atom_adps_model, self._atom_msps_model, self._fitables_model,
            self._status_model
        ]
        for model in models:
            model.setCalculatorInterface(self._calculator_interface)

        self._refine_thread = Refiner(self._calculator_interface, 'refine')
        self._refine_thread.failed.connect(self._thread_failed)
        self._refine_thread.finished.connect(self._thread_finished)
        self._refine_thread.finished.connect(
            self._status_model.onRefinementDone)

        # We can't link signals as the manager signals emitted before the dict is updated :-(
        self.projectChanged.emit()
def test_AtomMspsModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._project_dict, ProjectDict)

    assert m._label_role == 257
    assert m._chi23_role == 265

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 3
    assert m._model.columnCount() == 1

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data() == 'Fe3A'
    assert m._model.item(0, 0).data(role=m._type_role) == 'Cani'
    assert m._model.item(0, 0).data(role=m._chiiso_role) == ''
    assert m._model.item(0, 0).data(role=m._chi11_role) == -3.468
    assert m._model.item(0, 0).data(role=m._chi22_role) == -3.468
    assert m._model.item(0, 0).data(role=m._chi33_role) == -3.468
    assert m._model.item(0, 0).data(role=m._chi12_role) == 0.0
    assert m._model.item(0, 0).data(role=m._chi13_role) == 0.0
    assert m._model.item(0, 0).data(role=m._chi23_role) == 0.0

    assert m._model.item(1, 0).data() == 'Fe3B'
    assert m._model.item(1, 0).data(role=m._type_role) == 'Cani'
    assert m._model.item(1, 0).data(role=m._chiiso_role) == ''
    assert m._model.item(1, 0).data(role=m._chi11_role) == 3.041
    assert m._model.item(1, 0).data(role=m._chi22_role) == 3.041
    assert m._model.item(1, 0).data(role=m._chi33_role) == 3.041
    assert m._model.item(1, 0).data(role=m._chi12_role) == 0.0
    assert m._model.item(1, 0).data(role=m._chi13_role) == 0.0
    assert m._model.item(1, 0).data(role=m._chi23_role) == 0.0

    assert m._model.item(2, 0).data() == 'O'
    assert m._model.item(2, 0).data(role=m._type_role) is None
    assert m._model.item(2, 0).data(role=m._chi11_role) is None
    assert m._model.item(2, 0).data(role=m._chi11_role) is None
    assert m._model.item(2, 0).data(role=m._chi22_role) is None
    assert m._model.item(2, 0).data(role=m._chi33_role) is None
    assert m._model.item(2, 0).data(role=m._chi12_role) is None
    assert m._model.item(2, 0).data(role=m._chi13_role) is None
    assert m._model.item(2, 0).data(role=m._chi23_role) is None

    # test asModel
    assert m._model == m.asModel()
예제 #9
0
def test_addExperimentDefinition(cal):
    calc = CryspyCalculator('')
    interface = QtCalculatorInterface(calc)
    interface.setPhaseDefinition(phase_path)
    interface.addExperimentDefinition(exp_path)
    exp_added = interface.getExperiment('pd')
    exp_ref = cal.getExperiment('pd')
    assert exp_added['name'] == exp_ref['name']
    assert exp_added['wavelength'].value == exp_ref['wavelength'].value
    assert exp_added['offset'].value == exp_ref['offset'].value
    assert exp_added['phase']['Fe3O4']['name'] == exp_ref['phase']['Fe3O4'][
        'name']
    assert exp_added['phase']['Fe3O4']['scale'].value == exp_ref['phase'][
        'Fe3O4']['scale'].value
def test_AtomSitesModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._project_dict, ProjectDict)

    assert m._label_role == 257
    assert m._occupancy_role == 263

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 3
    assert m._model.columnCount() == 1

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data() == 'Fe3A'
    assert m._model.item(0, 0).data(role=m._atom_role) == 'Fe3+'
    assert m._model.item(0, 0).data(role=m._color_role) == 0.945
    assert m._model.item(0, 0).data(role=m._x_role) == 0.125
    assert m._model.item(0, 0).data(role=m._y_role) == 0.125
    assert m._model.item(0, 0).data(role=m._z_role) == 0.125
    assert m._model.item(0, 0).data(role=m._occupancy_role) == 1.0

    assert m._model.item(1, 0).data() == 'Fe3B'
    assert m._model.item(1, 0).data(role=m._atom_role) == 'Fe3+'
    assert m._model.item(1, 0).data(role=m._color_role) == 0.945
    assert m._model.item(1, 0).data(role=m._x_role) == 0.5
    assert m._model.item(1, 0).data(role=m._y_role) == 0.5
    assert m._model.item(1, 0).data(role=m._z_role) == 0.5
    assert m._model.item(1, 0).data(role=m._occupancy_role) == 1.0

    assert m._model.item(2, 0).data() == 'O'
    assert m._model.item(2, 0).data(role=m._atom_role) == 'O2-'
    assert m._model.item(2, 0).data(role=m._color_role) == 0.5803
    assert m._model.item(2, 0).data(role=m._x_role) == 0.25521
    assert m._model.item(2, 0).data(role=m._y_role) == 0.25521
    assert m._model.item(2, 0).data(role=m._z_role) == 0.25521
    assert m._model.item(2, 0).data(role=m._occupancy_role) == 1.0

    # test asModel
    assert m._model == m.asModel()
예제 #11
0
def test_StatusModelModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._statusBarModel, QStandardItemModel)
    assert isinstance(m._chartDisplayModel, QStandardItemModel)

    # assure _setModelFromProject got called
    assert m._statusBarModel.rowCount() == 4
    assert m._statusBarModel.columnCount() == 1

    assert m._chartDisplayModel.rowCount() == 2
    assert m._chartDisplayModel.columnCount() == 1

    assert len(m._statusBarModel.roleNames()) == len(m._roles_dict['status'])
    assert len(m._chartDisplayModel.roleNames()) == len(m._roles_dict['plot'])

    assert b'label' in m._roles_dict['status'].values()
    assert b'value' in m._roles_dict['status'].values()

    assert b'label' in m._roles_dict['plot'].values()
    assert b'value' in m._roles_dict['plot'].values()

    fr = Qt.UserRole + 1
    offset = 100
    assert m._statusBarModel.item(0,
                                  0).data(role=fr + 1) == pytest.approx(340.79)
    assert m._statusBarModel.item(2, 0).data(role=fr + 1) == 1
    assert m._statusBarModel.item(3, 0).data(role=fr + 1) == 1
    assert m._statusBarModel.item(1, 0).data(role=fr + 1) == 1

    assert m._chartDisplayModel.item(0, 0).data(role=fr + offset +
                                                1) == pytest.approx(340.79)
    assert m._chartDisplayModel.item(1, 0).data(role=fr + offset + 1) == 1

    assert m._statusBarModel == m.returnStatusBarModel()
    assert m._chartDisplayModel == m.returnChartModel()
def test_MeasuredDataModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)


    assert isinstance(m._model, QStandardItemModel)
    assert isinstance(m._headers_model, QStandardItemModel)

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 381
    assert m._model.columnCount() == 9

    assert m._headers_model.rowCount() == 1
    assert m._headers_model.columnCount() == 9

    # Test stuff from _setModelFromProject here
    assert m._model.item(0, 0).data(role=Qt.DisplayRole) == 4.0
    assert m._model.item(0, 6).data(role=Qt.DisplayRole) == 128.97
    assert m._model.item(380, 0).data(role=Qt.DisplayRole) == 80.0
    assert m._model.item(380, 6).data(role=Qt.DisplayRole) == 27.81

    assert m._headers_model.item(0, 0).data(role=Qt.DisplayRole) == 'x'
    assert m._headers_model.item(0, 1).data(role=Qt.DisplayRole) == 'y_obs'
    assert m._headers_model.item(0, 2).data(role=Qt.DisplayRole) == 'sy_obs'
    assert m._headers_model.item(0, 3).data(role=Qt.DisplayRole) == 'y_obs_diff'
    assert m._headers_model.item(0, 4).data(role=Qt.DisplayRole) == 'sy_obs_diff'
    assert m._headers_model.item(0, 5).data(role=Qt.DisplayRole) == 'y_obs_up'
    assert m._headers_model.item(0, 6).data(role=Qt.DisplayRole) == 'sy_obs_up'
    assert m._headers_model.item(0, 7).data(role=Qt.DisplayRole) == 'y_obs_down'
    assert m._headers_model.item(0, 8).data(role=Qt.DisplayRole) == 'sy_obs_down'

    # test asModel
    assert m._model == m.asModel()
    assert m._headers_model == m.asHeadersModel()
def test_FitablesModelModel():

    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)

    assert isinstance(m._model, QStandardItemModel)

    # assure _setModelFromProject got called
    assert m._model.rowCount() == 24
    assert m._model.columnCount() == 1

    assert len(m._model.roleNames()) == len(m._roles_dict)
    assert b'path' in m._roles_dict.values()
    assert b'refine' in m._roles_dict.values()

    # Test stuff from _setModelFromProject here
    # first and last row
    assert m._model.item(0, 0).data(role=Qt.UserRole+2) == 'phases Fe3O4 cell length_a'
    assert m._model.item(0, 0).data(role=Qt.UserRole+3) == 8.36212
    assert m._model.item(0, 0).data(role=Qt.UserRole+4) == 0.0
    assert m._model.item(0, 0).data(role=Qt.UserRole+5) == pytest.approx(6.68969)
    assert m._model.item(0, 0).data(role=Qt.UserRole+6) == 10.034544
    assert m._model.item(0, 0).data(role=Qt.UserRole+7) is True
    assert m._model.item(0, 0).data(role=Qt.UserRole+8) == '\u212B'

    assert m._model.item(21, 0).data(role=Qt.UserRole+2) == 'experiments pd resolution y'
    assert m._model.item(21, 0).data(role=Qt.UserRole+3) == 0.0
    assert m._model.item(21, 0).data(role=Qt.UserRole+4) == 0.0
    assert m._model.item(21, 0).data(role=Qt.UserRole+5) == -1.0
    assert m._model.item(21, 0).data(role=Qt.UserRole+6) == 1.0
    assert m._model.item(21, 0).data(role=Qt.UserRole+7) is False
    assert m._model.item(21, 0).data(role=Qt.UserRole+8) == ''

    # test asModel
    assert m._model == m.asModel()
예제 #14
0
def test_creation_Empty():
    calc = CryspyCalculator()
    interface = QtCalculatorInterface(calc)
예제 #15
0
def test_creation_WrongStr():
    path = os.path.join(test_data, 'mainf.cif')
    calc = CryspyCalculator(path)
    interface = QtCalculatorInterface(calc)
예제 #16
0
def test_creation_None():
    calc = CryspyCalculator(None)
    interface = QtCalculatorInterface(calc)
예제 #17
0
def cal():
    calc = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calc)
    return interface
예제 #18
0
def test_setPhaseDefinition_EmptyStr():
    calc = CryspyCalculator('')
    interface = QtCalculatorInterface(calc)
    interface.setPhaseDefinition('')
예제 #19
0
def test_setPhaseDefinition_None():
    calc = CryspyCalculator('')
    interface = QtCalculatorInterface(calc)
    interface.setPhaseDefinition(None)
예제 #20
0
class ProxyPyQml(QObject):
    def __init__(self, release_config_file_path, parent=None):
        self.__log = logger.getLogger(__name__)
        self.__log.info("")
        super().__init__(parent)

        self.info = Config(release_config_file_path)['release']

        self.projectChanged.connect(self.onProjectChanged)

        self._project_rcif_path = None
        self._samples_rcif_path = None
        self._experiment_rcif_path = None
        self._calculator_interface = QtCalculatorInterface(CryspyCalculator())
        self._project_dict_copy = {}

        self._project_control = ProjectControl()
        self._measured_data_model = MeasuredDataModel()
        self._calculated_data_model = CalculatedDataModel()
        self._bragg_peaks_model = BraggPeaksModel()
        self._cell_parameters_model = CellParametersModel()
        self._cell_box_model = CellBoxModel()
        self._atom_sites_model = AtomSitesModel()
        self._atom_adps_model = AtomAdpsModel()
        self._atom_msps_model = AtomMspsModel()
        self._fitables_model = FitablesModel()
        self._status_model = StatusModel()

        self._refine_thread = None
        self._refinement_running = False
        self._refinement_done = False
        self._refinement_result = {}

        self._calculator_interface.clearUndoStack()
        self._need_to_save = False

    @Slot()
    def loadPhasesFromFile(self):
        """
        Replace internal structure models based on requested content from CIF
        """
        self._samples_rcif_path = self._project_control.phases_rcif_path
        self._calculator_interface.addPhaseDefinition(self._samples_rcif_path)
        # explicit emit required for the view to reload the model content
        self._calculator_interface.clearUndoStack()
        self.projectChanged.emit()
        self._need_to_save = False
        self.projectSaveStateChanged.emit()
        #self.onProjectUnsaved()

    @Slot()
    def loadExperiment(self):
        """
        Selects the appropriate loading algorithm
        """

        if self._project_control.experiment_file_format == "cif":
            self.loadExperimentFromCif()
        elif self._project_control.experiment_file_format == "xye":
            self.loadExperimentFromXye()
        else:
            raise IOError(
                "Unexpected experiment_file_format in ProjectControl.")

    def loadExperimentFromXye(self):
        """
        Loads non cif data files, adds fake cif information, and loads
        """

        cif_string = self._project_control._cif_string
        cif_string = cif_string.replace(
            "PHASE_NAME",
            self._calculator_interface.phasesIds()[0])

        self._calculator_interface.addExperimentDefinitionFromString(
            cif_string)
        self._measured_data_model.setCalculatorInterface(
            self._calculator_interface)
        # explicit emit required for the view to reload the model content
        self._calculator_interface.clearUndoStack()
        self.projectChanged.emit()
        self._need_to_save = False
        self.projectSaveStateChanged.emit()
        #self.onProjectUnsaved()

    def loadExperimentFromCif(self):
        """
        Replace internal experiment models based on requested content from CIF
        """
        self._experiment_rcif_path = self._project_control.experiment_rcif_path
        self._calculator_interface.addExperimentDefinition(
            self._experiment_rcif_path)
        self._measured_data_model.setCalculatorInterface(
            self._calculator_interface)
        # explicit emit required for the view to reload the model content
        self._calculator_interface.updateCalculations()
        self._calculator_interface.clearUndoStack()
        self.projectChanged.emit()
        self._need_to_save = False
        self.projectSaveStateChanged.emit()
        #self.onProjectUnsaved()

    @Slot(str)
    def updateMainCifFromGui(self, cif_string):
        name_list = [
            str_line for str_line in cif_string.split('\n')
            if str_line.startswith('_name ')
        ]
        if len(name_list) != 1:
            self.__log.warning(
                'Project name can not be set. Returning previous value')
            return
        name = name_list[0].split('_name ')[1]
        if name[-1] == ' ':
            name = name[:-1]
        keywords_list = [
            str_line for str_line in cif_string.split('\n')
            if str_line.startswith('_keywords ')
        ]
        if len(keywords_list) != 1:
            self.__log.warning(
                'Project keywords can not be set. Returning previous values')
            return
        keywords = keywords_list[0].split('_keywords ')[1]
        if keywords[-1] == ' ':
            keywords = keywords[:-1]
        keywords = keywords.strip('\'')
        keywords = keywords.split(',')
        keywords = [
            keyword[1:] if keyword[0] == ' ' else keyword
            for keyword in keywords
        ]
        keywords = [
            keyword[:-1] if keyword[-1] == ' ' else keyword
            for keyword in keywords
        ]
        self._project_control.manager.projectName = name
        self._calculator_interface.setProjectName(name)
        self._project_control.manager.projectKeywords = keywords
        self._calculator_interface.setProjectKeywords(keywords)
        self._need_to_save = True
        self.projectSaveStateChanged.emit()

    @Slot(str)
    def updatePhaseFromGui(self, cif_string):
        phase_name = self._calculator_interface.phasesIds()[0]
        cif_string = cif_string[cif_string.find('data_'):]
        new_phase = self._calculator_interface.getPhaseFromCif(cif_string)
        old_phase = self._calculator_interface.getPhase(phase_name)
        keys, values, modifier = old_phase.dictComparison(new_phase)
        modded_keys = [['phases', phase_name, *key]
                       for key, value in zip(keys, values)
                       if key[-1] != 'mapping' and key[-1] != 'hide']
        modded_values = [
            value for key, value in zip(keys, values)
            if key[-1] != 'mapping' and key[-1] != 'hide'
        ]

        self._calculator_interface.project_dict.startBulkUpdate(
            'Manual update of samples.cif')
        self._calculator_interface.project_dict.bulkUpdate(
            modded_keys, modded_values)
        self._calculator_interface.setCalculatorFromProject()
        self._calculator_interface.updateCalculations()
        self._calculator_interface.project_dict.endBulkUpdate()

        #self.projectChanged.emit()
        self._calculator_interface.projectDictChanged.emit()
        self._need_to_save = True
        self.projectSaveStateChanged.emit()

    @Slot(str)
    def updateExperimentFromGui(self, cif_string):
        exp_name = self._calculator_interface.experimentsIds()[0]
        cif_string = cif_string[cif_string.find('data_'):]
        new_experiment = self._calculator_interface.getExperimentFromCif(
            cif_string)
        old_exp = self._calculator_interface.getExperiment(exp_name)
        keys, values, modifier = old_exp.dictComparison(new_experiment)
        modded_keys = [['experiments', exp_name, *key] for key in keys
                       if key[-1] != 'mapping' and key[-1] != 'hide']
        modded_values = [
            value for key, value in zip(keys, values)
            if key[-1] != 'mapping' and key[-1] != 'hide'
        ]

        self._calculator_interface.project_dict.startBulkUpdate(
            'Manual update of experiments.cif')
        self._calculator_interface.project_dict.bulkUpdate(
            modded_keys, modded_values)
        self._calculator_interface.setCalculatorFromProject()
        self._calculator_interface.updateCalculations()
        self._calculator_interface.project_dict.endBulkUpdate()

        #self.projectChanged.emit()
        self._calculator_interface.projectDictChanged.emit()
        self._need_to_save = True
        self.projectSaveStateChanged.emit()

    # Load CIF method, accessible from QML
    @Slot()
    def initialize(self):
        self.__log.info("")
        self._project_rcif_path = self._project_control.project_rcif_path
        #logging.info(self._calculator.asCifDict())
        # TODO This is where you would choose the calculator and import the module
        self._calculator_interface = QtCalculatorInterface(
            CryspyCalculator(self._project_rcif_path))
        self._calculator_interface.project_dict['app']['name'] = self.info[
            'name']
        self._calculator_interface.project_dict['app']['version'] = self.info[
            'version']
        self._calculator_interface.project_dict['app']['url'] = self.info[
            'url']
        self._calculator_interface.projectDictChanged.connect(
            self.projectChanged)
        self._calculator_interface.canUndoOrRedoChanged.connect(
            self.canUndoOrRedoChanged)
        self._calculator_interface.clearUndoStack()
        # TODO generates dictdiffer
        #  `ValueError: The truth value of an array with more than one element is ambiguous`
        #  Temp fix - Andrew
        self.onProjectSaved()

        models = [
            self._measured_data_model, self._calculated_data_model,
            self._bragg_peaks_model, self._cell_parameters_model,
            self._cell_box_model, self._atom_sites_model,
            self._atom_adps_model, self._atom_msps_model, self._fitables_model,
            self._status_model
        ]
        for model in models:
            model.setCalculatorInterface(self._calculator_interface)

        self._refine_thread = Refiner(self._calculator_interface, 'refine')
        self._refine_thread.failed.connect(self._thread_failed)
        self._refine_thread.finished.connect(self._thread_finished)
        self._refine_thread.finished.connect(
            self._status_model.onRefinementDone)

        # We can't link signals as the manager signals emitted before the dict is updated :-(
        self.projectChanged.emit()

    @Slot()
    def createProjectZip(self):
        self.__log.debug("")
        self._calculator_interface.writeMainCif(
            self._project_control.tempDir.name)
        writeEmptyProject(self._project_control,
                          self._project_control.project_file)
        self.onProjectSaved()

    @Slot(str)
    def createProject(self, file_path):
        self.__log.debug("")
        self._project_control.createProject(file_path)
        # Note that the main rcif of self._project_control.project_rcif_path has not ben cleared
        self._project_control.project_rcif_path = ''
        self.onProjectSaved()
        self.initialize()
        self.projectChanged.emit()

    @Slot(str)
    def saveProjectAs(self, file_path):
        self.__log.debug("")
        self._project_control.project_file = file_path
        self.saveProject()

    @Slot()
    def saveProject(self):
        self.__log.debug("")
        self._calculator_interface.saveCifs(self._project_control.tempDir.name)
        writeProject(self._project_control, self._project_control.project_file)
        self.onProjectSaved()

    def onProjectSaved(self):
        self.__log.debug("")
        self._project_dict_copy = deepcopy(
            self._calculator_interface.project_dict)
        self._need_to_save = False
        self.projectSaveStateChanged.emit()
        self.projectChanged.emit(
        )  # update project.cif in gui (when filenames changed)

    def onProjectUnsaved(self):
        self.__log.debug("")
        self._need_to_save = True
        self.projectSaveStateChanged.emit()

    def onProjectChanged(self):
        keys, _, _ = self._calculator_interface.project_dict.dictComparison(
            self._project_dict_copy)
        self.__log.debug(f"keys: {keys}")
        self._need_to_save = True
        if not keys:
            self._need_to_save = False
        self.__log.debug(f"needToSave: {self._need_to_save}")
        self.projectSaveStateChanged.emit()

    def calculatorInterface(self):
        self.__log.debug("---")
        return self._calculator_interface

    def needToSave(self):
        self.__log.debug("+++")
        return self._need_to_save

    def projectFilePathSelected(self):
        self.__log.debug("***")
        return bool(self._project_control.project_file)

    # ##############
    # QML Properties
    # ##############

    # Notifications of changes for QML GUI about projectDictChanged,
    # which calls another signal projectChanged

    projectChanged = Signal()
    projectSaveStateChanged = Signal()
    canUndoOrRedoChanged = Signal()

    _calculatorInterface = Property('QVariant',
                                    calculatorInterface,
                                    notify=projectChanged)
    _needToSave = Property(bool, needToSave, notify=projectSaveStateChanged)
    _projectFilePathSelected = Property(bool,
                                        projectFilePathSelected,
                                        notify=projectSaveStateChanged)

    _undoText = Property('QVariant',
                         lambda self: self._calculator_interface.undoText(),
                         notify=canUndoOrRedoChanged)
    _redoText = Property('QVariant',
                         lambda self: self._calculator_interface.redoText(),
                         notify=canUndoOrRedoChanged)
    _canUndo = Property('QVariant',
                        lambda self: self._calculator_interface.canUndo(),
                        notify=canUndoOrRedoChanged)
    _canRedo = Property('QVariant',
                        lambda self: self._calculator_interface.canRedo(),
                        notify=canUndoOrRedoChanged)

    # Notifications of changes for QML GUI are done, when needed, in the
    # respective classes via dataChanged.emit() or layotChanged.emit() signals

    _proxy = Property('QVariant', lambda self: self, constant=True)
    _releaseInfo = Property('QVariant',
                            lambda self: self.releaseInfo,
                            constant=True)

    _projectControl = Property('QVariant',
                               lambda self: self._project_control,
                               constant=True)
    _projectManager = Property('QVariant',
                               lambda self: self._project_control.manager,
                               constant=True)

    _measuredData = Property('QVariant',
                             lambda self: self._measured_data_model,
                             constant=True)
    _calculatedData = Property('QVariant',
                               lambda self: self._calculated_data_model,
                               constant=True)
    _braggPeaks = Property('QVariant',
                           lambda self: self._bragg_peaks_model,
                           constant=True)
    _cellParameters = Property(
        'QVariant',
        lambda self: self._cell_parameters_model.asModel(),
        constant=True)
    _cellBox = Property('QVariant',
                        lambda self: self._cell_box_model.asModel(),
                        constant=True)
    _atomSites = Property('QVariant',
                          lambda self: self._atom_sites_model.asModel(),
                          constant=True)
    _atomAdps = Property('QVariant',
                         lambda self: self._atom_adps_model.asModel(),
                         constant=True)
    _atomMsps = Property('QVariant',
                         lambda self: self._atom_msps_model.asModel(),
                         constant=True)
    _fitables = Property('QVariant',
                         lambda self: self._fitables_model.asModel(),
                         constant=True)

    _statusInfo = Property(
        'QVariant',
        lambda self: self._status_model.returnStatusBarModel(),
        constant=True)
    _chartInfo = Property('QVariant',
                          lambda self: self._status_model.returnChartModel(),
                          constant=True)

    _releaseInfo = Property('QVariant', lambda self: self.info, constant=True)

    # ###############
    # REFINEMENT TYPE
    # ###############

    def refineSum(self):
        if not self._calculator_interface.experimentsIds():
            return False
        experiment_name = self._calculator_interface.experimentsIds()[0]
        return self._calculator_interface.project_dict['experiments'][
            experiment_name]['refinement_type'].sum

    def refineDiff(self):
        if not self._calculator_interface.experimentsIds():
            return False
        experiment_name = self._calculator_interface.experimentsIds()[0]
        return self._calculator_interface.project_dict['experiments'][
            experiment_name]['refinement_type'].diff

    def setRefineSum(self, state):
        experiment_name = self._calculator_interface.experimentsIds()[0]
        if self._calculator_interface.project_dict['experiments'][
                experiment_name]['refinement_type'].sum == state:
            return
        self._calculator_interface.project_dict['experiments'][
            experiment_name]['refinement_type'].sum = state

    def setRefineDiff(self, state):
        experiment_name = self._calculator_interface.experimentsIds()[0]
        if self._calculator_interface.project_dict['experiments'][
                experiment_name]['refinement_type'].diff == state:
            return
        self._calculator_interface.project_dict['experiments'][
            experiment_name]['refinement_type'].diff = state

    _refineSum = Property(bool, refineSum, setRefineSum, notify=projectChanged)
    _refineDiff = Property(bool,
                           refineDiff,
                           setRefineDiff,
                           notify=projectChanged)

    # ##########
    # REFINEMENT
    # ##########

    def _thread_finished(self, res):
        """
        Notfy the listeners about refinement results
        """
        self._refinement_running = False
        self._refinement_done = True
        self._refinement_result = deepcopy(res)
        self.refinementStatusChanged.emit()

    def _thread_failed(self, reason):
        """
        Notify the GUI about failure so a message can be shown
        """
        self.__log.info("Refinement failed: " + str(reason))
        self._refinement_running = False
        self._refinement_done = False
        self.refinementStatusChanged.emit()

    @Slot()
    def refine(self):
        """
        Start refinement as a separate thread
        """
        self._calculator_interface.setCalculatorFromProject()
        self.__log.info("")
        if self._refinement_running:
            self.__log.info("Fitting stopped")
            # This lacks actual stopping functionality, needs to be added
            self._refinement_running = False
            self._refinement_done = True
            self.refinementStatusChanged.emit()
            return
        self._refinement_running = True
        self._refinement_done = False
        self.refinementStatusChanged.emit()
        self._refine_thread.start()

    refinementStatusChanged = Signal()
    _refinementStatus = Property('QVariant',
                                 lambda self: [
                                     self._refinement_running, self.
                                     _refinement_done, self._refinement_result
                                 ],
                                 notify=refinementStatusChanged)

    # ######
    # REPORT
    # ######

    @Slot(str)
    def store_report(self, report=""):
        """
        Keep the QML generated HTML report for saving
        """
        self.report_html = report

    @Slot(str, str)
    def save_report(self, filename="", extension=".HTML"):
        """
        Save the generated report to the specified file
        Currently only html
        """
        full_filename = filename + extension.lower()
        full_filename = os.path.join(
            self._project_control.get_project_dir_absolute_path(),
            full_filename)

        if not self.report_html:
            self.__log.info("No report to save")
            return

        if extension == '.HTML':
            # HTML can contain non-ascii, so need to open with right encoding
            with open(full_filename, 'w', encoding='utf-8') as report_file:
                report_file.write(self.report_html)
                self.__log.info("Report written")
        elif extension == '.PDF':
            document = QTextDocument(parent=None)
            document.setHtml(self.report_html)
            printer = QPdfWriter(full_filename)
            printer.setPageSize(printer.A3)  # A3 to fit A4 page
            document.print_(printer)
        else:
            raise NotImplementedError

        # Show the generated report in the default browser
        url = os.path.realpath(full_filename)
        open_url(url=url)
def test_onModelChanged():
    file_path = QUrl(TEST_FILE).toLocalFile()
    calculator = CryspyCalculator(file_path)
    interface = QtCalculatorInterface(calculator)

    m = Model()
    m.setCalculatorInterface(interface)
    phase_index = m._model.index(2, 0)                           # 3rd element (from phase block)
    experiment_index = m._model.index(m._model.rowCount()-3, 0)  # 4th from below (from experiment block)

    # ######################
    # Check unsupported role
    # ######################

    edit_role = Qt.UserRole + 101  # path edit role
    new_edit = []
    m._model.setData(phase_index, new_edit, edit_role)

    # ######################
    # Check refine parameter
    # ######################

    display_role = Qt.UserRole + 7
    edit_role = Qt.UserRole + 107
    old_display = False
    new_display = True
    old_edit = None
    new_edit = True

    # Initial state
    assert m._model.data(phase_index, display_role) == old_display
    assert m._model.data(phase_index, edit_role) == old_edit
    assert m._model.data(experiment_index, display_role) == old_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via display role
    m._model.setData(phase_index, new_display, display_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_display, display_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via edit role
    m._model.setData(phase_index, new_edit, edit_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_edit, edit_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # #####################
    # Check value parameter
    # #####################

    display_role = Qt.UserRole + 3
    edit_role = Qt.UserRole + 103
    old_display = 0
    old_edit = None
    new_display = 0.5
    new_edit = 0.5

    # Initial state
    assert m._model.data(phase_index, display_role) == old_display
    assert m._model.data(phase_index, edit_role) == old_edit
    assert m._model.data(experiment_index, display_role) == old_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via display role
    m._model.setData(phase_index, new_display, display_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_display, display_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via edit role
    m._model.setData(phase_index, new_edit, edit_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_edit, edit_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via edit role outside min/max limits
    m._model.setData(phase_index, -100, Qt.UserRole + 103)
    assert m._model.data(phase_index, Qt.UserRole + 5) == -120
    m._model.setData(phase_index, 100, Qt.UserRole + 103)
    assert m._model.data(phase_index, Qt.UserRole + 6) == 120
    m._model.setData(phase_index, 1000, Qt.UserRole + 103)
    m._model.setData(phase_index, 1000, Qt.UserRole + 105)
    m._model.setData(phase_index, 1000, Qt.UserRole + 106)
    m._model.setData(phase_index,  100, Qt.UserRole + 103)
    assert m._model.data(phase_index, Qt.UserRole + 5) == 80
    m._model.setData(phase_index, -1000, Qt.UserRole + 103)
    m._model.setData(phase_index, -1000, Qt.UserRole + 105)
    m._model.setData(phase_index, -1000, Qt.UserRole + 106)
    m._model.setData(phase_index,  -100, Qt.UserRole + 103)
    assert m._model.data(phase_index, Qt.UserRole + 6) == -80
    m._model.setData(phase_index, 0, Qt.UserRole + 105)
    m._model.setData(phase_index, 0, Qt.UserRole + 106)
    m._model.setData(phase_index, 0, Qt.UserRole + 103)
    assert m._model.data(phase_index, Qt.UserRole + 5) == -1
    assert m._model.data(phase_index, Qt.UserRole + 6) == 1
    m._model.setData(experiment_index, 0, Qt.UserRole + 105)
    m._model.setData(experiment_index, 0, Qt.UserRole + 106)
    m._model.setData(experiment_index, 0, Qt.UserRole + 103)
    assert m._model.data(experiment_index, Qt.UserRole + 5) == -1
    assert m._model.data(experiment_index, Qt.UserRole + 6) == 1

    # ###################
    # Check min parameter
    # ###################

    display_role = Qt.UserRole + 5
    edit_role = Qt.UserRole + 105
    old_display = -1
    old_edit = None
    new_display = -0.5
    new_edit = -0.5

    # Initial state
    assert m._model.data(phase_index, display_role) == old_display
    assert m._model.data(phase_index, edit_role) == old_edit
    assert m._model.data(experiment_index, display_role) == old_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via display role
    m._model.setData(phase_index, new_display, display_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_display, display_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via edit role
    m._model.setData(phase_index, new_edit, edit_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_edit, edit_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # ###################
    # Check max parameter
    # ###################

    display_role = Qt.UserRole + 6
    edit_role = Qt.UserRole + 106
    old_display = 1
    old_edit = None
    new_display = 0.5
    new_edit = 0.5

    # Initial state
    assert m._model.data(phase_index, display_role) == old_display
    assert m._model.data(phase_index, edit_role) == old_edit
    assert m._model.data(experiment_index, display_role) == old_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via display role
    m._model.setData(phase_index, new_display, display_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_display, display_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit

    # Model changes via edit role
    m._model.setData(phase_index, new_edit, edit_role)
    assert m._model.data(phase_index, display_role) == new_display
    assert m._model.data(phase_index, edit_role) == old_edit
    m._model.setData(experiment_index, new_edit, edit_role)
    assert m._model.data(experiment_index, display_role) == new_display
    assert m._model.data(experiment_index, edit_role) == old_edit