Ejemplo n.º 1
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
Ejemplo n.º 2
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)