summaryrefslogtreecommitdiff
path: root/silx/gui/fit/Parameters.py
blob: 62e327865158f2f3e2432df1b9d1b19d92d62a99 (plain)
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
# coding: utf-8
# /*##########################################################################
# Copyright (C) 2004-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.
#
# ######################################################################### */
"""This module defines a table widget that is specialized in displaying fit
parameter results and associated constraints."""
__authors__ = ["V.A. Sole", "P. Knobel"]
__license__ = "MIT"
__date__ = "25/11/2016"

import sys
from collections import OrderedDict

from silx.gui import qt
from silx.gui.widgets.TableWidget import TableWidget


def float_else_zero(sstring):
    """Return converted string to float. If conversion fail, return zero.

    :param sstring: String to be converted
    :return: ``float(sstrinq)`` if ``sstring`` can be converted to float
        (e.g. ``"3.14"``), else ``0``
    """
    try:
        return float(sstring)
    except ValueError:
        return 0


class QComboTableItem(qt.QComboBox):
    """:class:`qt.QComboBox` augmented with a ``sigCellChanged`` signal
    to emit a tuple of ``(row, column)`` coordinates when the value is
    changed.

    This signal can be used to locate the modified combo box in a table.

    :param row: Row number of the table cell containing this widget
    :param col: Column number of the table cell containing this widget"""
    sigCellChanged = qt.Signal(int, int)
    """Signal emitted when this ``QComboBox`` is activated.
    A ``(row, column)`` tuple is passed."""

    def __init__(self, parent=None, row=None, col=None):
        self._row = row
        self._col = col
        qt.QComboBox.__init__(self, parent)
        self.activated[int].connect(self._cellChanged)

    def _cellChanged(self, idx):  # noqa
        self.sigCellChanged.emit(self._row, self._col)


class QCheckBoxItem(qt.QCheckBox):
    """:class:`qt.QCheckBox` augmented with a ``sigCellChanged`` signal
    to emit a tuple of ``(row, column)`` coordinates when the check box has
    been clicked on.

    This signal can be used to locate the modified check box in a table.

    :param row: Row number of the table cell containing this widget
    :param col: Column number of the table cell containing this widget"""
    sigCellChanged = qt.Signal(int, int)
    """Signal emitted when this ``QCheckBox`` is clicked.
    A ``(row, column)`` tuple is passed."""

    def __init__(self, parent=None, row=None, col=None):
        self._row = row
        self._col = col
        qt.QCheckBox.__init__(self, parent)
        self.clicked.connect(self._cellChanged)

    def _cellChanged(self):
        self.sigCellChanged.emit(self._row, self._col)


class Parameters(TableWidget):
    """:class:`TableWidget` customized to display fit results
    and to interact with :class:`FitManager` objects.

    Data and references to cell widgets are kept in a dictionary
    attribute :attr:`parameters`.

    :param parent: Parent widget
    :param labels: Column headers. If ``None``, default headers will be used.
    :type labels: List of strings or None
    :param paramlist: List of fit parameters to be displayed for each fitted
        peak.
    :type paramlist: list[str] or None
    """
    def __init__(self, parent=None, paramlist=None):
        TableWidget.__init__(self, parent)
        self.setContentsMargins(0, 0, 0, 0)

        labels = ['Parameter', 'Estimation', 'Fit Value', 'Sigma',
                  'Constraints', 'Min/Parame', 'Max/Factor/Delta']
        tooltips = ["Fit parameter name",
                    "Estimated value for fit parameter. You can edit this column.",
                    "Actual value for parameter, after fit",
                    "Uncertainty (same unit as the parameter)",
                    "Constraint to be applied to the parameter for fit",
                    "First parameter for constraint (name of another param or min value)",
                    "Second parameter for constraint (max value, or factor/delta)"]

        self.columnKeys = ['name', 'estimation', 'fitresult',
                           'sigma', 'code', 'val1', 'val2']
        """This list assigns shorter keys to refer to columns than the
        displayed labels."""

        self.__configuring = False

        # column headers and associated tooltips
        self.setColumnCount(len(labels))

        for i, label in enumerate(labels):
            item = self.horizontalHeaderItem(i)
            if item is None:
                item = qt.QTableWidgetItem(label,
                                           qt.QTableWidgetItem.Type)
                self.setHorizontalHeaderItem(i, item)

            item.setText(label)
            if tooltips is not None:
                item.setToolTip(tooltips[i])

        # resize columns
        for col_key in ["name", "estimation", "sigma", "val1", "val2"]:
            col_idx = self.columnIndexByField(col_key)
            self.resizeColumnToContents(col_idx)

        # Initialize the table with one line per supplied parameter
        paramlist = paramlist if paramlist is not None else []
        self.parameters = OrderedDict()
        """This attribute stores all the data in an ordered dictionary.
        New data can be added using :meth:`newParameterLine`.
        Existing data can be modified using :meth:`configureLine`

        Keys of the dictionary are:

            -  'name': parameter name
            -  'line': line index for the parameter in the table
            -  'estimation'
            -  'fitresult'
            -  'sigma'
            -  'code': constraint code (one of the elements of
                :attr:`code_options`)
            -  'val1': first parameter related to constraint, formatted
                as a string, as typed in the table
            -  'val2': second parameter related to constraint, formatted
                as a string, as typed in the table
            -  'cons1': scalar representation of 'val1'
                (e.g. when val1 is the name of a fit parameter, cons1
                will be the line index of this parameter)
            -  'cons2': scalar representation of 'val2'
            -  'vmin': equal to 'val1' when 'code' is "QUOTED"
            -  'vmax': equal to 'val2' when 'code' is "QUOTED"
            -  'relatedto': name of related parameter when this parameter
                is constrained to another parameter (same as 'val1')
            -  'factor': same as 'val2' when 'code' is 'FACTOR'
            -  'delta': same as 'val2' when 'code' is 'DELTA'
            -  'sum': same as 'val2' when 'code' is 'SUM'
            -  'group': group index for the parameter
            -  'xmin': data range minimum
            -  'xmax': data range maximum
        """
        for line, param in enumerate(paramlist):
            self.newParameterLine(param, line)

        self.code_options = ["FREE", "POSITIVE", "QUOTED", "FIXED",
                             "FACTOR", "DELTA", "SUM", "IGNORE", "ADD"]
        """Possible values in the combo boxes in the 'Constraints' column.
        """

        # connect signal
        self.cellChanged[int, int].connect(self.onCellChanged)

    def newParameterLine(self, param, line):
        """Add a line to the :class:`QTableWidget`.

        Each line represents one of the fit parameters for one of
        the fitted peaks.

        :param param: Name of the fit parameter
        :type param: str
        :param line: 0-based line index
        :type line: int
        """
        # get current number of lines
        nlines = self.rowCount()
        self.__configuring = True
        if line >= nlines:
            self.setRowCount(line + 1)

        # default configuration for fit parameters
        self.parameters[param] = OrderedDict((('line', line),
                                              ('estimation', '0'),
                                              ('fitresult', ''),
                                              ('sigma', ''),
                                              ('code', 'FREE'),
                                              ('val1', ''),
                                              ('val2', ''),
                                              ('cons1', 0),
                                              ('cons2', 0),
                                              ('vmin', '0'),
                                              ('vmax', '1'),
                                              ('relatedto', ''),
                                              ('factor', '1.0'),
                                              ('delta', '0.0'),
                                              ('sum', '0.0'),
                                              ('group', ''),
                                              ('name', param),
                                              ('xmin', None),
                                              ('xmax', None)))
        self.setReadWrite(param, 'estimation')
        self.setReadOnly(param, ['name', 'fitresult', 'sigma', 'val1', 'val2'])

        # Constraint codes
        a = []
        for option in self.code_options:
            a.append(option)

        code_column_index = self.columnIndexByField('code')
        cellWidget = self.cellWidget(line, code_column_index)
        if cellWidget is None:
            cellWidget = QComboTableItem(self, row=line,
                                         col=code_column_index)
            cellWidget.addItems(a)
            self.setCellWidget(line, code_column_index, cellWidget)
            cellWidget.sigCellChanged[int, int].connect(self.onCellChanged)
        self.parameters[param]['code_item'] = cellWidget
        self.parameters[param]['relatedto_item'] = None
        self.__configuring = False

    def columnIndexByField(self, field):
        """

        :param field: Field name (column key)
        :return: Index of the column with this field name
        """
        return self.columnKeys.index(field)

    def fillFromFit(self, fitresults):
        """Fill table with values from a  list of dictionaries
        (see :attr:`silx.math.fit.fitmanager.FitManager.fit_results`)

        :param fitresults: List of parameters as recorded
             in the ``paramlist`` attribute of a :class:`FitManager` object
        :type fitresults: list[dict]
        """
        self.setRowCount(len(fitresults))

        # Reinitialize and fill self.parameters
        self.parameters = OrderedDict()
        for (line, param) in enumerate(fitresults):
            self.newParameterLine(param['name'], line)

        for param in fitresults:
            name = param['name']
            code = str(param['code'])
            if code not in self.code_options:
                # convert code from int to descriptive string
                code = self.code_options[int(code)]
            val1 = param['cons1']
            val2 = param['cons2']
            estimation = param['estimation']
            group = param['group']
            sigma = param['sigma']
            fitresult = param['fitresult']

            xmin = param.get('xmin')
            xmax = param.get('xmax')

            self.configureLine(name=name,
                               code=code,
                               val1=val1, val2=val2,
                               estimation=estimation,
                               fitresult=fitresult,
                               sigma=sigma,
                               group=group,
                               xmin=xmin, xmax=xmax)

    def getConfiguration(self):
        """Return ``FitManager.paramlist`` dictionary
        encapsulated in another dictionary"""
        return {'parameters': self.getFitResults()}

    def setConfiguration(self, ddict):
        """Fill table with values from a ``FitManager.paramlist`` dictionary
        encapsulated in another dictionary"""
        self.fillFromFit(ddict['parameters'])

    def getFitResults(self):
        """Return fit parameters as a list of dictionaries in the format used
        by :class:`FitManager` (attribute ``paramlist``).
        """
        fitparameterslist = []
        for param in self.parameters:
            fitparam = {}
            name = param
            estimation, [code, cons1, cons2] = self.getEstimationConstraints(name)
            buf = str(self.parameters[param]['fitresult'])
            xmin = self.parameters[param]['xmin']
            xmax = self.parameters[param]['xmax']
            if len(buf):
                fitresult = float(buf)
            else:
                fitresult = 0.0
            buf = str(self.parameters[param]['sigma'])
            if len(buf):
                sigma = float(buf)
            else:
                sigma = 0.0
            buf = str(self.parameters[param]['group'])
            if len(buf):
                group = float(buf)
            else:
                group = 0
            fitparam['name'] = name
            fitparam['estimation'] = estimation
            fitparam['fitresult'] = fitresult
            fitparam['sigma'] = sigma
            fitparam['group'] = group
            fitparam['code'] = code
            fitparam['cons1'] = cons1
            fitparam['cons2'] = cons2
            fitparam['xmin'] = xmin
            fitparam['xmax'] = xmax
            fitparameterslist.append(fitparam)
        return fitparameterslist

    def onCellChanged(self, row, col):
        """Slot called when ``cellChanged`` signal is emitted.
        Checks the validity of the new text in the cell, then calls
        :meth:`configureLine` to update the internal ``self.parameters``
        dictionary.

        :param row: Row number of the changed cell (0-based index)
        :param col: Column number of the changed cell (0-based index)
        """
        if (col != self.columnIndexByField("code")) and (col != -1):
            if row != self.currentRow():
                return
            if col != self.currentColumn():
                return
        if self.__configuring:
            return
        param = list(self.parameters)[row]
        field = self.columnKeys[col]
        oldvalue = self.parameters[param][field]
        if col != 4:
            item = self.item(row, col)
            if item is not None:
                newvalue = item.text()
            else:
                newvalue = ''
        else:
            # this is the combobox
            widget = self.cellWidget(row, col)
            newvalue = widget.currentText()
        if self.validate(param, field, oldvalue, newvalue):
            paramdict = {"name": param, field: newvalue}
            self.configureLine(**paramdict)
        else:
            if field == 'code':
                # New code not valid, try restoring the old one
                index = self.code_options.index(oldvalue)
                self.__configuring = True
                try:
                    self.parameters[param]['code_item'].setCurrentIndex(index)
                finally:
                    self.__configuring = False
            else:
                paramdict = {"name": param, field: oldvalue}
                self.configureLine(**paramdict)

    def validate(self, param, field, oldvalue, newvalue):
        """Check validity of ``newvalue`` when a cell's value is modified.

        :param param: Fit parameter name
        :param field: Column name
        :param oldvalue: Cell value before change attempt
        :param newvalue: New value to be validated
        :return: True if new cell value is valid, else False
        """
        if field == 'code':
            return self.setCodeValue(param, oldvalue, newvalue)
            # FIXME: validate() shouldn't have side effects. Move this bit to configureLine()?
        if field == 'val1' and str(self.parameters[param]['code']) in ['DELTA', 'FACTOR', 'SUM']:
            _, candidates = self.getRelatedCandidates(param)
            # We expect val1 to be a fit parameter name
            if str(newvalue) in candidates:
                return True
            else:
                return False
        # except for code, val1 and name (which is read-only and does not need
        # validation), all fields must always be convertible to float
        else:
            try:
                float(str(newvalue))
            except ValueError:
                return False
        return True

    def setCodeValue(self, param, oldvalue, newvalue):
        """Update 'code' and 'relatedto' fields when code cell is
        changed.

        :param param: Fit parameter name
        :param oldvalue: Cell value before change attempt
        :param newvalue: New value to be validated
        :return: ``True`` if code was successfully updated
        """

        if str(newvalue) in ['FREE', 'POSITIVE', 'QUOTED', 'FIXED']:
            self.configureLine(name=param,
                               code=newvalue)
            if str(oldvalue) == 'IGNORE':
                self.freeRestOfGroup(param)
            return True
        elif str(newvalue) in ['FACTOR', 'DELTA', 'SUM']:
            # I should check here that some parameter is set
            best, candidates = self.getRelatedCandidates(param)
            if len(candidates) == 0:
                return False
            self.configureLine(name=param,
                               code=newvalue,
                               relatedto=best)
            if str(oldvalue) == 'IGNORE':
                self.freeRestOfGroup(param)
            return True

        elif str(newvalue) == 'IGNORE':
            # I should check if the group can be ignored
            # for the time being I just fix all of them to ignore
            group = int(float(str(self.parameters[param]['group'])))
            candidates = []
            for param in self.parameters.keys():
                if group == int(float(str(self.parameters[param]['group']))):
                    candidates.append(param)
            # print candidates
            # I should check here if there is any relation to them
            for param in candidates:
                self.configureLine(name=param,
                                   code=newvalue)
            return True
        elif str(newvalue) == 'ADD':
            group = int(float(str(self.parameters[param]['group'])))
            if group == 0:
                # One cannot add a background group
                return False
            i = 0
            for param in self.parameters:
                if i <= int(float(str(self.parameters[param]['group']))):
                    i += 1
            if (group == 0) and (i == 1):   # FIXME: why +1?
                i += 1
            self.addGroup(i, group)
            return False
        elif str(newvalue) == 'SHOW':
            print(self.getEstimationConstraints(param))
            return False

    def addGroup(self, newg, gtype):
        """Add a fit parameter group with the same fit parameters as an
        existing group.

        This function is called when the user selects "ADD" in the
        "constraints" combobox.

        :param int newg: New group number
        :param int gtype: Group number whose parameters we want to copy

        """
        newparam = []
        # loop through parameters until we encounter group number `gtype`
        for param in list(self.parameters):
            paramgroup = int(float(str(self.parameters[param]['group'])))
            # copy parameter names in group number `gtype`
            if paramgroup == gtype:
                # but replace `gtype` with `newg`
                newparam.append(param.rstrip("0123456789") + "%d" % newg)

                xmin = self.parameters[param]['xmin']
                xmax = self.parameters[param]['xmax']

        # Add new parameters (one table line per parameter) and configureLine each
        # one by updating xmin and xmax to the same values as group `gtype`
        line = len(list(self.parameters))
        for param in newparam:
            self.newParameterLine(param, line)
            line += 1
        for param in newparam:
            self.configureLine(name=param, group=newg, xmin=xmin, xmax=xmax)

    def freeRestOfGroup(self, workparam):
        """Set ``code`` to ``"FREE"`` for all fit parameters belonging to
        the same group as ``workparam``. This is done when the entire group
        of parameters was previously ignored and one of them has his code
        set to something different than ``"IGNORE"``.

        :param workparam: Fit parameter name
        """
        if workparam in self.parameters.keys():
            group = int(float(str(self.parameters[workparam]['group'])))
            for param in self.parameters:
                if param != workparam and\
                        group == int(float(str(self.parameters[param]['group']))):
                    self.configureLine(name=param,
                                       code='FREE',
                                       cons1=0,
                                       cons2=0,
                                       val1='',
                                       val2='')

    def getRelatedCandidates(self, workparam):
        """If fit parameter ``workparam`` has a constraint that involves other
        fit parameters, find possible candidates and try to guess which one
        is the most likely.

        :param workparam: Fit parameter name
        :return: (best_candidate, possible_candidates) tuple
        :rtype: (str, list[str])
        """
        candidates = []
        for param_name in self.parameters:
            if param_name != workparam:
                # ignore parameters that are fixed by a constraint
                if str(self.parameters[param_name]['code']) not in\
                        ['IGNORE', 'FACTOR', 'DELTA', 'SUM']:
                    candidates.append(param_name)
        # take the previous one (before code cell changed) if possible
        if str(self.parameters[workparam]['relatedto']) in candidates:
            best = str(self.parameters[workparam]['relatedto'])
            return best, candidates
        # take the first with same base name (after removing numbers)
        for param_name in candidates:
            basename = param_name.rstrip("0123456789")
            try:
                pos = workparam.index(basename)
                if pos == 0:
                    best = param_name
                    return best, candidates
            except ValueError:
                pass
        # take the first
        return candidates[0], candidates

    def setReadOnly(self, parameter, fields):
        """Make table cells read-only by setting it's flags and omitting
        flag ``qt.Qt.ItemIsEditable``

        :param parameter: Fit parameter names identifying the rows
        :type parameter: str or list[str]
        :param fields: Field names identifying the columns
        :type fields: str or list[str]
        """
        editflags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled
        self.setField(parameter, fields, editflags)

    def setReadWrite(self, parameter, fields):
        """Make table cells read-write by setting it's flags including
        flag ``qt.Qt.ItemIsEditable``

        :param parameter: Fit parameter names identifying the rows
        :type parameter: str or list[str]
        :param fields: Field names identifying the columns
        :type fields: str or list[str]
        """
        editflags = qt.Qt.ItemIsSelectable |\
            qt.Qt.ItemIsEnabled |\
            qt.Qt.ItemIsEditable
        self.setField(parameter, fields, editflags)

    def setField(self, parameter, fields, edit_flags):
        """Set text and flags in a table cell.

        :param parameter: Fit parameter names identifying the rows
        :type parameter: str or list[str]
        :param fields: Field names identifying the columns
        :type fields: str or list[str]
        :param edit_flags: Flag combination, e.g::

            qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled |
            qt.Qt.ItemIsEditable
        """
        if isinstance(parameter, list) or \
           isinstance(parameter, tuple):
            paramlist = parameter
        else:
            paramlist = [parameter]
        if isinstance(fields, list) or \
           isinstance(fields, tuple):
            fieldlist = fields
        else:
            fieldlist = [fields]

        # Set _configuring flag to ignore cellChanged signals in
        # self.onCellChanged
        _oldvalue = self.__configuring
        self.__configuring = True

        # 2D loop through parameter list and field list
        # to update their cells
        for param in paramlist:
            row = list(self.parameters.keys()).index(param)
            for field in fieldlist:
                col = self.columnIndexByField(field)
                if field != 'code':
                    key = field + "_item"
                    item = self.item(row, col)
                    if item is None:
                        item = qt.QTableWidgetItem()
                        item.setText(self.parameters[param][field])
                        self.setItem(row, col, item)
                    else:
                        item.setText(self.parameters[param][field])
                    self.parameters[param][key] = item
                    item.setFlags(edit_flags)

        # Restore previous _configuring flag
        self.__configuring = _oldvalue

    def configureLine(self, name, code=None, val1=None, val2=None,
                      sigma=None, estimation=None, fitresult=None,
                      group=None, xmin=None, xmax=None, relatedto=None,
                      cons1=None, cons2=None):
        """This function updates values in a line of the table

        :param name: Name of the parameter (serves as unique identifier for
                     a line).
        :param code: Constraint code *FREE, FIXED, POSITIVE, DELTA, FACTOR,
                     SUM, QUOTED, IGNORE*
        :param val1: Constraint 1 (can be the index or name of another
                     parameter for code *DELTA, FACTOR, SUM*, or a min value
                     for code *QUOTED*)
        :param val2: Constraint 2
        :param sigma: Standard deviation for a fit parameter
        :param estimation: Estimated initial value for a fit parameter (used
                           as input to iterative fit)
        :param fitresult: Final result of fit
        :param group: Group number of a fit parameter (peak number when doing
                      multi-peak fitting, as each peak corresponds to a group
                      of several consecutive parameters)
        :param xmin:
        :param xmax:
        :param relatedto: Index or name of another fit parameter
                          to which this parameter is related to (constraints)
        :param cons1: similar meaning to ``val1``, but is always a number
        :param cons2: similar meaning to ``val2``, but is always a number
        :return:
        """
        paramlist = list(self.parameters.keys())

        if name not in self.parameters:
            raise KeyError("'%s' is not in the parameter list" % name)

        # update code first, if specified
        if code is not None:
            code = str(code)
            self.parameters[name]['code'] = code
            # update combobox
            index = self.parameters[name]['code_item'].findText(code)
            self.parameters[name]['code_item'].setCurrentIndex(index)
        else:
            # set code to previous value, used later for setting val1 val2
            code = self.parameters[name]['code']

        # val1 and sigma have special formats
        if val1 is not None:
            fmt = None if self.parameters[name]['code'] in\
                          ['DELTA', 'FACTOR', 'SUM'] else "%8g"
            self._updateField(name, "val1", val1, fmat=fmt)

        if sigma is not None:
            self._updateField(name, "sigma", sigma, fmat="%6.3g")

        # other fields are formatted as "%8g"
        keys_params = (("val2", val2), ("estimation", estimation),
                       ("fitresult", fitresult))
        for key, value in keys_params:
            if value is not None:
                self._updateField(name, key, value, fmat="%8g")

        # the rest of the parameters are treated as strings and don't need
        # validation
        keys_params = (("group", group), ("xmin", xmin),
                       ("xmax", xmax), ("relatedto", relatedto),
                       ("cons1", cons1), ("cons2", cons2))
        for key, value in keys_params:
            if value is not None:
                self.parameters[name][key] = str(value)

        # val1 and val2 have different meanings depending on the code
        if code == 'QUOTED':
            if val1 is not None:
                self.parameters[name]['vmin'] = self.parameters[name]['val1']
            else:
                self.parameters[name]['val1'] = self.parameters[name]['vmin']
            if val2 is not None:
                self.parameters[name]['vmax'] = self.parameters[name]['val2']
            else:
                self.parameters[name]['val2'] = self.parameters[name]['vmax']

            # cons1 and cons2 are scalar representations of val1 and val2
            self.parameters[name]['cons1'] =\
                float_else_zero(self.parameters[name]['val1'])
            self.parameters[name]['cons2'] =\
                float_else_zero(self.parameters[name]['val2'])

            # cons1, cons2 = min(val1, val2), max(val1, val2)
            if self.parameters[name]['cons1'] > self.parameters[name]['cons2']:
                self.parameters[name]['cons1'], self.parameters[name]['cons2'] =\
                    self.parameters[name]['cons2'], self.parameters[name]['cons1']

        elif code in ['DELTA', 'SUM', 'FACTOR']:
            # For these codes, val1 is the fit parameter name on which the
            # constraint depends
            if val1 is not None and val1 in paramlist:
                self.parameters[name]['relatedto'] = self.parameters[name]["val1"]

            elif val1 is not None:
                # val1 could be the index of the fit parameter
                try:
                    self.parameters[name]['relatedto'] = paramlist[int(val1)]
                except ValueError:
                    self.parameters[name]['relatedto'] = self.parameters[name]["val1"]

            elif relatedto is not None:
                # code changed, val1 not specified but relatedto specified:
                # set val1 to relatedto (pre-fill best guess)
                self.parameters[name]["val1"] = relatedto

            # update fields "delta", "sum" or "factor"
            key = code.lower()
            self.parameters[name][key] = self.parameters[name]["val2"]

            # FIXME: val1 is sometimes specified as an index rather than a param name
            self.parameters[name]['val1'] = self.parameters[name]['relatedto']

            # cons1 is the index of the fit parameter in the ordered dictionary
            if self.parameters[name]['val1'] in paramlist:
                self.parameters[name]['cons1'] =\
                    paramlist.index(self.parameters[name]['val1'])

            # cons2 is the constraint value (factor, delta or sum)
            try:
                self.parameters[name]['cons2'] =\
                    float(str(self.parameters[name]['val2']))
            except ValueError:
                self.parameters[name]['cons2'] = 1.0 if code == "FACTOR" else 0.0

        elif code in ['FREE', 'POSITIVE', 'IGNORE', 'FIXED']:
            self.parameters[name]['val1'] = ""
            self.parameters[name]['val2'] = ""
            self.parameters[name]['cons1'] = 0
            self.parameters[name]['cons2'] = 0

        self._updateCellRWFlags(name, code)

    def _updateField(self, name, field, value, fmat=None):
        """Update field in ``self.parameters`` dictionary, if the new value
        is valid.

        :param name: Fit parameter name
        :param field: Field name
        :param value: New value to assign
        :type value: String
        :param fmat: Format string (e.g. "%8g") to be applied if value represents
            a scalar. If ``None``, format is not modified. If ``value`` is an
            empty string, ``fmat`` is ignored.
        """
        if value is not None:
            oldvalue = self.parameters[name][field]
            if fmat is not None:
                newvalue = fmat % float(value) if value != "" else ""
            else:
                newvalue = value
            self.parameters[name][field] = newvalue if\
                self.validate(name, field, oldvalue, newvalue) else\
                oldvalue

    def _updateCellRWFlags(self, name, code=None):
        """Set read-only or read-write flags in a row,
        depending on the constraint code

        :param name: Fit parameter name identifying the row
        :param code: Constraint code, in `'FREE', 'POSITIVE', 'IGNORE',`
            `'FIXED', 'FACTOR', 'DELTA', 'SUM', 'ADD'`
        :return:
        """
        if code in ['FREE', 'POSITIVE', 'IGNORE', 'FIXED']:
            self.setReadWrite(name, 'estimation')
            self.setReadOnly(name, ['fitresult', 'sigma', 'val1', 'val2'])
        else:
            self.setReadWrite(name, ['estimation', 'val1', 'val2'])
            self.setReadOnly(name, ['fitresult', 'sigma'])

    def getEstimationConstraints(self, param):
        """
        Return tuple ``(estimation, constraints)`` where ``estimation`` is the
        value in the ``estimate`` field and ``constraints`` are the relevant
        constraints according to the active code
        """
        estimation = None
        constraints = None
        if param in self.parameters.keys():
            buf = str(self.parameters[param]['estimation'])
            if len(buf):
                estimation = float(buf)
            else:
                estimation = 0
            if str(self.parameters[param]['code']) in self.code_options:
                code = self.code_options.index(
                    str(self.parameters[param]['code']))
            else:
                code = str(self.parameters[param]['code'])
            cons1 = self.parameters[param]['cons1']
            cons2 = self.parameters[param]['cons2']
            constraints = [code, cons1, cons2]
        return estimation, constraints


def main(args):
    from silx.math.fit import fittheories
    from silx.math.fit import fitmanager
    try:
        from PyMca5 import PyMcaDataDir
    except ImportError:
        raise ImportError("This demo requires PyMca data. Install PyMca5.")
    import numpy
    import os
    app = qt.QApplication(args)
    tab = Parameters(paramlist=['Height', 'Position', 'FWHM'])
    tab.showGrid()
    tab.configureLine(name='Height', estimation='1234', group=0)
    tab.configureLine(name='Position', code='FIXED', group=1)
    tab.configureLine(name='FWHM', group=1)

    y = numpy.loadtxt(os.path.join(PyMcaDataDir.PYMCA_DATA_DIR,
                      "XRFSpectrum.mca"))    # FIXME

    x = numpy.arange(len(y)) * 0.0502883 - 0.492773
    fit = fitmanager.FitManager()
    fit.setdata(x=x, y=y, xmin=20, xmax=150)

    fit.loadtheories(fittheories)

    fit.settheory('ahypermet')
    fit.configure(Yscaling=1.,
                  PositiveFwhmFlag=True,
                  PositiveHeightAreaFlag=True,
                  FwhmPoints=16,
                  QuotedPositionFlag=1,
                  HypermetTails=1)
    fit.setbackground('Linear')
    fit.estimate()
    fit.runfit()
    tab.fillFromFit(fit.fit_results)
    tab.show()
    app.exec_()

if __name__ == "__main__":
    main(sys.argv)