normalization.py 11.8 KB
Newer Older
payno's avatar
payno committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/

__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "06/07/2019"


31
32
33
import functools
import logging

payno's avatar
payno committed
34
from Orange.widgets import gui
payno's avatar
payno committed
35
from Orange.widgets.settings import Setting
36
from Orange.widgets.widget import OWWidget
37
38
from Orange.widgets.widget import Input, Output
import Orange.data
payno's avatar
payno committed
39
40
41
from PyMca5.PyMcaGui.physics.xas.XASNormalizationParameters import (
    XASNormalizationParameters,
)
42
43
from silx.gui import qt
from silx.gui.plot import LegendSelector
payno's avatar
payno committed
44
45
46
47
48
49
from est.gui.XasObjectViewer import (
    _plot_edge,
    _plot_norm,
    _plot_post_edge,
    _plot_pre_edge,
)
payno's avatar
payno committed
50
51
from orangecontrib.est.process import _ProcessForOrangeMixIn
from orangecontrib.est.process import ProcessRunnable
52
from orangecontrib.est.widgets.container import _ParameterWindowContainer
payno's avatar
payno committed
53
54
from orangecontrib.est.progress import QProgress
from orangecontrib.est.utils import Converter
55
56
57
58
from est.gui.e0calculator import E0CalculatorDialog
from est.core.types import XASObject
from est.gui.XasObjectViewer import XasObjectViewer, ViewType
import est.core.process.pymca.normalization
payno's avatar
payno committed
59

payno's avatar
payno committed
60
61
_logger = logging.getLogger(__file__)

62

63
64
65
66
67
68
69
70
71
72
73
74
75
class _XASNormalizationParametersPatched(XASNormalizationParameters):
    """This class will try to patch the XASNormalizationParameters with the
    E0Calculation widget"""

    sigE0CalculationRequested = qt.Signal()
    """Signal emitted when E0 computation is required"""

    def __init__(self, *args, **kwargs):
        XASNormalizationParameters.__init__(self, *args, **kwargs)
        # add E0CalculationWidget if can
        try:
            self.__addE0CalculationDialog()
        except Exception as e:
payno's avatar
payno committed
76
            _logger.warning("Fail to add the E0CalculationDialog. Reason is", str(e))
77
78
79
80
81
82
        else:
            self._e0CalcPB.pressed.connect(self._launchE0Calculator)

    def setE0(self, e0):
        e0_min = self.e0SpinBox.minimum()
        e0_max = self.e0SpinBox.maximum()
payno's avatar
payno committed
83
84
85
86
87
        if not (e0_min <= e0 <= e0_max):
            _logger.warning(
                "given e0 value (%s) is invalid, value should be "
                "between %s and %s" % (e0, e0_min, e0_max)
            )
88
89
90

        params = self.getParameters()
        params["E0Value"] = e0
payno's avatar
payno committed
91
        params["E0Method"] = "Manual"
92
93
94
95
        self.setParameters(ddict=params, signal=False)

    def __addE0CalculationDialog(self):
        # check we know where to set the button
payno's avatar
payno committed
96
        for attr in ("e0SpinBox", "jumpLine", "e0CheckBox"):
97
            if not hasattr(self, attr):
payno's avatar
payno committed
98
99
100
                raise NameError(
                    "%s not defined - pymca version not " "recognized" % attr
                )
101
102
        for widget, widget_index in zip((self.e0SpinBox, self.jumpLine), (3, 5)):
            if self.layout().indexOf(widget) != widget_index:
payno's avatar
payno committed
103
                raise ValueError("XASNormalizationParameters layout is not recognized.")
104
105
106

        style = qt.QApplication.instance().style()
        icon = style.standardIcon(qt.QStyle.SP_FileDialogContentsView)
payno's avatar
payno committed
107
        self._e0CalcPB = qt.QPushButton(icon, "", self)
108
109
110
111
112
113
        self.layout().addWidget(self._e0CalcPB, 1, 2)

    def _launchE0Calculator(self, *args, **kwargs):
        self.sigE0CalculationRequested.emit()


payno's avatar
payno committed
114
class NormalizationWindow(qt.QMainWindow):
115
116
117
118
119
120
    """Widget embedding the pymca parameter window and the display of the
    data currently process"""

    sigE0CalculationRequested = qt.Signal()
    """Signal emitted when E0 computation is required"""

payno's avatar
payno committed
121
122
    def __init__(self, parent=None):
        qt.QMainWindow.__init__(self, parent)
123
124

        # xas object viewer
payno's avatar
payno committed
125
        mapKeys = ["mu", "NormalizedMu", "NormalizedSignal", "NormalizedBackground"]
126
        self.xasObjViewer = XasObjectViewer(mapKeys=mapKeys)
payno's avatar
payno committed
127
128
129
        self.xasObjViewer._spectrumViews[0]._plotWidget.getXAxis().setLabel(
            "Energy (eV)"
        )
130
        self.xasObjViewer._spectrumViews[0]._plotWidget.getYAxis().setLabel(
payno's avatar
payno committed
131
132
            "Absorption (a.u.)"
        )
133
        self.setCentralWidget(self.xasObjViewer)
payno's avatar
payno committed
134
135
136
        self._pymcaWindow = _ParameterWindowContainer(
            parent=self, parametersWindow=_XASNormalizationParametersPatched
        )
payno's avatar
payno committed
137
138
139
140
141
142
143
144
        dockWidget = qt.QDockWidget(parent=self)

        # pymca window
        dockWidget.setWidget(self._pymcaWindow)
        self.addDockWidget(qt.Qt.RightDockWidgetArea, dockWidget)
        dockWidget.setAllowedAreas(qt.Qt.RightDockWidgetArea | qt.Qt.LeftDockWidgetArea)
        dockWidget.setFeatures(qt.QDockWidget.NoDockWidgetFeatures)

145
        # legend selector
payno's avatar
payno committed
146
        self.legendDockWidget = LegendSelector.LegendsDockWidget(
147
            parent=self, plot=self.xasObjViewer._spectrumViews[0]._plotWidget
payno's avatar
payno committed
148
149
150
151
        )
        self.legendDockWidget.setAllowedAreas(
            qt.Qt.RightDockWidgetArea | qt.Qt.LeftDockWidgetArea
        )
payno's avatar
payno committed
152
153
154
        self.legendDockWidget.setFeatures(qt.QDockWidget.NoDockWidgetFeatures)
        self.addDockWidget(qt.Qt.RightDockWidgetArea, self.legendDockWidget)

155
        # volume key selection
payno's avatar
payno committed
156
157
158
        self.addDockWidget(
            qt.Qt.RightDockWidgetArea, self.xasObjViewer._mapView.keySelectionDocker
        )
payno's avatar
payno committed
159

160
        # plot settings
161
        for ope in (_plot_edge, _plot_norm, _plot_post_edge, _plot_pre_edge):
162
            self.xasObjViewer._spectrumViews[0].addCurveOperation(ope)
163

164
165
        self.setWindowFlags(qt.Qt.Widget)

166
167
168
        # expose API
        self.setE0 = self._pymcaWindow._mainwidget.setE0

169
170
171
172
173
174
        # connect signal / slot
        self.xasObjViewer.viewTypeChanged.connect(self._updateLegendView)

        # set up
        self._updateLegendView()

175
    def getNCurves(self):
176
        return len(self.xasObjViewer._spectrumViews._plot.getAllCurves())
177
178

    def _updateLegendView(self):
179
180
        index, viewType = self.xasObjViewer.getViewType()
        self.legendDockWidget.setVisible(viewType is ViewType.spectrum)
payno's avatar
payno committed
181
182
183
        self.xasObjViewer._mapView.keySelectionDocker.setVisible(
            viewType is ViewType.map
        )
184

payno's avatar
payno committed
185

186
187
class NormalizationOW(
    _ProcessForOrangeMixIn,
188
    OWWidget,
189
):
payno's avatar
payno committed
190
191
192
    """
    Widget used for signal extraction
    """
payno's avatar
payno committed
193

payno's avatar
payno committed
194
    name = "normalization"
payno's avatar
payno committed
195
    id = "orange.widgets.est.pymca.normalization"
196
    description = "Progress spectra normalization"
payno's avatar
payno committed
197
    icon = "icons/normalization.png"
198
    priority = 1
payno's avatar
payno committed
199
200
201
202
203
    category = "esrfWidgets"
    keywords = ["spectroscopy", "normalization"]

    want_main_area = True
    resizing_enabled = True
204
    allows_cycle = False
205
    ewokstaskclass = est.core.process.pymca.normalization.PyMca_normalization
payno's avatar
payno committed
206

payno's avatar
payno committed
207
208
209
    _pymcaSettings = Setting(dict())
    """Store the configuration of the PyMca XASClass"""

210
    class Inputs:
payno's avatar
payno committed
211
        xas_obj = Input("xas_obj", XASObject, default=True)
212
213
        # simple compatibility for some Orange widget and especialy the
        # 'spectroscopy add-on'
payno's avatar
payno committed
214
        data_table = Input("Data", Orange.data.Table)
215
216

    class Outputs:
217
        xas_obj = Output("xas_obj", XASObject)
218
219
220
        # by default we want to avoid sending 'Orange.data.Table' to avoid
        # loosing the XASObject flow process and results.

payno's avatar
payno committed
221
    def __init__(self):
222
        super().__init__()
223
        # super().__init__()
224
        self._latest_xas_obj = None
payno's avatar
payno committed
225
        self._window = NormalizationWindow(parent=self)
payno's avatar
payno committed
226
        layout = gui.vBox(self.mainArea, "normalization").layout()
payno's avatar
payno committed
227
        layout.addWidget(self._window)
payno's avatar
payno committed
228
        self._window.xasObjViewer.setWindowTitle("spectra")
payno's avatar
payno committed
229

230
231
232
        # expose API
        self.setE0 = self._window.setE0

payno's avatar
payno committed
233
234
235
236
        # manage settings
        if self._pymcaSettings != dict():
            self._window._pymcaWindow.setParameters(self._pymcaSettings)

237
        # connect signals / slots
238
239
        pymcaWindowContainer = self._window._pymcaWindow
        _sig = pymcaWindowContainer.sigChanged.connect(self._updateProcess)
payno's avatar
payno committed
240
241
242
243
        if hasattr(pymcaWindowContainer._mainwidget, "sigE0CalculationRequested"):
            pymcaWindowContainer._mainwidget.sigE0CalculationRequested.connect(
                self._getE0FrmDialog
            )
244

245
        # self.handleNewSignals()
246

247
248
249
250
251
252
253
254
    def _updateProcess(self):
        self._update_settings()
        if self._latest_xas_obj:
            self.process(self._latest_xas_obj)

    def _update_settings(self):
        self._pymcaSettings = self._window._pymcaWindow.getParameters()

255
256
257
258
259
260
261
    @Inputs.data_table
    def processFrmDataTable(self, data_table):
        if data_table is None:
            return
        self.process(Converter.toXASObject(data_table=data_table))

    @Inputs.xas_obj
262
263
    def process(self, xas_obj):
        if xas_obj is None:
payno's avatar
payno committed
264
            return
265
        if not self._canProcess():
payno's avatar
payno committed
266
267
268
269
            _logger.warning(
                "There is some processing on going already, will"
                "not process the new dataset"
            )
270

271
        self._latest_xas_obj = xas_obj
272
        # update E0 if necessary
payno's avatar
payno committed
273
274
        if "e0" in xas_obj.configuration:
            self.setE0(xas_obj.configuration["e0"])
275
        self._startProcess()
276

277
        # setup the normalization process
278
        process_obj = QPyMca_normalization(inputs={"xas_obj": xas_obj})
279
        process_obj._advancement.sigProgress.connect(self._setProgressValue)
280
        process_obj.set_properties(
payno's avatar
payno committed
281
282
            {"_pymcaSettings": self._window._pymcaWindow.getParameters()}
        )
283

284
        # update the processing thread
285
        thread = self.getProcessingThread()
286
        thread.init(process_obj=process_obj, xas_obj=self._latest_xas_obj)
payno's avatar
payno committed
287
288
289
        self._callback_finish = functools.partial(
            self._endProcess, self._latest_xas_obj
        )
290
291
292
293
        thread.finished.connect(self._callback_finish)
        # start processing
        thread.start(priority=qt.QThread.LowPriority)

294
295
296
297
298
299
300
301
    def _getE0FrmDialog(self):
        """Pop up an instance of E0CalculationDialog and get E0 from it"""
        if self._latest_xas_obj is None:
            return
        dialog = E0CalculatorDialog(xas_obj=self._latest_xas_obj, parent=None)
        if dialog.exec_():
            self.setE0(dialog.getE0())

302

payno's avatar
payno committed
303
class QPyMca_normalization(est.core.process.pymca.normalization.PyMca_normalization):
304
305
306
    """
    Normalization able to give advancement using qt.Signal and QThreadPool
    """
307

308
309
310
311
    def __init__(self, *args, **kwargs):
        est.core.process.pymca.normalization.PyMca_normalization.__init__(
            self, *args, **kwargs
        )
payno's avatar
payno committed
312
        self._advancement = QProgress("normalization")
313

314
    def _pool_process(self, xas_obj):
315
316
        self.pool = qt.QThreadPool()
        self.pool.setMaxThreadCount(5)
317
        for spectrum in xas_obj.spectra:
payno's avatar
payno committed
318
319
320
321
322
323
            runnable = ProcessRunnable(
                fct=est.core.process.pymca.normalization.process_spectr_norm,
                spectrum=spectrum,
                configuration=xas_obj.configuration,
                callback=self._advancement.increaseAdvancement,
            )
324
325
            self.pool.start(runnable)
        self.pool.waitForDone()