source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py @ bc7371fd

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since bc7371fd was bc7371fd, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

update docstrings

  • Property mode set to 100644
File size: 25.1 KB
Line 
1import copy
2
3from PyQt5 import QtCore
4from PyQt5 import QtGui
5
6import numpy
7
8from sas.qtgui.Plotting.PlotterData import Data1D
9from sas.qtgui.Plotting.PlotterData import Data2D
10
11from sas.qtgui.Perspectives.Fitting.AssociatedComboBox import AssociatedComboBox
12
13model_header_captions = ['Parameter', 'Value', 'Min', 'Max', 'Units']
14
15model_header_tooltips = ['Select parameter for fitting',
16                         'Enter parameter value',
17                         'Enter minimum value for parameter',
18                         'Enter maximum value for parameter',
19                         'Unit of the parameter']
20
21poly_header_captions = ['Parameter', 'PD[ratio]', 'Min', 'Max', 'Npts', 'Nsigs',
22                        'Function', 'Filename']
23
24poly_header_tooltips = ['Select parameter for fitting',
25                        'Enter polydispersity ratio (STD/mean). '
26                        'STD: standard deviation from the mean value',
27                        'Enter minimum value for parameter',
28                        'Enter maximum value for parameter',
29                        'Enter number of points for parameter',
30                        'Enter number of sigmas parameter',
31                        'Select distribution function',
32                        'Select filename with user-definable distribution']
33
34error_tooltip = 'Error value for fitted parameter'
35header_error_caption = 'Error'
36
37def replaceShellName(param_name, value):
38    """
39    Updates parameter name from <param_name>[n_shell] to <param_name>value
40    """
41    assert '[' in param_name
42    return param_name[:param_name.index('[')]+str(value)
43
44def getIterParams(model):
45    """
46    Returns a list of all multi-shell parameters in 'model'
47    """
48    return list([par for par in model.iq_parameters if "[" in par.name])
49
50def getMultiplicity(model):
51    """
52    Finds out if 'model' has multishell parameters.
53    If so, returns the name of the counter parameter and the number of shells
54    """
55    iter_params = getIterParams(model)
56    param_name = ""
57    param_length = 0
58    if iter_params:
59        param_length = iter_params[0].length
60        param_name = iter_params[0].length_control
61        if param_name is None and '[' in iter_params[0].name:
62            param_name = iter_params[0].name[:iter_params[0].name.index('[')]
63    return (param_name, param_length)
64
65def createFixedChoiceComboBox(param, item_row):
66    """
67    Determines whether param is a fixed-choice parameter, modifies items in item_row appropriately and returns a combo
68    box containing the fixed choices. Returns None if param is not fixed-choice.
69   
70    item_row is a list of QStandardItem objects for insertion into the parameter table.
71    """
72
73    # Determine whether this is a fixed-choice parameter. There are lots of conditionals, simply because the
74    # implementation is not yet concrete; there are several possible indicators that the parameter is fixed-choice.
75    # TODO: (when the sasmodels implementation is concrete, clean this up)
76    choices = None
77    if isinstance(param.choices, (list, tuple)) and len(param.choices) > 0:
78        # The choices property is concrete in sasmodels, probably will use this
79        choices = param.choices
80    elif isinstance(param.units, (list, tuple)):
81        choices = [str(x) for x in param.units]
82
83    cbox = None
84    if choices is not None:
85        # Use combo box for input, if it is fixed-choice
86        cbox = AssociatedComboBox(item_row[1], idx_as_value=True)
87        cbox.addItems(choices)
88        item_row[2].setEditable(False)
89        item_row[3].setEditable(False)
90
91    return cbox
92
93def addParametersToModel(parameters, kernel_module, is2D, model=None, view=None):
94    """
95    Update local ModelModel with sasmodel parameters.
96    Actually appends to model, if model and view params are not None.
97    Always returns list of lists of QStandardItems.
98    """
99    multishell_parameters = getIterParams(parameters)
100    multishell_param_name, _ = getMultiplicity(parameters)
101
102    if is2D:
103        params = [p for p in parameters.kernel_parameters if p.type != 'magnetic']
104    else:
105        params = parameters.iq_parameters
106
107    rows = []
108    for param in params:
109        # don't include shell parameters
110        if param.name == multishell_param_name:
111            continue
112
113        # Modify parameter name from <param>[n] to <param>1
114        item_name = param.name
115        if param in multishell_parameters:
116            continue
117
118        item1 = QtGui.QStandardItem(item_name)
119        item1.setCheckable(True)
120        item1.setEditable(False)
121
122        # check for polydisp params
123        if param.polydisperse:
124            poly_item = QtGui.QStandardItem("Polydispersity")
125            poly_item.setEditable(False)
126            item1_1 = QtGui.QStandardItem("Distribution")
127            item1_1.setEditable(False)
128
129            # Find param in volume_params
130            for p in parameters.form_volume_parameters:
131                if p.name != param.name:
132                    continue
133                width = kernel_module.getParam(p.name+'.width')
134                ptype = kernel_module.getParam(p.name+'.type')
135                item1_2 = QtGui.QStandardItem(str(width))
136                item1_2.setEditable(False)
137                item1_3 = QtGui.QStandardItem()
138                item1_3.setEditable(False)
139                item1_4 = QtGui.QStandardItem()
140                item1_4.setEditable(False)
141                item1_5 = QtGui.QStandardItem(ptype)
142                item1_5.setEditable(False)
143                poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
144                break
145
146            # Add the polydisp item as a child
147            item1.appendRow([poly_item])
148
149        # Param values
150        item2 = QtGui.QStandardItem(str(param.default))
151        item3 = QtGui.QStandardItem(str(param.limits[0]))
152        item4 = QtGui.QStandardItem(str(param.limits[1]))
153        item5 = QtGui.QStandardItem(str(param.units))
154        item5.setEditable(False)
155
156        # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
157        row = [item1, item2, item3, item4, item5]
158        cbox = createFixedChoiceComboBox(param, row)
159
160        # Append to the model and use the combobox, if required
161        if None not in (model, view):
162            model.appendRow(row)
163            if cbox:
164                view.setIndexWidget(item2.index(), cbox)
165        rows.append(row)
166
167    return rows
168
169def addSimpleParametersToModel(parameters, is2D, model=None, view=None):
170    """
171    Update local ModelModel with sasmodel parameters (non-dispersed, non-magnetic)
172    Actually appends to model, if model and view params are not None.
173    Always returns list of lists of QStandardItems.
174    """
175    if is2D:
176        params = [p for p in parameters.kernel_parameters if p.type != 'magnetic']
177    else:
178        params = parameters.iq_parameters
179
180    rows = []
181    for param in params:
182        # Create the top level, checkable item
183        item_name = param.name
184        item1 = QtGui.QStandardItem(item_name)
185        item1.setCheckable(True)
186        item1.setEditable(False)
187
188        # Param values
189        # TODO: add delegate for validation of cells
190        item2 = QtGui.QStandardItem(str(param.default))
191        item3 = QtGui.QStandardItem(str(param.limits[0]))
192        item4 = QtGui.QStandardItem(str(param.limits[1]))
193        item5 = QtGui.QStandardItem(str(param.units))
194        item5.setEditable(False)
195
196        # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
197        row = [item1, item2, item3, item4, item5]
198        cbox = createFixedChoiceComboBox(param, row)
199
200        # Append to the model and use the combobox, if required
201        if None not in (model, view):
202            model.appendRow(row)
203            if cbox:
204                view.setIndexWidget(item2.index(), cbox)
205        rows.append(row)
206
207    return rows
208
209def markParameterDisabled(model, row):
210    """Given the QModel row number, format to show it is not available for fitting"""
211
212    # If an error column is present, there are a total of 6 columns.
213    items = [model.item(row, c) for c in range(6)]
214
215    model.blockSignals(True)
216
217    for item in items:
218        if item is None:
219            continue
220        item.setEditable(False)
221        item.setCheckable(False)
222
223    item = items[0]
224
225    font = QtGui.QFont()
226    font.setItalic(True)
227    item.setFont(font)
228    item.setForeground(QtGui.QBrush(QtGui.QColor(100, 100, 100)))
229    item.setToolTip("This parameter cannot be fitted.")
230
231    model.blockSignals(False)
232
233def addCheckedListToModel(model, param_list):
234    """
235    Add a QItem to model. Makes the QItem checkable
236    """
237    assert isinstance(model, QtGui.QStandardItemModel)
238    item_list = [QtGui.QStandardItem(item) for item in param_list]
239    item_list[0].setCheckable(True)
240    model.appendRow(item_list)
241
242def addHeadersToModel(model):
243    """
244    Adds predefined headers to the model
245    """
246    for i, item in enumerate(model_header_captions):
247        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
248
249    model.header_tooltips = copy.copy(model_header_tooltips)
250
251def addErrorHeadersToModel(model):
252    """
253    Adds predefined headers to the model
254    """
255    model_header_error_captions = copy.copy(model_header_captions)
256    model_header_error_captions.insert(2, header_error_caption)
257    for i, item in enumerate(model_header_error_captions):
258        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
259
260    model_header_error_tooltips = copy.copy(model_header_tooltips)
261    model_header_error_tooltips.insert(2, error_tooltip)
262    model.header_tooltips = copy.copy(model_header_error_tooltips)
263
264def addPolyHeadersToModel(model):
265    """
266    Adds predefined headers to the model
267    """
268    for i, item in enumerate(poly_header_captions):
269        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
270
271    model.header_tooltips = copy.copy(poly_header_tooltips)
272
273
274def addErrorPolyHeadersToModel(model):
275    """
276    Adds predefined headers to the model
277    """
278    poly_header_error_captions = copy.copy(poly_header_captions)
279    poly_header_error_captions.insert(2, header_error_caption)
280    for i, item in enumerate(poly_header_error_captions):
281        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
282
283    poly_header_error_tooltips = copy.copy(poly_header_tooltips)
284    poly_header_error_tooltips.insert(2, error_tooltip)
285    model.header_tooltips = copy.copy(poly_header_error_tooltips)
286
287def addShellsToModel(parameters, model, index, view=None):
288    """
289    Find out multishell parameters and update the model with the requested number of them
290    Always appends to model. If view param is not None, supports fixed-choice params.
291    Returns list of lists of QStandardItems.
292    """
293    multishell_parameters = getIterParams(parameters)
294
295    rows = []
296    for i in range(index):
297        for par in multishell_parameters:
298            # Create the name: <param>[<i>], e.g. "sld1" for parameter "sld[n]"
299            param_name = replaceShellName(par.name, i+1)
300            item1 = QtGui.QStandardItem(param_name)
301            item1.setCheckable(True)
302            # check for polydisp params
303            if par.polydisperse:
304                poly_item = QtGui.QStandardItem("Polydispersity")
305                item1_1 = QtGui.QStandardItem("Distribution")
306                # Find param in volume_params
307                for p in parameters.form_volume_parameters:
308                    if p.name != par.name:
309                        continue
310                    item1_2 = QtGui.QStandardItem(str(p.default))
311                    item1_3 = QtGui.QStandardItem(str(p.limits[0]))
312                    item1_4 = QtGui.QStandardItem(str(p.limits[1]))
313                    item1_5 = QtGui.QStandardItem(str(p.units))
314                    poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
315                    break
316                item1.appendRow([poly_item])
317
318            item2 = QtGui.QStandardItem(str(par.default))
319            item3 = QtGui.QStandardItem(str(par.limits[0]))
320            item4 = QtGui.QStandardItem(str(par.limits[1]))
321            item5 = QtGui.QStandardItem(str(par.units))
322            item5.setEditable(False)
323
324            # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
325            row = [item1, item2, item3, item4, item5]
326            cbox = createFixedChoiceComboBox(par, row)
327
328            # Always append to the model
329            model.appendRow(row)
330
331            # Apply combobox if required
332            if None not in (view, cbox):
333                view.setIndexWidget(item2.index(), cbox)
334
335            rows.append(row)
336
337    return rows
338
339def calculateChi2(reference_data, current_data):
340    """
341    Calculate Chi2 value between two sets of data
342    """
343    if reference_data is None or current_data is None:
344        return None
345    # WEIGHING INPUT
346    #from sas.sasgui.perspectives.fitting.utils import get_weight
347    #flag = self.get_weight_flag()
348    #weight = get_weight(data=self.data, is2d=self._is_2D(), flag=flag)
349    chisqr = None
350    if reference_data is None:
351        return chisqr
352
353    # temporary default values for index and weight
354    index = None
355    weight = None
356
357    # Get data: data I, theory I, and data dI in order
358    if isinstance(reference_data, Data2D):
359        if index is None:
360            index = numpy.ones(len(current_data.data), dtype=bool)
361        if weight is not None:
362            current_data.err_data = weight
363        # get rid of zero error points
364        index = index & (current_data.err_data != 0)
365        index = index & (numpy.isfinite(current_data.data))
366        fn = current_data.data[index]
367        gn = reference_data.data[index]
368        en = current_data.err_data[index]
369    else:
370        # 1 d theory from model_thread is only in the range of index
371        if index is None:
372            index = numpy.ones(len(current_data.y), dtype=bool)
373        if weight is not None:
374            current_data.dy = weight
375        if current_data.dy is None or current_data.dy == []:
376            dy = numpy.ones(len(current_data.y))
377        else:
378            ## Set consistently w/AbstractFitengine:
379            # But this should be corrected later.
380            dy = copy.deepcopy(current_data.dy)
381            dy[dy == 0] = 1
382        fn = current_data.y[index]
383        gn = reference_data.y
384        en = dy[index]
385    # Calculate the residual
386    try:
387        res = (fn - gn) / en
388    except ValueError:
389        #print "Chi2 calculations: Unmatched lengths %s, %s, %s" % (len(fn), len(gn), len(en))
390        return None
391
392    residuals = res[numpy.isfinite(res)]
393    chisqr = numpy.average(residuals * residuals)
394
395    return chisqr
396
397def residualsData1D(reference_data, current_data):
398    """
399    Calculate the residuals for difference of two Data1D sets
400    """
401    # temporary default values for index and weight
402    index = None
403    weight = None
404
405    # 1d theory from model_thread is only in the range of index
406    if current_data.dy is None or current_data.dy == []:
407        dy = numpy.ones(len(current_data.y))
408    else:
409        dy = weight if weight is not None else numpy.ones(len(current_data.y))
410        dy[dy == 0] = 1
411    fn = current_data.y[index][0]
412    gn = reference_data.y
413    en = dy[index][0]
414
415    # x values
416    x_current = current_data.x
417    x_reference = reference_data.x
418
419    # build residuals
420    residuals = Data1D()
421    if len(fn) == len(gn):
422        y = (fn - gn)/en
423        residuals.y = -y
424    elif len(fn) > len(gn):
425        residuals.y = (fn - gn[1:len(fn)])/en
426    else:
427        try:
428            y = numpy.zeros(len(current_data.y))
429            begin = 0
430            for i, x_value in enumerate(x_reference):
431                if x_value in x_current:
432                    begin = i
433                    break
434            end = len(x_reference)
435            endl = 0
436            for i, x_value in enumerate(list(x_reference)[::-1]):
437                if x_value in x_current:
438                    endl = i
439                    break
440            # make sure we have correct lengths
441            assert len(x_current) == len(x_reference[begin:end-endl])
442
443            y = (fn - gn[begin:end-endl])/en
444            residuals.y = y
445        except ValueError:
446            # value errors may show up every once in a while for malformed columns,
447            # just reuse what's there already
448            pass
449
450    residuals.x = current_data.x[index][0]
451    residuals.dy = numpy.ones(len(residuals.y))
452    residuals.dx = None
453    residuals.dxl = None
454    residuals.dxw = None
455    residuals.ytransform = 'y'
456    # For latter scale changes
457    residuals.xaxis('\\rm{Q} ', 'A^{-1}')
458    residuals.yaxis('\\rm{Residuals} ', 'normalized')
459
460    return residuals
461
462def residualsData2D(reference_data, current_data):
463    """
464    Calculate the residuals for difference of two Data2D sets
465    """
466    # temporary default values for index and weight
467    # index = None
468    weight = None
469
470    # build residuals
471    residuals = Data2D()
472    # Not for trunk the line below, instead use the line above
473    current_data.clone_without_data(len(current_data.data), residuals)
474    residuals.data = None
475    fn = current_data.data
476    gn = reference_data.data
477    en = current_data.err_data if weight is None else weight
478    residuals.data = (fn - gn) / en
479    residuals.qx_data = current_data.qx_data
480    residuals.qy_data = current_data.qy_data
481    residuals.q_data = current_data.q_data
482    residuals.err_data = numpy.ones(len(residuals.data))
483    residuals.xmin = min(residuals.qx_data)
484    residuals.xmax = max(residuals.qx_data)
485    residuals.ymin = min(residuals.qy_data)
486    residuals.ymax = max(residuals.qy_data)
487    residuals.q_data = current_data.q_data
488    residuals.mask = current_data.mask
489    residuals.scale = 'linear'
490    # check the lengths
491    if len(residuals.data) != len(residuals.q_data):
492        return None
493    return residuals
494
495def plotResiduals(reference_data, current_data):
496    """
497    Create Data1D/Data2D with residuals, ready for plotting
498    """
499    data_copy = copy.deepcopy(current_data)
500    # Get data: data I, theory I, and data dI in order
501    method_name = current_data.__class__.__name__
502    residuals_dict = {"Data1D": residualsData1D,
503                      "Data2D": residualsData2D}
504
505    residuals = residuals_dict[method_name](reference_data, data_copy)
506
507    theory_name = str(current_data.name.split()[0])
508    res_name = reference_data.filename if reference_data.filename else reference_data.name
509    residuals.name = "Residuals for " + str(theory_name) + "[" + res_name + "]"
510    residuals.title = residuals.name
511    residuals.ytransform = 'y'
512
513    # when 2 data have the same id override the 1 st plotted
514    # include the last part if keeping charts for separate models is required
515    residuals.id = "res" + str(reference_data.id) # + str(theory_name)
516    # group_id specify on which panel to plot this data
517    group_id = reference_data.group_id
518    residuals.group_id = "res" + str(group_id)
519
520    # Symbol
521    residuals.symbol = 0
522    residuals.hide_error = False
523
524    return residuals
525
526def binary_encode(i, digits):
527    return [i >> d & 1 for d in range(digits)]
528
529def getWeight(data, is2d, flag=None):
530    """
531    Received flag and compute error on data.
532    :param flag: flag to transform error of data.
533    """
534    weight = None
535    if data is None:
536        return []
537    if is2d:
538        if not hasattr(data, 'err_data'):
539            return []
540        dy_data = data.err_data
541        data = data.data
542    else:
543        if not hasattr(data, 'dy'):
544            return []
545        dy_data = data.dy
546        data = data.y
547
548    if flag == 0:
549        weight = numpy.ones_like(data)
550    elif flag == 1:
551        weight = dy_data
552    elif flag == 2:
553        weight = numpy.sqrt(numpy.abs(data))
554    elif flag == 3:
555        weight = numpy.abs(data)
556    return weight
557
558def updateKernelWithResults(kernel, results):
559    """
560    Takes model kernel and applies results dict to its parameters,
561    returning the modified (deep) copy of the kernel.
562    """
563    assert isinstance(results, dict)
564    local_kernel = copy.deepcopy(kernel)
565
566    for parameter in results.keys():
567        # Update the parameter value - note: this supports +/-inf as well
568        local_kernel.setParam(parameter, results[parameter][0])
569
570    return local_kernel
571
572
573def getStandardParam(model=None):
574    """
575    Returns a list with standard parameters for the current model
576    """
577    param = []
578    num_rows = model.rowCount()
579    if num_rows < 1:
580        return None
581
582    for row in range(num_rows):
583        param_name = model.item(row, 0).text()
584        checkbox_state = model.item(row, 0).checkState() == QtCore.Qt.Checked
585        value = model.item(row, 1).text()
586        column_shift = 0
587        if model.columnCount() == 5: # no error column
588            error_state = False
589            error_value = 0.0
590        else:
591            error_state = True
592            error_value = model.item(row, 2).text()
593            column_shift = 1
594        min_state = True
595        max_state = True
596        min_value = model.item(row, 2+column_shift).text()
597        max_value = model.item(row, 3+column_shift).text()
598        unit = ""
599        if model.item(row, 4+column_shift) is not None:
600            unit = model.item(row, 4+column_shift).text()
601
602        param.append([checkbox_state, param_name, value, "",
603                        [error_state, error_value],
604                        [min_state, min_value],
605                        [max_state, max_value], unit])
606
607    return param
608
609def getOrientationParam(kernel_module=None):
610    """
611    Get the dictionary with orientation parameters
612    """
613    param = []
614    if kernel_module is None:
615        return None
616    for param_name in list(kernel_module.params.keys()):
617        name = param_name
618        value = kernel_module.params[param_name]
619        min_state = True
620        max_state = True
621        error_state = False
622        error_value = 0.0
623        checkbox_state = True #??
624        details = kernel_module.details[param_name] #[unit, mix, max]
625        param.append([checkbox_state, name, value, "",
626                     [error_state, error_value],
627                     [min_state, details[1]],
628                     [max_state, details[2]], details[0]])
629
630    return param
631
632def formatParameters(parameters):
633    """
634    Prepare the parameter string in the standard SasView layout
635    """
636    assert parameters is not None
637    assert isinstance(parameters, list)
638    output_string = "sasview_parameter_values:"
639    for parameter in parameters:
640        output_string += ",".join([p for p in parameter if p is not None])
641        output_string += ":"
642    return output_string
643
644def formatParametersExcel(parameters):
645    """
646    Prepare the parameter string in the Excel format (tab delimited)
647    """
648    assert parameters is not None
649    assert isinstance(parameters, list)
650    crlf = chr(13) + chr(10)
651    tab = chr(9)
652
653    output_string = ""
654    # names
655    names = ""
656    values = ""
657    for parameter in parameters:
658        names += parameter[0]+tab
659        # Add the error column if fitted
660        if parameter[1] == "True" and parameter[3] is not None:
661            names += parameter[0]+"_err"+tab
662
663        values += parameter[2]+tab
664        if parameter[1] == "True" and parameter[3] is not None:
665            values += parameter[3]+tab
666        # add .npts and .nsigmas when necessary
667        if parameter[0][-6:] == ".width":
668            names += parameter[0].replace('.width', '.nsigmas') + tab
669            names += parameter[0].replace('.width', '.npts') + tab
670            values += parameter[5] + tab + parameter[4] + tab
671
672    output_string = names + crlf + values
673    return output_string
674
675def formatParametersLatex(parameters):
676    """
677    Prepare the parameter string in latex
678    """
679    assert parameters is not None
680    assert isinstance(parameters, list)
681    output_string = r'\begin{table}'
682    output_string += r'\begin{tabular}[h]'
683
684    crlf = chr(13) + chr(10)
685    output_string += '{|'
686    output_string += 'l|l|'*len(parameters)
687    output_string += r'}\hline'
688    output_string += crlf
689
690    for index, parameter in enumerate(parameters):
691        name = parameter[0] # Parameter name
692        output_string += name.replace('_', r'\_')  # Escape underscores
693        # Add the error column if fitted
694        if parameter[1] == "True" and parameter[3] is not None:
695            output_string += ' & '
696            output_string += parameter[0]+r'\_err'
697
698        if index < len(parameters) - 1:
699            output_string += ' & '
700
701        # add .npts and .nsigmas when necessary
702        if parameter[0][-6:] == ".width":
703            output_string += parameter[0].replace('.width', '.nsigmas') + ' & '
704            output_string += parameter[0].replace('.width', '.npts')
705
706            if index < len(parameters) - 1:
707                output_string += ' & '
708
709    output_string += r'\\ \hline'
710    output_string += crlf
711
712    # Construct row of values and errors
713    for index, parameter in enumerate(parameters):
714        output_string += parameter[2]
715        if parameter[1] == "True" and parameter[3] is not None:
716            output_string += ' & '
717            output_string += parameter[3]
718
719        if index < len(parameters) - 1:
720            output_string += ' & '
721
722        # add .npts and .nsigmas when necessary
723        if parameter[0][-6:] == ".width":
724            output_string += parameter[5] + ' & '
725            output_string += parameter[4]
726
727            if index < len(parameters) - 1:
728                output_string += ' & '
729
730    output_string += r'\\ \hline'
731    output_string += crlf
732    output_string += r'\end{tabular}'
733    output_string += r'\end{table}'
734
735    return output_string
Note: See TracBrowser for help on using the repository browser.