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

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 a758043 was a758043, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

function signatures and return values now maintain backwards compatibility, and do not break tests

  • Property mode set to 100644
File size: 24.8 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    """
173    if is2D:
174        params = [p for p in parameters.kernel_parameters if p.type != 'magnetic']
175    else:
176        params = parameters.iq_parameters
177
178    rows = []
179    for param in params:
180        # Create the top level, checkable item
181        item_name = param.name
182        item1 = QtGui.QStandardItem(item_name)
183        item1.setCheckable(True)
184        item1.setEditable(False)
185
186        # Param values
187        # TODO: add delegate for validation of cells
188        item2 = QtGui.QStandardItem(str(param.default))
189        item3 = QtGui.QStandardItem(str(param.limits[0]))
190        item4 = QtGui.QStandardItem(str(param.limits[1]))
191        item5 = QtGui.QStandardItem(str(param.units))
192        item5.setEditable(False)
193
194        # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
195        row = [item1, item2, item3, item4, item5]
196        cbox = createFixedChoiceComboBox(param, row)
197
198        # Append to the model and use the combobox, if required
199        if None not in (model, view):
200            model.appendRow(row)
201            if cbox:
202                view.setIndexWidget(item2.index(), cbox)
203        rows.append(row)
204
205    return rows
206
207def markParameterDisabled(model, row):
208    """Given the QModel row number, format to show it is not available for fitting"""
209
210    # If an error column is present, there are a total of 6 columns.
211    items = [model.item(row, c) for c in range(6)]
212
213    model.blockSignals(True)
214
215    for item in items:
216        if item is None:
217            continue
218        item.setEditable(False)
219        item.setCheckable(False)
220
221    item = items[0]
222
223    font = QtGui.QFont()
224    font.setItalic(True)
225    item.setFont(font)
226    item.setForeground(QtGui.QBrush(QtGui.QColor(100, 100, 100)))
227    item.setToolTip("This parameter cannot be fitted.")
228
229    model.blockSignals(False)
230
231def addCheckedListToModel(model, param_list):
232    """
233    Add a QItem to model. Makes the QItem checkable
234    """
235    assert isinstance(model, QtGui.QStandardItemModel)
236    item_list = [QtGui.QStandardItem(item) for item in param_list]
237    item_list[0].setCheckable(True)
238    model.appendRow(item_list)
239
240def addHeadersToModel(model):
241    """
242    Adds predefined headers to the model
243    """
244    for i, item in enumerate(model_header_captions):
245        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
246
247    model.header_tooltips = copy.copy(model_header_tooltips)
248
249def addErrorHeadersToModel(model):
250    """
251    Adds predefined headers to the model
252    """
253    model_header_error_captions = copy.copy(model_header_captions)
254    model_header_error_captions.insert(2, header_error_caption)
255    for i, item in enumerate(model_header_error_captions):
256        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
257
258    model_header_error_tooltips = copy.copy(model_header_tooltips)
259    model_header_error_tooltips.insert(2, error_tooltip)
260    model.header_tooltips = copy.copy(model_header_error_tooltips)
261
262def addPolyHeadersToModel(model):
263    """
264    Adds predefined headers to the model
265    """
266    for i, item in enumerate(poly_header_captions):
267        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
268
269    model.header_tooltips = copy.copy(poly_header_tooltips)
270
271
272def addErrorPolyHeadersToModel(model):
273    """
274    Adds predefined headers to the model
275    """
276    poly_header_error_captions = copy.copy(poly_header_captions)
277    poly_header_error_captions.insert(2, header_error_caption)
278    for i, item in enumerate(poly_header_error_captions):
279        model.setHeaderData(i, QtCore.Qt.Horizontal, item)
280
281    poly_header_error_tooltips = copy.copy(poly_header_tooltips)
282    poly_header_error_tooltips.insert(2, error_tooltip)
283    model.header_tooltips = copy.copy(poly_header_error_tooltips)
284
285def addShellsToModel(parameters, model, index, view=None):
286    """
287    Find out multishell parameters and update the model with the requested number of them
288    """
289    multishell_parameters = getIterParams(parameters)
290
291    rows = []
292    for i in range(index):
293        for par in multishell_parameters:
294            # Create the name: <param>[<i>], e.g. "sld1" for parameter "sld[n]"
295            param_name = replaceShellName(par.name, i+1)
296            item1 = QtGui.QStandardItem(param_name)
297            item1.setCheckable(True)
298            # check for polydisp params
299            if par.polydisperse:
300                poly_item = QtGui.QStandardItem("Polydispersity")
301                item1_1 = QtGui.QStandardItem("Distribution")
302                # Find param in volume_params
303                for p in parameters.form_volume_parameters:
304                    if p.name != par.name:
305                        continue
306                    item1_2 = QtGui.QStandardItem(str(p.default))
307                    item1_3 = QtGui.QStandardItem(str(p.limits[0]))
308                    item1_4 = QtGui.QStandardItem(str(p.limits[1]))
309                    item1_5 = QtGui.QStandardItem(str(p.units))
310                    poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
311                    break
312                item1.appendRow([poly_item])
313
314            item2 = QtGui.QStandardItem(str(par.default))
315            item3 = QtGui.QStandardItem(str(par.limits[0]))
316            item4 = QtGui.QStandardItem(str(par.limits[1]))
317            item5 = QtGui.QStandardItem(str(par.units))
318            item5.setEditable(False)
319
320            # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
321            row = [item1, item2, item3, item4, item5]
322            cbox = createFixedChoiceComboBox(par, row)
323
324            # Always append to the model
325            model.appendRow(row)
326
327            # Apply combobox if required
328            if None not in (view, cbox):
329                view.setIndexWidget(item2.index(), cbox)
330
331            rows.append(row)
332
333    return rows
334
335def calculateChi2(reference_data, current_data):
336    """
337    Calculate Chi2 value between two sets of data
338    """
339    if reference_data is None or current_data is None:
340        return None
341    # WEIGHING INPUT
342    #from sas.sasgui.perspectives.fitting.utils import get_weight
343    #flag = self.get_weight_flag()
344    #weight = get_weight(data=self.data, is2d=self._is_2D(), flag=flag)
345    chisqr = None
346    if reference_data is None:
347        return chisqr
348
349    # temporary default values for index and weight
350    index = None
351    weight = None
352
353    # Get data: data I, theory I, and data dI in order
354    if isinstance(reference_data, Data2D):
355        if index is None:
356            index = numpy.ones(len(current_data.data), dtype=bool)
357        if weight is not None:
358            current_data.err_data = weight
359        # get rid of zero error points
360        index = index & (current_data.err_data != 0)
361        index = index & (numpy.isfinite(current_data.data))
362        fn = current_data.data[index]
363        gn = reference_data.data[index]
364        en = current_data.err_data[index]
365    else:
366        # 1 d theory from model_thread is only in the range of index
367        if index is None:
368            index = numpy.ones(len(current_data.y), dtype=bool)
369        if weight is not None:
370            current_data.dy = weight
371        if current_data.dy is None or current_data.dy == []:
372            dy = numpy.ones(len(current_data.y))
373        else:
374            ## Set consistently w/AbstractFitengine:
375            # But this should be corrected later.
376            dy = copy.deepcopy(current_data.dy)
377            dy[dy == 0] = 1
378        fn = current_data.y[index]
379        gn = reference_data.y
380        en = dy[index]
381    # Calculate the residual
382    try:
383        res = (fn - gn) / en
384    except ValueError:
385        #print "Chi2 calculations: Unmatched lengths %s, %s, %s" % (len(fn), len(gn), len(en))
386        return None
387
388    residuals = res[numpy.isfinite(res)]
389    chisqr = numpy.average(residuals * residuals)
390
391    return chisqr
392
393def residualsData1D(reference_data, current_data):
394    """
395    Calculate the residuals for difference of two Data1D sets
396    """
397    # temporary default values for index and weight
398    index = None
399    weight = None
400
401    # 1d theory from model_thread is only in the range of index
402    if current_data.dy is None or current_data.dy == []:
403        dy = numpy.ones(len(current_data.y))
404    else:
405        dy = weight if weight is not None else numpy.ones(len(current_data.y))
406        dy[dy == 0] = 1
407    fn = current_data.y[index][0]
408    gn = reference_data.y
409    en = dy[index][0]
410
411    # x values
412    x_current = current_data.x
413    x_reference = reference_data.x
414
415    # build residuals
416    residuals = Data1D()
417    if len(fn) == len(gn):
418        y = (fn - gn)/en
419        residuals.y = -y
420    elif len(fn) > len(gn):
421        residuals.y = (fn - gn[1:len(fn)])/en
422    else:
423        try:
424            y = numpy.zeros(len(current_data.y))
425            begin = 0
426            for i, x_value in enumerate(x_reference):
427                if x_value in x_current:
428                    begin = i
429                    break
430            end = len(x_reference)
431            endl = 0
432            for i, x_value in enumerate(list(x_reference)[::-1]):
433                if x_value in x_current:
434                    endl = i
435                    break
436            # make sure we have correct lengths
437            assert len(x_current) == len(x_reference[begin:end-endl])
438
439            y = (fn - gn[begin:end-endl])/en
440            residuals.y = y
441        except ValueError:
442            # value errors may show up every once in a while for malformed columns,
443            # just reuse what's there already
444            pass
445
446    residuals.x = current_data.x[index][0]
447    residuals.dy = numpy.ones(len(residuals.y))
448    residuals.dx = None
449    residuals.dxl = None
450    residuals.dxw = None
451    residuals.ytransform = 'y'
452    # For latter scale changes
453    residuals.xaxis('\\rm{Q} ', 'A^{-1}')
454    residuals.yaxis('\\rm{Residuals} ', 'normalized')
455
456    return residuals
457
458def residualsData2D(reference_data, current_data):
459    """
460    Calculate the residuals for difference of two Data2D sets
461    """
462    # temporary default values for index and weight
463    # index = None
464    weight = None
465
466    # build residuals
467    residuals = Data2D()
468    # Not for trunk the line below, instead use the line above
469    current_data.clone_without_data(len(current_data.data), residuals)
470    residuals.data = None
471    fn = current_data.data
472    gn = reference_data.data
473    en = current_data.err_data if weight is None else weight
474    residuals.data = (fn - gn) / en
475    residuals.qx_data = current_data.qx_data
476    residuals.qy_data = current_data.qy_data
477    residuals.q_data = current_data.q_data
478    residuals.err_data = numpy.ones(len(residuals.data))
479    residuals.xmin = min(residuals.qx_data)
480    residuals.xmax = max(residuals.qx_data)
481    residuals.ymin = min(residuals.qy_data)
482    residuals.ymax = max(residuals.qy_data)
483    residuals.q_data = current_data.q_data
484    residuals.mask = current_data.mask
485    residuals.scale = 'linear'
486    # check the lengths
487    if len(residuals.data) != len(residuals.q_data):
488        return None
489    return residuals
490
491def plotResiduals(reference_data, current_data):
492    """
493    Create Data1D/Data2D with residuals, ready for plotting
494    """
495    data_copy = copy.deepcopy(current_data)
496    # Get data: data I, theory I, and data dI in order
497    method_name = current_data.__class__.__name__
498    residuals_dict = {"Data1D": residualsData1D,
499                      "Data2D": residualsData2D}
500
501    residuals = residuals_dict[method_name](reference_data, data_copy)
502
503    theory_name = str(current_data.name.split()[0])
504    res_name = reference_data.filename if reference_data.filename else reference_data.name
505    residuals.name = "Residuals for " + str(theory_name) + "[" + res_name + "]"
506    residuals.title = residuals.name
507    residuals.ytransform = 'y'
508
509    # when 2 data have the same id override the 1 st plotted
510    # include the last part if keeping charts for separate models is required
511    residuals.id = "res" + str(reference_data.id) # + str(theory_name)
512    # group_id specify on which panel to plot this data
513    group_id = reference_data.group_id
514    residuals.group_id = "res" + str(group_id)
515
516    # Symbol
517    residuals.symbol = 0
518    residuals.hide_error = False
519
520    return residuals
521
522def binary_encode(i, digits):
523    return [i >> d & 1 for d in range(digits)]
524
525def getWeight(data, is2d, flag=None):
526    """
527    Received flag and compute error on data.
528    :param flag: flag to transform error of data.
529    """
530    weight = None
531    if data is None:
532        return []
533    if is2d:
534        if not hasattr(data, 'err_data'):
535            return []
536        dy_data = data.err_data
537        data = data.data
538    else:
539        if not hasattr(data, 'dy'):
540            return []
541        dy_data = data.dy
542        data = data.y
543
544    if flag == 0:
545        weight = numpy.ones_like(data)
546    elif flag == 1:
547        weight = dy_data
548    elif flag == 2:
549        weight = numpy.sqrt(numpy.abs(data))
550    elif flag == 3:
551        weight = numpy.abs(data)
552    return weight
553
554def updateKernelWithResults(kernel, results):
555    """
556    Takes model kernel and applies results dict to its parameters,
557    returning the modified (deep) copy of the kernel.
558    """
559    assert isinstance(results, dict)
560    local_kernel = copy.deepcopy(kernel)
561
562    for parameter in results.keys():
563        # Update the parameter value - note: this supports +/-inf as well
564        local_kernel.setParam(parameter, results[parameter][0])
565
566    return local_kernel
567
568
569def getStandardParam(model=None):
570    """
571    Returns a list with standard parameters for the current model
572    """
573    param = []
574    num_rows = model.rowCount()
575    if num_rows < 1:
576        return None
577
578    for row in range(num_rows):
579        param_name = model.item(row, 0).text()
580        checkbox_state = model.item(row, 0).checkState() == QtCore.Qt.Checked
581        value = model.item(row, 1).text()
582        column_shift = 0
583        if model.columnCount() == 5: # no error column
584            error_state = False
585            error_value = 0.0
586        else:
587            error_state = True
588            error_value = model.item(row, 2).text()
589            column_shift = 1
590        min_state = True
591        max_state = True
592        min_value = model.item(row, 2+column_shift).text()
593        max_value = model.item(row, 3+column_shift).text()
594        unit = ""
595        if model.item(row, 4+column_shift) is not None:
596            unit = model.item(row, 4+column_shift).text()
597
598        param.append([checkbox_state, param_name, value, "",
599                        [error_state, error_value],
600                        [min_state, min_value],
601                        [max_state, max_value], unit])
602
603    return param
604
605def getOrientationParam(kernel_module=None):
606    """
607    Get the dictionary with orientation parameters
608    """
609    param = []
610    if kernel_module is None:
611        return None
612    for param_name in list(kernel_module.params.keys()):
613        name = param_name
614        value = kernel_module.params[param_name]
615        min_state = True
616        max_state = True
617        error_state = False
618        error_value = 0.0
619        checkbox_state = True #??
620        details = kernel_module.details[param_name] #[unit, mix, max]
621        param.append([checkbox_state, name, value, "",
622                     [error_state, error_value],
623                     [min_state, details[1]],
624                     [max_state, details[2]], details[0]])
625
626    return param
627
628def formatParameters(parameters):
629    """
630    Prepare the parameter string in the standard SasView layout
631    """
632    assert parameters is not None
633    assert isinstance(parameters, list)
634    output_string = "sasview_parameter_values:"
635    for parameter in parameters:
636        output_string += ",".join([p for p in parameter if p is not None])
637        output_string += ":"
638    return output_string
639
640def formatParametersExcel(parameters):
641    """
642    Prepare the parameter string in the Excel format (tab delimited)
643    """
644    assert parameters is not None
645    assert isinstance(parameters, list)
646    crlf = chr(13) + chr(10)
647    tab = chr(9)
648
649    output_string = ""
650    # names
651    names = ""
652    values = ""
653    for parameter in parameters:
654        names += parameter[0]+tab
655        # Add the error column if fitted
656        if parameter[1] == "True" and parameter[3] is not None:
657            names += parameter[0]+"_err"+tab
658
659        values += parameter[2]+tab
660        if parameter[1] == "True" and parameter[3] is not None:
661            values += parameter[3]+tab
662        # add .npts and .nsigmas when necessary
663        if parameter[0][-6:] == ".width":
664            names += parameter[0].replace('.width', '.nsigmas') + tab
665            names += parameter[0].replace('.width', '.npts') + tab
666            values += parameter[5] + tab + parameter[4] + tab
667
668    output_string = names + crlf + values
669    return output_string
670
671def formatParametersLatex(parameters):
672    """
673    Prepare the parameter string in latex
674    """
675    assert parameters is not None
676    assert isinstance(parameters, list)
677    output_string = r'\begin{table}'
678    output_string += r'\begin{tabular}[h]'
679
680    crlf = chr(13) + chr(10)
681    output_string += '{|'
682    output_string += 'l|l|'*len(parameters)
683    output_string += r'}\hline'
684    output_string += crlf
685
686    for index, parameter in enumerate(parameters):
687        name = parameter[0] # Parameter name
688        output_string += name.replace('_', r'\_')  # Escape underscores
689        # Add the error column if fitted
690        if parameter[1] == "True" and parameter[3] is not None:
691            output_string += ' & '
692            output_string += parameter[0]+r'\_err'
693
694        if index < len(parameters) - 1:
695            output_string += ' & '
696
697        # add .npts and .nsigmas when necessary
698        if parameter[0][-6:] == ".width":
699            output_string += parameter[0].replace('.width', '.nsigmas') + ' & '
700            output_string += parameter[0].replace('.width', '.npts')
701
702            if index < len(parameters) - 1:
703                output_string += ' & '
704
705    output_string += r'\\ \hline'
706    output_string += crlf
707
708    # Construct row of values and errors
709    for index, parameter in enumerate(parameters):
710        output_string += parameter[2]
711        if parameter[1] == "True" and parameter[3] is not None:
712            output_string += ' & '
713            output_string += parameter[3]
714
715        if index < len(parameters) - 1:
716            output_string += ' & '
717
718        # add .npts and .nsigmas when necessary
719        if parameter[0][-6:] == ".width":
720            output_string += parameter[5] + ' & '
721            output_string += parameter[4]
722
723            if index < len(parameters) - 1:
724                output_string += ' & '
725
726    output_string += r'\\ \hline'
727    output_string += crlf
728    output_string += r'\end{tabular}'
729    output_string += r'\end{table}'
730
731    return output_string
Note: See TracBrowser for help on using the repository browser.